@decentnetwork/beagle 0.1.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.
@@ -0,0 +1,3379 @@
1
+ window.__DK_UI_VERSION="0.1.0";
2
+ const ICON_PATHS = {
3
+ // ---- tab bar (the four must feel like one set) ----
4
+ users: '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>',
5
+ grid: '<rect x="3" y="3" width="7.5" height="7.5" rx="1.6"/><rect x="13.5" y="3" width="7.5" height="7.5" rx="1.6"/><rect x="13.5" y="13.5" width="7.5" height="7.5" rx="1.6"/><rect x="3" y="13.5" width="7.5" height="7.5" rx="1.6"/>',
6
+ wallet: '<path d="M19 7V6a2 2 0 0 0-2-2H6a3 3 0 0 0-3 3v10a3 3 0 0 0 3 3h12a2 2 0 0 0 2-2v-1"/><path d="M3 7a3 3 0 0 0 3 3h13a2 2 0 0 1 2 2v0a2 2 0 0 1-2 2h-3"/><circle cx="16.5" cy="14" r="1.15" fill="currentColor" stroke="none"/>',
7
+ userRound: '<circle cx="12" cy="8" r="4.5"/><path d="M20 21a8 8 0 0 0-16 0"/>',
8
+ // ---- actions / chrome ----
9
+ search: '<circle cx="11" cy="11" r="7.5"/><path d="m21 21-4.3-4.3"/>',
10
+ plus: '<path d="M5 12h14M12 5v14"/>',
11
+ chevronRight: '<path d="m9 18 6-6-6-6"/>',
12
+ chevronLeft: '<path d="m15 18-6-6 6-6"/>',
13
+ info: '<circle cx="12" cy="12" r="9.5"/><path d="M12 16.5v-5M12 7.6h.01"/>',
14
+ more: '<circle cx="5" cy="12" r="1.4" fill="currentColor" stroke="none"/><circle cx="12" cy="12" r="1.4" fill="currentColor" stroke="none"/><circle cx="19" cy="12" r="1.4" fill="currentColor" stroke="none"/>',
15
+ x: '<path d="M18 6 6 18M6 6l12 12"/>',
16
+ check: '<path d="M20 6 9 17l-5-5"/>',
17
+ checkCheck: '<path d="M18 6 7 17l-5-5"/><path d="m22 10-7.6 7.6L13 16"/>',
18
+ copy: '<rect x="9" y="9" width="12" height="12" rx="2.4"/><path d="M6 15H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v1"/>',
19
+ // Three linked nodes — an address is for passing ON to someone else, so it
20
+ // must not wear the same glyph as the userid's plain copy.
21
+ share: '<circle cx="18" cy="5" r="2.6"/><circle cx="6" cy="12" r="2.6"/><circle cx="18" cy="19" r="2.6"/><path d="M8.3 10.8l7.4-4.3M8.3 13.2l7.4 4.3"/>',
22
+ refresh: '<path d="M21 12a9 9 0 1 1-2.6-6.3"/><path d="M21 4v4.5h-4.5"/>',
23
+ clock: '<circle cx="12" cy="12" r="9.5"/><path d="M12 7.5V12l3 2"/>',
24
+ edit: '<path d="M11 4H6a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-5"/><path d="M18.5 2.5a2.12 2.12 0 0 1 3 3L12 15l-4 1 1-4Z"/>',
25
+ qr: '<rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="3" width="7" height="7" rx="1.5"/><rect x="3" y="14" width="7" height="7" rx="1.5"/><path d="M14 14h3v3M21 14v.01M14 21h.01M17 21h.01M21 17v4"/>',
26
+ // ---- app tiles ----
27
+ video: '<path d="m22 8.5-5.4 3.5 5.4 3.5V8.5Z"/><rect x="2" y="6" width="14.5" height="12" rx="3"/>',
28
+ monitor: '<rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/>',
29
+ sparkles: '<path d="M12 3.5 13.6 9 19 10.5 13.6 12 12 17.5 10.4 12 5 10.5 10.4 9 12 3.5Z"/><path d="M19 3.5v3.2M20.6 5.1h-3.2"/><path d="M5 16v2.4M6.2 17.2H3.8"/>',
30
+ mic: '<rect x="9" y="2.5" width="6" height="11.5" rx="3"/><path d="M5.5 11a6.5 6.5 0 0 0 13 0"/><path d="M12 17.5v3.5"/>',
31
+ flask: '<path d="M9.5 3v6.2L4.8 17.4A2 2 0 0 0 6.5 20.5h11a2 2 0 0 0 1.7-3.1L14.5 9.2V3"/><path d="M8.5 3h7"/><path d="M7.3 15h9.4"/>',
32
+ headphones: '<path d="M3.5 14v-1.5a8.5 8.5 0 0 1 17 0V14"/><path d="M3.5 14.5a2.5 2.5 0 0 1 2.5-2.5h0a1.5 1.5 0 0 1 1.5 1.5v3.5a1.5 1.5 0 0 1-1.5 1.5h0a2.5 2.5 0 0 1-2.5-2.5Z"/><path d="M20.5 14.5a2.5 2.5 0 0 0-2.5-2.5h0a1.5 1.5 0 0 0-1.5 1.5v3.5a1.5 1.5 0 0 0 1.5 1.5h0a2.5 2.5 0 0 0 2.5-2.5Z"/>',
33
+ globe: '<circle cx="12" cy="12" r="9.5"/><path d="M2.5 12h19"/><path d="M12 2.5a14 14 0 0 1 0 19 14 14 0 0 1 0-19Z"/>',
34
+ store: '<path d="M4 9.5V19a1.5 1.5 0 0 0 1.5 1.5h13A1.5 1.5 0 0 0 20 19V9.5"/><path d="M3 6.5 4.5 3.5h15L21 6.5a2.5 2.5 0 0 1-4.5 1.5 2.5 2.5 0 0 1-4.5 0 2.5 2.5 0 0 1-4.5 0A2.5 2.5 0 0 1 3 6.5Z"/>',
35
+ // ---- chat composer ----
36
+ camera: '<path d="M14.5 4.5h-5L7.2 7.2H4.5A2.5 2.5 0 0 0 2 9.7V18a2.5 2.5 0 0 0 2.5 2.5h15A2.5 2.5 0 0 0 22 18V9.7a2.5 2.5 0 0 0-2.5-2.5h-2.7L14.5 4.5Z"/><circle cx="12" cy="13.5" r="3.2"/>',
37
+ phone: '<path d="M16.5 21a2 2 0 0 0 2-2.2 1.9 1.9 0 0 0-.6-1.2l-2-1.7a2 2 0 0 0-2.4-.1l-.9.6a14 14 0 0 1-5-5l.6-.9a2 2 0 0 0-.1-2.4l-1.7-2a1.9 1.9 0 0 0-1.2-.6A2 2 0 0 0 3 5.5 16 16 0 0 0 16.5 21Z"/>',
38
+ bolt: '<path d="M13 2 4 13h7l-1 9 9-11h-7l1-9Z"/>',
39
+ arrowUp: '<path d="M12 20V5M6 11l6-6 6 6"/>',
40
+ send: '<path d="M21.5 4.5 10.5 13.5"/><path d="M21.5 4.5 14.8 21.5 11 13l-8.5-3.8 19-4.7Z"/>',
41
+ // ---- account ----
42
+ shield: '<path d="M12 21.5s7.5-3.8 7.5-9.5V5.5L12 2.8 4.5 5.5V12c0 5.7 7.5 9.5 7.5 9.5Z"/>',
43
+ download: '<path d="M12 3.5v11M7.5 10l4.5 4.5 4.5-4.5"/><path d="M5 20.5h14"/>',
44
+ // Folder with a magnifier corner — "reveal / show in file manager".
45
+ folderOpen: '<path d="M3 7.5a2 2 0 0 1 2-2h4l2 2.5h8a2 2 0 0 1 2 2V17a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z"/><circle cx="12" cy="13" r="2.4"/><path d="M13.9 14.9 15.5 16.5"/>',
46
+ migrate: '<path d="M8 4 4 8l4 4"/><path d="M4 8h13a3 3 0 0 1 3 3v1"/><path d="m16 20 4-4-4-4"/><path d="M20 16H7a3 3 0 0 1-3-3v-1"/>',
47
+ starCoin: '<circle cx="12" cy="12" r="9"/><path d="m12 7.2 1.4 2.9 3.2.4-2.3 2.2.6 3.1-2.9-1.5-2.9 1.5.6-3.1L8.4 10.5l3.2-.4L12 7.2Z" fill="currentColor" stroke="none"/>',
48
+ link: '<path d="M10.5 13.5a4 4 0 0 0 5.7 0l2.3-2.3a4 4 0 0 0-5.7-5.7l-1 1"/><path d="M13.5 10.5a4 4 0 0 0-5.7 0l-2.3 2.3a4 4 0 0 0 5.7 5.7l1-1"/>',
49
+ trendingUp: '<path d="M22 7.5 13.5 16l-4.5-4.5L2 18.5"/><path d="M16.5 7.5H22v5.5"/>',
50
+ bell: '<path d="M18 8.5a6 6 0 0 0-12 0c0 6.5-2.5 8-2.5 8h17S18 15 18 8.5Z"/><path d="M13.7 20.5a2 2 0 0 1-3.4 0"/>',
51
+ bot: '<rect x="3.5" y="9" width="17" height="11" rx="3"/><path d="M12 4.5v4.5"/><circle cx="12" cy="3" r="1.4"/><path d="M8.5 14.5h.01M15.5 14.5h.01"/><path d="M2 13.5v3M22 13.5v3"/>',
52
+ scan: '<path d="M3 8V6a3 3 0 0 1 3-3h2"/><path d="M16 3h2a3 3 0 0 1 3 3v2"/><path d="M21 16v2a3 3 0 0 1-3 3h-2"/><path d="M8 21H6a3 3 0 0 1-3-3v-2"/><path d="M7 12h10"/>',
53
+ userPlus: '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M19 8v6M22 11h-6"/>',
54
+ at: '<circle cx="12" cy="12" r="4"/><path d="M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-3.9 7.9"/>',
55
+ keyRound: '<circle cx="7.5" cy="15.5" r="5.5"/><path d="m21 2-9.6 9.6"/><path d="m15.5 7.5 3 3L22 7l-3-3"/>',
56
+ alert: '<path d="M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0Z"/><path d="M12 9v4M12 17h.01"/>',
57
+ chevronDown: '<path d="m6 9 6 6 6-6"/>',
58
+ eye: '<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/>',
59
+ book: '<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2Z"/>',
60
+ trash: '<path d="M3 6h18"/><path d="M8 6V4.5A1.5 1.5 0 0 1 9.5 3h5A1.5 1.5 0 0 1 16 4.5V6"/><path d="M18.5 6l-1 13.5a2 2 0 0 1-2 1.5h-7a2 2 0 0 1-2-1.5L5.5 6"/><path d="M10 10.5v6M14 10.5v6"/>',
61
+ arrowDown: '<path d="M12 4v15M6 13l6 6 6-6"/>',
62
+ shrink: '<path d="M4 14h6v6"/><path d="M20 10h-6V4"/><path d="m14 10 7-7"/><path d="m3 21 7-7"/>',
63
+ maximize: '<path d="M8 3H5a2 2 0 0 0-2 2v3"/><path d="M21 8V5a2 2 0 0 0-2-2h-3"/><path d="M3 16v3a2 2 0 0 0 2 2h3"/><path d="M16 21h3a2 2 0 0 0 2-2v-3"/>',
64
+ theater: '<rect x="2.5" y="6.5" width="19" height="11" rx="2"/>',
65
+ speaker: '<path d="M11 5 6 9H3v6h3l5 4V5Z"/><path d="M16 9a3 3 0 0 1 0 6"/><path d="M19 7a6 6 0 0 1 0 10"/>',
66
+ micFill: '<rect x="9" y="2.5" width="6" height="11.5" rx="3"/><path d="M5.5 11a6.5 6.5 0 0 0 13 0"/><path d="M12 17.5v3.5"/>'
67
+ };
68
+ function Icon({ name, size = 24, stroke = 1.75, color = "currentColor", fill = "none", style }) {
69
+ const d = ICON_PATHS[name];
70
+ if (!d)
71
+ return null;
72
+ return /* @__PURE__ */ React.createElement(
73
+ "svg",
74
+ {
75
+ width: size,
76
+ height: size,
77
+ viewBox: "0 0 24 24",
78
+ fill,
79
+ stroke: color,
80
+ strokeWidth: stroke,
81
+ strokeLinecap: "round",
82
+ strokeLinejoin: "round",
83
+ style: { display: "block", flexShrink: 0, ...style },
84
+ dangerouslySetInnerHTML: { __html: d }
85
+ }
86
+ );
87
+ }
88
+ Object.assign(window, { Icon, ICON_PATHS });
89
+ Object.assign(ICON_PATHS, {
90
+ message: '<path d="M21 12a8.5 8.5 0 0 1-12.2 7.7L3 21l1.3-5.8A8.5 8.5 0 1 1 21 12Z"/>',
91
+ network: '<circle cx="12" cy="5" r="2.4"/><circle cx="5" cy="19" r="2.4"/><circle cx="19" cy="19" r="2.4"/><path d="M10.6 6.9 6.4 16.7M13.4 6.9l4.2 9.8M7.4 19h9.2"/>',
92
+ route: '<circle cx="6.5" cy="18.5" r="2.4"/><circle cx="17.5" cy="5.5" r="2.4"/><path d="M9 18.5h5.5a3.5 3.5 0 0 0 0-7h-4a3.5 3.5 0 0 1 0-7H15"/>',
93
+ globe2: '<circle cx="12" cy="12" r="9"/><path d="M3 12h18"/><path d="M12 3a14 14 0 0 1 0 18 14 14 0 0 1 0-18Z"/>',
94
+ server: '<rect x="3" y="4" width="18" height="7" rx="2"/><rect x="3" y="13" width="18" height="7" rx="2"/><path d="M7 7.5h.01M7 16.5h.01"/>',
95
+ power: '<path d="M12 4v8"/><path d="M7.6 7.2a7 7 0 1 0 8.8 0"/>',
96
+ ban: '<circle cx="12" cy="12" r="9"/><path d="m5.6 5.6 12.8 12.8"/>',
97
+ terminal: '<path d="m4.5 17 6-6-6-6"/><path d="M12 18.5h7.5"/>',
98
+ signal: '<path d="M4.8 12.8a10 10 0 0 1 14.4 0"/><path d="M8.3 16.3a5 5 0 0 1 7.4 0"/><path d="M12 19.6h.01"/>',
99
+ paperclip: '<path d="m20.5 9-8.7 8.7a4.2 4.2 0 0 1-6-6l8.6-8.6a2.8 2.8 0 0 1 4 4l-8.6 8.6a1.4 1.4 0 0 1-2-2l7.9-7.9"/>',
100
+ file: '<path d="M14 3v5h5"/><path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8l-5-5Z"/>',
101
+ fileText: '<path d="M14 3v5h5"/><path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8l-5-5Z"/><path d="M8.5 13h7M8.5 16.5h5"/>',
102
+ image: '<rect x="3" y="4" width="18" height="16" rx="2"/><circle cx="8.5" cy="9.5" r="1.6"/><path d="m4 17 4.5-4.5a2 2 0 0 1 2.8 0L20 21"/>',
103
+ arrowRight: '<path d="M5 12h13M13 6l6 6-6 6"/>',
104
+ dotFill: '<circle cx="12" cy="12" r="4" fill="currentColor" stroke="none"/>',
105
+ hash: '<path d="M4 9h16M3.5 15h16M10 4 8 20M16 4l-2 16"/>',
106
+ sliders: '<path d="M4 6h11M4 12h7M4 18h13"/><circle cx="18" cy="6" r="2"/><circle cx="13.5" cy="12" r="2"/><circle cx="19" cy="18" r="2"/>',
107
+ unlink: '<path d="M9 15 5.5 18.5a3.5 3.5 0 0 1-5-5L4 10"/><path d="m15 9 3.5-3.5a3.5 3.5 0 0 1 5 5L20 14"/><path d="M8 8 4 4M16 16l4 4M3 11h2M11 3v2"/>',
108
+ zap: '<path d="M13 2 4 13h7l-1 9 9-11h-7l1-9Z"/>',
109
+ clock: '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3.5 2"/>',
110
+ plug: '<path d="M9 2v6M15 2v6"/><path d="M7 8h10v3a5 5 0 0 1-10 0V8Z"/><path d="M12 16v6"/>'
111
+ });
112
+ function dkHash(s) {
113
+ let h = 2166136261;
114
+ for (let i = 0; i < s.length; i++) {
115
+ h ^= s.charCodeAt(i);
116
+ h = Math.imul(h, 16777619);
117
+ }
118
+ return h >>> 0;
119
+ }
120
+ function dkRng(a) {
121
+ return function() {
122
+ a |= 0;
123
+ a = a + 1831565813 | 0;
124
+ let t = Math.imul(a ^ a >>> 15, 1 | a);
125
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
126
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
127
+ };
128
+ }
129
+ function dkIdenticon(seed) {
130
+ const rnd = dkRng(dkHash(seed || "x"));
131
+ const hue = Math.floor(rnd() * 360);
132
+ const grid = [];
133
+ for (let y = 0; y < 5; y++) {
134
+ const row = [];
135
+ for (let x = 0; x < 3; x++)
136
+ row.push(rnd() > 0.5);
137
+ grid.push([row[0], row[1], row[2], row[1], row[0]]);
138
+ }
139
+ return { cells: grid, hue };
140
+ }
141
+ function dkClock(ts) {
142
+ if (!ts)
143
+ return "";
144
+ const d = new Date(ts);
145
+ return String(d.getHours()).padStart(2, "0") + ":" + String(d.getMinutes()).padStart(2, "0");
146
+ }
147
+ function dkFileSize(n) {
148
+ if (!n || n < 0)
149
+ return "0 B";
150
+ if (n < 1024)
151
+ return n + " B";
152
+ if (n < 1024 * 1024)
153
+ return (n / 1024).toFixed(1) + " KB";
154
+ if (n < 1024 * 1024 * 1024)
155
+ return (n / (1024 * 1024)).toFixed(1) + " MB";
156
+ return (n / (1024 * 1024 * 1024)).toFixed(2) + " GB";
157
+ }
158
+ function dkSpeed(kbps) {
159
+ if (kbps == null || !(kbps >= 0))
160
+ return "";
161
+ return kbps >= 1024 ? (kbps / 1024).toFixed(2) + " MB/s" : Math.round(kbps) + " KB/s";
162
+ }
163
+ function dkPct(p) {
164
+ return p == null ? "\u2026" : (p >= 100 ? "100" : p.toFixed(2)) + "%";
165
+ }
166
+ function dkFileUrl(name) {
167
+ return "/api/file-download?name=" + encodeURIComponent(name);
168
+ }
169
+ function dkFileDownloadUrl(name) {
170
+ return dkFileUrl(name) + "&dl=1";
171
+ }
172
+ function dkFileMediaKind(name) {
173
+ const ext = (String(name || "").split(".").pop() || "").toLowerCase();
174
+ if (["png", "jpg", "jpeg", "gif", "webp", "svg", "heic", "bmp"].includes(ext))
175
+ return "image";
176
+ if (["mp4", "mov", "webm", "m4v", "mkv", "avi"].includes(ext))
177
+ return "video";
178
+ if (["mp3", "m4a", "aac", "wav", "ogg", "flac", "opus"].includes(ext))
179
+ return "audio";
180
+ return "file";
181
+ }
182
+ function dkDayLabel(ts) {
183
+ if (!ts)
184
+ return "Today";
185
+ const d = new Date(ts), now = /* @__PURE__ */ new Date();
186
+ if (d.toDateString() === now.toDateString())
187
+ return "Today";
188
+ const y = new Date(now.getTime() - 864e5);
189
+ if (d.toDateString() === y.toDateString())
190
+ return "Yesterday";
191
+ return d.toLocaleDateString();
192
+ }
193
+ async function dkGet(path) {
194
+ const r = await fetch(path, { headers: { "cache-control": "no-cache" } });
195
+ return r.json();
196
+ }
197
+ async function dkPost(path, body) {
198
+ const ctrl = new AbortController();
199
+ const timer = setTimeout(() => ctrl.abort(), 15e3);
200
+ try {
201
+ const r = await fetch(path, {
202
+ method: "POST",
203
+ headers: { "content-type": "application/json" },
204
+ body: JSON.stringify(body || {}),
205
+ signal: ctrl.signal
206
+ });
207
+ try {
208
+ return await r.json();
209
+ } catch (e) {
210
+ return { ok: r.ok };
211
+ }
212
+ } catch (e) {
213
+ return { ok: false, error: e && e.name === "AbortError" ? "timed out" : e && e.message || "network error" };
214
+ } finally {
215
+ clearTimeout(timer);
216
+ }
217
+ }
218
+ function dkCopy(text) {
219
+ const s = text == null ? "" : String(text);
220
+ if (navigator.clipboard && window.isSecureContext) {
221
+ return navigator.clipboard.writeText(s).then(() => true).catch(() => dkCopyLegacy(s));
222
+ }
223
+ return Promise.resolve(dkCopyLegacy(s));
224
+ }
225
+ function dkCopyLegacy(s) {
226
+ try {
227
+ const ta = document.createElement("textarea");
228
+ ta.value = s;
229
+ ta.setAttribute("readonly", "");
230
+ ta.style.cssText = "position:fixed;top:-1000px;left:-1000px;opacity:0";
231
+ document.body.appendChild(ta);
232
+ ta.select();
233
+ ta.setSelectionRange(0, s.length);
234
+ const ok = document.execCommand("copy");
235
+ document.body.removeChild(ta);
236
+ return ok;
237
+ } catch (e) {
238
+ return false;
239
+ }
240
+ }
241
+ const dkApi = {
242
+ send: (userid, text) => dkPost("/api/chat-send", { userid, text }),
243
+ logLocal: (userid, dir, text) => dkPost("/api/chat-log-local", { userid, dir, text }),
244
+ add: (address) => dkPost("/api/add", { address }),
245
+ accept: (userid) => dkPost("/api/accept", { userid }),
246
+ reject: (userid) => dkPost("/api/reject", { userid }),
247
+ remove: (userid) => dkPost("/api/friend-remove", { userid }),
248
+ alias: (userid, alias) => dkPost("/api/friend-alias", { userid, alias }),
249
+ markRead: (userid) => dkPost("/api/chat-mark-read", { userid }),
250
+ delFiles: (userid, ids) => dkPost("/api/file-delete", { userid, ids }),
251
+ // Reveal a received file in the OS file manager (localhost-only on the daemon side).
252
+ revealFile: (name) => dkPost("/api/file-reveal", { name }),
253
+ cancelSend: (userid, ids) => dkPost("/api/file-cancel", { userid, ids }),
254
+ retrySend: (userid, ids) => dkPost("/api/file-retry", { userid, ids }),
255
+ setProfile: (name, description) => dkPost("/api/set-profile", { name, description }),
256
+ saveWebrtcFile: async (userid, name, blob) => {
257
+ try {
258
+ const r = await fetch(
259
+ "/api/webrtc-file-save?userid=" + encodeURIComponent(userid) + "&name=" + encodeURIComponent(name),
260
+ { method: "POST", body: blob }
261
+ );
262
+ const txt = await r.text();
263
+ try {
264
+ return JSON.parse(txt);
265
+ } catch {
266
+ return { ok: false, error: (txt || "HTTP " + r.status).slice(0, 120) };
267
+ }
268
+ } catch (e) {
269
+ return { ok: false, error: String(e) };
270
+ }
271
+ },
272
+ sendFile: async (userid, file) => {
273
+ try {
274
+ const r = await fetch(
275
+ "/api/file-send?userid=" + encodeURIComponent(userid) + "&name=" + encodeURIComponent(file.name),
276
+ { method: "POST", body: file }
277
+ );
278
+ const txt = await r.text();
279
+ try {
280
+ return JSON.parse(txt);
281
+ } catch {
282
+ if (r.status === 404)
283
+ return { ok: false, error: "file upload not supported \u2014 update & restart `agentnet ui`" };
284
+ return { ok: false, error: (txt || "HTTP " + r.status).slice(0, 120) };
285
+ }
286
+ } catch (e) {
287
+ return { ok: false, error: String(e) };
288
+ }
289
+ }
290
+ };
291
+ const DK_ME_FALLBACK = {
292
+ name: "\u2026",
293
+ handle: "@decentnetwork/peer",
294
+ userId: "",
295
+ carrier: "",
296
+ netKey: "",
297
+ ip: "",
298
+ online: false,
299
+ lanVer: "",
300
+ peerVer: "",
301
+ channel: "@next",
302
+ wire: "163"
303
+ };
304
+ function useDaemonData() {
305
+ const [snap, setSnap] = React.useState({
306
+ me: DK_ME_FALLBACK,
307
+ peers: [],
308
+ requests: [],
309
+ exits: [],
310
+ activeExit: null,
311
+ loaded: false
312
+ });
313
+ const [threads, setThreads] = React.useState({});
314
+ const refresh = React.useCallback(async () => {
315
+ var _a;
316
+ try {
317
+ const d = await dkGet("/api/desktop");
318
+ setSnap({
319
+ me: d.me || DK_ME_FALLBACK,
320
+ peers: d.peers || [],
321
+ requests: d.requests || [],
322
+ exits: d.exits || [],
323
+ activeExit: (_a = d.activeExit) != null ? _a : null,
324
+ loaded: true
325
+ });
326
+ } catch (e) {
327
+ }
328
+ }, []);
329
+ React.useEffect(() => {
330
+ refresh();
331
+ const t = setInterval(refresh, 2500);
332
+ return () => clearInterval(t);
333
+ }, [refresh]);
334
+ const loadThread = React.useCallback(async (peerId) => {
335
+ if (!peerId)
336
+ return;
337
+ try {
338
+ const d = await dkGet("/api/chat-history?peer=" + encodeURIComponent(peerId));
339
+ const arr = d.chats && d.chats[peerId] || [];
340
+ const msgs = arr.map((m) => ({
341
+ id: m.id,
342
+ from: m.dir === "out" ? "me" : "them",
343
+ time: dkClock(m.ts),
344
+ text: m.text,
345
+ file: m.file ? {
346
+ name: m.file.name,
347
+ size: dkFileSize(m.file.size),
348
+ dir: m.dir,
349
+ media: dkFileMediaKind(m.file.name),
350
+ status: m.file.status,
351
+ pct: m.file.status === "sending" && m.file.size ? Math.min(100, (m.file.sent || 0) / m.file.size * 100) : void 0,
352
+ kbps: m.file.kbps
353
+ } : void 0,
354
+ status: m.dir === "out" ? m.status === "queued" ? "queued" : "read" : void 0,
355
+ // Delivery path: "online" = live session, "offline" = express relay.
356
+ // Incoming messages carry this; the bubble colors them differently.
357
+ via: m.via
358
+ }));
359
+ const withDay = msgs.length ? [{ day: dkDayLabel(arr[0].ts) }].concat(msgs) : [];
360
+ setThreads((t) => Object.assign({}, t, { [peerId]: withDay }));
361
+ } catch (e) {
362
+ }
363
+ }, []);
364
+ return Object.assign({}, snap, { threads, refresh, loadThread });
365
+ }
366
+ Object.assign(window, {
367
+ dkHash,
368
+ dkIdenticon,
369
+ dkClock,
370
+ dkDayLabel,
371
+ dkApi,
372
+ useDaemonData,
373
+ DK_ME_FALLBACK,
374
+ dkCopy
375
+ });
376
+ function dkRtcParseEnvelope(data) {
377
+ const text = String(data || "").replace(/\0+$/u, "").trim();
378
+ if (!text)
379
+ return null;
380
+ try {
381
+ const obj = JSON.parse(text);
382
+ if (obj && obj.dkRtc === 1 && typeof obj.kind === "string")
383
+ return obj;
384
+ } catch (e) {
385
+ }
386
+ return null;
387
+ }
388
+ function dkRtcSignalBus() {
389
+ if (window.__dkRtcSignalBus)
390
+ return window.__dkRtcSignalBus;
391
+ const PW = window.PeerWebRTC;
392
+ const handlers = /* @__PURE__ */ new Map();
393
+ let started = false;
394
+ let stopped = false;
395
+ function emit(kind, userid, payload) {
396
+ const set = handlers.get(kind);
397
+ if (!set)
398
+ return;
399
+ for (const h of Array.from(set)) {
400
+ try {
401
+ h(userid, payload);
402
+ } catch (e) {
403
+ console.warn("[rtc] signal handler failed", e);
404
+ }
405
+ }
406
+ }
407
+ async function pollLoop() {
408
+ while (!stopped) {
409
+ let signals = [];
410
+ try {
411
+ const r = await fetch("/api/call-poll", { headers: { "cache-control": "no-cache" } });
412
+ const d = await r.json();
413
+ signals = d && d.signals || [];
414
+ } catch (e) {
415
+ await new Promise((res) => setTimeout(res, 1e3));
416
+ continue;
417
+ }
418
+ for (const s of signals) {
419
+ const env = dkRtcParseEnvelope(s.data);
420
+ if (env) {
421
+ emit(env.kind, s.userid, env);
422
+ continue;
423
+ }
424
+ if (PW) {
425
+ try {
426
+ emit("call", s.userid, PW.decodeSignal(s.data));
427
+ } catch (e) {
428
+ }
429
+ }
430
+ }
431
+ }
432
+ }
433
+ const bus = {
434
+ start() {
435
+ if (started)
436
+ return;
437
+ started = true;
438
+ stopped = false;
439
+ pollLoop();
440
+ },
441
+ on(kind, cb) {
442
+ if (!handlers.has(kind))
443
+ handlers.set(kind, /* @__PURE__ */ new Set());
444
+ handlers.get(kind).add(cb);
445
+ this.start();
446
+ return () => {
447
+ var _a;
448
+ return (_a = handlers.get(kind)) == null ? void 0 : _a.delete(cb);
449
+ };
450
+ },
451
+ async send(userid, data) {
452
+ const body = JSON.stringify({ userid, data: typeof data === "string" ? data : JSON.stringify(data) });
453
+ const r = await fetch("/api/call-signal", {
454
+ method: "POST",
455
+ headers: { "content-type": "application/json" },
456
+ body
457
+ });
458
+ const d = await r.json().catch(() => ({ ok: r.ok }));
459
+ if (!d.ok)
460
+ throw new Error(d.error || "call-signal send failed");
461
+ },
462
+ stop() {
463
+ stopped = true;
464
+ }
465
+ };
466
+ window.__dkRtcSignalBus = bus;
467
+ return bus;
468
+ }
469
+ function makeDaemonSignaling() {
470
+ const PW = window.PeerWebRTC;
471
+ const bus = dkRtcSignalBus();
472
+ let off = null;
473
+ return {
474
+ async send(peerId, signal) {
475
+ await bus.send(peerId, PW.encodeSignal(signal));
476
+ },
477
+ onSignal(cb) {
478
+ if (off)
479
+ off();
480
+ off = bus.on("call", cb);
481
+ },
482
+ stop() {
483
+ if (off) {
484
+ off();
485
+ off = null;
486
+ }
487
+ }
488
+ };
489
+ }
490
+ Object.assign(window, { dkRtcSignalBus, makeDaemonSignaling });
491
+ function DkIdenticon({ seed, size = 30, radius = 7 }) {
492
+ const { cells, hue } = dkIdenticon(seed);
493
+ const cell = size / 5;
494
+ const fg = `oklch(0.62 0.15 ${hue})`;
495
+ const bg = `oklch(var(--ident-l) 0.05 ${hue})`;
496
+ return /* @__PURE__ */ React.createElement("svg", { width: size, height: size, viewBox: `0 0 ${size} ${size}`, style: { display: "block", borderRadius: radius } }, /* @__PURE__ */ React.createElement("rect", { width: size, height: size, fill: bg }), cells.map((row, y) => row.map((on, x) => on ? /* @__PURE__ */ React.createElement("rect", { key: `${x}-${y}`, x: x * cell, y: y * cell, width: cell + 0.4, height: cell + 0.4, fill: fg }) : null)));
497
+ }
498
+ function DkBot({ seed, size = 30, radius = 7 }) {
499
+ const hue = dkIdenticon(seed).hue;
500
+ return /* @__PURE__ */ React.createElement("div", { style: {
501
+ width: size,
502
+ height: size,
503
+ borderRadius: radius,
504
+ flexShrink: 0,
505
+ background: `linear-gradient(150deg, oklch(0.6 0.17 ${hue}), oklch(0.5 0.18 ${(hue + 30) % 360}))`,
506
+ display: "flex",
507
+ alignItems: "center",
508
+ justifyContent: "center"
509
+ } }, /* @__PURE__ */ React.createElement(Icon, { name: "bot", size: size * 0.56, color: "#fff", stroke: 1.9 }));
510
+ }
511
+ function DkAvatar({ peer, size = 30, radius = 7, dot = true }) {
512
+ const d = Math.max(8, size * 0.3);
513
+ return /* @__PURE__ */ React.createElement("div", { style: { position: "relative", width: size, height: size, flexShrink: 0 } }, peer.agent ? /* @__PURE__ */ React.createElement(DkBot, { seed: peer.id, size, radius }) : /* @__PURE__ */ React.createElement(DkIdenticon, { seed: peer.userId || peer.id, size, radius }), dot && /* @__PURE__ */ React.createElement("span", { style: {
514
+ position: "absolute",
515
+ right: -2,
516
+ bottom: -2,
517
+ width: d,
518
+ height: d,
519
+ borderRadius: 999,
520
+ background: peer.online ? "var(--online)" : "var(--off)",
521
+ border: "2px solid var(--panel)",
522
+ boxSizing: "border-box"
523
+ } }));
524
+ }
525
+ function StatusDot({ online, size = 8 }) {
526
+ return /* @__PURE__ */ React.createElement("span", { style: {
527
+ width: size,
528
+ height: size,
529
+ borderRadius: 999,
530
+ flexShrink: 0,
531
+ background: online ? "var(--online)" : "var(--off)",
532
+ boxShadow: online ? "0 0 0 3px color-mix(in oklab, var(--online), transparent 80%)" : "none"
533
+ } });
534
+ }
535
+ function Mono({ children, dim, size = 12.5, copy, title, icon = "copy" }) {
536
+ const [hit, setHit] = React.useState(false);
537
+ const onCopy = (e) => {
538
+ e.stopPropagation();
539
+ dkCopy(copy === true ? String(children) : copy);
540
+ setHit(true);
541
+ setTimeout(() => setHit(false), 900);
542
+ };
543
+ return /* @__PURE__ */ React.createElement("span", { title, onClick: copy ? onCopy : void 0, style: {
544
+ fontFamily: "var(--mono)",
545
+ fontSize: size,
546
+ letterSpacing: -0.2,
547
+ color: dim ? "var(--dim)" : "var(--text)",
548
+ cursor: copy ? "pointer" : "default",
549
+ display: "inline-flex",
550
+ alignItems: "center",
551
+ gap: 5,
552
+ whiteSpace: "nowrap"
553
+ } }, children, copy && /* @__PURE__ */ React.createElement(Icon, { name: hit ? "check" : icon, size: 12, stroke: 2, color: hit ? "var(--online)" : "var(--faint)" }));
554
+ }
555
+ function shortKey(s, head = 6, tail = 5) {
556
+ if (!s || s.length <= head + tail + 1)
557
+ return s;
558
+ return s.slice(0, head) + "\u2026" + s.slice(-tail);
559
+ }
560
+ function RouteTag({ peer }) {
561
+ if (!peer.online)
562
+ return /* @__PURE__ */ React.createElement(Tag, { tone: "off" }, "offline");
563
+ const relay = peer.via === "relay";
564
+ const via = peer.via || "online";
565
+ return /* @__PURE__ */ React.createElement(Tag, { tone: relay ? "warn" : "ok" }, peer.ping != null ? `${via} \xB7 ${peer.ping}ms` : via);
566
+ }
567
+ function Tag({ children, tone = "neutral", style, title }) {
568
+ const tones = {
569
+ neutral: { bg: "var(--chip)", fg: "var(--dim)", bd: "transparent" },
570
+ ok: { bg: "color-mix(in oklab, var(--online), transparent 86%)", fg: "var(--online)", bd: "transparent" },
571
+ warn: { bg: "color-mix(in oklab, var(--warn), transparent 86%)", fg: "var(--warn)", bd: "transparent" },
572
+ off: { bg: "var(--chip)", fg: "var(--faint)", bd: "transparent" },
573
+ accent: { bg: "color-mix(in oklab, var(--accent), transparent 86%)", fg: "var(--accent)", bd: "transparent" },
574
+ danger: { bg: "color-mix(in oklab, var(--danger), transparent 88%)", fg: "var(--danger)", bd: "transparent" }
575
+ };
576
+ const t = tones[tone] || tones.neutral;
577
+ return /* @__PURE__ */ React.createElement("span", { title, style: {
578
+ display: "inline-flex",
579
+ alignItems: "center",
580
+ gap: 4,
581
+ height: 18,
582
+ padding: "0 6px",
583
+ borderRadius: 4,
584
+ background: t.bg,
585
+ color: t.fg,
586
+ border: `1px solid ${t.bd}`,
587
+ fontFamily: "var(--mono)",
588
+ fontSize: 11,
589
+ fontWeight: 600,
590
+ letterSpacing: -0.1,
591
+ lineHeight: 1,
592
+ whiteSpace: "nowrap",
593
+ ...style
594
+ } }, children);
595
+ }
596
+ function Unread({ n }) {
597
+ if (!n)
598
+ return null;
599
+ return /* @__PURE__ */ React.createElement("span", { style: {
600
+ minWidth: 18,
601
+ height: 18,
602
+ padding: "0 5px",
603
+ borderRadius: 999,
604
+ flexShrink: 0,
605
+ background: "var(--accent)",
606
+ color: "#fff",
607
+ fontFamily: "var(--mono)",
608
+ fontSize: 11,
609
+ fontWeight: 700,
610
+ lineHeight: "18px",
611
+ textAlign: "center"
612
+ } }, n);
613
+ }
614
+ function Btn({ children, icon, tone = "ghost", onClick, size = "md", title, style }) {
615
+ const tones = {
616
+ ghost: { bg: "transparent", fg: "var(--text)", bd: "var(--line)" },
617
+ solid: { bg: "var(--accent)", fg: "#fff", bd: "transparent" },
618
+ soft: { bg: "var(--chip)", fg: "var(--text)", bd: "transparent" },
619
+ ok: { bg: "color-mix(in oklab, var(--online), transparent 84%)", fg: "var(--online)", bd: "transparent" },
620
+ danger: { bg: "transparent", fg: "var(--danger)", bd: "color-mix(in oklab, var(--danger), transparent 70%)" }
621
+ };
622
+ const t = tones[tone] || tones.ghost;
623
+ const h = size === "sm" ? 26 : size === "lg" ? 36 : 30;
624
+ return /* @__PURE__ */ React.createElement("button", { onClick, title, style: {
625
+ height: h,
626
+ padding: icon && !children ? 0 : "0 11px",
627
+ minWidth: icon && !children ? h : "auto",
628
+ borderRadius: 6,
629
+ cursor: "pointer",
630
+ fontFamily: "var(--mono)",
631
+ fontWeight: 600,
632
+ fontSize: size === "sm" ? 12 : 12.5,
633
+ letterSpacing: -0.1,
634
+ background: t.bg,
635
+ color: t.fg,
636
+ border: `1px solid ${t.bd}`,
637
+ display: "inline-flex",
638
+ alignItems: "center",
639
+ justifyContent: "center",
640
+ gap: 6,
641
+ whiteSpace: "nowrap",
642
+ ...style
643
+ } }, icon && /* @__PURE__ */ React.createElement(Icon, { name: icon, size: 15, stroke: 2 }), children);
644
+ }
645
+ function Section({ label, count, trailing, style }) {
646
+ return /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8, padding: "0 2px", ...style } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, fontWeight: 700, letterSpacing: 1, textTransform: "uppercase", color: "var(--faint)" } }, label), count != null && /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, fontWeight: 700, color: "var(--faint)" } }, count), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, height: 1, background: "var(--line)" } }), trailing);
647
+ }
648
+ Object.assign(window, { DkIdenticon, DkBot, DkAvatar, StatusDot, Mono, shortKey, RouteTag, Tag, Unread, Btn, Section });
649
+ const __TWEAKS_STYLE = `
650
+ .twk-panel{position:fixed;right:16px;bottom:16px;z-index:2147483646;width:280px;
651
+ max-height:calc(100vh - 32px);display:flex;flex-direction:column;
652
+ transform:scale(var(--dc-inv-zoom,1));transform-origin:bottom right;
653
+ background:rgba(250,249,247,.78);color:#29261b;
654
+ -webkit-backdrop-filter:blur(24px) saturate(160%);backdrop-filter:blur(24px) saturate(160%);
655
+ border:.5px solid rgba(255,255,255,.6);border-radius:14px;
656
+ box-shadow:0 1px 0 rgba(255,255,255,.5) inset,0 12px 40px rgba(0,0,0,.18);
657
+ font:11.5px/1.4 ui-sans-serif,system-ui,-apple-system,sans-serif;overflow:hidden}
658
+ .twk-hd{display:flex;align-items:center;justify-content:space-between;
659
+ padding:10px 8px 10px 14px;cursor:move;user-select:none}
660
+ .twk-hd b{font-size:12px;font-weight:600;letter-spacing:.01em}
661
+ .twk-x{appearance:none;border:0;background:transparent;color:rgba(41,38,27,.55);
662
+ width:22px;height:22px;border-radius:6px;cursor:default;font-size:13px;line-height:1}
663
+ .twk-x:hover{background:rgba(0,0,0,.06);color:#29261b}
664
+ .twk-body{padding:2px 14px 14px;display:flex;flex-direction:column;gap:10px;
665
+ overflow-y:auto;overflow-x:hidden;min-height:0;
666
+ scrollbar-width:thin;scrollbar-color:rgba(0,0,0,.15) transparent}
667
+ .twk-body::-webkit-scrollbar{width:8px}
668
+ .twk-body::-webkit-scrollbar-track{background:transparent;margin:2px}
669
+ .twk-body::-webkit-scrollbar-thumb{background:rgba(0,0,0,.15);border-radius:4px;
670
+ border:2px solid transparent;background-clip:content-box}
671
+ .twk-body::-webkit-scrollbar-thumb:hover{background:rgba(0,0,0,.25);
672
+ border:2px solid transparent;background-clip:content-box}
673
+ .twk-row{display:flex;flex-direction:column;gap:5px}
674
+ .twk-row-h{flex-direction:row;align-items:center;justify-content:space-between;gap:10px}
675
+ .twk-lbl{display:flex;justify-content:space-between;align-items:baseline;
676
+ color:rgba(41,38,27,.72)}
677
+ .twk-lbl>span:first-child{font-weight:500}
678
+ .twk-val{color:rgba(41,38,27,.5);font-variant-numeric:tabular-nums}
679
+
680
+ .twk-sect{font-size:10px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;
681
+ color:rgba(41,38,27,.45);padding:10px 0 0}
682
+ .twk-sect:first-child{padding-top:0}
683
+
684
+ .twk-field{appearance:none;box-sizing:border-box;width:100%;min-width:0;height:26px;padding:0 8px;
685
+ border:.5px solid rgba(0,0,0,.1);border-radius:7px;
686
+ background:rgba(255,255,255,.6);color:inherit;font:inherit;outline:none}
687
+ .twk-field:focus{border-color:rgba(0,0,0,.25);background:rgba(255,255,255,.85)}
688
+ select.twk-field{padding-right:22px;
689
+ background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'><path fill='rgba(0,0,0,.5)' d='M0 0h10L5 6z'/></svg>");
690
+ background-repeat:no-repeat;background-position:right 8px center}
691
+
692
+ .twk-slider{appearance:none;-webkit-appearance:none;width:100%;height:4px;margin:6px 0;
693
+ border-radius:999px;background:rgba(0,0,0,.12);outline:none}
694
+ .twk-slider::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;
695
+ width:14px;height:14px;border-radius:50%;background:#fff;
696
+ border:.5px solid rgba(0,0,0,.12);box-shadow:0 1px 3px rgba(0,0,0,.2);cursor:default}
697
+ .twk-slider::-moz-range-thumb{width:14px;height:14px;border-radius:50%;
698
+ background:#fff;border:.5px solid rgba(0,0,0,.12);box-shadow:0 1px 3px rgba(0,0,0,.2);cursor:default}
699
+
700
+ .twk-seg{position:relative;display:flex;padding:2px;border-radius:8px;
701
+ background:rgba(0,0,0,.06);user-select:none}
702
+ .twk-seg-thumb{position:absolute;top:2px;bottom:2px;border-radius:6px;
703
+ background:rgba(255,255,255,.9);box-shadow:0 1px 2px rgba(0,0,0,.12);
704
+ transition:left .15s cubic-bezier(.3,.7,.4,1),width .15s}
705
+ .twk-seg.dragging .twk-seg-thumb{transition:none}
706
+ .twk-seg button{appearance:none;position:relative;z-index:1;flex:1;border:0;
707
+ background:transparent;color:inherit;font:inherit;font-weight:500;min-height:22px;
708
+ border-radius:6px;cursor:default;padding:4px 6px;line-height:1.2;
709
+ overflow-wrap:anywhere}
710
+
711
+ .twk-toggle{position:relative;width:32px;height:18px;border:0;border-radius:999px;
712
+ background:rgba(0,0,0,.15);transition:background .15s;cursor:default;padding:0}
713
+ .twk-toggle[data-on="1"]{background:#34c759}
714
+ .twk-toggle i{position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%;
715
+ background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.25);transition:transform .15s}
716
+ .twk-toggle[data-on="1"] i{transform:translateX(14px)}
717
+
718
+ .twk-num{display:flex;align-items:center;box-sizing:border-box;min-width:0;height:26px;padding:0 0 0 8px;
719
+ border:.5px solid rgba(0,0,0,.1);border-radius:7px;background:rgba(255,255,255,.6)}
720
+ .twk-num-lbl{font-weight:500;color:rgba(41,38,27,.6);cursor:ew-resize;
721
+ user-select:none;padding-right:8px}
722
+ .twk-num input{flex:1;min-width:0;height:100%;border:0;background:transparent;
723
+ font:inherit;font-variant-numeric:tabular-nums;text-align:right;padding:0 8px 0 0;
724
+ outline:none;color:inherit;-moz-appearance:textfield}
725
+ .twk-num input::-webkit-inner-spin-button,.twk-num input::-webkit-outer-spin-button{
726
+ -webkit-appearance:none;margin:0}
727
+ .twk-num-unit{padding-right:8px;color:rgba(41,38,27,.45)}
728
+
729
+ .twk-btn{appearance:none;height:26px;padding:0 12px;border:0;border-radius:7px;
730
+ background:rgba(0,0,0,.78);color:#fff;font:inherit;font-weight:500;cursor:default}
731
+ .twk-btn:hover{background:rgba(0,0,0,.88)}
732
+ .twk-btn.secondary{background:rgba(0,0,0,.06);color:inherit}
733
+ .twk-btn.secondary:hover{background:rgba(0,0,0,.1)}
734
+
735
+ .twk-swatch{appearance:none;-webkit-appearance:none;width:56px;height:22px;
736
+ border:.5px solid rgba(0,0,0,.1);border-radius:6px;padding:0;cursor:default;
737
+ background:transparent;flex-shrink:0}
738
+ .twk-swatch::-webkit-color-swatch-wrapper{padding:0}
739
+ .twk-swatch::-webkit-color-swatch{border:0;border-radius:5.5px}
740
+ .twk-swatch::-moz-color-swatch{border:0;border-radius:5.5px}
741
+
742
+ .twk-chips{display:flex;gap:6px}
743
+ .twk-chip{position:relative;appearance:none;flex:1;min-width:0;height:46px;
744
+ padding:0;border:0;border-radius:6px;overflow:hidden;cursor:default;
745
+ box-shadow:0 0 0 .5px rgba(0,0,0,.12),0 1px 2px rgba(0,0,0,.06);
746
+ transition:transform .12s cubic-bezier(.3,.7,.4,1),box-shadow .12s}
747
+ .twk-chip:hover{transform:translateY(-1px);
748
+ box-shadow:0 0 0 .5px rgba(0,0,0,.18),0 4px 10px rgba(0,0,0,.12)}
749
+ .twk-chip[data-on="1"]{box-shadow:0 0 0 1.5px rgba(0,0,0,.85),
750
+ 0 2px 6px rgba(0,0,0,.15)}
751
+ .twk-chip>span{position:absolute;top:0;bottom:0;right:0;width:34%;
752
+ display:flex;flex-direction:column;box-shadow:-1px 0 0 rgba(0,0,0,.1)}
753
+ .twk-chip>span>i{flex:1;box-shadow:0 -1px 0 rgba(0,0,0,.1)}
754
+ .twk-chip>span>i:first-child{box-shadow:none}
755
+ .twk-chip svg{position:absolute;top:6px;left:6px;width:13px;height:13px;
756
+ filter:drop-shadow(0 1px 1px rgba(0,0,0,.3))}
757
+ `;
758
+ const TWEAKS_STORAGE_KEY = "decentlan.tweaks";
759
+ function useTweaks(defaults) {
760
+ const [values, setValues] = React.useState(() => {
761
+ try {
762
+ const saved = JSON.parse(localStorage.getItem(TWEAKS_STORAGE_KEY) || "{}");
763
+ return { ...defaults, ...saved };
764
+ } catch (e) {
765
+ return defaults;
766
+ }
767
+ });
768
+ const setTweak = React.useCallback((keyOrEdits, val) => {
769
+ const edits = typeof keyOrEdits === "object" && keyOrEdits !== null ? keyOrEdits : { [keyOrEdits]: val };
770
+ setValues((prev) => {
771
+ const next = { ...prev, ...edits };
772
+ try {
773
+ localStorage.setItem(TWEAKS_STORAGE_KEY, JSON.stringify(next));
774
+ } catch (e) {
775
+ }
776
+ return next;
777
+ });
778
+ try {
779
+ window.dispatchEvent(new CustomEvent("tweakchange", { detail: edits }));
780
+ } catch (e) {
781
+ }
782
+ }, []);
783
+ return [values, setTweak];
784
+ }
785
+ function TweaksPanel({ title = "Tweaks", children }) {
786
+ const [open, setOpen] = React.useState(false);
787
+ const dragRef = React.useRef(null);
788
+ const offsetRef = React.useRef({ x: 16, y: 16 });
789
+ const PAD = 16;
790
+ const clampToViewport = React.useCallback(() => {
791
+ const panel = dragRef.current;
792
+ if (!panel)
793
+ return;
794
+ const w = panel.offsetWidth, h = panel.offsetHeight;
795
+ const maxRight = Math.max(PAD, window.innerWidth - w - PAD);
796
+ const maxBottom = Math.max(PAD, window.innerHeight - h - PAD);
797
+ offsetRef.current = {
798
+ x: Math.min(maxRight, Math.max(PAD, offsetRef.current.x)),
799
+ y: Math.min(maxBottom, Math.max(PAD, offsetRef.current.y))
800
+ };
801
+ panel.style.right = offsetRef.current.x + "px";
802
+ panel.style.bottom = offsetRef.current.y + "px";
803
+ }, []);
804
+ React.useEffect(() => {
805
+ if (!open)
806
+ return;
807
+ clampToViewport();
808
+ if (typeof ResizeObserver === "undefined") {
809
+ window.addEventListener("resize", clampToViewport);
810
+ return () => window.removeEventListener("resize", clampToViewport);
811
+ }
812
+ const ro = new ResizeObserver(clampToViewport);
813
+ ro.observe(document.documentElement);
814
+ return () => ro.disconnect();
815
+ }, [open, clampToViewport]);
816
+ React.useEffect(() => {
817
+ const onMsg = (e) => {
818
+ var _a;
819
+ const t = (_a = e == null ? void 0 : e.data) == null ? void 0 : _a.type;
820
+ if (t === "__activate_edit_mode")
821
+ setOpen(true);
822
+ else if (t === "__deactivate_edit_mode")
823
+ setOpen(false);
824
+ };
825
+ window.addEventListener("message", onMsg);
826
+ window.parent.postMessage({ type: "__edit_mode_available" }, "*");
827
+ return () => window.removeEventListener("message", onMsg);
828
+ }, []);
829
+ const dismiss = () => {
830
+ setOpen(false);
831
+ window.parent.postMessage({ type: "__edit_mode_dismissed" }, "*");
832
+ };
833
+ const onDragStart = (e) => {
834
+ const panel = dragRef.current;
835
+ if (!panel)
836
+ return;
837
+ const r = panel.getBoundingClientRect();
838
+ const sx = e.clientX, sy = e.clientY;
839
+ const startRight = window.innerWidth - r.right;
840
+ const startBottom = window.innerHeight - r.bottom;
841
+ const move = (ev) => {
842
+ offsetRef.current = {
843
+ x: startRight - (ev.clientX - sx),
844
+ y: startBottom - (ev.clientY - sy)
845
+ };
846
+ clampToViewport();
847
+ };
848
+ const up = () => {
849
+ window.removeEventListener("mousemove", move);
850
+ window.removeEventListener("mouseup", up);
851
+ };
852
+ window.addEventListener("mousemove", move);
853
+ window.addEventListener("mouseup", up);
854
+ };
855
+ if (!open)
856
+ return /* @__PURE__ */ React.createElement("button", { onClick: () => setOpen(true), title, "aria-label": title, style: {
857
+ position: "fixed",
858
+ right: 16,
859
+ bottom: 16,
860
+ zIndex: 2147483646,
861
+ width: 40,
862
+ height: 40,
863
+ borderRadius: 11,
864
+ cursor: "pointer",
865
+ background: "var(--panel-2)",
866
+ border: "1px solid var(--line)",
867
+ color: "var(--dim)",
868
+ display: "flex",
869
+ alignItems: "center",
870
+ justifyContent: "center",
871
+ boxShadow: "0 6px 20px rgba(0,0,0,0.28)"
872
+ } }, /* @__PURE__ */ React.createElement(Icon, { name: "sliders", size: 18, stroke: 1.9 }));
873
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("style", null, __TWEAKS_STYLE), /* @__PURE__ */ React.createElement(
874
+ "div",
875
+ {
876
+ ref: dragRef,
877
+ className: "twk-panel",
878
+ "data-omelette-chrome": "",
879
+ style: { right: offsetRef.current.x, bottom: offsetRef.current.y }
880
+ },
881
+ /* @__PURE__ */ React.createElement("div", { className: "twk-hd", onMouseDown: onDragStart }, /* @__PURE__ */ React.createElement("b", null, title), /* @__PURE__ */ React.createElement(
882
+ "button",
883
+ {
884
+ className: "twk-x",
885
+ "aria-label": "Close tweaks",
886
+ onMouseDown: (e) => e.stopPropagation(),
887
+ onClick: dismiss
888
+ },
889
+ "\u2715"
890
+ )),
891
+ /* @__PURE__ */ React.createElement("div", { className: "twk-body" }, children)
892
+ ));
893
+ }
894
+ function TweakSection({ label, children }) {
895
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { className: "twk-sect" }, label), children);
896
+ }
897
+ function TweakRow({ label, value, children, inline = false }) {
898
+ return /* @__PURE__ */ React.createElement("div", { className: inline ? "twk-row twk-row-h" : "twk-row" }, /* @__PURE__ */ React.createElement("div", { className: "twk-lbl" }, /* @__PURE__ */ React.createElement("span", null, label), value != null && /* @__PURE__ */ React.createElement("span", { className: "twk-val" }, value)), children);
899
+ }
900
+ function TweakSlider({ label, value, min = 0, max = 100, step = 1, unit = "", onChange }) {
901
+ return /* @__PURE__ */ React.createElement(TweakRow, { label, value: `${value}${unit}` }, /* @__PURE__ */ React.createElement(
902
+ "input",
903
+ {
904
+ type: "range",
905
+ className: "twk-slider",
906
+ min,
907
+ max,
908
+ step,
909
+ value,
910
+ onChange: (e) => onChange(Number(e.target.value))
911
+ }
912
+ ));
913
+ }
914
+ function TweakToggle({ label, value, onChange }) {
915
+ return /* @__PURE__ */ React.createElement("div", { className: "twk-row twk-row-h" }, /* @__PURE__ */ React.createElement("div", { className: "twk-lbl" }, /* @__PURE__ */ React.createElement("span", null, label)), /* @__PURE__ */ React.createElement(
916
+ "button",
917
+ {
918
+ type: "button",
919
+ className: "twk-toggle",
920
+ "data-on": value ? "1" : "0",
921
+ role: "switch",
922
+ "aria-checked": !!value,
923
+ onClick: () => onChange(!value)
924
+ },
925
+ /* @__PURE__ */ React.createElement("i", null)
926
+ ));
927
+ }
928
+ function TweakRadio({ label, value, options, onChange }) {
929
+ var _a;
930
+ const trackRef = React.useRef(null);
931
+ const [dragging, setDragging] = React.useState(false);
932
+ const valueRef = React.useRef(value);
933
+ valueRef.current = value;
934
+ const labelLen = (o) => String(typeof o === "object" ? o.label : o).length;
935
+ const maxLen = options.reduce((m, o) => Math.max(m, labelLen(o)), 0);
936
+ const fitsAsSegments = maxLen <= ((_a = { 2: 16, 3: 10 }[options.length]) != null ? _a : 0);
937
+ if (!fitsAsSegments) {
938
+ const resolve = (s) => {
939
+ const m = options.find((o) => String(typeof o === "object" ? o.value : o) === s);
940
+ return m === void 0 ? s : typeof m === "object" ? m.value : m;
941
+ };
942
+ return /* @__PURE__ */ React.createElement(
943
+ TweakSelect,
944
+ {
945
+ label,
946
+ value,
947
+ options,
948
+ onChange: (s) => onChange(resolve(s))
949
+ }
950
+ );
951
+ }
952
+ const opts = options.map((o) => typeof o === "object" ? o : { value: o, label: o });
953
+ const idx = Math.max(0, opts.findIndex((o) => o.value === value));
954
+ const n = opts.length;
955
+ const segAt = (clientX) => {
956
+ const r = trackRef.current.getBoundingClientRect();
957
+ const inner = r.width - 4;
958
+ const i = Math.floor((clientX - r.left - 2) / inner * n);
959
+ return opts[Math.max(0, Math.min(n - 1, i))].value;
960
+ };
961
+ const onPointerDown = (e) => {
962
+ setDragging(true);
963
+ const v0 = segAt(e.clientX);
964
+ if (v0 !== valueRef.current)
965
+ onChange(v0);
966
+ const move = (ev) => {
967
+ if (!trackRef.current)
968
+ return;
969
+ const v = segAt(ev.clientX);
970
+ if (v !== valueRef.current)
971
+ onChange(v);
972
+ };
973
+ const up = () => {
974
+ setDragging(false);
975
+ window.removeEventListener("pointermove", move);
976
+ window.removeEventListener("pointerup", up);
977
+ };
978
+ window.addEventListener("pointermove", move);
979
+ window.addEventListener("pointerup", up);
980
+ };
981
+ return /* @__PURE__ */ React.createElement(TweakRow, { label }, /* @__PURE__ */ React.createElement(
982
+ "div",
983
+ {
984
+ ref: trackRef,
985
+ role: "radiogroup",
986
+ onPointerDown,
987
+ className: dragging ? "twk-seg dragging" : "twk-seg"
988
+ },
989
+ /* @__PURE__ */ React.createElement(
990
+ "div",
991
+ {
992
+ className: "twk-seg-thumb",
993
+ style: {
994
+ left: `calc(2px + ${idx} * (100% - 4px) / ${n})`,
995
+ width: `calc((100% - 4px) / ${n})`
996
+ }
997
+ }
998
+ ),
999
+ opts.map((o) => /* @__PURE__ */ React.createElement("button", { key: o.value, type: "button", role: "radio", "aria-checked": o.value === value }, o.label))
1000
+ ));
1001
+ }
1002
+ function TweakSelect({ label, value, options, onChange }) {
1003
+ return /* @__PURE__ */ React.createElement(TweakRow, { label }, /* @__PURE__ */ React.createElement("select", { className: "twk-field", value, onChange: (e) => onChange(e.target.value) }, options.map((o) => {
1004
+ const v = typeof o === "object" ? o.value : o;
1005
+ const l = typeof o === "object" ? o.label : o;
1006
+ return /* @__PURE__ */ React.createElement("option", { key: v, value: v }, l);
1007
+ })));
1008
+ }
1009
+ function TweakText({ label, value, placeholder, onChange }) {
1010
+ return /* @__PURE__ */ React.createElement(TweakRow, { label }, /* @__PURE__ */ React.createElement(
1011
+ "input",
1012
+ {
1013
+ className: "twk-field",
1014
+ type: "text",
1015
+ value,
1016
+ placeholder,
1017
+ onChange: (e) => onChange(e.target.value)
1018
+ }
1019
+ ));
1020
+ }
1021
+ function TweakNumber({ label, value, min, max, step = 1, unit = "", onChange }) {
1022
+ const clamp = (n) => {
1023
+ if (min != null && n < min)
1024
+ return min;
1025
+ if (max != null && n > max)
1026
+ return max;
1027
+ return n;
1028
+ };
1029
+ const startRef = React.useRef({ x: 0, val: 0 });
1030
+ const onScrubStart = (e) => {
1031
+ e.preventDefault();
1032
+ startRef.current = { x: e.clientX, val: value };
1033
+ const decimals = (String(step).split(".")[1] || "").length;
1034
+ const move = (ev) => {
1035
+ const dx = ev.clientX - startRef.current.x;
1036
+ const raw = startRef.current.val + dx * step;
1037
+ const snapped = Math.round(raw / step) * step;
1038
+ onChange(clamp(Number(snapped.toFixed(decimals))));
1039
+ };
1040
+ const up = () => {
1041
+ window.removeEventListener("pointermove", move);
1042
+ window.removeEventListener("pointerup", up);
1043
+ };
1044
+ window.addEventListener("pointermove", move);
1045
+ window.addEventListener("pointerup", up);
1046
+ };
1047
+ return /* @__PURE__ */ React.createElement("div", { className: "twk-num" }, /* @__PURE__ */ React.createElement("span", { className: "twk-num-lbl", onPointerDown: onScrubStart }, label), /* @__PURE__ */ React.createElement(
1048
+ "input",
1049
+ {
1050
+ type: "number",
1051
+ value,
1052
+ min,
1053
+ max,
1054
+ step,
1055
+ onChange: (e) => onChange(clamp(Number(e.target.value)))
1056
+ }
1057
+ ), unit && /* @__PURE__ */ React.createElement("span", { className: "twk-num-unit" }, unit));
1058
+ }
1059
+ function __twkIsLight(hex) {
1060
+ const h = String(hex).replace("#", "");
1061
+ const x = h.length === 3 ? h.replace(/./g, (c) => c + c) : h.padEnd(6, "0");
1062
+ const n = parseInt(x.slice(0, 6), 16);
1063
+ if (Number.isNaN(n))
1064
+ return true;
1065
+ const r = n >> 16 & 255, g = n >> 8 & 255, b = n & 255;
1066
+ return r * 299 + g * 587 + b * 114 > 148e3;
1067
+ }
1068
+ const __TwkCheck = ({ light }) => /* @__PURE__ */ React.createElement("svg", { viewBox: "0 0 14 14", "aria-hidden": "true" }, /* @__PURE__ */ React.createElement(
1069
+ "path",
1070
+ {
1071
+ d: "M3 7.2 5.8 10 11 4.2",
1072
+ fill: "none",
1073
+ strokeWidth: "2.2",
1074
+ strokeLinecap: "round",
1075
+ strokeLinejoin: "round",
1076
+ stroke: light ? "rgba(0,0,0,.78)" : "#fff"
1077
+ }
1078
+ ));
1079
+ function TweakColor({ label, value, options, onChange }) {
1080
+ if (!options || !options.length) {
1081
+ return /* @__PURE__ */ React.createElement("div", { className: "twk-row twk-row-h" }, /* @__PURE__ */ React.createElement("div", { className: "twk-lbl" }, /* @__PURE__ */ React.createElement("span", null, label)), /* @__PURE__ */ React.createElement(
1082
+ "input",
1083
+ {
1084
+ type: "color",
1085
+ className: "twk-swatch",
1086
+ value,
1087
+ onChange: (e) => onChange(e.target.value)
1088
+ }
1089
+ ));
1090
+ }
1091
+ const key = (o) => String(JSON.stringify(o)).toLowerCase();
1092
+ const cur = key(value);
1093
+ return /* @__PURE__ */ React.createElement(TweakRow, { label }, /* @__PURE__ */ React.createElement("div", { className: "twk-chips", role: "radiogroup" }, options.map((o, i) => {
1094
+ const colors = Array.isArray(o) ? o : [o];
1095
+ const [hero, ...rest] = colors;
1096
+ const sup = rest.slice(0, 4);
1097
+ const on = key(o) === cur;
1098
+ return /* @__PURE__ */ React.createElement(
1099
+ "button",
1100
+ {
1101
+ key: i,
1102
+ type: "button",
1103
+ className: "twk-chip",
1104
+ role: "radio",
1105
+ "aria-checked": on,
1106
+ "data-on": on ? "1" : "0",
1107
+ "aria-label": colors.join(", "),
1108
+ title: colors.join(" \xB7 "),
1109
+ style: { background: hero },
1110
+ onClick: () => onChange(o)
1111
+ },
1112
+ sup.length > 0 && /* @__PURE__ */ React.createElement("span", null, sup.map((c, j) => /* @__PURE__ */ React.createElement("i", { key: j, style: { background: c } }))),
1113
+ on && /* @__PURE__ */ React.createElement(__TwkCheck, { light: __twkIsLight(hero) })
1114
+ );
1115
+ })));
1116
+ }
1117
+ function TweakButton({ label, onClick, secondary = false }) {
1118
+ return /* @__PURE__ */ React.createElement(
1119
+ "button",
1120
+ {
1121
+ type: "button",
1122
+ className: secondary ? "twk-btn secondary" : "twk-btn",
1123
+ onClick
1124
+ },
1125
+ label
1126
+ );
1127
+ }
1128
+ Object.assign(window, {
1129
+ useTweaks,
1130
+ TweaksPanel,
1131
+ TweakSection,
1132
+ TweakRow,
1133
+ TweakSlider,
1134
+ TweakToggle,
1135
+ TweakRadio,
1136
+ TweakSelect,
1137
+ TweakText,
1138
+ TweakNumber,
1139
+ TweakColor,
1140
+ TweakButton
1141
+ });
1142
+ function RequestsBlock({ T, requests, onAct }) {
1143
+ if (!requests.length)
1144
+ return null;
1145
+ return /* @__PURE__ */ React.createElement("div", { style: { padding: "4px 0 8px" } }, /* @__PURE__ */ React.createElement(Section, { label: T.requests, count: requests.length, style: { margin: "4px 4px 8px" } }), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 6 } }, requests.map((r) => /* @__PURE__ */ React.createElement("div", { key: r.id, style: {
1146
+ padding: "9px 10px",
1147
+ borderRadius: 8,
1148
+ background: "var(--panel-2)",
1149
+ border: "1px solid var(--line)",
1150
+ display: "flex",
1151
+ flexDirection: "column",
1152
+ gap: 8
1153
+ } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8 } }, /* @__PURE__ */ React.createElement(DkIdenticon, { seed: r.carrier, size: 26, radius: 6 }), /* @__PURE__ */ React.createElement("div", { style: { minWidth: 0, flex: 1 } }, /* @__PURE__ */ React.createElement(Mono, { size: 12, copy: r.carrier, title: r.carrier }, shortKey(r.carrier, 8, 6)), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 10.5, color: "var(--faint)", marginTop: 2 } }, "via ", r.via, " \xB7 ", r.time))), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: 6 } }, /* @__PURE__ */ React.createElement(Btn, { tone: "ok", icon: "check", size: "sm", onClick: () => onAct(r.id, "accept"), style: { flex: 1 } }, T.accept), /* @__PURE__ */ React.createElement(Btn, { tone: "danger", icon: "x", size: "sm", onClick: () => onAct(r.id, "reject"), style: { flex: 1 } }, T.reject))))));
1154
+ }
1155
+ function PeerRow({ peer, T, active, onClick }) {
1156
+ const name = peer.alias || peer.userId;
1157
+ return /* @__PURE__ */ React.createElement("button", { onClick, style: {
1158
+ display: "flex",
1159
+ alignItems: "center",
1160
+ gap: 10,
1161
+ width: "100%",
1162
+ textAlign: "left",
1163
+ padding: "var(--row-pad)",
1164
+ borderRadius: 8,
1165
+ cursor: "pointer",
1166
+ fontFamily: "inherit",
1167
+ border: "1px solid " + (active ? "var(--line)" : "transparent"),
1168
+ background: active ? "var(--panel-2)" : "transparent"
1169
+ } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer, size: 32, radius: 7 }), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0 } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 6 } }, /* @__PURE__ */ React.createElement("span", { style: {
1170
+ fontFamily: peer.alias ? "var(--ui)" : "var(--mono)",
1171
+ fontSize: 13.5,
1172
+ fontWeight: 600,
1173
+ color: "var(--text)",
1174
+ whiteSpace: "nowrap",
1175
+ overflow: "hidden",
1176
+ textOverflow: "ellipsis"
1177
+ } }, peer.alias || shortKey(peer.userId, 8, 5)), peer.agent && /* @__PURE__ */ React.createElement(Icon, { name: "bot", size: 13, color: "var(--faint)", stroke: 2 }), peer.wire === "64" && /* @__PURE__ */ React.createElement(Tag, { tone: "off", style: { height: 15, fontSize: 9.5, padding: "0 4px" } }, "w64")), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 6, marginTop: 2 } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, color: "var(--faint)" } }, peer.ip), /* @__PURE__ */ React.createElement("span", { style: { color: "var(--line)", fontSize: 11 } }, "\xB7"), /* @__PURE__ */ React.createElement("span", { style: {
1178
+ fontSize: 11,
1179
+ color: "var(--dim)",
1180
+ whiteSpace: "nowrap",
1181
+ overflow: "hidden",
1182
+ textOverflow: "ellipsis",
1183
+ flex: 1,
1184
+ minWidth: 0
1185
+ } }, peer.lastMsg))), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", flexDirection: "column", alignItems: "flex-end", gap: 4, flexShrink: 0 } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10.5, color: "var(--faint)" } }, peer.lastTime), peer.unread ? /* @__PURE__ */ React.createElement(Unread, { n: peer.unread }) : /* @__PURE__ */ React.createElement(StatusDot, { online: peer.online })));
1186
+ }
1187
+ function PeerSidebar({ T, peers, requests, activeId, onSelect, onAct, onAdd, prefillAddr, onPrefillConsumed }) {
1188
+ const [q, setQ] = React.useState("");
1189
+ const [addr, setAddr] = React.useState("");
1190
+ const [addState, setAddState] = React.useState(null);
1191
+ const [fromDeepLink, setFromDeepLink] = React.useState(false);
1192
+ React.useEffect(() => {
1193
+ if (prefillAddr) {
1194
+ setAddr(prefillAddr);
1195
+ setFromDeepLink(true);
1196
+ }
1197
+ }, [prefillAddr]);
1198
+ const submitAddr = () => {
1199
+ const v = addr.trim();
1200
+ if (!v || addState && addState.kind === "sending")
1201
+ return;
1202
+ setAddState({ kind: "sending", msg: T.addSending });
1203
+ Promise.resolve(onAdd(v)).then((r) => {
1204
+ if (r && r.ok) {
1205
+ setAddr("");
1206
+ setFromDeepLink(false);
1207
+ onPrefillConsumed && onPrefillConsumed();
1208
+ setAddState({ kind: "ok", msg: T.addSent });
1209
+ } else {
1210
+ setAddState({ kind: "err", msg: r && r.error ? `${T.addFailed}: ${r.error}` : T.addFailed });
1211
+ }
1212
+ }).catch((e) => {
1213
+ setAddState({ kind: "err", msg: `${T.addFailed}: ${e && e.message || e}` });
1214
+ }).finally(() => {
1215
+ setTimeout(() => setAddState((s) => s && s.kind !== "sending" ? null : s), 6e3);
1216
+ });
1217
+ };
1218
+ const filtered = peers.filter((p) => {
1219
+ if (!q)
1220
+ return true;
1221
+ const s = (p.alias || "") + " " + p.userId + " " + p.ip;
1222
+ return s.toLowerCase().includes(q.toLowerCase());
1223
+ });
1224
+ const online = peers.filter((p) => p.online).length;
1225
+ return /* @__PURE__ */ React.createElement("div", { style: { width: 320, flexShrink: 0, borderRight: "1px solid var(--line)", display: "flex", flexDirection: "column", background: "var(--panel)" } }, /* @__PURE__ */ React.createElement("div", { style: { padding: "12px 12px 10px", borderBottom: "1px solid var(--line)", display: "flex", flexDirection: "column", gap: 8 } }, fromDeepLink && /* @__PURE__ */ React.createElement("div", { style: {
1226
+ fontFamily: "var(--ui)",
1227
+ fontSize: 12,
1228
+ lineHeight: 1.4,
1229
+ color: "var(--text)",
1230
+ background: "color-mix(in oklab, var(--accent), transparent 88%)",
1231
+ border: "1px solid var(--accent)",
1232
+ borderRadius: 8,
1233
+ padding: "7px 10px"
1234
+ } }, T.deepLinkConfirm || "\u901A\u8FC7\u94FE\u63A5\u6DFB\u52A0\u8FD9\u4F4D\u597D\u53CB? \u70B9\u300C\u6DFB\u52A0\u300D\u53D1\u9001\u597D\u53CB\u8BF7\u6C42 / Add this contact from the link? Tap Add to send a friend request."), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: 7 } }, /* @__PURE__ */ React.createElement(
1235
+ "input",
1236
+ {
1237
+ value: addr,
1238
+ onChange: (e) => {
1239
+ setAddr(e.target.value);
1240
+ if (fromDeepLink)
1241
+ setFromDeepLink(false);
1242
+ },
1243
+ onKeyDown: (e) => {
1244
+ if (e.key === "Enter")
1245
+ submitAddr();
1246
+ },
1247
+ placeholder: T.addPlaceholder,
1248
+ style: { ...inputStyle, ...fromDeepLink ? { borderColor: "var(--accent)" } : null }
1249
+ }
1250
+ ), /* @__PURE__ */ React.createElement(Btn, { tone: "solid", icon: "userPlus", onClick: submitAddr }, T.add)), addState && /* @__PURE__ */ React.createElement("div", { style: {
1251
+ fontFamily: "var(--mono)",
1252
+ fontSize: 11.5,
1253
+ lineHeight: 1.35,
1254
+ color: addState.kind === "err" ? "var(--bad, #e5484d)" : addState.kind === "ok" ? "var(--good, #46a758)" : "var(--faint)"
1255
+ } }, addState.msg), /* @__PURE__ */ React.createElement("div", { style: { position: "relative", display: "flex", alignItems: "center" } }, /* @__PURE__ */ React.createElement(Icon, { name: "search", size: 15, color: "var(--faint)", stroke: 2, style: { position: "absolute", left: 9 } }), /* @__PURE__ */ React.createElement("input", { value: q, onChange: (e) => setQ(e.target.value), placeholder: T.search, style: { ...inputStyle, paddingLeft: 30 } }))), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, overflow: "auto", padding: "10px 8px 16px" } }, /* @__PURE__ */ React.createElement(RequestsBlock, { T, requests, onAct }), /* @__PURE__ */ React.createElement(Section, { label: T.peers, count: `${online}/${peers.length}`, style: { margin: "6px 4px 8px" } }), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 2 } }, filtered.map((p) => /* @__PURE__ */ React.createElement(PeerRow, { key: p.id, peer: p, T, active: p.id === activeId, onClick: () => onSelect(p.id) })), !filtered.length && /* @__PURE__ */ React.createElement("div", { style: { padding: 16, textAlign: "center", fontFamily: "var(--mono)", fontSize: 12, color: "var(--faint)" } }, "no matches"))));
1256
+ }
1257
+ const inputStyle = {
1258
+ flex: 1,
1259
+ height: 32,
1260
+ borderRadius: 7,
1261
+ border: "1px solid var(--line)",
1262
+ background: "var(--panel-2)",
1263
+ color: "var(--text)",
1264
+ fontFamily: "var(--mono)",
1265
+ fontSize: 12.5,
1266
+ padding: "0 10px",
1267
+ outline: "none",
1268
+ minWidth: 0
1269
+ };
1270
+ const mediaBtnStyle = {
1271
+ width: 28,
1272
+ height: 28,
1273
+ borderRadius: 8,
1274
+ border: "none",
1275
+ background: "rgba(0,0,0,0.5)",
1276
+ cursor: "pointer",
1277
+ display: "flex",
1278
+ alignItems: "center",
1279
+ justifyContent: "center",
1280
+ lineHeight: 0
1281
+ };
1282
+ function dkRtcFileFromText(peerId, text) {
1283
+ const m = /^received file via WebRTC: (.+) \(([^)]+)\)$/.exec(String(text || ""));
1284
+ if (!m || !window.__dkRtcFiles)
1285
+ return null;
1286
+ const name = m[1];
1287
+ for (const item of window.__dkRtcFiles.values()) {
1288
+ if (item && item.peerId === peerId && item.name === name)
1289
+ return item;
1290
+ }
1291
+ return null;
1292
+ }
1293
+ function dkCallFromText(text) {
1294
+ const m = /^WebRTC (audio|video) call: (incoming|outgoing)$/.exec(String(text || ""));
1295
+ return m ? { kind: m[1], direction: m[2] } : null;
1296
+ }
1297
+ function dkHtmlEscape(s) {
1298
+ return String(s == null ? "" : s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1299
+ }
1300
+ function dkSafeHref(s) {
1301
+ const href = String(s || "").trim();
1302
+ if (!href)
1303
+ return "";
1304
+ try {
1305
+ const u = new URL(href, window.location.href);
1306
+ if (u.protocol === "http:" || u.protocol === "https:" || u.protocol === "mailto:")
1307
+ return u.href;
1308
+ } catch (e) {
1309
+ }
1310
+ return "";
1311
+ }
1312
+ function dkInlineMarkdown(s) {
1313
+ const text = String(s == null ? "" : s);
1314
+ const tokenRe = /(`[^`\n]+`|\[[^\]\n]+\]\([^) \n]+(?: [^)]+)?\)|\*\*[^*\n]+?\*\*|\*[^*\n]+?\*)/g;
1315
+ let out = "";
1316
+ let last = 0;
1317
+ let m;
1318
+ while (m = tokenRe.exec(text)) {
1319
+ out += dkHtmlEscape(text.slice(last, m.index));
1320
+ const tok = m[0];
1321
+ if (tok[0] === "`") {
1322
+ out += "<code>" + dkHtmlEscape(tok.slice(1, -1)) + "</code>";
1323
+ } else if (tok.startsWith("[")) {
1324
+ const lm = /^\[([^\]\n]+)\]\(([^) \n]+)(?: [^)]+)?\)$/.exec(tok);
1325
+ const href = lm && dkSafeHref(lm[2]);
1326
+ out += href ? '<a href="' + dkHtmlEscape(href) + '" target="_blank" rel="noopener">' + dkHtmlEscape(lm[1]) + "</a>" : dkHtmlEscape(tok);
1327
+ } else if (tok.startsWith("**")) {
1328
+ out += "<strong>" + dkHtmlEscape(tok.slice(2, -2)) + "</strong>";
1329
+ } else if (tok.startsWith("*")) {
1330
+ out += "<em>" + dkHtmlEscape(tok.slice(1, -1)) + "</em>";
1331
+ } else {
1332
+ out += dkHtmlEscape(tok);
1333
+ }
1334
+ last = tokenRe.lastIndex;
1335
+ }
1336
+ out += dkHtmlEscape(text.slice(last));
1337
+ return out;
1338
+ }
1339
+ function dkMarkdownHtml(src) {
1340
+ const lines = String(src == null ? "" : src).replace(/\r\n?/g, "\n").split("\n");
1341
+ let html = "";
1342
+ let i = 0;
1343
+ const isBlockStart = (line) => /^```/.test(line) || /^#{1,6}\s+/.test(line) || /^\s*[-*]\s+/.test(line) || /^\s*\d+\.\s+/.test(line);
1344
+ while (i < lines.length) {
1345
+ const line = lines[i];
1346
+ if (!line.trim()) {
1347
+ i += 1;
1348
+ continue;
1349
+ }
1350
+ if (/^```/.test(line)) {
1351
+ i += 1;
1352
+ const code = [];
1353
+ while (i < lines.length && !/^```/.test(lines[i])) {
1354
+ code.push(lines[i]);
1355
+ i += 1;
1356
+ }
1357
+ if (i < lines.length)
1358
+ i += 1;
1359
+ html += "<pre><code>" + dkHtmlEscape(code.join("\n")) + "</code></pre>";
1360
+ continue;
1361
+ }
1362
+ const hm = /^(#{1,6})\s+(.+)$/.exec(line);
1363
+ if (hm) {
1364
+ const level = Math.min(6, hm[1].length);
1365
+ html += "<h" + level + ">" + dkInlineMarkdown(hm[2]) + "</h" + level + ">";
1366
+ i += 1;
1367
+ continue;
1368
+ }
1369
+ if (/^\s*[-*]\s+/.test(line) || /^\s*\d+\.\s+/.test(line)) {
1370
+ const ordered = /^\s*\d+\.\s+/.test(line);
1371
+ html += ordered ? "<ol>" : "<ul>";
1372
+ while (i < lines.length && (ordered ? /^\s*\d+\.\s+/.test(lines[i]) : /^\s*[-*]\s+/.test(lines[i]))) {
1373
+ html += "<li>" + dkInlineMarkdown(lines[i].replace(ordered ? /^\s*\d+\.\s+/ : /^\s*[-*]\s+/, "")) + "</li>";
1374
+ i += 1;
1375
+ }
1376
+ html += ordered ? "</ol>" : "</ul>";
1377
+ continue;
1378
+ }
1379
+ const para = [line];
1380
+ i += 1;
1381
+ while (i < lines.length && lines[i].trim() && !isBlockStart(lines[i])) {
1382
+ para.push(lines[i]);
1383
+ i += 1;
1384
+ }
1385
+ html += "<p>" + para.map(dkInlineMarkdown).join("<br>") + "</p>";
1386
+ }
1387
+ return html || dkHtmlEscape(src);
1388
+ }
1389
+ function MarkdownText({ text }) {
1390
+ return /* @__PURE__ */ React.createElement("div", { className: "dk-md", dangerouslySetInnerHTML: { __html: dkMarkdownHtml(text) } });
1391
+ }
1392
+ function Msg({ m, peer, T, onTheater, onDelete, onCancel, onRetry, onReveal, onCall, selMode, selected, onToggleSel, busy }) {
1393
+ const mine = m.from === "me";
1394
+ const rtcFile = !mine && !m.file ? dkRtcFileFromText(peer.userId || peer.id, m.text) : null;
1395
+ const callRec = !m.file ? dkCallFromText(m.text) : null;
1396
+ return /* @__PURE__ */ React.createElement(
1397
+ "div",
1398
+ {
1399
+ onClick: selMode ? () => onToggleSel(m.id) : void 0,
1400
+ style: {
1401
+ display: "flex",
1402
+ justifyContent: mine ? "flex-end" : "flex-start",
1403
+ alignItems: "flex-end",
1404
+ gap: 8,
1405
+ margin: "3px 0",
1406
+ cursor: selMode ? "pointer" : "default",
1407
+ borderRadius: 8,
1408
+ background: selMode && selected ? "rgba(91,140,255,0.12)" : "transparent",
1409
+ padding: selMode ? "2px 4px" : 0
1410
+ }
1411
+ },
1412
+ selMode && /* @__PURE__ */ React.createElement("div", { style: {
1413
+ alignSelf: "center",
1414
+ width: 18,
1415
+ height: 18,
1416
+ borderRadius: 5,
1417
+ flexShrink: 0,
1418
+ border: "1.5px solid " + (selected ? "var(--accent)" : "var(--line)"),
1419
+ background: selected ? "var(--accent)" : "transparent",
1420
+ display: "flex",
1421
+ alignItems: "center",
1422
+ justifyContent: "center"
1423
+ } }, selected && /* @__PURE__ */ React.createElement(Icon, { name: "check", size: 12, stroke: 3, color: "#fff" })),
1424
+ !mine && /* @__PURE__ */ React.createElement(DkAvatar, { peer, size: 24, radius: 6, dot: false }),
1425
+ /* @__PURE__ */ React.createElement("div", { style: { maxWidth: m.file && (m.file.media === "image" || m.file.media === "video") || rtcFile && (rtcFile.media === "image" || rtcFile.media === "video") ? "min(680px, 88%)" : "64%", display: "flex", flexDirection: "column", alignItems: mine ? "flex-end" : "flex-start" } }, callRec ? /* @__PURE__ */ React.createElement(
1426
+ "button",
1427
+ {
1428
+ onClick: () => onCall && onCall(peer.userId, callRec.kind === "video"),
1429
+ style: { display: "flex", alignItems: "center", gap: 10, padding: "9px 12px", borderRadius: 12, border: "1px solid var(--line)", background: mine ? "var(--bub-me)" : "var(--bub-them)", color: mine ? "#fff" : "var(--text)", cursor: "pointer", fontFamily: "var(--mono)", fontSize: 12.5 }
1430
+ },
1431
+ /* @__PURE__ */ React.createElement(Icon, { name: callRec.kind === "video" ? "video" : "phone", size: 17, stroke: 2, color: mine ? "#fff" : "var(--accent)" }),
1432
+ /* @__PURE__ */ React.createElement("span", null, callRec.direction === "incoming" ? "Incoming" : "Outgoing", " ", callRec.kind, " call"),
1433
+ /* @__PURE__ */ React.createElement("span", { style: { color: mine ? "rgba(255,255,255,0.75)" : "var(--faint)" } }, "\xB7 redial")
1434
+ ) : rtcFile ? /* @__PURE__ */ React.createElement("div", { style: {
1435
+ display: "flex",
1436
+ flexDirection: "column",
1437
+ gap: 6,
1438
+ maxWidth: rtcFile.media === "image" || rtcFile.media === "video" ? "100%" : 300,
1439
+ width: rtcFile.media === "image" || rtcFile.media === "video" ? "100%" : void 0,
1440
+ background: "var(--bub-them)",
1441
+ border: "1px solid var(--line)",
1442
+ borderRadius: 12,
1443
+ padding: 6
1444
+ } }, (rtcFile.media === "image" || rtcFile.media === "video") && /* @__PURE__ */ React.createElement("div", { "data-media": true, style: { position: "relative", lineHeight: 0 } }, rtcFile.media === "image" ? /* @__PURE__ */ React.createElement(
1445
+ "img",
1446
+ {
1447
+ src: rtcFile.url,
1448
+ alt: rtcFile.name,
1449
+ style: { display: "block", width: "100%", maxHeight: "72vh", borderRadius: 8, objectFit: "contain" }
1450
+ }
1451
+ ) : /* @__PURE__ */ React.createElement(
1452
+ "video",
1453
+ {
1454
+ src: rtcFile.url,
1455
+ controls: true,
1456
+ preload: "metadata",
1457
+ style: { display: "block", width: "100%", maxHeight: "72vh", borderRadius: 8, objectFit: "contain", background: "#000" }
1458
+ }
1459
+ ), /* @__PURE__ */ React.createElement("div", { style: { position: "absolute", top: 8, right: 8, display: "flex", gap: 6 } }, /* @__PURE__ */ React.createElement(
1460
+ "button",
1461
+ {
1462
+ onClick: () => onTheater({ kind: rtcFile.media, url: rtcFile.url, name: rtcFile.name }),
1463
+ title: "theater / \u7F51\u9875\u5168\u5C4F",
1464
+ style: mediaBtnStyle
1465
+ },
1466
+ /* @__PURE__ */ React.createElement(Icon, { name: "theater", size: 16, stroke: 2, color: "#fff" })
1467
+ ), /* @__PURE__ */ React.createElement(
1468
+ "button",
1469
+ {
1470
+ onClick: (e) => {
1471
+ const w = e.currentTarget.closest("[data-media]");
1472
+ const el = w && w.querySelector("video,img");
1473
+ if (el && el.requestFullscreen)
1474
+ el.requestFullscreen();
1475
+ },
1476
+ title: "fullscreen / \u5168\u5C4F",
1477
+ style: mediaBtnStyle
1478
+ },
1479
+ /* @__PURE__ */ React.createElement(Icon, { name: "maximize", size: 16, stroke: 2, color: "#fff" })
1480
+ ))), rtcFile.media === "audio" && /* @__PURE__ */ React.createElement("audio", { src: rtcFile.url, controls: true, style: { width: "100%" } }), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8, padding: "0 2px" } }, /* @__PURE__ */ React.createElement("span", { style: { flex: 1, minWidth: 0, fontFamily: "var(--mono)", fontSize: 11, color: "var(--faint)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } }, rtcFile.name), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, color: "var(--faint)", flexShrink: 0 } }, dkFileSize(rtcFile.size)), /* @__PURE__ */ React.createElement("a", { href: rtcFile.url, download: rtcFile.name, title: "download", style: { display: "inline-flex", flexShrink: 0 } }, /* @__PURE__ */ React.createElement(Icon, { name: "download", size: 15, stroke: 2, color: "var(--accent)" })))) : m.file ? (
1481
+ // Inline preview/player. Received media: as soon as it's saved.
1482
+ // Sent media: once it's fully delivered ('sent') — the daemon keeps a
1483
+ // local copy so it plays on the sender's side too.
1484
+ (m.file.media === "image" || m.file.media === "video" || m.file.media === "audio") && (mine ? m.file.status === "sent" : m.file.status !== "sending" && m.file.status !== "failed" && m.file.status !== "queued" && m.file.status !== "cancelled") ? (
1485
+ // Received media → inline preview / player, with name, size, download.
1486
+ /* @__PURE__ */ React.createElement("div", { style: {
1487
+ display: "flex",
1488
+ flexDirection: "column",
1489
+ gap: 6,
1490
+ maxWidth: m.file.media === "image" || m.file.media === "video" ? "100%" : 300,
1491
+ width: m.file.media === "image" || m.file.media === "video" ? "100%" : void 0,
1492
+ background: "var(--bub-them)",
1493
+ border: "1px solid var(--line)",
1494
+ borderRadius: 12,
1495
+ padding: 6
1496
+ } }, (m.file.media === "image" || m.file.media === "video") && /* @__PURE__ */ React.createElement("div", { "data-media": true, style: { position: "relative", lineHeight: 0 } }, m.file.media === "image" ? /* @__PURE__ */ React.createElement(
1497
+ "img",
1498
+ {
1499
+ src: dkFileUrl(m.file.name),
1500
+ alt: m.file.name,
1501
+ style: { display: "block", width: "100%", maxHeight: "72vh", borderRadius: 8, objectFit: "contain" }
1502
+ }
1503
+ ) : /* @__PURE__ */ React.createElement(
1504
+ "video",
1505
+ {
1506
+ src: dkFileUrl(m.file.name),
1507
+ controls: true,
1508
+ preload: "metadata",
1509
+ style: { display: "block", width: "100%", maxHeight: "72vh", borderRadius: 8, objectFit: "contain", background: "#000" }
1510
+ }
1511
+ ), /* @__PURE__ */ React.createElement("div", { style: { position: "absolute", top: 8, right: 8, display: "flex", gap: 6 } }, /* @__PURE__ */ React.createElement(
1512
+ "button",
1513
+ {
1514
+ onClick: () => onTheater({ kind: m.file.media, url: dkFileUrl(m.file.name), name: m.file.name }),
1515
+ title: "theater / \u7F51\u9875\u5168\u5C4F",
1516
+ style: mediaBtnStyle
1517
+ },
1518
+ /* @__PURE__ */ React.createElement(Icon, { name: "theater", size: 16, stroke: 2, color: "#fff" })
1519
+ ), /* @__PURE__ */ React.createElement(
1520
+ "button",
1521
+ {
1522
+ onClick: (e) => {
1523
+ const w = e.currentTarget.closest("[data-media]");
1524
+ const el = w && w.querySelector("video,img");
1525
+ if (el && el.requestFullscreen)
1526
+ el.requestFullscreen();
1527
+ },
1528
+ title: "fullscreen / \u5168\u5C4F",
1529
+ style: mediaBtnStyle
1530
+ },
1531
+ /* @__PURE__ */ React.createElement(Icon, { name: "maximize", size: 16, stroke: 2, color: "#fff" })
1532
+ ))), m.file.media === "audio" && /* @__PURE__ */ React.createElement("audio", { src: dkFileUrl(m.file.name), controls: true, style: { width: "100%" } }), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8, padding: "0 2px" } }, /* @__PURE__ */ React.createElement("span", { style: { flex: 1, minWidth: 0, fontFamily: "var(--mono)", fontSize: 11, color: "var(--faint)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } }, m.file.name), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, color: "var(--faint)", flexShrink: 0 } }, m.file.size), onReveal && /* @__PURE__ */ React.createElement(
1533
+ "button",
1534
+ {
1535
+ title: T.revealInFinder || "\u5728\u6587\u4EF6\u7BA1\u7406\u5668\u4E2D\u663E\u793A / Show in file manager",
1536
+ onClick: (e) => {
1537
+ e.stopPropagation();
1538
+ onReveal(m.file.name);
1539
+ },
1540
+ style: { background: "none", border: "none", cursor: "pointer", padding: 0, display: "inline-flex", flexShrink: 0 }
1541
+ },
1542
+ /* @__PURE__ */ React.createElement(Icon, { name: "folderOpen", size: 15, stroke: 2, color: "var(--accent)" })
1543
+ ), /* @__PURE__ */ React.createElement("a", { href: dkFileDownloadUrl(m.file.name), download: m.file.name, title: "download", style: { display: "inline-flex", flexShrink: 0 } }, /* @__PURE__ */ React.createElement(Icon, { name: "download", size: 15, stroke: 2, color: "var(--accent)" }))))
1544
+ ) : (() => {
1545
+ const cardStyle = {
1546
+ display: "flex",
1547
+ alignItems: "center",
1548
+ gap: 10,
1549
+ padding: "10px 12px",
1550
+ borderRadius: 10,
1551
+ background: mine ? "var(--bub-me)" : "var(--bub-them)",
1552
+ border: "1px solid " + (mine ? "transparent" : "var(--line)"),
1553
+ minWidth: 200,
1554
+ textDecoration: "none",
1555
+ cursor: mine ? "default" : "pointer"
1556
+ };
1557
+ const icon = /* @__PURE__ */ React.createElement("div", { style: { width: 34, height: 34, borderRadius: 7, flexShrink: 0, background: mine ? "rgba(255,255,255,0.16)" : "var(--chip)", display: "flex", alignItems: "center", justifyContent: "center", color: mine ? "#fff" : "var(--accent)" } }, /* @__PURE__ */ React.createElement(Icon, { name: m.file.media === "image" ? "image" : m.file.media === "video" ? "video" : m.file.media === "audio" ? "play" : "file", size: 18, stroke: 1.9 }));
1558
+ const meta = /* @__PURE__ */ React.createElement("div", { style: { minWidth: 0, flex: 1 } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 12.5, fontWeight: 600, color: mine ? "#fff" : "var(--text)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } }, m.file.name), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 11, color: mine ? "rgba(255,255,255,0.7)" : "var(--faint)", marginTop: 1 } }, busy === "cancel" ? `${m.file.size} \xB7 ${T.cancelling || "Cancelling\u2026"}` : busy === "retry" ? `${m.file.size} \xB7 ${T.retrying || "Retrying\u2026"}` : m.file.status === "queued" ? `${m.file.size} \xB7 ${T.queued || "queued"}` : m.file.status === "sending" ? `${m.file.size} \xB7 ${dkPct(m.file.pct)}${m.file.kbps ? " \xB7 " + dkSpeed(m.file.kbps) : ""}` : m.file.status === "failed" ? `${m.file.size} \xB7 failed` : m.file.status === "cancelled" ? `${m.file.size} \xB7 ${T.cancelled || "cancelled"}` : mine ? `${m.file.size} \xB7 sent` : `${m.file.size} \xB7 ${T.open || "open"}`));
1559
+ if (mine) {
1560
+ const btnBusy = !!busy;
1561
+ return /* @__PURE__ */ React.createElement("div", { style: { ...cardStyle, opacity: btnBusy ? 0.85 : 1 } }, icon, meta, busy === "cancel" || busy === "retry" ? /* @__PURE__ */ React.createElement(Icon, { name: "clock", size: 16, stroke: 2, color: "rgba(255,255,255,0.85)" }) : m.file.status === "queued" ? /* @__PURE__ */ React.createElement(Icon, { name: "clock", size: 16, stroke: 2, color: "rgba(255,255,255,0.85)" }) : m.file.status === "sending" ? /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, fontWeight: 700, color: "rgba(255,255,255,0.9)", whiteSpace: "nowrap" } }, dkPct(m.file.pct), m.file.kbps ? " \xB7 " + dkSpeed(m.file.kbps) : "") : m.file.status === "failed" || m.file.status === "cancelled" ? /* @__PURE__ */ React.createElement(Icon, { name: "x", size: 16, stroke: 2.4, color: "rgba(255,200,190,0.95)" }) : /* @__PURE__ */ React.createElement(Icon, { name: "checkCheck", size: 16, stroke: 2, color: "rgba(255,255,255,0.85)" }), (m.file.status === "sending" || m.file.status === "queued" || busy === "cancel") && m.id && /* @__PURE__ */ React.createElement(
1562
+ "button",
1563
+ {
1564
+ title: T.cancel || "cancel",
1565
+ disabled: btnBusy,
1566
+ onClick: (e) => {
1567
+ e.stopPropagation();
1568
+ if (!btnBusy)
1569
+ onCancel(m.id);
1570
+ },
1571
+ style: { background: "rgba(255,255,255,0.16)", border: "none", borderRadius: 6, cursor: btnBusy ? "wait" : "pointer", padding: 3, marginLeft: 2, display: "inline-flex", flexShrink: 0, opacity: btnBusy ? 0.5 : 1 }
1572
+ },
1573
+ /* @__PURE__ */ React.createElement(Icon, { name: "x", size: 13, stroke: 2.4, color: "#fff" })
1574
+ ), m.file.status !== "sent" && m.id && /* @__PURE__ */ React.createElement(
1575
+ "button",
1576
+ {
1577
+ title: T.retry || "retry",
1578
+ disabled: btnBusy,
1579
+ onClick: (e) => {
1580
+ e.stopPropagation();
1581
+ if (!btnBusy)
1582
+ onRetry(m.id);
1583
+ },
1584
+ style: { background: "rgba(255,255,255,0.16)", border: "none", borderRadius: 6, cursor: btnBusy ? "wait" : "pointer", padding: 3, marginLeft: 2, display: "inline-flex", flexShrink: 0, opacity: btnBusy ? 0.5 : 1 }
1585
+ },
1586
+ /* @__PURE__ */ React.createElement(Icon, { name: "refresh", size: 13, stroke: 2.2, color: "#fff" })
1587
+ ));
1588
+ }
1589
+ return /* @__PURE__ */ React.createElement(
1590
+ "div",
1591
+ {
1592
+ style: cardStyle,
1593
+ title: "open",
1594
+ onClick: () => window.open(dkFileUrl(m.file.name), "_blank", "noopener")
1595
+ },
1596
+ icon,
1597
+ meta,
1598
+ onReveal && /* @__PURE__ */ React.createElement(
1599
+ "button",
1600
+ {
1601
+ title: T.revealInFinder || "\u5728\u6587\u4EF6\u7BA1\u7406\u5668\u4E2D\u663E\u793A / Show in file manager",
1602
+ onClick: (e) => {
1603
+ e.stopPropagation();
1604
+ onReveal(m.file.name);
1605
+ },
1606
+ style: { background: "none", border: "none", cursor: "pointer", padding: 0, display: "inline-flex", flexShrink: 0 }
1607
+ },
1608
+ /* @__PURE__ */ React.createElement(Icon, { name: "folderOpen", size: 16, stroke: 2, color: "var(--accent)" })
1609
+ ),
1610
+ /* @__PURE__ */ React.createElement(
1611
+ "a",
1612
+ {
1613
+ href: dkFileDownloadUrl(m.file.name),
1614
+ download: m.file.name,
1615
+ title: "download",
1616
+ onClick: (e) => e.stopPropagation(),
1617
+ style: { display: "inline-flex", flexShrink: 0 }
1618
+ },
1619
+ /* @__PURE__ */ React.createElement(Icon, { name: "download", size: 16, stroke: 2, color: "var(--accent)" })
1620
+ )
1621
+ );
1622
+ })()
1623
+ ) : /* @__PURE__ */ React.createElement("div", { style: {
1624
+ padding: "8px 12px",
1625
+ borderRadius: 12,
1626
+ borderBottomRightRadius: mine ? 4 : 12,
1627
+ borderBottomLeftRadius: mine ? 12 : 4,
1628
+ // Incoming messages are tinted by delivery path so the split is
1629
+ // visible: amber = arrived via the express relay (offline), plain =
1630
+ // live online session. Outgoing keep the accent bubble.
1631
+ background: mine ? "var(--bub-me)" : m.via === "offline" ? "rgba(245,158,11,0.16)" : "var(--bub-them)",
1632
+ color: mine ? "#fff" : "var(--text)",
1633
+ border: "1px solid " + (mine ? "transparent" : m.via === "offline" ? "rgba(245,158,11,0.55)" : "var(--line)"),
1634
+ fontFamily: "var(--ui)",
1635
+ fontSize: 13.5,
1636
+ lineHeight: 1.4,
1637
+ letterSpacing: -0.1,
1638
+ wordBreak: "break-word"
1639
+ } }, /* @__PURE__ */ React.createElement(MarkdownText, { text: m.text })), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 4, margin: "3px 3px 0" } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10, color: "var(--faint)" } }, m.time), !mine && m.via && /* @__PURE__ */ React.createElement(
1640
+ "span",
1641
+ {
1642
+ title: m.via === "offline" ? "delivered via express relay (offline)" : "delivered over a live session (online)",
1643
+ style: { fontFamily: "var(--mono)", fontSize: 9, color: m.via === "offline" ? "#f59e0b" : "var(--faint)" }
1644
+ },
1645
+ m.via === "offline" ? "\u79BB\u7EBF" : "\u5728\u7EBF"
1646
+ ), !selMode && m.id && /* @__PURE__ */ React.createElement(
1647
+ "button",
1648
+ {
1649
+ title: T.delete || "delete",
1650
+ onClick: (e) => {
1651
+ e.stopPropagation();
1652
+ onDelete(m.id);
1653
+ },
1654
+ style: { background: "none", border: "none", cursor: "pointer", padding: 0, marginLeft: 2, display: "inline-flex", opacity: 0.55 }
1655
+ },
1656
+ /* @__PURE__ */ React.createElement(Icon, { name: "trash", size: 11, stroke: 2, color: "var(--faint)" })
1657
+ ), mine && m.status === "queued" ? /* @__PURE__ */ React.createElement("span", { style: { display: "inline-flex", alignItems: "center", gap: 2 }, title: "waiting for peer \u2014 will send when they're online" }, /* @__PURE__ */ React.createElement(Icon, { name: "clock", size: 11, stroke: 2.2, color: "var(--faint)" }), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10, color: "var(--faint)" } }, T.queued || "queued")) : mine && m.status && /* @__PURE__ */ React.createElement(Icon, { name: "checkCheck", size: 12, stroke: 2.2, color: m.status === "read" ? "var(--accent)" : "var(--faint)" })))
1658
+ );
1659
+ }
1660
+ function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread }) {
1661
+ const scrollRef = React.useRef(null);
1662
+ const followScrollRef = React.useRef(true);
1663
+ const fileRef = React.useRef(null);
1664
+ const rtcFileRef = React.useRef(null);
1665
+ const [menu, setMenu] = React.useState(false);
1666
+ const [selMode, setSelMode] = React.useState(false);
1667
+ const [sel, setSel] = React.useState(() => /* @__PURE__ */ new Set());
1668
+ const [hidden, setHidden] = React.useState(() => /* @__PURE__ */ new Set());
1669
+ const [fileBusy, setFileBusy] = React.useState(() => ({}));
1670
+ const [filePatch, setFilePatch] = React.useState(() => ({}));
1671
+ const [flash, setFlash] = React.useState(null);
1672
+ const flashTimer = React.useRef(null);
1673
+ const showFlash = (kind, msg) => {
1674
+ if (flashTimer.current)
1675
+ clearTimeout(flashTimer.current);
1676
+ setFlash({ kind, msg });
1677
+ flashTimer.current = setTimeout(() => setFlash(null), 2500);
1678
+ };
1679
+ React.useEffect(() => () => {
1680
+ if (flashTimer.current)
1681
+ clearTimeout(flashTimer.current);
1682
+ }, []);
1683
+ React.useEffect(() => {
1684
+ setSelMode(false);
1685
+ setSel(/* @__PURE__ */ new Set());
1686
+ setHidden(/* @__PURE__ */ new Set());
1687
+ setFileBusy({});
1688
+ setFilePatch({});
1689
+ setFlash(null);
1690
+ followScrollRef.current = true;
1691
+ }, [peer.id]);
1692
+ React.useEffect(() => {
1693
+ setFilePatch((p) => {
1694
+ let changed = false;
1695
+ const next = { ...p };
1696
+ for (const m of threadProp || []) {
1697
+ if (!m || !m.id || !next[m.id] || !m.file)
1698
+ continue;
1699
+ if (m.file.status === next[m.id].status) {
1700
+ delete next[m.id];
1701
+ changed = true;
1702
+ }
1703
+ }
1704
+ return changed ? next : p;
1705
+ });
1706
+ }, [threadProp]);
1707
+ const baseThread = threadProp && threadProp.length ? threadProp : [{ day: "Today" }, { from: "them", time: "\u2014", text: lang === "zh" ? "\u6682\u65E0\u6D88\u606F\u8BB0\u5F55\uFF0C\u53D1\u4E2A\u6D88\u606F\u6253\u4E2A\u62DB\u547C\u5427\u3002" : "No messages yet. Say hi." }];
1708
+ const thread = React.useMemo(() => {
1709
+ if (!Object.keys(filePatch).length)
1710
+ return baseThread;
1711
+ return baseThread.map((m) => {
1712
+ if (!m || !m.id || !m.file || !filePatch[m.id])
1713
+ return m;
1714
+ return { ...m, file: { ...m.file, ...filePatch[m.id] } };
1715
+ });
1716
+ }, [baseThread, filePatch]);
1717
+ const scrollToBottom = () => {
1718
+ const el = scrollRef.current;
1719
+ if (el)
1720
+ el.scrollTop = el.scrollHeight;
1721
+ };
1722
+ const updateFollowScroll = () => {
1723
+ const el = scrollRef.current;
1724
+ if (!el)
1725
+ return;
1726
+ followScrollRef.current = el.scrollHeight - el.scrollTop - el.clientHeight < 80;
1727
+ };
1728
+ React.useLayoutEffect(() => {
1729
+ if (followScrollRef.current)
1730
+ scrollToBottom();
1731
+ }, [peer.id, thread]);
1732
+ const toggleSel = (id) => setSel((s) => {
1733
+ const n = new Set(s);
1734
+ n.has(id) ? n.delete(id) : n.add(id);
1735
+ return n;
1736
+ });
1737
+ const clearBusy = (id) => setFileBusy((b) => {
1738
+ const n = { ...b };
1739
+ delete n[id];
1740
+ return n;
1741
+ });
1742
+ const doReveal = (name) => {
1743
+ if (!name)
1744
+ return;
1745
+ dkApi.revealFile(name).then((r) => {
1746
+ if (!r || r.ok === false) {
1747
+ showFlash("err", (lang === "zh" ? "\u65E0\u6CD5\u5728\u6587\u4EF6\u7BA1\u7406\u5668\u4E2D\u663E\u793A: " : "Could not reveal file: ") + (r && r.error || ""));
1748
+ }
1749
+ });
1750
+ };
1751
+ const doCancel = (id) => {
1752
+ if (!id || fileBusy[id])
1753
+ return;
1754
+ setFileBusy((b) => ({ ...b, [id]: "cancel" }));
1755
+ showFlash("ok", T.cancelling || "Cancelling\u2026");
1756
+ dkApi.cancelSend(peer.userId, [id]).then((r) => {
1757
+ if (!r || r.ok === false) {
1758
+ const msg = (T.cancelFailed || "Cancel failed") + ": " + (r && r.error || "daemon unavailable");
1759
+ showFlash("err", msg);
1760
+ window.alert(msg);
1761
+ } else if (!r.cancelled) {
1762
+ const msg = T.cancelMissing || "No active transfer was found. Refresh and try again.";
1763
+ showFlash("err", msg);
1764
+ window.alert(msg);
1765
+ } else {
1766
+ setFilePatch((p) => ({ ...p, [id]: { status: "cancelled", pct: void 0, kbps: void 0 } }));
1767
+ showFlash("ok", T.cancelled || "Transfer cancelled");
1768
+ if (onReloadThread)
1769
+ onReloadThread();
1770
+ }
1771
+ }).catch((e) => {
1772
+ const msg = (T.cancelFailed || "Cancel failed") + ": " + String(e);
1773
+ showFlash("err", msg);
1774
+ window.alert(msg);
1775
+ }).finally(() => clearBusy(id));
1776
+ };
1777
+ const doRetry = (id) => {
1778
+ if (!id || fileBusy[id])
1779
+ return;
1780
+ setFileBusy((b) => ({ ...b, [id]: "retry" }));
1781
+ setFilePatch((p) => ({ ...p, [id]: { status: "sending", pct: 0, kbps: void 0 } }));
1782
+ showFlash("ok", T.retrying || "Retrying\u2026");
1783
+ dkApi.retrySend(peer.userId, [id]).then((r) => {
1784
+ if (r && r.ok === false) {
1785
+ setFilePatch((p) => {
1786
+ const n = { ...p };
1787
+ delete n[id];
1788
+ return n;
1789
+ });
1790
+ const msg = (T.retryFailed || "Retry failed") + ": " + (r.error || "");
1791
+ showFlash("err", msg);
1792
+ window.alert(msg);
1793
+ } else if (!r || !r.retried) {
1794
+ setFilePatch((p) => {
1795
+ const n = { ...p };
1796
+ delete n[id];
1797
+ return n;
1798
+ });
1799
+ const msg = T.retryMissing || "No retryable transfer or original file is unavailable.";
1800
+ showFlash("err", msg);
1801
+ window.alert(msg);
1802
+ } else {
1803
+ showFlash("ok", T.retried || "Transfer restarted");
1804
+ if (onReloadThread)
1805
+ onReloadThread();
1806
+ }
1807
+ }).catch((e) => {
1808
+ setFilePatch((p) => {
1809
+ const n = { ...p };
1810
+ delete n[id];
1811
+ return n;
1812
+ });
1813
+ const msg = (T.retryFailed || "Retry failed") + ": " + String(e);
1814
+ showFlash("err", msg);
1815
+ window.alert(msg);
1816
+ }).finally(() => clearBusy(id));
1817
+ };
1818
+ const doDelete = (ids) => {
1819
+ ids = ids.filter(Boolean);
1820
+ if (!ids.length)
1821
+ return;
1822
+ if (!window.confirm(ids.length === 1 ? T.delete1 || "Delete this message and its file?" : (T.deleteN || "Delete {n} selected?").replace("{n}", ids.length)))
1823
+ return;
1824
+ setHidden((h) => {
1825
+ const n = new Set(h);
1826
+ ids.forEach((i) => n.add(i));
1827
+ return n;
1828
+ });
1829
+ dkApi.delFiles(peer.userId, ids).catch(() => {
1830
+ });
1831
+ setSel(/* @__PURE__ */ new Set());
1832
+ setSelMode(false);
1833
+ };
1834
+ const [draft, setDraft] = React.useState("");
1835
+ const [dragOver, setDragOver] = React.useState(false);
1836
+ const [sending, setSending] = React.useState(null);
1837
+ const [theater, setTheater] = React.useState(null);
1838
+ React.useEffect(() => {
1839
+ setDraft("");
1840
+ }, [peer.id]);
1841
+ React.useEffect(() => {
1842
+ setTheater(null);
1843
+ }, [peer.id]);
1844
+ React.useEffect(() => {
1845
+ if (!theater)
1846
+ return;
1847
+ const onKey = (e) => {
1848
+ if (e.key === "Escape")
1849
+ setTheater(null);
1850
+ };
1851
+ window.addEventListener("keydown", onKey);
1852
+ return () => window.removeEventListener("keydown", onKey);
1853
+ }, [theater]);
1854
+ const sendDraft = () => {
1855
+ if (draft.trim()) {
1856
+ followScrollRef.current = true;
1857
+ onSend(draft);
1858
+ setDraft("");
1859
+ requestAnimationFrame(scrollToBottom);
1860
+ }
1861
+ };
1862
+ const sendFiles = (files, mode) => {
1863
+ const sender = mode === "webrtc" ? onSendRtcFile : onSendFile;
1864
+ if (!files || !files.length || !sender)
1865
+ return;
1866
+ const file = files[0];
1867
+ followScrollRef.current = true;
1868
+ setSending({ name: file.name, via: mode, pct: 0, sent: 0, size: file.size });
1869
+ const onProgress = (p) => setSending((s) => s && s.name === file.name ? {
1870
+ ...s,
1871
+ pct: p && p.pct,
1872
+ sent: p && p.sent,
1873
+ size: p && p.size || s.size,
1874
+ kbps: p && p.kbps
1875
+ } : s);
1876
+ Promise.resolve(sender(file, onProgress)).then((r) => {
1877
+ if (r && r.ok === false)
1878
+ window.alert((lang === "zh" ? "\u53D1\u9001\u5931\u8D25: " : "Send failed: ") + (r.error || ""));
1879
+ else
1880
+ showFlash("ok", mode === "webrtc" ? lang === "zh" ? "WebRTC \u6587\u4EF6\u53D1\u9001\u5B8C\u6210" : "WebRTC file sent" : lang === "zh" ? "\u6587\u4EF6\u5DF2\u63D0\u4EA4\u53D1\u9001" : "File send started");
1881
+ }).finally(() => {
1882
+ setSending(null);
1883
+ requestAnimationFrame(scrollToBottom);
1884
+ });
1885
+ };
1886
+ const onDrop = (e) => {
1887
+ e.preventDefault();
1888
+ setDragOver(false);
1889
+ sendFiles(e.dataTransfer.files);
1890
+ };
1891
+ return /* @__PURE__ */ React.createElement(
1892
+ "div",
1893
+ {
1894
+ style: { flex: 1, minWidth: 0, minHeight: 0, display: "flex", flexDirection: "column", background: "var(--bg)", position: "relative" },
1895
+ onDragOver: (e) => {
1896
+ e.preventDefault();
1897
+ if (!dragOver)
1898
+ setDragOver(true);
1899
+ },
1900
+ onDragLeave: (e) => {
1901
+ if (e.currentTarget === e.target)
1902
+ setDragOver(false);
1903
+ },
1904
+ onDrop
1905
+ },
1906
+ dragOver && /* @__PURE__ */ React.createElement("div", { style: { position: "absolute", inset: 0, zIndex: 30, background: "rgba(91,140,255,0.10)", border: "2px dashed var(--accent)", borderRadius: 12, display: "flex", alignItems: "center", justifyContent: "center", pointerEvents: "none", fontFamily: "var(--mono)", fontSize: 14, color: "var(--accent)" } }, lang === "zh" ? "\u677E\u5F00\u53D1\u9001\u6587\u4EF6" : "Drop to send file"),
1907
+ /* @__PURE__ */ React.createElement("div", { style: { height: 60, flexShrink: 0, borderBottom: "1px solid var(--line)", display: "flex", alignItems: "center", gap: 12, padding: "0 16px", background: "var(--panel)" } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer, size: 34, radius: 8 }), /* @__PURE__ */ React.createElement("div", { style: { minWidth: 0 } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8 } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: peer.alias ? "var(--ui)" : "var(--mono)", fontSize: 15, fontWeight: 700, color: "var(--text)" } }, peer.alias || shortKey(peer.userId, 10, 6)), peer.agent && /* @__PURE__ */ React.createElement(Tag, { tone: "accent" }, "agent"), /* @__PURE__ */ React.createElement(RouteTag, { peer })), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8, marginTop: 2 } }, /* @__PURE__ */ React.createElement(Mono, { size: 11.5, dim: true, copy: peer.userId, title: peer.userId }, shortKey(peer.userId, 10, 6)), /* @__PURE__ */ React.createElement("span", { style: { color: "var(--line)" } }, "\xB7"), /* @__PURE__ */ React.createElement("button", { onClick: () => onOpenNet(peer), title: T && T.openNet || "Network view", style: { background: "none", border: "none", cursor: "pointer", padding: 0, color: "var(--accent)", display: "inline-flex", alignItems: "center" } }, /* @__PURE__ */ React.createElement(Icon, { name: "network", size: 12, stroke: 2 })), /* @__PURE__ */ React.createElement(Mono, { size: 11.5, dim: true, copy: peer.ip, title: peer.ip }, peer.ip), peer.address && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("span", { style: { color: "var(--line)" } }, "\xB7"), /* @__PURE__ */ React.createElement(
1908
+ Mono,
1909
+ {
1910
+ size: 11.5,
1911
+ dim: true,
1912
+ icon: "share",
1913
+ copy: peer.address,
1914
+ title: `${T && T.copyAddr || "Copy address \u2014 share it so someone else can add this friend"}
1915
+ ${peer.address}`
1916
+ },
1917
+ shortKey(peer.address, 8, 6)
1918
+ )))), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), onCall && /* @__PURE__ */ React.createElement(Btn, { icon: "phone", title: T && T.audioCall || "Audio call", onClick: () => onCall(peer.userId, false) }), onCall && /* @__PURE__ */ React.createElement(Btn, { icon: "video", title: T && T.videoCall || "Video call", onClick: () => onCall(peer.userId, true) }), onSendRtcFile && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(
1919
+ "input",
1920
+ {
1921
+ ref: rtcFileRef,
1922
+ type: "file",
1923
+ style: { display: "none" },
1924
+ onChange: (e) => {
1925
+ sendFiles(e.target.files, "webrtc");
1926
+ e.target.value = "";
1927
+ }
1928
+ }
1929
+ ), /* @__PURE__ */ React.createElement(Btn, { icon: "file", title: T && T.sendWebrtcFile || "Send file via WebRTC", onClick: () => rtcFileRef.current && rtcFileRef.current.click() })), /* @__PURE__ */ React.createElement("div", { style: { position: "relative" } }, /* @__PURE__ */ React.createElement(Btn, { icon: "more", onClick: () => setMenu((v) => !v) }), menu && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { onClick: () => setMenu(false), style: { position: "fixed", inset: 0, zIndex: 40 } }), /* @__PURE__ */ React.createElement("div", { style: { position: "absolute", right: 0, top: 36, zIndex: 50, width: 180, background: "var(--panel-2)", border: "1px solid var(--line)", borderRadius: 9, padding: 6, boxShadow: "0 14px 40px rgba(0,0,0,0.4)" } }, /* @__PURE__ */ React.createElement(MenuItem, { icon: "hash", label: T.alias, onClick: () => {
1930
+ setMenu(false);
1931
+ onAlias(peer);
1932
+ } }), /* @__PURE__ */ React.createElement(MenuItem, { icon: "check", label: T.selectDelete || "Select & delete", onClick: () => {
1933
+ setMenu(false);
1934
+ setSelMode(true);
1935
+ } }), /* @__PURE__ */ React.createElement("div", { style: { height: 1, background: "var(--line)", margin: "5px 4px" } }), /* @__PURE__ */ React.createElement(MenuItem, { icon: "trash", label: T.remove, danger: true, onClick: () => {
1936
+ setMenu(false);
1937
+ onRemove(peer);
1938
+ } }))))),
1939
+ /* @__PURE__ */ React.createElement("div", { ref: scrollRef, onScroll: updateFollowScroll, style: { flex: 1, overflow: "auto", padding: "18px 22px" } }, /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 760, margin: "0 auto" } }, thread.filter((m) => m.day || !hidden.has(m.id)).map((m, i) => m.day ? /* @__PURE__ */ React.createElement("div", { key: i, style: { display: "flex", justifyContent: "center", margin: "14px 0" } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10.5, fontWeight: 600, color: "var(--faint)", background: "var(--chip)", padding: "3px 10px", borderRadius: 999 } }, m.day)) : /* @__PURE__ */ React.createElement(
1940
+ Msg,
1941
+ {
1942
+ key: m.id || i,
1943
+ m,
1944
+ peer,
1945
+ T,
1946
+ onTheater: setTheater,
1947
+ onDelete: (id) => doDelete([id]),
1948
+ onCancel: doCancel,
1949
+ onRetry: doRetry,
1950
+ onReveal: doReveal,
1951
+ onCall,
1952
+ busy: m.id ? fileBusy[m.id] : void 0,
1953
+ selMode,
1954
+ selected: sel.has(m.id),
1955
+ onToggleSel: toggleSel
1956
+ }
1957
+ )))),
1958
+ selMode && /* @__PURE__ */ React.createElement("div", { style: { flexShrink: 0, borderTop: "1px solid var(--line)", padding: "10px 16px", background: "var(--panel)", display: "flex", alignItems: "center", gap: 10 } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12, color: "var(--faint)" } }, sel.size, " ", T.selected || "selected"), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement(Btn, { size: "sm", onClick: () => {
1959
+ setSelMode(false);
1960
+ setSel(/* @__PURE__ */ new Set());
1961
+ } }, T.cancel || "Cancel"), /* @__PURE__ */ React.createElement(Btn, { size: "sm", tone: "danger", icon: "trash", onClick: () => doDelete([...sel]) }, T.delete || "Delete")),
1962
+ /* @__PURE__ */ React.createElement("div", { style: { flexShrink: 0, borderTop: "1px solid var(--line)", padding: "12px 16px", background: "var(--panel)" } }, flash && /* @__PURE__ */ React.createElement("div", { style: {
1963
+ maxWidth: 760,
1964
+ margin: "0 auto 8px",
1965
+ display: "flex",
1966
+ alignItems: "center",
1967
+ gap: 8,
1968
+ fontFamily: "var(--mono)",
1969
+ fontSize: 11.5,
1970
+ color: flash.kind === "err" ? "var(--danger)" : "var(--accent)"
1971
+ } }, /* @__PURE__ */ React.createElement(
1972
+ Icon,
1973
+ {
1974
+ name: flash.kind === "err" ? "x" : "check",
1975
+ size: 13,
1976
+ stroke: 2.2,
1977
+ color: flash.kind === "err" ? "var(--danger)" : "var(--accent)"
1978
+ }
1979
+ ), flash.msg), sending && /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 760, margin: "0 auto 8px", display: "flex", alignItems: "center", gap: 8, fontFamily: "var(--mono)", fontSize: 11.5, color: "var(--faint)" } }, /* @__PURE__ */ React.createElement(Icon, { name: "paperclip", size: 13, stroke: 2, color: "var(--accent)" }), (lang === "zh" ? "\u53D1\u9001\u4E2D: " : "Sending: ") + (sending.via === "webrtc" ? "WebRTC \xB7 " : "") + sending.name + (sending.size ? " \xB7 " + dkFileSize(sending.sent || 0) + " / " + dkFileSize(sending.size) : "") + (sending.pct != null ? " \xB7 " + Math.min(100, Math.max(0, sending.pct)).toFixed(1) + "%" : "") + (sending.kbps != null ? " \xB7 " + dkSpeed(sending.kbps) : "")), /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 760, margin: "0 auto", display: "flex", alignItems: "center", gap: 10 } }, /* @__PURE__ */ React.createElement(
1980
+ "input",
1981
+ {
1982
+ ref: fileRef,
1983
+ type: "file",
1984
+ style: { display: "none" },
1985
+ onChange: (e) => {
1986
+ sendFiles(e.target.files);
1987
+ e.target.value = "";
1988
+ }
1989
+ }
1990
+ ), /* @__PURE__ */ React.createElement(Btn, { icon: "paperclip", title: T.sendFile, onClick: () => fileRef.current && fileRef.current.click() }), /* @__PURE__ */ React.createElement(
1991
+ "input",
1992
+ {
1993
+ value: draft,
1994
+ onChange: (e) => setDraft(e.target.value),
1995
+ onKeyDown: (e) => {
1996
+ if (e.key === "Enter" && !e.shiftKey) {
1997
+ e.preventDefault();
1998
+ sendDraft();
1999
+ }
2000
+ },
2001
+ placeholder: `${T.message} ${peer.alias || shortKey(peer.userId, 6, 4)}\u2026`,
2002
+ style: { flex: 1, height: 38, borderRadius: 9, border: "1px solid var(--line)", background: "var(--panel-2)", color: "var(--text)", fontFamily: "var(--ui)", fontSize: 13.5, padding: "0 14px", outline: "none", minWidth: 0 }
2003
+ }
2004
+ ), /* @__PURE__ */ React.createElement(Btn, { tone: "solid", icon: "arrowUp", title: T.send, onClick: sendDraft }))),
2005
+ theater && /* @__PURE__ */ React.createElement("div", { onClick: () => setTheater(null), style: {
2006
+ position: "absolute",
2007
+ inset: 0,
2008
+ zIndex: 60,
2009
+ background: "rgba(0,0,0,0.86)",
2010
+ display: "flex",
2011
+ flexDirection: "column"
2012
+ } }, /* @__PURE__ */ React.createElement("div", { onClick: (e) => e.stopPropagation(), style: { flexShrink: 0, height: 46, display: "flex", alignItems: "center", justifyContent: "space-between", gap: 10, padding: "0 14px" } }, /* @__PURE__ */ React.createElement("span", { style: { minWidth: 0, fontFamily: "var(--mono)", fontSize: 12, color: "rgba(255,255,255,0.72)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } }, theater.name), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8, flexShrink: 0 } }, /* @__PURE__ */ React.createElement(
2013
+ "button",
2014
+ {
2015
+ title: lang === "zh" ? "\u5168\u5C4F" : "fullscreen",
2016
+ onClick: () => {
2017
+ const el = document.getElementById("dk-theater-media");
2018
+ if (el && el.requestFullscreen)
2019
+ el.requestFullscreen();
2020
+ },
2021
+ style: { width: 34, height: 34, borderRadius: 8, border: "none", background: "rgba(255,255,255,0.14)", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center" }
2022
+ },
2023
+ /* @__PURE__ */ React.createElement(Icon, { name: "maximize", size: 18, stroke: 2, color: "#fff" })
2024
+ ), /* @__PURE__ */ React.createElement(
2025
+ "button",
2026
+ {
2027
+ title: (lang === "zh" ? "\u5173\u95ED" : "close") + " \xB7 Esc",
2028
+ onClick: () => setTheater(null),
2029
+ style: { width: 34, height: 34, borderRadius: 8, border: "none", background: "rgba(255,255,255,0.14)", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center" }
2030
+ },
2031
+ /* @__PURE__ */ React.createElement(Icon, { name: "x", size: 20, stroke: 2.2, color: "#fff" })
2032
+ ))), /* @__PURE__ */ React.createElement("div", { onClick: () => setTheater(null), style: { flex: 1, minHeight: 0, display: "flex", alignItems: "center", justifyContent: "center", padding: "0 16px 16px" } }, theater.kind === "video" ? /* @__PURE__ */ React.createElement(
2033
+ "video",
2034
+ {
2035
+ id: "dk-theater-media",
2036
+ src: theater.url,
2037
+ controls: true,
2038
+ autoPlay: true,
2039
+ onClick: (e) => e.stopPropagation(),
2040
+ style: { maxWidth: "100%", maxHeight: "100%", objectFit: "contain", borderRadius: 6, background: "#000" }
2041
+ }
2042
+ ) : /* @__PURE__ */ React.createElement(
2043
+ "img",
2044
+ {
2045
+ id: "dk-theater-media",
2046
+ src: theater.url,
2047
+ alt: theater.name,
2048
+ onClick: (e) => e.stopPropagation(),
2049
+ style: { maxWidth: "100%", maxHeight: "100%", objectFit: "contain", borderRadius: 6 }
2050
+ }
2051
+ )))
2052
+ );
2053
+ }
2054
+ function MenuItem({ icon, label, onClick, danger }) {
2055
+ return /* @__PURE__ */ React.createElement("button", { onClick, style: {
2056
+ display: "flex",
2057
+ alignItems: "center",
2058
+ gap: 10,
2059
+ width: "100%",
2060
+ textAlign: "left",
2061
+ padding: "8px 9px",
2062
+ borderRadius: 6,
2063
+ border: "none",
2064
+ cursor: "pointer",
2065
+ background: "transparent",
2066
+ fontFamily: "var(--mono)",
2067
+ fontSize: 12.5,
2068
+ fontWeight: 600,
2069
+ color: danger ? "var(--danger)" : "var(--text)"
2070
+ } }, /* @__PURE__ */ React.createElement(Icon, { name: icon, size: 15, stroke: 2, color: danger ? "var(--danger)" : "var(--dim)" }), " ", label);
2071
+ }
2072
+ function ChatEmpty({ T }) {
2073
+ return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 12, color: "var(--faint)", background: "var(--bg)" } }, /* @__PURE__ */ React.createElement(Icon, { name: "message", size: 40, stroke: 1.4, color: "var(--line)" }), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 13 } }, T.pickPeer));
2074
+ }
2075
+ function ChatTab({ T, lang, peers, requests, activeId, thread, onSelect, onAct, onAdd, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, prefillAddr, onPrefillConsumed }) {
2076
+ const peer = peers.find((p) => p.id === activeId);
2077
+ return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, display: "flex", minWidth: 0, minHeight: 0 } }, /* @__PURE__ */ React.createElement(PeerSidebar, { T, peers, requests, activeId, onSelect, onAct, onAdd, prefillAddr, onPrefillConsumed }), peer ? /* @__PURE__ */ React.createElement(Conversation, { T, peer, lang, thread, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread }) : /* @__PURE__ */ React.createElement(ChatEmpty, { T }));
2078
+ }
2079
+ Object.assign(window, { ChatTab });
2080
+ function StatTile({ label, value, sub, tone }) {
2081
+ return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0, padding: "13px 15px", borderRadius: 10, background: "var(--panel)", border: "1px solid var(--line)" } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 10.5, fontWeight: 700, letterSpacing: 1, textTransform: "uppercase", color: "var(--faint)" } }, label), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 21, fontWeight: 700, letterSpacing: -0.5, marginTop: 5, color: tone || "var(--text)" } }, value), sub && /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 11, color: "var(--dim)", marginTop: 2 } }, sub));
2082
+ }
2083
+ function MyNode({ T, me, activeExit, peers, reqCount = 0 }) {
2084
+ const online = peers.filter((p) => p.online).length;
2085
+ const direct = peers.filter((p) => p.online && p.via === "direct").length;
2086
+ return /* @__PURE__ */ React.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 12 } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 14, padding: "16px 18px", borderRadius: 12, background: "var(--panel)", border: "1px solid var(--line)" } }, /* @__PURE__ */ React.createElement("div", { style: { position: "relative" } }, /* @__PURE__ */ React.createElement(DkIdenticon, { seed: me.userId, size: 46, radius: 11 }), /* @__PURE__ */ React.createElement("span", { style: { position: "absolute", right: -3, bottom: -3, width: 14, height: 14, borderRadius: 999, background: "var(--online)", border: "2.5px solid var(--panel)" } })), /* @__PURE__ */ React.createElement("div", { style: { minWidth: 0 } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 9 } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 17, fontWeight: 700, color: "var(--text)" } }, me.name), /* @__PURE__ */ React.createElement(Tag, { tone: "ok" }, "online"), /* @__PURE__ */ React.createElement(Tag, { tone: "accent" }, me.channel), me.isExit && /* @__PURE__ */ React.createElement(Tag, { tone: "warn" }, "exit", me.exitRegion ? ` \xB7 ${me.exitRegion.toUpperCase()}` : "")), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8, marginTop: 5 } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11.5, color: "var(--faint)" } }, T.myIp), /* @__PURE__ */ React.createElement(Mono, { size: 13, copy: me.ip }, me.ip), /* @__PURE__ */ React.createElement("span", { style: { color: "var(--line)" } }, "\xB7"), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11.5, color: "var(--faint)" } }, "wire ", me.wire), /* @__PURE__ */ React.createElement("span", { style: { color: "var(--line)" } }, "\xB7"), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11.5, color: "var(--faint)" } }, "lan ", me.lanVer, " / peer ", me.peerVer))), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement(Btn, { icon: "copy", onClick: () => dkCopy(me.carrier) }, T.copyAddr)), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: 12 } }, /* @__PURE__ */ React.createElement(StatTile, { label: T.peersOnline, value: `${online}/${peers.length}`, sub: `${direct} ${T.direct}`, tone: "var(--online)" }), /* @__PURE__ */ React.createElement(StatTile, { label: T.activeEgress, value: activeExit ? activeExit : T.directEgress, sub: activeExit ? "china \xB7 cn-sh-01" : T.noProxy, tone: activeExit ? "var(--warn)" : "var(--text)" }), /* @__PURE__ */ React.createElement(StatTile, { label: T.wireLabel, value: me.wire, sub: `${me.channel} \xB7 ${T.lossless}` }), /* @__PURE__ */ React.createElement(StatTile, { label: T.reqs, value: String(reqCount), sub: T.pending, tone: reqCount ? "var(--accent)" : "var(--text)" })));
2087
+ }
2088
+ function PeerTable({ T, peers, onOpenChat }) {
2089
+ return /* @__PURE__ */ React.createElement("div", { style: { borderRadius: 11, border: "1px solid var(--line)", overflow: "hidden", background: "var(--panel)" } }, /* @__PURE__ */ React.createElement("div", { style: { display: "grid", gridTemplateColumns: "1.4fr 1fr 1fr 0.8fr 90px", gap: 0, padding: "9px 16px", borderBottom: "1px solid var(--line)", background: "var(--panel-2)" } }, [T.colPeer, T.colVip, T.colPath, T.colWire, ""].map((h, i) => /* @__PURE__ */ React.createElement("span", { key: i, style: { fontFamily: "var(--mono)", fontSize: 10.5, fontWeight: 700, letterSpacing: 0.8, textTransform: "uppercase", color: "var(--faint)", textAlign: i === 4 ? "right" : "left" } }, h))), peers.map((p, i) => /* @__PURE__ */ React.createElement("div", { key: p.id, style: {
2090
+ display: "grid",
2091
+ gridTemplateColumns: "1.4fr 1fr 1fr 0.8fr 90px",
2092
+ gap: 0,
2093
+ alignItems: "center",
2094
+ padding: "11px 16px",
2095
+ borderBottom: i < peers.length - 1 ? "1px solid var(--line)" : "none"
2096
+ } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 10, minWidth: 0 } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: p, size: 26, radius: 6 }), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: p.alias ? "var(--ui)" : "var(--mono)", fontSize: 13, fontWeight: 600, color: "var(--text)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } }, p.alias || shortKey(p.userId, 8, 5)), p.agent && /* @__PURE__ */ React.createElement(Icon, { name: "bot", size: 12, color: "var(--faint)", stroke: 2 })), /* @__PURE__ */ React.createElement(Mono, { size: 12.5, dim: true, copy: p.ip }, p.ip), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement(RouteTag, { peer: p })), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11.5, color: p.wire === "64" ? "var(--warn)" : "var(--dim)" } }, p.wire === "64" ? "old\xB764" : "new\xB7163"), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", justifyContent: "flex-end", gap: 6 } }, /* @__PURE__ */ React.createElement(Btn, { icon: "message", size: "sm", title: T.openChat, onClick: () => onOpenChat(p.id) }), /* @__PURE__ */ React.createElement(Btn, { icon: "signal", size: "sm", title: T.ping })))));
2097
+ }
2098
+ function ExitCard({ T, region, activeExit, onSetExit }) {
2099
+ return /* @__PURE__ */ React.createElement("div", { style: { borderRadius: 11, border: "1px solid var(--line)", overflow: "hidden", background: "var(--panel)" } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 10, padding: "11px 14px", borderBottom: "1px solid var(--line)", background: "var(--panel-2)" } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, fontWeight: 700, letterSpacing: 0.5, padding: "2px 7px", borderRadius: 5, background: "var(--chip)", color: "var(--dim)" } }, region.flag), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--ui)", fontSize: 13.5, fontWeight: 600, color: "var(--text)" } }, region.label), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), (() => {
2100
+ const reach = region.nodes.filter((n) => n.reachable).length;
2101
+ const down = region.nodes.length - reach;
2102
+ return /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, color: down > 0 ? "var(--warn)" : "var(--faint)" } }, reach, "/", region.nodes.length, " ", T.up, down > 0 ? ` \xB7 ${down} down` : "");
2103
+ })()), /* @__PURE__ */ React.createElement("div", null, region.nodes.map((n, i) => {
2104
+ const active = n.ip === activeExit;
2105
+ const stuck = n.online && !n.reachable;
2106
+ return /* @__PURE__ */ React.createElement("div", { key: n.ip, style: { display: "flex", alignItems: "center", gap: 12, padding: "10px 14px", borderBottom: i < region.nodes.length - 1 ? "1px solid var(--line)" : "none", background: active ? "color-mix(in oklab, var(--accent), transparent 92%)" : "transparent" } }, /* @__PURE__ */ React.createElement(StatusDot, { online: n.reachable }), /* @__PURE__ */ React.createElement(Mono, { size: 13, copy: n.ip }, n.ip), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, color: "var(--faint)" } }, n.host), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), n.reachable ? /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11.5, color: n.ping > 200 ? "var(--warn)" : "var(--dim)" } }, n.ping, "ms") : stuck ? /* @__PURE__ */ React.createElement(Tag, { tone: "warn", title: "Announces online but IP won't pass \u2014 session desync or NAT-blocked" }, "online \xB7 no route") : /* @__PURE__ */ React.createElement(Tag, { tone: "off" }, "offline"), active ? /* @__PURE__ */ React.createElement(Btn, { tone: "ok", icon: "check", size: "sm", onClick: () => onSetExit(null) }, T.routing) : /* @__PURE__ */ React.createElement(Btn, { size: "sm", icon: "route", onClick: () => n.reachable && onSetExit(n.ip), style: { opacity: n.reachable ? 1 : 0.4 } }, T.routeThru));
2107
+ })));
2108
+ }
2109
+ function NetworkTab({ T, me, peers, exits, activeExit, reqCount, onSetExit, onOpenChat }) {
2110
+ return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, overflow: "auto", background: "var(--bg)" } }, /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 1040, margin: "0 auto", padding: "24px 28px 60px", display: "flex", flexDirection: "column", gap: 26 } }, /* @__PURE__ */ React.createElement(MyNode, { T, me, activeExit, peers, reqCount }), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 12 } }, /* @__PURE__ */ React.createElement(Section, { label: T.peerRouting, count: peers.length }), /* @__PURE__ */ React.createElement(PeerTable, { T, peers, onOpenChat })), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 12 } }, /* @__PURE__ */ React.createElement(Section, { label: T.exitNodes, trailing: /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: 6 } }, /* @__PURE__ */ React.createElement(Btn, { icon: "plus", size: "sm" }, T.addExit)) }), activeExit && /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 10, padding: "11px 14px", borderRadius: 10, background: "color-mix(in oklab, var(--warn), transparent 90%)", border: "1px solid color-mix(in oklab, var(--warn), transparent 70%)" } }, /* @__PURE__ */ React.createElement(Icon, { name: "route", size: 17, color: "var(--warn)", stroke: 2 }), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12.5, color: "var(--text)" } }, T.egressVia), /* @__PURE__ */ React.createElement(Mono, { size: 13, copy: activeExit }, activeExit), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement(Btn, { tone: "danger", icon: "unlink", size: "sm", onClick: () => onSetExit(null) }, T.stopRouting)), /* @__PURE__ */ React.createElement("div", { style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 } }, exits.map((r) => /* @__PURE__ */ React.createElement(ExitCard, { key: r.region, T, region: r, activeExit, onSetExit }))))));
2111
+ }
2112
+ Object.assign(window, { NetworkTab });
2113
+ function CopyBtn({ value, copiedText = "Copied", copyFailedText = "Copy failed", copyTitle = "Copy" }) {
2114
+ const [status, setStatus] = React.useState(null);
2115
+ const timer = React.useRef(null);
2116
+ React.useEffect(() => () => {
2117
+ if (timer.current)
2118
+ clearTimeout(timer.current);
2119
+ }, []);
2120
+ const onCopy = async () => {
2121
+ if (timer.current)
2122
+ clearTimeout(timer.current);
2123
+ setStatus("copied");
2124
+ timer.current = setTimeout(() => setStatus(null), 1400);
2125
+ const ok = await dkCopy(value);
2126
+ if (!ok) {
2127
+ setStatus("failed");
2128
+ if (timer.current)
2129
+ clearTimeout(timer.current);
2130
+ timer.current = setTimeout(() => setStatus(null), 1400);
2131
+ }
2132
+ };
2133
+ const message = status === "copied" ? copiedText : copyFailedText;
2134
+ return /* @__PURE__ */ React.createElement("span", { "aria-live": "polite", style: { display: "inline-flex", flexShrink: 0 } }, /* @__PURE__ */ React.createElement(
2135
+ Btn,
2136
+ {
2137
+ icon: status === "copied" ? "check" : status === "failed" ? "x" : "copy",
2138
+ tone: status === "copied" ? "ok" : status === "failed" ? "danger" : "ghost",
2139
+ size: "sm",
2140
+ title: status ? message : copyTitle,
2141
+ onClick: onCopy
2142
+ }
2143
+ ));
2144
+ }
2145
+ function FieldRow({ T, label, value, mono = true, copy, qr, onQr, last }) {
2146
+ return /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 14, padding: "13px 16px", borderBottom: last ? "none" : "1px solid var(--line)" } }, /* @__PURE__ */ React.createElement("span", { style: { width: 130, flexShrink: 0, fontFamily: "var(--mono)", fontSize: 11.5, fontWeight: 600, color: "var(--faint)", textTransform: "uppercase", letterSpacing: 0.5 } }, label), /* @__PURE__ */ React.createElement("span", { style: { flex: 1, minWidth: 0, fontFamily: mono ? "var(--mono)" : "var(--ui)", fontSize: 13.5, color: "var(--text)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, value), copy && /* @__PURE__ */ React.createElement(CopyBtn, { value, copiedText: T.copied, copyFailedText: T.copyFailed, copyTitle: T.copy }), qr && /* @__PURE__ */ React.createElement(Btn, { icon: "qr", size: "sm", onClick: () => onQr && onQr(value, label) }));
2147
+ }
2148
+ function DkQrModal({ T, value, label, onClose }) {
2149
+ const qr = React.useMemo(() => {
2150
+ if (typeof qrcode === "undefined")
2151
+ return null;
2152
+ try {
2153
+ const q = qrcode(0, "M");
2154
+ q.addData(value);
2155
+ q.make();
2156
+ return q;
2157
+ } catch (e) {
2158
+ return null;
2159
+ }
2160
+ }, [value]);
2161
+ const count = qr ? qr.getModuleCount() : 0;
2162
+ const quiet = 4;
2163
+ const total = count + quiet * 2;
2164
+ const px = 248;
2165
+ return /* @__PURE__ */ React.createElement("div", { onClick: onClose, style: { position: "fixed", inset: 0, zIndex: 90, background: "color-mix(in oklab, #000, transparent 38%)", display: "flex", alignItems: "center", justifyContent: "center", padding: 24 } }, /* @__PURE__ */ React.createElement("div", { onClick: (e) => e.stopPropagation(), style: { background: "var(--panel)", border: "1px solid var(--line)", borderRadius: 16, padding: 22, display: "flex", flexDirection: "column", alignItems: "center", gap: 16, maxWidth: 340 } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 10, alignSelf: "stretch" } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11.5, fontWeight: 700, letterSpacing: 0.5, textTransform: "uppercase", color: "var(--faint)" } }, label), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement(Btn, { icon: "x", size: "sm", onClick: onClose })), qr ? /* @__PURE__ */ React.createElement("svg", { width: px, height: px, viewBox: `0 0 ${total} ${total}`, shapeRendering: "crispEdges", style: { display: "block", borderRadius: 10, background: "#fff" } }, /* @__PURE__ */ React.createElement("rect", { width: total, height: total, fill: "#fff" }), Array.from({ length: count }).map(
2166
+ (_, r) => Array.from({ length: count }).map(
2167
+ (__, c) => qr.isDark(r, c) ? /* @__PURE__ */ React.createElement("rect", { key: `${r}-${c}`, x: c + quiet, y: r + quiet, width: 1.04, height: 1.04, fill: "#000" }) : null
2168
+ )
2169
+ )) : /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 12, color: "var(--faint)", padding: 48 } }, "QR unavailable"), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 11, color: "var(--dim)", wordBreak: "break-all", textAlign: "center", maxWidth: 280, lineHeight: 1.5 } }, value), /* @__PURE__ */ React.createElement(CopyBtn, { value, copiedText: T.copied, copyFailedText: T.copyFailed, copyTitle: T.copy })));
2170
+ }
2171
+ function Card({ label, children, trailing }) {
2172
+ return /* @__PURE__ */ React.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 10 } }, /* @__PURE__ */ React.createElement(Section, { label, trailing }), /* @__PURE__ */ React.createElement("div", { style: { borderRadius: 11, border: "1px solid var(--line)", overflow: "hidden", background: "var(--panel)" } }, children));
2173
+ }
2174
+ function DkEditModal({ T, me, onClose, onSave }) {
2175
+ const [name, setName] = React.useState(me.name || "");
2176
+ const [desc, setDesc] = React.useState(me.description || "");
2177
+ const save = () => {
2178
+ const n = name.trim();
2179
+ if (n)
2180
+ onSave(n, desc);
2181
+ };
2182
+ const field = { height: 38, borderRadius: 9, border: "1px solid var(--line)", background: "var(--panel-2)", color: "var(--text)", fontFamily: "var(--ui)", fontSize: 13.5, padding: "0 12px", outline: "none" };
2183
+ const lbl = { fontFamily: "var(--mono)", fontSize: 11.5, color: "var(--faint)", textTransform: "uppercase", letterSpacing: 0.5 };
2184
+ const onKey = (e) => {
2185
+ if (e.key === "Enter")
2186
+ save();
2187
+ if (e.key === "Escape")
2188
+ onClose();
2189
+ };
2190
+ return /* @__PURE__ */ React.createElement("div", { onClick: onClose, style: { position: "fixed", inset: 0, zIndex: 90, background: "color-mix(in oklab, #000, transparent 38%)", display: "flex", alignItems: "center", justifyContent: "center", padding: 24 } }, /* @__PURE__ */ React.createElement("div", { onClick: (e) => e.stopPropagation(), style: { width: 440, maxWidth: "92vw", background: "var(--panel)", border: "1px solid var(--line)", borderRadius: 16, padding: 22, display: "flex", flexDirection: "column", gap: 14 } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center" } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 13, fontWeight: 700, color: "var(--text)" } }, T.editProfile), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement(Btn, { icon: "x", size: "sm", onClick: onClose })), /* @__PURE__ */ React.createElement("label", { style: { display: "flex", flexDirection: "column", gap: 6 } }, /* @__PURE__ */ React.createElement("span", { style: lbl }, "display name"), /* @__PURE__ */ React.createElement("input", { value: name, onChange: (e) => setName(e.target.value), onKeyDown: onKey, autoFocus: true, maxLength: 48, style: field })), /* @__PURE__ */ React.createElement("label", { style: { display: "flex", flexDirection: "column", gap: 6 } }, /* @__PURE__ */ React.createElement("span", { style: lbl }, "status message"), /* @__PURE__ */ React.createElement("input", { value: desc, onChange: (e) => setDesc(e.target.value), onKeyDown: onKey, maxLength: 120, placeholder: "optional \u2014 a short bio friends will see", style: field })), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 11.5, color: "var(--faint)" } }, "Your userid (the unique identity) can't change \u2014 only the display name + status."), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", justifyContent: "flex-end", gap: 8, marginTop: 2 } }, /* @__PURE__ */ React.createElement(Btn, { size: "sm", onClick: onClose }, T.cancel || "cancel"), /* @__PURE__ */ React.createElement(Btn, { tone: "accent", size: "sm", onClick: save }, T.save || "save"))));
2191
+ }
2192
+ function ProfileTab({ T, me, onEdit }) {
2193
+ const [qr, setQr] = React.useState(null);
2194
+ const [editing, setEditing] = React.useState(false);
2195
+ return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, overflow: "auto", background: "var(--bg)" } }, /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 760, margin: "0 auto", padding: "24px 28px 60px", display: "flex", flexDirection: "column", gap: 24 } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 18, padding: "20px 22px", borderRadius: 14, background: "var(--panel)", border: "1px solid var(--line)" } }, /* @__PURE__ */ React.createElement("div", { style: { position: "relative" } }, /* @__PURE__ */ React.createElement(DkIdenticon, { seed: me.userId, size: 68, radius: 16 }), /* @__PURE__ */ React.createElement("span", { style: { position: "absolute", right: -3, bottom: -3, width: 18, height: 18, borderRadius: 999, background: "var(--online)", border: "3px solid var(--panel)" } })), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0 } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 10 } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 22, fontWeight: 700, letterSpacing: -0.5, color: "var(--text)" } }, me.name), /* @__PURE__ */ React.createElement(Tag, { tone: "ok" }, "online"), me.isExit && /* @__PURE__ */ React.createElement(Tag, { tone: "warn" }, "exit", me.exitRegion ? ` \xB7 ${me.exitRegion.toUpperCase()}` : "")), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 13, color: "var(--dim)", marginTop: 4 } }, me.handle), me.description ? /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 12.5, color: "var(--faint)", marginTop: 3 } }, me.description) : null), /* @__PURE__ */ React.createElement(Btn, { icon: "edit", onClick: () => setEditing(true) }, T.editProfile)), /* @__PURE__ */ React.createElement(Card, { label: T.identity }, /* @__PURE__ */ React.createElement(FieldRow, { T, label: T.userId, value: me.userId, copy: true, qr: true, onQr: (v, l) => setQr({ value: v, label: l }) }), /* @__PURE__ */ React.createElement(FieldRow, { T, label: T.carrierAddr, value: me.carrier, copy: true, qr: true, onQr: (v, l) => setQr({ value: v, label: l }) }), /* @__PURE__ */ React.createElement(FieldRow, { T, label: T.netKey, value: me.netKey, copy: true, last: true })), /* @__PURE__ */ React.createElement(Card, { label: T.network }, /* @__PURE__ */ React.createElement(FieldRow, { T, label: T.virtualIp, value: me.ip, copy: true }), /* @__PURE__ */ React.createElement(FieldRow, { T, label: T.wireLabel, value: `${me.wire} \xB7 lossless`, mono: false }), /* @__PURE__ */ React.createElement(FieldRow, { T, label: T.version, value: `lan ${me.lanVer} \xB7 peer ${me.peerVer} \xB7 ${me.channel}`, last: true })), /* @__PURE__ */ React.createElement(Card, { label: T.dangerZone }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 13, padding: "14px 16px" } }, /* @__PURE__ */ React.createElement("div", { style: { width: 34, height: 34, borderRadius: 8, flexShrink: 0, background: "color-mix(in oklab, var(--danger), transparent 86%)", display: "flex", alignItems: "center", justifyContent: "center", color: "var(--danger)" } }, /* @__PURE__ */ React.createElement(Icon, { name: "trash", size: 17, stroke: 2 })), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 13.5, fontWeight: 600, color: "var(--danger)" } }, T.deleteNode), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 12, color: "var(--faint)", marginTop: 1 } }, T.deleteSub)), /* @__PURE__ */ React.createElement(Btn, { tone: "danger", size: "sm" }, T.delete)))), qr && /* @__PURE__ */ React.createElement(DkQrModal, { T, value: qr.value, label: qr.label, onClose: () => setQr(null) }), editing && /* @__PURE__ */ React.createElement(DkEditModal, { T, me, onClose: () => setEditing(false), onSave: (name, description) => {
2196
+ if (onEdit)
2197
+ onEdit(name, description);
2198
+ setEditing(false);
2199
+ } }));
2200
+ }
2201
+ Object.assign(window, { ProfileTab });
2202
+ const DK_FILE_RTC_KIND = "file";
2203
+ const DK_FILE_RTC_CHUNK = 16 * 1024;
2204
+ const DK_FILE_RTC_OPEN_TIMEOUT_MS = 2e4;
2205
+ const DK_FILE_RTC_SIGNAL_TTL_MS = 10 * 60 * 1e3;
2206
+ function dkFileRtcId() {
2207
+ if (window.crypto && window.crypto.randomUUID)
2208
+ return window.crypto.randomUUID();
2209
+ return "frtc-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2);
2210
+ }
2211
+ function dkFileRtcEnvelope(fileId, type, extra) {
2212
+ return Object.assign({ dkRtc: 1, kind: DK_FILE_RTC_KIND, fileId, type, ts: Date.now() }, extra || {});
2213
+ }
2214
+ function dkRtcState(pc) {
2215
+ if (!pc)
2216
+ return "";
2217
+ return "ice=" + pc.iceConnectionState + " conn=" + pc.connectionState + " gather=" + pc.iceGatheringState;
2218
+ }
2219
+ function dkWaitForOpen(dc, timeoutMs, describeState) {
2220
+ if (dc.readyState === "open")
2221
+ return Promise.resolve();
2222
+ return new Promise((resolve, reject) => {
2223
+ const timer = setTimeout(() => {
2224
+ cleanup();
2225
+ const s = describeState ? describeState() : "";
2226
+ reject(new Error("WebRTC data channel open timed out" + (s ? " (" + s + ")" : "")));
2227
+ }, timeoutMs);
2228
+ const cleanup = () => {
2229
+ clearTimeout(timer);
2230
+ dc.removeEventListener("open", onOpen);
2231
+ dc.removeEventListener("error", onError);
2232
+ dc.removeEventListener("close", onClose);
2233
+ };
2234
+ const onOpen = () => {
2235
+ cleanup();
2236
+ resolve();
2237
+ };
2238
+ const onError = () => {
2239
+ cleanup();
2240
+ const s = describeState ? describeState() : "";
2241
+ reject(new Error("WebRTC data channel failed" + (s ? " (" + s + ")" : "")));
2242
+ };
2243
+ const onClose = () => {
2244
+ cleanup();
2245
+ const s = describeState ? describeState() : "";
2246
+ reject(new Error("WebRTC data channel closed before open" + (s ? " (" + s + ")" : "")));
2247
+ };
2248
+ dc.addEventListener("open", onOpen);
2249
+ dc.addEventListener("error", onError);
2250
+ dc.addEventListener("close", onClose);
2251
+ });
2252
+ }
2253
+ function dkWaitBufferedLow(dc, highWater) {
2254
+ if (dc.bufferedAmount < highWater)
2255
+ return Promise.resolve();
2256
+ return new Promise((resolve, reject) => {
2257
+ const prev = dc.bufferedAmountLowThreshold;
2258
+ dc.bufferedAmountLowThreshold = Math.floor(highWater / 2);
2259
+ let poll = null;
2260
+ let timer = null;
2261
+ const done = () => {
2262
+ cleanup();
2263
+ resolve();
2264
+ };
2265
+ const cleanup = () => {
2266
+ if (poll)
2267
+ clearInterval(poll);
2268
+ if (timer)
2269
+ clearTimeout(timer);
2270
+ dc.removeEventListener("bufferedamountlow", done);
2271
+ dc.removeEventListener("close", onClose);
2272
+ dc.removeEventListener("error", onError);
2273
+ dc.bufferedAmountLowThreshold = prev;
2274
+ };
2275
+ const check = () => {
2276
+ if (dc.readyState !== "open")
2277
+ return onClose();
2278
+ if (dc.bufferedAmount < highWater)
2279
+ done();
2280
+ };
2281
+ const onClose = () => {
2282
+ cleanup();
2283
+ reject(new Error("WebRTC data channel closed while draining"));
2284
+ };
2285
+ const onError = () => {
2286
+ cleanup();
2287
+ reject(new Error("WebRTC data channel failed while draining"));
2288
+ };
2289
+ dc.addEventListener("bufferedamountlow", done);
2290
+ dc.addEventListener("close", onClose);
2291
+ dc.addEventListener("error", onError);
2292
+ poll = setInterval(check, 250);
2293
+ timer = setTimeout(() => {
2294
+ cleanup();
2295
+ reject(new Error("WebRTC data channel drain timed out buffered=" + dc.bufferedAmount));
2296
+ }, 3e4);
2297
+ });
2298
+ }
2299
+ function dkFileDownload(blob, name) {
2300
+ const url = URL.createObjectURL(blob);
2301
+ const a = document.createElement("a");
2302
+ a.href = url;
2303
+ a.download = name || "agentnet-file";
2304
+ a.textContent = name || "agentnet-file";
2305
+ return { url, name: a.download };
2306
+ }
2307
+ function dkRememberRtcFile(peerId, fileId, name, size, url) {
2308
+ if (!window.__dkRtcFiles)
2309
+ window.__dkRtcFiles = /* @__PURE__ */ new Map();
2310
+ const item = { id: fileId, peerId, name, size, url, media: dkFileMediaKind(name) };
2311
+ window.__dkRtcFiles.set(peerId + "|" + name + "|" + size, item);
2312
+ return item;
2313
+ }
2314
+ function useRtcFileController(selfId, onReceivedFile) {
2315
+ const [incoming, setIncoming] = React.useState([]);
2316
+ const peersRef = React.useRef(/* @__PURE__ */ new Map());
2317
+ const onReceivedFileRef = React.useRef(onReceivedFile);
2318
+ React.useEffect(() => {
2319
+ onReceivedFileRef.current = onReceivedFile;
2320
+ }, [onReceivedFile]);
2321
+ React.useEffect(() => {
2322
+ if (!selfId || !window.RTCPeerConnection || !window.dkRtcSignalBus)
2323
+ return;
2324
+ const bus = dkRtcSignalBus();
2325
+ const cleanup = (fileId) => {
2326
+ const sess = peersRef.current.get(fileId);
2327
+ if (!sess)
2328
+ return;
2329
+ try {
2330
+ sess.dc && sess.dc.close();
2331
+ } catch (e) {
2332
+ }
2333
+ try {
2334
+ sess.pc && sess.pc.close();
2335
+ } catch (e) {
2336
+ }
2337
+ peersRef.current.delete(fileId);
2338
+ };
2339
+ const sendSignal = (peerId, fileId, type, extra) => bus.send(peerId, dkFileRtcEnvelope(fileId, type, extra));
2340
+ const handleDataChannel = (peerId, fileId, pc, dc, initialMeta) => {
2341
+ dc.binaryType = "arraybuffer";
2342
+ const prev = peersRef.current.get(fileId);
2343
+ const sess = Object.assign(prev || {}, { pc, dc, peerId, meta: initialMeta || null, chunks: [], received: 0 });
2344
+ peersRef.current.set(fileId, sess);
2345
+ dc.onmessage = (ev) => {
2346
+ if (typeof ev.data === "string") {
2347
+ let msg = null;
2348
+ try {
2349
+ msg = JSON.parse(ev.data);
2350
+ } catch (e) {
2351
+ }
2352
+ if (!msg)
2353
+ return;
2354
+ if (msg.type === "meta") {
2355
+ sess.meta = msg;
2356
+ sess.chunks = [];
2357
+ sess.received = 0;
2358
+ } else if (msg.type === "done") {
2359
+ const meta = sess.meta || {};
2360
+ const blob = new Blob(sess.chunks, { type: meta.mime || "application/octet-stream" });
2361
+ const dl = dkFileDownload(blob, meta.name || "agentnet-file");
2362
+ dkRememberRtcFile(peerId, fileId, dl.name, blob.size, dl.url);
2363
+ setIncoming((arr) => [{ id: fileId, peerId, name: dl.name, size: blob.size, url: dl.url }].concat(arr).slice(0, 8));
2364
+ if (onReceivedFileRef.current)
2365
+ onReceivedFileRef.current(peerId, dl.name, blob);
2366
+ cleanup(fileId);
2367
+ } else if (msg.type === "cancel") {
2368
+ cleanup(fileId);
2369
+ }
2370
+ return;
2371
+ }
2372
+ sess.chunks.push(ev.data);
2373
+ sess.received += ev.data.byteLength || ev.data.size || 0;
2374
+ };
2375
+ dc.onclose = () => setTimeout(() => cleanup(fileId), 1e3);
2376
+ dc.onerror = () => cleanup(fileId);
2377
+ };
2378
+ const off = bus.on(DK_FILE_RTC_KIND, async (peerId, env) => {
2379
+ if (!env || !env.fileId || !env.type)
2380
+ return;
2381
+ if (Date.now() - (env.ts || Date.now()) > DK_FILE_RTC_SIGNAL_TTL_MS)
2382
+ return;
2383
+ const fileId = env.fileId;
2384
+ let sess = peersRef.current.get(fileId);
2385
+ try {
2386
+ if (env.type === "offer") {
2387
+ if (sess && sess.pc && sess.pc.signalingState !== "closed") {
2388
+ console.warn("[file-rtc] duplicate offer ignored for " + fileId);
2389
+ return;
2390
+ }
2391
+ const pc = new RTCPeerConnection({ iceServers: CALL_ICE_SERVERS });
2392
+ pc.oniceconnectionstatechange = () => console.warn("[file-rtc] receiver ice=" + pc.iceConnectionState + " conn=" + pc.connectionState);
2393
+ pc.onconnectionstatechange = () => console.warn("[file-rtc] receiver conn=" + pc.connectionState + " ice=" + pc.iceConnectionState);
2394
+ pc.onicecandidate = (ev) => {
2395
+ if (ev.candidate)
2396
+ sendSignal(peerId, fileId, "candidate", { candidate: ev.candidate.toJSON() }).catch(() => {
2397
+ });
2398
+ };
2399
+ pc.ondatachannel = (ev) => handleDataChannel(peerId, fileId, pc, ev.channel, env.meta || null);
2400
+ peersRef.current.set(fileId, { pc, dc: null, peerId, chunks: [], meta: env.meta || null, pendingCandidates: [] });
2401
+ await pc.setRemoteDescription({ type: "offer", sdp: env.sdp });
2402
+ sess = peersRef.current.get(fileId);
2403
+ if (sess && sess.pendingCandidates) {
2404
+ for (const c of sess.pendingCandidates.splice(0)) {
2405
+ try {
2406
+ await pc.addIceCandidate(c);
2407
+ } catch (e) {
2408
+ }
2409
+ }
2410
+ }
2411
+ const answer = await pc.createAnswer();
2412
+ await pc.setLocalDescription(answer);
2413
+ await sendSignal(peerId, fileId, "answer", { sdp: answer.sdp });
2414
+ } else if (env.type === "answer" && sess) {
2415
+ if (sess.pc.signalingState !== "have-local-offer") {
2416
+ console.warn("[file-rtc] stale answer ignored for " + fileId + " state=" + sess.pc.signalingState);
2417
+ return;
2418
+ }
2419
+ await sess.pc.setRemoteDescription({ type: "answer", sdp: env.sdp });
2420
+ if (sess.pendingCandidates) {
2421
+ for (const c of sess.pendingCandidates.splice(0)) {
2422
+ try {
2423
+ await sess.pc.addIceCandidate(c);
2424
+ } catch (e) {
2425
+ }
2426
+ }
2427
+ }
2428
+ } else if (env.type === "candidate" && sess && env.candidate) {
2429
+ if (sess.pc.remoteDescription) {
2430
+ try {
2431
+ await sess.pc.addIceCandidate(env.candidate);
2432
+ } catch (e) {
2433
+ }
2434
+ } else {
2435
+ if (!sess.pendingCandidates)
2436
+ sess.pendingCandidates = [];
2437
+ sess.pendingCandidates.push(env.candidate);
2438
+ }
2439
+ } else if (env.type === "bye") {
2440
+ cleanup(fileId);
2441
+ }
2442
+ } catch (e) {
2443
+ console.warn("[file-rtc] signal failed", e);
2444
+ cleanup(fileId);
2445
+ }
2446
+ });
2447
+ return () => {
2448
+ off();
2449
+ for (const id of Array.from(peersRef.current.keys()))
2450
+ cleanup(id);
2451
+ };
2452
+ }, [selfId]);
2453
+ const sendFile = React.useCallback(async (peerId, file, onProgress) => {
2454
+ if (!peerId || !file || !window.RTCPeerConnection || !window.dkRtcSignalBus) {
2455
+ return { ok: false, fallback: true, error: "WebRTC unavailable" };
2456
+ }
2457
+ const bus = dkRtcSignalBus();
2458
+ const fileId = dkFileRtcId();
2459
+ const pc = new RTCPeerConnection({ iceServers: CALL_ICE_SERVERS });
2460
+ const dc = pc.createDataChannel("agentnet-file", { ordered: true });
2461
+ const sess = { pc, dc, peerId, chunks: [], meta: null, pendingCandidates: [] };
2462
+ peersRef.current.set(fileId, sess);
2463
+ const cleanup = () => {
2464
+ try {
2465
+ dc.close();
2466
+ } catch (e) {
2467
+ }
2468
+ try {
2469
+ pc.close();
2470
+ } catch (e) {
2471
+ }
2472
+ peersRef.current.delete(fileId);
2473
+ };
2474
+ const sendSignal = (type, extra) => bus.send(peerId, dkFileRtcEnvelope(fileId, type, extra));
2475
+ try {
2476
+ pc.oniceconnectionstatechange = () => console.warn("[file-rtc] sender ice=" + pc.iceConnectionState + " conn=" + pc.connectionState);
2477
+ pc.onconnectionstatechange = () => console.warn("[file-rtc] sender conn=" + pc.connectionState + " ice=" + pc.iceConnectionState);
2478
+ pc.onicecandidate = (ev) => {
2479
+ if (ev.candidate)
2480
+ sendSignal("candidate", { candidate: ev.candidate.toJSON() }).catch(() => {
2481
+ });
2482
+ };
2483
+ const offer = await pc.createOffer();
2484
+ await pc.setLocalDescription(offer);
2485
+ await sendSignal("offer", {
2486
+ sdp: offer.sdp,
2487
+ meta: { name: file.name, size: file.size, mime: file.type || "application/octet-stream" }
2488
+ });
2489
+ await dkWaitForOpen(dc, DK_FILE_RTC_OPEN_TIMEOUT_MS, () => dkRtcState(pc));
2490
+ dc.send(JSON.stringify({ type: "meta", name: file.name, size: file.size, mime: file.type || "application/octet-stream" }));
2491
+ let sent = 0;
2492
+ const startedAt = Date.now();
2493
+ let lastProgressAt = 0;
2494
+ const highWater = 4 * 1024 * 1024;
2495
+ const reportProgress = (force) => {
2496
+ if (!onProgress)
2497
+ return;
2498
+ const now = Date.now();
2499
+ if (!force && now - lastProgressAt < 250 && sent < file.size)
2500
+ return;
2501
+ lastProgressAt = now;
2502
+ const elapsedMs = Math.max(1, now - startedAt);
2503
+ const kbps = sent / 1024 / (elapsedMs / 1e3);
2504
+ onProgress({
2505
+ sent,
2506
+ size: file.size,
2507
+ pct: file.size ? sent / file.size * 100 : 100,
2508
+ kbps,
2509
+ elapsedMs,
2510
+ via: "webrtc"
2511
+ });
2512
+ };
2513
+ reportProgress(true);
2514
+ while (sent < file.size) {
2515
+ if (dc.readyState !== "open")
2516
+ throw new Error("WebRTC data channel closed");
2517
+ const end = Math.min(file.size, sent + DK_FILE_RTC_CHUNK);
2518
+ const buf = await file.slice(sent, end).arrayBuffer();
2519
+ dc.send(buf);
2520
+ sent = end;
2521
+ reportProgress(false);
2522
+ await dkWaitBufferedLow(dc, highWater);
2523
+ }
2524
+ reportProgress(true);
2525
+ dc.send(JSON.stringify({ type: "done" }));
2526
+ await sendSignal("bye", { reason: "done" }).catch(() => {
2527
+ });
2528
+ setTimeout(cleanup, 1500);
2529
+ return { ok: true, via: "webrtc", fileId };
2530
+ } catch (e) {
2531
+ await sendSignal("bye", { reason: "fallback" }).catch(() => {
2532
+ });
2533
+ cleanup();
2534
+ return { ok: false, fallback: true, error: e && e.message || String(e) };
2535
+ }
2536
+ }, []);
2537
+ const dismissIncoming = React.useCallback((id) => {
2538
+ setIncoming((arr) => {
2539
+ const item = arr.find((x) => x.id === id);
2540
+ if (item && item.url)
2541
+ setTimeout(() => URL.revokeObjectURL(item.url), 1e3);
2542
+ return arr.filter((x) => x.id !== id);
2543
+ });
2544
+ }, []);
2545
+ return { incoming, sendFile, dismissIncoming };
2546
+ }
2547
+ function RtcFileInbox({ T, peers, ctl }) {
2548
+ const items = ctl && ctl.incoming || [];
2549
+ if (!items.length)
2550
+ return null;
2551
+ const closeLabel = T && T.receivedClose || "Close";
2552
+ const downloadLabel = T && T.downloadFile || "Download";
2553
+ return /* @__PURE__ */ React.createElement("div", { style: { position: "fixed", right: 72, bottom: 18, zIndex: 90, width: 360, maxWidth: "calc(100vw - 96px)", display: "flex", flexDirection: "column", gap: 10 } }, items.map((it) => {
2554
+ const peer = (peers || []).find((p) => p.id === it.peerId || p.userId === it.peerId) || {};
2555
+ const who = peer.alias || peer.name || shortKey(it.peerId, 8, 5);
2556
+ return /* @__PURE__ */ React.createElement("div", { key: it.id, style: { border: "1px solid var(--line)", background: "var(--panel)", borderRadius: 12, padding: 12, boxShadow: "0 14px 40px rgba(0,0,0,0.35)" } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 12, color: "var(--faint)", marginBottom: 6 } }, T && T.receivedFile || "Received file", " \xB7 ", who), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 13, fontWeight: 700, color: "var(--text)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, it.name), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 11, color: "var(--faint)", marginTop: 3 } }, dkFileSize(it.size)), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", justifyContent: "flex-end", gap: 8, marginTop: 10, paddingRight: 2 } }, /* @__PURE__ */ React.createElement("button", { onClick: () => ctl.dismissIncoming(it.id), style: { height: 30, minWidth: 58, borderRadius: 8, border: "1px solid var(--line)", background: "transparent", color: "var(--dim)", cursor: "pointer" } }, closeLabel), /* @__PURE__ */ React.createElement("a", { href: it.url, download: it.name, onClick: () => setTimeout(() => ctl.dismissIncoming(it.id), 500), style: { height: 30, minWidth: 76, textAlign: "center", borderRadius: 8, padding: "6px 10px", background: "var(--accent)", color: "#fff", textDecoration: "none", fontFamily: "var(--mono)", fontSize: 12 } }, downloadLabel)));
2557
+ }));
2558
+ }
2559
+ Object.assign(window, { useRtcFileController, RtcFileInbox });
2560
+ const CALL_ICE_SERVERS = [
2561
+ { urls: "stun:stun.l.google.com:19302" },
2562
+ { urls: "turn:tokyo.fi.chat:3478", username: "allcom", credential: "allcompass" },
2563
+ { urls: "turn:tokyo.fi.chat:3478?transport=tcp", username: "allcom", credential: "allcompass" }
2564
+ ];
2565
+ const VIDEO_CODEC_KEEP = /^video\/(H264|VP8|rtx|red|ulpfec)$/i;
2566
+ const SDP_BUDGET = 8192;
2567
+ function trimVideoCodecs(pc) {
2568
+ var _a, _b, _c, _d, _e, _f, _g;
2569
+ const supported = (_c = (_b = (_a = window.RTCRtpSender) == null ? void 0 : _a.getCapabilities) == null ? void 0 : _b.call(_a, "video")) == null ? void 0 : _c.codecs;
2570
+ if (!supported)
2571
+ return;
2572
+ const keep = supported.filter((c) => VIDEO_CODEC_KEEP.test(c.mimeType));
2573
+ if (!keep.length || keep.length === supported.length)
2574
+ return;
2575
+ for (const t of pc.getTransceivers()) {
2576
+ const kind = ((_e = (_d = t.receiver) == null ? void 0 : _d.track) == null ? void 0 : _e.kind) || ((_g = (_f = t.sender) == null ? void 0 : _f.track) == null ? void 0 : _g.kind);
2577
+ if (kind !== "video" || !t.setCodecPreferences)
2578
+ continue;
2579
+ try {
2580
+ t.setCodecPreferences(keep);
2581
+ } catch (e) {
2582
+ console.warn("[call] setCodecPreferences failed", e);
2583
+ }
2584
+ }
2585
+ }
2586
+ function createLeanPeerConnection(config) {
2587
+ const pc = new RTCPeerConnection(config);
2588
+ for (const method of ["createOffer", "createAnswer"]) {
2589
+ const original = pc[method].bind(pc);
2590
+ pc[method] = async (...args) => {
2591
+ trimVideoCodecs(pc);
2592
+ const desc = await original(...args);
2593
+ const bytes = new TextEncoder().encode(desc.sdp || "").length;
2594
+ console[bytes > SDP_BUDGET ? "warn" : "log"](
2595
+ `[call] ${method} SDP ${bytes}B / ${SDP_BUDGET}B budget` + (bytes > SDP_BUDGET ? " \u2014 too large for the Carrier invite channel, falling back to the offline path" : "")
2596
+ );
2597
+ return desc;
2598
+ };
2599
+ }
2600
+ return pc;
2601
+ }
2602
+ function useCallController(selfId, onCallLog) {
2603
+ const [incoming, setIncoming] = React.useState(null);
2604
+ const [active, setActive] = React.useState(null);
2605
+ const [localStream, setLocalStream] = React.useState(null);
2606
+ const [remoteStream, setRemoteStream] = React.useState(null);
2607
+ const [sharing, setSharing] = React.useState(false);
2608
+ const engineRef = React.useRef(null);
2609
+ const onCallLogRef = React.useRef(onCallLog);
2610
+ React.useEffect(() => {
2611
+ onCallLogRef.current = onCallLog;
2612
+ }, [onCallLog]);
2613
+ React.useEffect(() => {
2614
+ if (!selfId || engineRef.current)
2615
+ return;
2616
+ if (!window.PeerWebRTC || !window.RTCPeerConnection) {
2617
+ console.warn("[call] WebRTC or peer-webrtc unavailable \u2014 calls disabled");
2618
+ return;
2619
+ }
2620
+ const { CallEngine } = window.PeerWebRTC;
2621
+ const signaling = makeDaemonSignaling();
2622
+ const engine = new CallEngine({
2623
+ selfId,
2624
+ signaling,
2625
+ createPeerConnection: (c) => createLeanPeerConnection(c),
2626
+ getLocalMedia: (k) => navigator.mediaDevices.getUserMedia({ audio: k.audio, video: k.video }),
2627
+ getDisplayMedia: (c) => navigator.mediaDevices.getDisplayMedia(c || { video: true, audio: false }),
2628
+ iceServers: CALL_ICE_SERVERS,
2629
+ logger: (m) => console.log(m)
2630
+ });
2631
+ engine.on("incomingCall", (info) => {
2632
+ setIncoming(info);
2633
+ if (onCallLogRef.current)
2634
+ onCallLogRef.current(info.peerId, !!info.video, "incoming");
2635
+ });
2636
+ engine.on("stateChanged", (info, state) => setActive((a) => a && a.callId === info.callId ? { ...a, state } : a));
2637
+ engine.on("localStream", (id, s) => {
2638
+ setLocalStream(s);
2639
+ try {
2640
+ setSharing(engine.isSharingScreen(id));
2641
+ } catch (e) {
2642
+ }
2643
+ });
2644
+ engine.on("remoteStream", (id, s) => setRemoteStream(s));
2645
+ engine.on("ended", (id) => {
2646
+ setActive((a) => a && a.callId === id ? null : a);
2647
+ setIncoming((i) => i && i.callId === id ? null : i);
2648
+ setLocalStream(null);
2649
+ setRemoteStream(null);
2650
+ });
2651
+ engineRef.current = engine;
2652
+ return () => {
2653
+ try {
2654
+ signaling.stop();
2655
+ engine.dispose();
2656
+ } catch (e) {
2657
+ }
2658
+ engineRef.current = null;
2659
+ };
2660
+ }, [selfId]);
2661
+ const start = React.useCallback((peerId, video) => {
2662
+ const eng = engineRef.current;
2663
+ if (!eng) {
2664
+ alert("Calls unavailable (WebRTC not supported here)");
2665
+ return;
2666
+ }
2667
+ setActive({ callId: null, peerId, video: !!video, direction: "outgoing", state: "ringing" });
2668
+ if (onCallLogRef.current)
2669
+ onCallLogRef.current(peerId, !!video, "outgoing");
2670
+ eng.call(peerId, { audio: true, video: !!video }).then((callId) => setActive((a) => a && a.peerId === peerId && a.callId === null ? { ...a, callId } : a)).catch((e) => {
2671
+ setActive((a) => a && a.peerId === peerId ? null : a);
2672
+ alert("Could not start call: " + (e && e.message || e));
2673
+ });
2674
+ }, []);
2675
+ const accept = React.useCallback(async () => {
2676
+ const eng = engineRef.current;
2677
+ setIncoming((inc) => {
2678
+ if (eng && inc) {
2679
+ setActive({ callId: inc.callId, peerId: inc.peerId, video: inc.video, direction: "incoming", state: "connecting" });
2680
+ eng.accept(inc.callId).catch((e) => alert("Accept failed: " + (e && e.message || e)));
2681
+ }
2682
+ return null;
2683
+ });
2684
+ }, []);
2685
+ const reject = React.useCallback(() => {
2686
+ const eng = engineRef.current;
2687
+ setIncoming((inc) => {
2688
+ if (eng && inc)
2689
+ eng.reject(inc.callId);
2690
+ return null;
2691
+ });
2692
+ }, []);
2693
+ const hangup = React.useCallback(() => {
2694
+ const eng = engineRef.current;
2695
+ setActive((a) => {
2696
+ if (eng && a && a.callId)
2697
+ eng.hangup(a.callId);
2698
+ return null;
2699
+ });
2700
+ setLocalStream(null);
2701
+ setRemoteStream(null);
2702
+ }, []);
2703
+ const setMuted = React.useCallback((m) => {
2704
+ const eng = engineRef.current, a = active;
2705
+ if (eng && a && a.callId)
2706
+ eng.setLocalTrackEnabled(a.callId, "audio", !m);
2707
+ }, [active]);
2708
+ const toggleShare = React.useCallback(async () => {
2709
+ const eng = engineRef.current, a = active;
2710
+ if (!eng || !a || !a.callId)
2711
+ return;
2712
+ try {
2713
+ if (eng.isSharingScreen(a.callId))
2714
+ await eng.stopScreenShare(a.callId);
2715
+ else
2716
+ await eng.shareScreen(a.callId);
2717
+ } catch (e) {
2718
+ if (e && e.name !== "NotAllowedError")
2719
+ alert("Screen share failed: " + (e && e.message || e));
2720
+ }
2721
+ }, [active]);
2722
+ const setVideoOn = React.useCallback((v) => {
2723
+ const eng = engineRef.current, a = active;
2724
+ if (eng && a && a.callId)
2725
+ eng.setLocalTrackEnabled(a.callId, "video", v);
2726
+ }, [active]);
2727
+ return { incoming, active, localStream, remoteStream, sharing, start, accept, reject, hangup, setMuted, setVideoOn, toggleShare };
2728
+ }
2729
+ function VideoSurface({ stream, muted, style }) {
2730
+ const ref = React.useCallback((el) => {
2731
+ if (el && el.srcObject !== (stream || null))
2732
+ el.srcObject = stream || null;
2733
+ }, [stream]);
2734
+ return /* @__PURE__ */ React.createElement("video", { ref, autoPlay: true, playsInline: true, muted: !!muted, style });
2735
+ }
2736
+ function AudioSink({ stream }) {
2737
+ const ref = React.useCallback((el) => {
2738
+ if (el && el.srcObject !== (stream || null))
2739
+ el.srcObject = stream || null;
2740
+ }, [stream]);
2741
+ return /* @__PURE__ */ React.createElement("audio", { ref, autoPlay: true });
2742
+ }
2743
+ function useRingtone(active) {
2744
+ React.useEffect(() => {
2745
+ if (!active)
2746
+ return void 0;
2747
+ let ctx;
2748
+ try {
2749
+ ctx = new (window.AudioContext || window.webkitAudioContext)();
2750
+ } catch (e) {
2751
+ return void 0;
2752
+ }
2753
+ if (ctx.state === "suspended") {
2754
+ ctx.resume().catch(() => {
2755
+ });
2756
+ }
2757
+ let stopped = false;
2758
+ const timers = [];
2759
+ const burst = () => {
2760
+ if (stopped || ctx.state === "closed")
2761
+ return;
2762
+ [440, 480].forEach((f) => {
2763
+ const o = ctx.createOscillator(), g = ctx.createGain();
2764
+ o.type = "sine";
2765
+ o.frequency.value = f;
2766
+ o.connect(g);
2767
+ g.connect(ctx.destination);
2768
+ const t = ctx.currentTime;
2769
+ g.gain.setValueAtTime(1e-4, t);
2770
+ g.gain.exponentialRampToValueAtTime(0.12, t + 0.05);
2771
+ g.gain.setValueAtTime(0.12, t + 0.9);
2772
+ g.gain.exponentialRampToValueAtTime(1e-4, t + 1);
2773
+ o.start(t);
2774
+ o.stop(t + 1.05);
2775
+ });
2776
+ timers.push(setTimeout(burst, 3e3));
2777
+ };
2778
+ burst();
2779
+ return () => {
2780
+ stopped = true;
2781
+ timers.forEach(clearTimeout);
2782
+ try {
2783
+ ctx.close();
2784
+ } catch (e) {
2785
+ }
2786
+ };
2787
+ }, [active]);
2788
+ }
2789
+ function IncomingCallModal({ T, ctl, peers }) {
2790
+ const inc = ctl.incoming;
2791
+ useRingtone(!!inc);
2792
+ if (!inc)
2793
+ return null;
2794
+ const peer = (peers || []).find((p) => p.id === inc.peerId) || { id: inc.peerId };
2795
+ const name = peer.alias || peer.name || shortKey(inc.peerId, 10, 6);
2796
+ return /* @__PURE__ */ React.createElement("div", { style: { position: "fixed", inset: 0, zIndex: 80, background: "color-mix(in oklab, var(--bg), transparent 15%)", display: "flex", alignItems: "center", justifyContent: "center" } }, /* @__PURE__ */ React.createElement("div", { style: { width: 320, padding: 26, borderRadius: 18, background: "var(--panel)", border: "1px solid var(--line)", textAlign: "center", boxShadow: "0 20px 60px rgba(0,0,0,0.4)" } }, /* @__PURE__ */ React.createElement("div", { style: { position: "relative", display: "inline-flex", alignItems: "center", justifyContent: "center", marginBottom: 14 } }, /* @__PURE__ */ React.createElement("span", { style: { position: "absolute", width: 96, height: 96, borderRadius: 999, border: "2px solid var(--accent)", animation: "dkpulse 2s ease-out infinite" } }), /* @__PURE__ */ React.createElement(DkIdenticon, { seed: inc.peerId, size: 72, radius: 18 })), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 17, fontWeight: 700, color: "var(--text)" } }, name), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 12.5, color: "var(--faint)", marginTop: 4, display: "flex", gap: 6, alignItems: "center", justifyContent: "center" } }, /* @__PURE__ */ React.createElement(Icon, { name: inc.video ? "video" : "phone", size: 14, stroke: 2 }), T && (inc.video ? T.incomingVideo : T.incomingAudio) || "Incoming " + (inc.video ? "video" : "audio") + " call"), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: 14, justifyContent: "center", marginTop: 22 } }, /* @__PURE__ */ React.createElement(CallBtn, { icon: "phone", label: T && T.decline || "Decline", danger: true, onClick: ctl.reject }), /* @__PURE__ */ React.createElement(CallBtn, { icon: inc.video ? "video" : "phone", label: T && T.accept || "Accept", active: true, onClick: ctl.accept }))));
2797
+ }
2798
+ function CallBtn({ icon, label, onClick, active, danger, wide }) {
2799
+ const bg = danger ? "var(--danger)" : active ? "var(--accent)" : "var(--chip)";
2800
+ const fg = danger || active ? "#fff" : "var(--text)";
2801
+ return /* @__PURE__ */ React.createElement("div", { style: { display: "flex", flexDirection: "column", alignItems: "center", gap: 8 } }, /* @__PURE__ */ React.createElement("button", { onClick, style: {
2802
+ width: wide ? 76 : 56,
2803
+ height: 56,
2804
+ borderRadius: wide ? 18 : 999,
2805
+ cursor: "pointer",
2806
+ background: bg,
2807
+ color: fg,
2808
+ border: "1px solid " + (active || danger ? "transparent" : "var(--line)"),
2809
+ display: "flex",
2810
+ alignItems: "center",
2811
+ justifyContent: "center"
2812
+ } }, /* @__PURE__ */ React.createElement(Icon, { name: icon, size: 22, stroke: 2 })), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10.5, color: "var(--faint)" } }, label));
2813
+ }
2814
+ function CallOverlay({ T, ctl, peers }) {
2815
+ const call = ctl.active;
2816
+ const [secs, setSecs] = React.useState(0);
2817
+ const [muted, setMuted] = React.useState(false);
2818
+ const [featured, setFeatured] = React.useState("remote");
2819
+ const connected = call && call.state === "connected";
2820
+ React.useEffect(() => {
2821
+ if (!connected)
2822
+ return;
2823
+ const id = setInterval(() => setSecs((s) => s + 1), 1e3);
2824
+ return () => clearInterval(id);
2825
+ }, [connected]);
2826
+ React.useEffect(() => {
2827
+ if (!call) {
2828
+ setSecs(0);
2829
+ setMuted(false);
2830
+ }
2831
+ }, [call]);
2832
+ if (!call)
2833
+ return null;
2834
+ const peer = (peers || []).find((p) => p.id === call.peerId) || { id: call.peerId };
2835
+ const name = peer.alias || peer.name || shortKey(call.peerId, 10, 6);
2836
+ const streamHasVideo = (s) => !!(s && s.getVideoTracks && s.getVideoTracks().length > 0);
2837
+ const video = call.video || ctl.sharing || streamHasVideo(ctl.remoteStream) || streamHasVideo(ctl.localStream);
2838
+ const mm = String(Math.floor(secs / 60)).padStart(2, "0");
2839
+ const ss = String(secs % 60).padStart(2, "0");
2840
+ const stateLabel = connected ? mm + ":" + ss : call.state === "ringing" ? T && T.ringing || "ringing\u2026" : call.state === "connecting" ? T && T.connecting || "connecting\u2026" : call.state;
2841
+ const toggleMute = () => {
2842
+ const n = !muted;
2843
+ setMuted(n);
2844
+ ctl.setMuted(n);
2845
+ };
2846
+ const canShare = connected && typeof navigator !== "undefined" && navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia;
2847
+ const controls = /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: 18, alignItems: "flex-start" } }, /* @__PURE__ */ React.createElement(CallBtn, { icon: muted ? "micFill" : "mic", label: muted ? T && T.unmute || "unmute" : T && T.mute || "mute", active: muted, onClick: toggleMute }), canShare && /* @__PURE__ */ React.createElement(CallBtn, { icon: "monitor", label: ctl.sharing ? T && T.stopShare || "stop share" : T && T.share || "share", active: ctl.sharing, onClick: ctl.toggleShare }), /* @__PURE__ */ React.createElement(CallBtn, { icon: "phone", label: T && T.endCall || "end", danger: true, wide: true, onClick: ctl.hangup }));
2848
+ return /* @__PURE__ */ React.createElement("div", { style: { position: "fixed", inset: 0, zIndex: 70, background: "var(--bg)", display: "flex", flexDirection: "column" } }, ctl.remoteStream && /* @__PURE__ */ React.createElement(AudioSink, { stream: ctl.remoteStream }), /* @__PURE__ */ React.createElement("div", { style: { height: 52, flexShrink: 0, display: "flex", alignItems: "center", gap: 10, padding: "0 18px", borderBottom: "1px solid var(--line)" } }, /* @__PURE__ */ React.createElement(Icon, { name: video ? "video" : "phone", size: 16, color: "var(--accent)", stroke: 2 }), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 13, fontWeight: 700, color: "var(--text)" } }, name), /* @__PURE__ */ React.createElement(Tag, { tone: connected ? "ok" : "warn" }, stateLabel), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), connected && /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 14, fontWeight: 700, color: "var(--text)", letterSpacing: 1 } }, mm, ":", ss)), video ? (() => {
2849
+ const tiles = [
2850
+ { key: "remote", stream: ctl.remoteStream, label: name, mirror: false },
2851
+ { key: "local", stream: ctl.localStream, label: ctl.sharing ? T && T.yourScreen || "Your screen" : T && T.you || "You", mirror: !ctl.sharing }
2852
+ ].filter((t) => streamHasVideo(t.stream));
2853
+ const feat = tiles.find((t) => t.key === featured) || tiles[0] || { key: "remote", stream: ctl.remoteStream, label: name };
2854
+ const others = tiles.filter((t) => t.key !== feat.key);
2855
+ return (
2856
+ // The thumbnail strip and the controls FLOAT over the video instead of
2857
+ // stacking under it. A phone camera is 9:16, so the featured tile is
2858
+ // height-limited — `object-fit: contain` already shows the whole frame,
2859
+ // but every pixel of chrome below the video shrinks the only dimension
2860
+ // that matters. Stacked, those two rows ate ~210px; in a 900px-tall
2861
+ // window that is a third of the portrait image. Overlaying them is also
2862
+ // what every other call app does.
2863
+ /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minHeight: 0, position: "relative", padding: 18 } }, /* @__PURE__ */ React.createElement("div", { style: { width: "100%", height: "100%", position: "relative", borderRadius: 14, overflow: "hidden", background: "#000", border: "1px solid var(--line)" } }, feat.stream ? /* @__PURE__ */ React.createElement(VideoSurface, { stream: feat.stream, muted: true, style: { width: "100%", height: "100%", objectFit: "contain", transform: feat.mirror ? "scaleX(-1)" : "none" } }) : /* @__PURE__ */ React.createElement("div", { style: { width: "100%", height: "100%", display: "flex", alignItems: "center", justifyContent: "center" } }, /* @__PURE__ */ React.createElement(DkIdenticon, { seed: call.peerId, size: 92, radius: 22 })), /* @__PURE__ */ React.createElement("div", { style: { position: "absolute", left: 14, top: 12, padding: "4px 10px", borderRadius: 8, background: "rgba(0,0,0,.55)", color: "#fff", fontFamily: "var(--mono)", fontSize: 12, fontWeight: 600 } }, feat.label), /* @__PURE__ */ React.createElement("div", { style: {
2864
+ position: "absolute",
2865
+ left: 0,
2866
+ right: 0,
2867
+ bottom: 0,
2868
+ paddingTop: 60,
2869
+ display: "flex",
2870
+ flexDirection: "column",
2871
+ alignItems: "center",
2872
+ gap: 12,
2873
+ background: "linear-gradient(to top, rgba(0,0,0,.65), rgba(0,0,0,0))",
2874
+ pointerEvents: "none"
2875
+ } }, others.length > 0 && /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: 12, justifyContent: "center", flexWrap: "wrap", pointerEvents: "auto" } }, others.map((t) => /* @__PURE__ */ React.createElement(
2876
+ "button",
2877
+ {
2878
+ key: t.key,
2879
+ onClick: () => setFeatured(t.key),
2880
+ title: `Show ${t.label}`,
2881
+ style: {
2882
+ position: "relative",
2883
+ width: 150,
2884
+ height: 96,
2885
+ borderRadius: 10,
2886
+ overflow: "hidden",
2887
+ background: "#000",
2888
+ cursor: "pointer",
2889
+ padding: 0,
2890
+ border: "1px solid var(--line)"
2891
+ }
2892
+ },
2893
+ /* @__PURE__ */ React.createElement(VideoSurface, { stream: t.stream, muted: true, style: { width: "100%", height: "100%", objectFit: "contain", transform: t.mirror ? "scaleX(-1)" : "none" } }),
2894
+ /* @__PURE__ */ React.createElement("span", { style: { position: "absolute", left: 6, bottom: 5, padding: "2px 6px", borderRadius: 6, background: "rgba(0,0,0,.6)", color: "#fff", fontSize: 10, fontFamily: "var(--mono)" } }, t.label)
2895
+ ))), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", justifyContent: "center", marginBottom: 16, pointerEvents: "auto" } }, /* @__PURE__ */ React.createElement("div", { style: { padding: "16px 26px", borderRadius: 20, background: "color-mix(in oklab, var(--panel), transparent 8%)", border: "1px solid var(--line)" } }, controls)))))
2896
+ );
2897
+ })() : /* @__PURE__ */ React.createElement("div", { style: { flex: 1, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 22 } }, /* @__PURE__ */ React.createElement("div", { style: { position: "relative", display: "flex", alignItems: "center", justifyContent: "center" } }, !connected && /* @__PURE__ */ React.createElement("span", { style: { position: "absolute", width: 150, height: 150, borderRadius: 999, border: "2px solid var(--accent)", animation: "dkpulse 2s ease-out infinite" } }), /* @__PURE__ */ React.createElement(DkIdenticon, { seed: call.peerId, size: 108, radius: 26 })), /* @__PURE__ */ React.createElement("div", { style: { textAlign: "center" } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 22, fontWeight: 700, color: "var(--text)" } }, name), /* @__PURE__ */ React.createElement("div", { style: { marginTop: 8, fontFamily: "var(--mono)", fontSize: 12.5, color: "var(--faint)" } }, stateLabel)), /* @__PURE__ */ React.createElement("div", { style: { marginTop: 18 } }, controls)));
2898
+ }
2899
+ Object.assign(window, { useCallController, CallOverlay, IncomingCallModal });
2900
+ function dkParseDeepLinkAddress() {
2901
+ try {
2902
+ const h = window.location.hash || "";
2903
+ const qi = h.indexOf("?");
2904
+ if (qi >= 0) {
2905
+ const a = new URLSearchParams(h.slice(qi + 1)).get("address");
2906
+ if (a && a.trim())
2907
+ return a.trim();
2908
+ }
2909
+ const a2 = new URLSearchParams(window.location.search || "").get("address");
2910
+ if (a2 && a2.trim())
2911
+ return a2.trim();
2912
+ } catch (e) {
2913
+ }
2914
+ return "";
2915
+ }
2916
+ const DK_DEFAULTS = (
2917
+ /*EDITMODE-BEGIN*/
2918
+ {
2919
+ "theme": "dark",
2920
+ "accent": "#7B6CF6",
2921
+ "density": "compact",
2922
+ "lang": "en",
2923
+ "startTab": "chat"
2924
+ }
2925
+ );
2926
+ function dkTheme(theme, accent) {
2927
+ const base = {
2928
+ "--ui": "'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace",
2929
+ "--mono": "'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace",
2930
+ "--accent": accent
2931
+ };
2932
+ if (theme === "light")
2933
+ return {
2934
+ ...base,
2935
+ "--ident-l": "0.92",
2936
+ "--bg": "oklch(0.965 0.004 264)",
2937
+ "--panel": "#ffffff",
2938
+ "--panel-2": "oklch(0.975 0.004 264)",
2939
+ "--line": "oklch(0.9 0.006 264)",
2940
+ "--text": "oklch(0.26 0.013 264)",
2941
+ "--dim": "oklch(0.5 0.013 264)",
2942
+ "--faint": "oklch(0.62 0.011 264)",
2943
+ "--chip": "oklch(0.945 0.006 264)",
2944
+ "--online": "oklch(0.62 0.16 150)",
2945
+ "--off": "oklch(0.8 0.01 264)",
2946
+ "--warn": "oklch(0.66 0.15 60)",
2947
+ "--danger": "oklch(0.58 0.21 25)",
2948
+ "--bub-me": accent,
2949
+ "--bub-them": "oklch(0.97 0.004 264)",
2950
+ "--rail": "oklch(0.99 0.003 264)"
2951
+ };
2952
+ return {
2953
+ ...base,
2954
+ "--ident-l": "0.32",
2955
+ "--bg": "oklch(0.165 0.012 264)",
2956
+ "--panel": "oklch(0.205 0.013 264)",
2957
+ "--panel-2": "oklch(0.235 0.014 264)",
2958
+ "--line": "oklch(0.295 0.014 264)",
2959
+ "--text": "oklch(0.93 0.008 264)",
2960
+ "--dim": "oklch(0.68 0.012 264)",
2961
+ "--faint": "oklch(0.52 0.013 264)",
2962
+ "--chip": "oklch(0.27 0.014 264)",
2963
+ "--online": "oklch(0.78 0.17 150)",
2964
+ "--off": "oklch(0.42 0.01 264)",
2965
+ "--warn": "oklch(0.78 0.15 70)",
2966
+ "--danger": "oklch(0.68 0.2 22)",
2967
+ "--bub-me": accent,
2968
+ "--bub-them": "oklch(0.255 0.014 264)",
2969
+ "--rail": "oklch(0.185 0.013 264)"
2970
+ };
2971
+ }
2972
+ const STR = {
2973
+ en: {
2974
+ chat: "Chat",
2975
+ network: "Network",
2976
+ profile: "Profile",
2977
+ addPlaceholder: "paste a friend's carrier address\u2026",
2978
+ add: "Add",
2979
+ search: "search peers / ip\u2026",
2980
+ addSending: "sending friend-request\u2026",
2981
+ addSent: "friend-request sent \u2014 delivered when they\u2019re online; appears here once they accept",
2982
+ addFailed: "could not send",
2983
+ requests: "Friend requests",
2984
+ accept: "accept",
2985
+ reject: "reject",
2986
+ peers: "Peers",
2987
+ call: "Voice call",
2988
+ alias: "Set alias",
2989
+ sendFile: "Send file",
2990
+ sendWebrtcFile: "Send file via WebRTC",
2991
+ block: "Block peer",
2992
+ remove: "Remove friend",
2993
+ receivedFile: "Received file",
2994
+ receivedClose: "Close",
2995
+ downloadFile: "Download",
2996
+ attach: "Attach file",
2997
+ message: "Message",
2998
+ send: "Send",
2999
+ pickPeer: "Select a peer to open the conversation",
3000
+ queued: "queued",
3001
+ open: "open",
3002
+ retry: "Retry",
3003
+ cancel: "Cancel",
3004
+ cancelling: "Cancelling\u2026",
3005
+ cancelled: "Transfer cancelled",
3006
+ retrying: "Retrying\u2026",
3007
+ retried: "Transfer restarted",
3008
+ cancelFailed: "Cancel failed",
3009
+ retryFailed: "Retry failed",
3010
+ cancelMissing: "No active transfer was found. Refresh and try again.",
3011
+ retryMissing: "No retryable transfer or original file is unavailable.",
3012
+ myIp: "my ip",
3013
+ copyAddr: "copy address",
3014
+ peersOnline: "peers online",
3015
+ direct: "direct (LAN)",
3016
+ activeEgress: "active egress",
3017
+ directEgress: "direct",
3018
+ noProxy: "no proxy",
3019
+ wireLabel: "wire",
3020
+ lossless: "lossless",
3021
+ reqs: "requests",
3022
+ pending: "pending inbound",
3023
+ peerRouting: "Peer routing",
3024
+ colPeer: "peer",
3025
+ colVip: "virtual ip",
3026
+ colPath: "path",
3027
+ colWire: "wire",
3028
+ openChat: "Open chat",
3029
+ ping: "Ping",
3030
+ exitNodes: "Exit nodes",
3031
+ addExit: "add exit",
3032
+ up: "up",
3033
+ routing: "routing",
3034
+ routeThru: "route",
3035
+ egressVia: "all egress routed via",
3036
+ stopRouting: "stop",
3037
+ identity: "Identity",
3038
+ userId: "user id",
3039
+ carrierAddr: "carrier address",
3040
+ netKey: "network key",
3041
+ virtualIp: "virtual ip",
3042
+ version: "version",
3043
+ editProfile: "edit",
3044
+ copy: "Copy",
3045
+ copied: "Copied",
3046
+ copyFailed: "Copy failed",
3047
+ dangerZone: "Danger zone",
3048
+ deleteNode: "Delete this node",
3049
+ deleteSub: "Permanently remove identity & keys from this device",
3050
+ delete: "delete",
3051
+ settings: "Settings",
3052
+ call: "Call",
3053
+ connected: "connected",
3054
+ mute: "mute",
3055
+ unmute: "unmute",
3056
+ speaker: "speaker",
3057
+ toVideo: "video",
3058
+ audioOnly: "audio",
3059
+ endCall: "end",
3060
+ you: "you",
3061
+ meeting: "Meeting",
3062
+ meetingRooms: "Meeting rooms",
3063
+ nextVersion: "next version",
3064
+ createRoom: "New room",
3065
+ meetingDesc: "Spin up audio & video rooms across servers and invite peers by virtual IP. Group calls, screen share, and shared exit routing land in the next release."
3066
+ },
3067
+ zh: {
3068
+ chat: "\u804A\u5929",
3069
+ network: "\u7F51\u7EDC",
3070
+ profile: "\u6211\u7684",
3071
+ addPlaceholder: "\u7C98\u8D34\u597D\u53CB\u7684 carrier \u5730\u5740\u2026",
3072
+ add: "\u6DFB\u52A0",
3073
+ search: "\u641C\u7D22\u597D\u53CB / IP\u2026",
3074
+ addSending: "\u6B63\u5728\u53D1\u9001\u597D\u53CB\u8BF7\u6C42\u2026",
3075
+ addSent: "\u597D\u53CB\u8BF7\u6C42\u5DF2\u53D1\u9001 \u2014 \u5BF9\u65B9\u4E0A\u7EBF\u540E\u9001\u8FBE\uFF0C\u63A5\u53D7\u540E\u51FA\u73B0\u5728\u5217\u8868",
3076
+ addFailed: "\u53D1\u9001\u5931\u8D25",
3077
+ requests: "\u597D\u53CB\u8BF7\u6C42",
3078
+ accept: "\u63A5\u53D7",
3079
+ reject: "\u62D2\u7EDD",
3080
+ peers: "\u597D\u53CB",
3081
+ call: "\u8BED\u97F3\u901A\u8BDD",
3082
+ alias: "\u8BBE\u7F6E\u5907\u6CE8",
3083
+ sendFile: "\u53D1\u9001\u6587\u4EF6",
3084
+ sendWebrtcFile: "\u901A\u8FC7 WebRTC \u53D1\u9001\u6587\u4EF6",
3085
+ block: "\u62C9\u9ED1",
3086
+ remove: "\u5220\u9664\u597D\u53CB",
3087
+ receivedFile: "\u6536\u5230\u6587\u4EF6",
3088
+ receivedClose: "\u5173\u95ED",
3089
+ downloadFile: "\u4E0B\u8F7D",
3090
+ attach: "\u9644\u4EF6",
3091
+ message: "\u6D88\u606F",
3092
+ send: "\u53D1\u9001",
3093
+ pickPeer: "\u9009\u62E9\u4E00\u4F4D\u597D\u53CB\u5F00\u59CB\u4F1A\u8BDD",
3094
+ queued: "\u5F85\u53D1\u9001",
3095
+ open: "\u6253\u5F00",
3096
+ retry: "\u91CD\u8BD5",
3097
+ cancel: "\u53D6\u6D88",
3098
+ cancelling: "\u6B63\u5728\u53D6\u6D88\u2026",
3099
+ cancelled: "\u5DF2\u53D6\u6D88\u4F20\u8F93",
3100
+ retrying: "\u6B63\u5728\u91CD\u8BD5\u2026",
3101
+ retried: "\u5DF2\u91CD\u65B0\u5F00\u59CB\u4F20\u8F93",
3102
+ cancelFailed: "\u53D6\u6D88\u5931\u8D25",
3103
+ retryFailed: "\u91CD\u8BD5\u5931\u8D25",
3104
+ cancelMissing: "\u6CA1\u6709\u627E\u5230\u6B63\u5728\u8FDB\u884C\u7684\u4F20\u8F93\uFF1B\u8BF7\u5237\u65B0\u9875\u9762\u540E\u91CD\u8BD5\u3002",
3105
+ retryMissing: "\u6CA1\u6709\u53EF\u91CD\u8BD5\u7684\u4F20\u8F93\uFF0C\u6216\u539F\u6587\u4EF6\u5DF2\u4E0D\u53EF\u7528\u3002",
3106
+ myIp: "\u6211\u7684 IP",
3107
+ copyAddr: "\u590D\u5236\u5730\u5740",
3108
+ peersOnline: "\u5728\u7EBF\u597D\u53CB",
3109
+ direct: "\u76F4\u8FDE (\u5C40\u57DF\u7F51)",
3110
+ activeEgress: "\u5F53\u524D\u51FA\u53E3",
3111
+ directEgress: "\u76F4\u8FDE",
3112
+ noProxy: "\u65E0\u4EE3\u7406",
3113
+ wireLabel: "\u534F\u8BAE",
3114
+ lossless: "\u65E0\u635F",
3115
+ reqs: "\u8BF7\u6C42",
3116
+ pending: "\u5F85\u5904\u7406",
3117
+ peerRouting: "\u8282\u70B9\u8DEF\u7531",
3118
+ colPeer: "\u597D\u53CB",
3119
+ colVip: "\u865A\u62DF IP",
3120
+ colPath: "\u8DEF\u5F84",
3121
+ colWire: "\u534F\u8BAE",
3122
+ openChat: "\u6253\u5F00\u804A\u5929",
3123
+ ping: "Ping",
3124
+ exitNodes: "\u51FA\u53E3\u8282\u70B9",
3125
+ addExit: "\u6DFB\u52A0\u51FA\u53E3",
3126
+ up: "\u5728\u7EBF",
3127
+ routing: "\u4F7F\u7528\u4E2D",
3128
+ routeThru: "\u8DEF\u7531",
3129
+ egressVia: "\u5168\u90E8\u51FA\u53E3\u6D41\u91CF\u7ECF\u7531",
3130
+ stopRouting: "\u505C\u6B62",
3131
+ identity: "\u8EAB\u4EFD",
3132
+ userId: "\u7528\u6237 ID",
3133
+ carrierAddr: "Carrier \u5730\u5740",
3134
+ netKey: "\u7F51\u7EDC\u516C\u94A5",
3135
+ virtualIp: "\u865A\u62DF IP",
3136
+ version: "\u7248\u672C",
3137
+ editProfile: "\u7F16\u8F91",
3138
+ copy: "\u590D\u5236",
3139
+ copied: "\u5DF2\u590D\u5236",
3140
+ copyFailed: "\u590D\u5236\u5931\u8D25",
3141
+ dangerZone: "\u5371\u9669\u64CD\u4F5C",
3142
+ deleteNode: "\u5220\u9664\u6B64\u8282\u70B9",
3143
+ deleteSub: "\u4ECE\u672C\u8BBE\u5907\u6C38\u4E45\u79FB\u9664\u8EAB\u4EFD\u4E0E\u5BC6\u94A5",
3144
+ delete: "\u5220\u9664",
3145
+ settings: "\u8BBE\u7F6E",
3146
+ call: "\u901A\u8BDD",
3147
+ connected: "\u5DF2\u8FDE\u63A5",
3148
+ mute: "\u9759\u97F3",
3149
+ unmute: "\u53D6\u6D88\u9759\u97F3",
3150
+ speaker: "\u626C\u58F0\u5668",
3151
+ toVideo: "\u89C6\u9891",
3152
+ audioOnly: "\u8BED\u97F3",
3153
+ endCall: "\u6302\u65AD",
3154
+ you: "\u6211",
3155
+ meeting: "\u4F1A\u8BAE",
3156
+ meetingRooms: "\u4F1A\u8BAE\u623F\u95F4",
3157
+ nextVersion: "\u4E0B\u4E2A\u7248\u672C",
3158
+ createRoom: "\u65B0\u5EFA\u623F\u95F4",
3159
+ meetingDesc: "\u8DE8\u670D\u52A1\u5668\u521B\u5EFA\u97F3\u89C6\u9891\u623F\u95F4\uFF0C\u901A\u8FC7\u865A\u62DF IP \u9080\u8BF7\u597D\u53CB\u3002\u7FA4\u7EC4\u901A\u8BDD\u3001\u5C4F\u5E55\u5171\u4EAB\u4E0E\u5171\u4EAB\u51FA\u53E3\u8DEF\u7531\u5C06\u5728\u4E0B\u4E2A\u7248\u672C\u4E0A\u7EBF\u3002"
3160
+ }
3161
+ };
3162
+ function RailBtn({ icon, label, active, soon, onClick }) {
3163
+ return /* @__PURE__ */ React.createElement("button", { onClick, title: label, style: {
3164
+ width: 52,
3165
+ height: 52,
3166
+ borderRadius: 11,
3167
+ cursor: "pointer",
3168
+ fontFamily: "var(--mono)",
3169
+ position: "relative",
3170
+ border: "1px solid " + (active ? "var(--line)" : "transparent"),
3171
+ background: active ? "var(--panel-2)" : "transparent",
3172
+ display: "flex",
3173
+ flexDirection: "column",
3174
+ alignItems: "center",
3175
+ justifyContent: "center",
3176
+ gap: 3
3177
+ } }, /* @__PURE__ */ React.createElement(Icon, { name: icon, size: 20, stroke: active ? 2.1 : 1.8, color: active ? "var(--accent)" : "var(--dim)" }), /* @__PURE__ */ React.createElement("span", { style: { fontSize: 9, fontWeight: 600, letterSpacing: 0.2, color: active ? "var(--accent)" : "var(--faint)" } }, label), soon && /* @__PURE__ */ React.createElement("span", { style: { position: "absolute", top: 5, right: 6, width: 6, height: 6, borderRadius: 999, background: "var(--warn)" } }));
3178
+ }
3179
+ function DkApp() {
3180
+ const [t, setTweak] = useTweaks(DK_DEFAULTS);
3181
+ const [tab, setTab] = React.useState(t.startTab || "chat");
3182
+ const [activeId, setActiveId] = React.useState(null);
3183
+ const data = useDaemonData();
3184
+ const me = data.me;
3185
+ React.useEffect(() => {
3186
+ const mine = window.__DK_UI_VERSION;
3187
+ const served = me && me.lanVer;
3188
+ if (!mine || !served || mine === served)
3189
+ return;
3190
+ const key = "dk-reloaded-for-" + served;
3191
+ if (sessionStorage.getItem(key))
3192
+ return;
3193
+ sessionStorage.setItem(key, "1");
3194
+ window.location.reload();
3195
+ }, [me && me.lanVer]);
3196
+ const peers = data.peers;
3197
+ const [pendingAddr, setPendingAddr] = React.useState(() => dkParseDeepLinkAddress());
3198
+ React.useEffect(() => {
3199
+ const onHash = () => {
3200
+ const a = dkParseDeepLinkAddress();
3201
+ if (a)
3202
+ setPendingAddr(a);
3203
+ };
3204
+ window.addEventListener("hashchange", onHash);
3205
+ return () => window.removeEventListener("hashchange", onHash);
3206
+ }, []);
3207
+ React.useEffect(() => {
3208
+ if (!pendingAddr)
3209
+ return;
3210
+ setTab("chat");
3211
+ if (!peers || !peers.length)
3212
+ return;
3213
+ const hit = peers.find((p) => p.address && p.address === pendingAddr);
3214
+ if (hit) {
3215
+ setActiveId(hit.id);
3216
+ setPendingAddr("");
3217
+ try {
3218
+ window.history.replaceState(null, "", window.location.pathname);
3219
+ } catch (e) {
3220
+ }
3221
+ } else {
3222
+ try {
3223
+ window.history.replaceState(null, "", window.location.pathname);
3224
+ } catch (e) {
3225
+ }
3226
+ }
3227
+ }, [pendingAddr, peers]);
3228
+ const requests = data.requests;
3229
+ const exits = data.exits;
3230
+ const activeExit = data.activeExit;
3231
+ const T = STR[t.lang] || STR.en;
3232
+ const vars = dkTheme(t.theme, t.accent);
3233
+ const rowPad = t.density === "comfortable" ? "11px 12px" : "7px 10px";
3234
+ const callCtl = useCallController(me.userId, (peerId, video, direction) => {
3235
+ const text = `WebRTC ${video ? "video" : "audio"} call: ${direction}`;
3236
+ dkApi.logLocal(peerId, direction === "outgoing" ? "out" : "in", text).then(() => {
3237
+ data.loadThread(peerId);
3238
+ data.refresh();
3239
+ });
3240
+ });
3241
+ const onCall = (peerId, video) => callCtl.start(peerId, !!video);
3242
+ const rtcFileCtl = useRtcFileController(me.userId, (peerId, name, blob) => {
3243
+ if (!blob)
3244
+ return;
3245
+ dkApi.saveWebrtcFile(peerId, name, blob).then((r) => {
3246
+ if (!r || r.ok === false) {
3247
+ return dkApi.logLocal(peerId, "in", "received file via WebRTC: " + name + " (" + dkFileSize(blob.size) + ")");
3248
+ }
3249
+ }).then(() => {
3250
+ data.loadThread(peerId);
3251
+ data.refresh();
3252
+ });
3253
+ });
3254
+ React.useEffect(() => {
3255
+ if (!activeId && peers.length)
3256
+ setActiveId(peers[0].id);
3257
+ }, [peers, activeId]);
3258
+ React.useEffect(() => {
3259
+ if (!activeId)
3260
+ return;
3261
+ data.loadThread(activeId);
3262
+ dkApi.markRead(activeId).then(data.refresh);
3263
+ const iv = setInterval(() => {
3264
+ data.loadThread(activeId);
3265
+ dkApi.markRead(activeId);
3266
+ }, 2500);
3267
+ return () => clearInterval(iv);
3268
+ }, [activeId]);
3269
+ const onSelect = (id) => setActiveId(id);
3270
+ const onAct = (id, kind) => {
3271
+ const r = requests.find((x) => x.id === id);
3272
+ const uid = r && r.userid || id;
3273
+ (kind === "accept" ? dkApi.accept(uid) : dkApi.reject(uid)).then(data.refresh);
3274
+ };
3275
+ const onRemove = (peer) => {
3276
+ dkApi.remove(peer.id).then(data.refresh);
3277
+ if (peer.id === activeId)
3278
+ setActiveId(null);
3279
+ };
3280
+ const onAdd = (address) => {
3281
+ if (!address || !address.trim())
3282
+ return Promise.resolve({ ok: false, error: "empty address" });
3283
+ return dkApi.add(address.trim()).then((r) => {
3284
+ data.refresh();
3285
+ return r;
3286
+ });
3287
+ };
3288
+ const onSend = (text) => {
3289
+ if (!activeId || !text || !text.trim())
3290
+ return;
3291
+ dkApi.send(activeId, text.trim()).then(() => data.loadThread(activeId)).then(data.refresh);
3292
+ };
3293
+ const onSendFile = (file) => {
3294
+ if (!activeId || !file)
3295
+ return Promise.resolve({ ok: false });
3296
+ return dkApi.sendFile(activeId, file).then((r) => {
3297
+ data.loadThread(activeId);
3298
+ data.refresh();
3299
+ return r;
3300
+ });
3301
+ };
3302
+ const onSendRtcFile = (file, onProgress) => {
3303
+ if (!activeId || !file)
3304
+ return Promise.resolve({ ok: false, error: "no active peer or file" });
3305
+ return rtcFileCtl.sendFile(activeId, file, onProgress).then((r) => {
3306
+ if (r && r.ok && r.via === "webrtc") {
3307
+ return dkApi.send(activeId, (t.lang === "zh" ? "\u5DF2\u901A\u8FC7 WebRTC \u53D1\u9001\u6587\u4EF6: " : "sent file via WebRTC: ") + file.name).then(() => ({ ok: true, via: "webrtc" }));
3308
+ }
3309
+ return { ok: false, error: r && r.error || "WebRTC file send failed" };
3310
+ }).then((r) => {
3311
+ data.loadThread(activeId);
3312
+ data.refresh();
3313
+ return r;
3314
+ });
3315
+ };
3316
+ const onAlias = (peer) => {
3317
+ const a = window.prompt("Set alias for this peer (empty to clear):", peer.alias || "");
3318
+ if (a !== null)
3319
+ dkApi.alias(peer.id, a).then(data.refresh);
3320
+ };
3321
+ const onEdit = (name, description) => dkApi.setProfile(name, description).then(data.refresh);
3322
+ const onSetExit = () => {
3323
+ };
3324
+ const onOpenChat = (id) => {
3325
+ setActiveId(id);
3326
+ setTab("chat");
3327
+ };
3328
+ const onOpenNet = () => setTab("network");
3329
+ const nav = [
3330
+ { id: "chat", icon: "message", label: T.chat },
3331
+ { id: "network", icon: "network", label: T.network },
3332
+ { id: "profile", icon: "userRound", label: T.profile }
3333
+ ];
3334
+ return /* @__PURE__ */ React.createElement("div", { style: { ...vars, "--row-pad": rowPad, position: "fixed", inset: 0, display: "flex", background: "var(--bg)", color: "var(--text)", fontFamily: "var(--ui)" } }, /* @__PURE__ */ React.createElement("div", { style: { width: 68, flexShrink: 0, borderRight: "1px solid var(--line)", background: "var(--rail)", display: "flex", flexDirection: "column", alignItems: "center", padding: "14px 0", gap: 8 } }, /* @__PURE__ */ React.createElement("div", { style: { width: 38, height: 38, borderRadius: 10, background: "var(--accent)", display: "flex", alignItems: "center", justifyContent: "center", marginBottom: 8 } }, /* @__PURE__ */ React.createElement(Icon, { name: "terminal", size: 20, color: "#fff", stroke: 2.2 })), nav.map((n) => /* @__PURE__ */ React.createElement(RailBtn, { key: n.id, icon: n.icon, label: n.label, active: tab === n.id, soon: n.soon, onClick: () => setTab(n.id) })), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement("div", { style: { position: "relative" } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 36, radius: 9 }))), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0, minHeight: 0, display: "flex", flexDirection: "column" } }, /* @__PURE__ */ React.createElement("div", { style: { height: 46, flexShrink: 0, borderBottom: "1px solid var(--line)", background: "var(--panel)", display: "flex", alignItems: "center", gap: 12, padding: "0 16px" } }, /* @__PURE__ */ React.createElement("svg", { width: 20, height: 20, viewBox: "0 0 24 24", fill: "none", stroke: "var(--accent)", strokeWidth: 2.2, strokeLinecap: "round", strokeLinejoin: "round", style: { display: "block", flexShrink: 0 } }, /* @__PURE__ */ React.createElement("path", { d: "m4.5 17 6-6-6-6" }), /* @__PURE__ */ React.createElement("path", { d: "M12 18.5h7.5" })), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 14, fontWeight: 700, letterSpacing: -0.3, color: "var(--text)" } }, "beagle"), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12, color: "var(--faint)" } }, "\xB7 ", nav.find((n) => n.id === tab).label.toLowerCase()), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement(Tag, { tone: "accent" }, me.channel, " \xB7 lan ", me.lanVer), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 7, padding: "0 4px" } }, /* @__PURE__ */ React.createElement(StatusDot, { online: me.online }), /* @__PURE__ */ React.createElement(Mono, { size: 12.5, copy: me.ip }, me.ip)), /* @__PURE__ */ React.createElement("span", { style: { width: 1, height: 22, background: "var(--line)" } }), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8 } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 26, radius: 7 }), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12.5, fontWeight: 600, color: "var(--text)" } }, me.name))), tab === "chat" && /* @__PURE__ */ React.createElement(ChatTab, { T, lang: t.lang, peers, requests, activeId, thread: data.threads[activeId], onSelect, onAct, onAdd, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread: () => activeId && data.loadThread(activeId), prefillAddr: pendingAddr, onPrefillConsumed: () => setPendingAddr("") }), tab === "network" && /* @__PURE__ */ React.createElement(NetworkTab, { T, me, peers, exits, activeExit, reqCount: requests.length, onSetExit, onOpenChat }), tab === "profile" && /* @__PURE__ */ React.createElement(ProfileTab, { T, me, onEdit })), /* @__PURE__ */ React.createElement(TweaksPanel, null, /* @__PURE__ */ React.createElement(TweakSection, { label: t.lang === "zh" ? "\u5916\u89C2" : "Appearance" }), /* @__PURE__ */ React.createElement(
3335
+ TweakRadio,
3336
+ {
3337
+ label: t.lang === "zh" ? "\u4E3B\u9898" : "Theme",
3338
+ value: t.theme,
3339
+ options: [{ value: "dark", label: t.lang === "zh" ? "\u6DF1\u8272" : "Dark" }, { value: "light", label: t.lang === "zh" ? "\u6D45\u8272" : "Light" }],
3340
+ onChange: (v) => setTweak("theme", v)
3341
+ }
3342
+ ), /* @__PURE__ */ React.createElement(
3343
+ TweakColor,
3344
+ {
3345
+ label: t.lang === "zh" ? "\u5F3A\u8C03\u8272" : "Accent",
3346
+ value: t.accent,
3347
+ options: ["#7B6CF6", "#3B82F6", "#10B981", "#F59E0B", "#EC4899", "#E0584B"],
3348
+ onChange: (v) => setTweak("accent", v)
3349
+ }
3350
+ ), /* @__PURE__ */ React.createElement(TweakSection, { label: t.lang === "zh" ? "\u5E03\u5C40" : "Layout" }), /* @__PURE__ */ React.createElement(
3351
+ TweakRadio,
3352
+ {
3353
+ label: t.lang === "zh" ? "\u5BC6\u5EA6" : "Density",
3354
+ value: t.density,
3355
+ options: [{ value: "compact", label: t.lang === "zh" ? "\u7D27\u51D1" : "Compact" }, { value: "comfortable", label: t.lang === "zh" ? "\u5BBD\u677E" : "Comfy" }],
3356
+ onChange: (v) => setTweak("density", v)
3357
+ }
3358
+ ), /* @__PURE__ */ React.createElement(TweakSection, { label: t.lang === "zh" ? "\u8BED\u8A00" : "Language" }), /* @__PURE__ */ React.createElement(
3359
+ TweakRadio,
3360
+ {
3361
+ label: t.lang === "zh" ? "\u754C\u9762\u8BED\u8A00" : "On-screen",
3362
+ value: t.lang,
3363
+ options: [{ value: "en", label: "EN" }, { value: "zh", label: "\u4E2D\u6587" }],
3364
+ onChange: (v) => setTweak("lang", v)
3365
+ }
3366
+ ), /* @__PURE__ */ React.createElement(TweakSection, { label: t.lang === "zh" ? "\u8DF3\u8F6C" : "Jump to" }), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: 6, flexWrap: "wrap" } }, nav.map((n) => /* @__PURE__ */ React.createElement("button", { key: n.id, onClick: () => setTab(n.id), style: {
3367
+ flex: "1 0 30%",
3368
+ height: 32,
3369
+ borderRadius: 8,
3370
+ cursor: "pointer",
3371
+ fontFamily: "var(--mono)",
3372
+ border: "1px solid rgba(255,255,255,0.14)",
3373
+ background: tab === n.id ? "rgba(255,255,255,0.18)" : "rgba(255,255,255,0.06)",
3374
+ color: "#fff",
3375
+ fontSize: 12,
3376
+ fontWeight: 600
3377
+ } }, n.label)))), /* @__PURE__ */ React.createElement(IncomingCallModal, { T, ctl: callCtl, peers }), /* @__PURE__ */ React.createElement(CallOverlay, { T, ctl: callCtl, peers }), /* @__PURE__ */ React.createElement(RtcFileInbox, { T, peers, ctl: rtcFileCtl }));
3378
+ }
3379
+ ReactDOM.createRoot(document.getElementById("root")).render(/* @__PURE__ */ React.createElement(DkApp, null));