@decentnetwork/beagle-ui 0.2.25 → 0.2.27

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.
Files changed (2) hide show
  1. package/dist/beagle-ui.js +349 -57
  2. package/package.json +1 -1
package/dist/beagle-ui.js CHANGED
@@ -1,4 +1,4 @@
1
- export const BEAGLE_UI_VERSION = "0.2.25";
1
+ export const BEAGLE_UI_VERSION = "0.2.27";
2
2
  // src/icons.jsx
3
3
  var ICON_PATHS = {
4
4
  // ---- tab bar (the four must feel like one set) ----
@@ -66,8 +66,8 @@ var ICON_PATHS = {
66
66
  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"/>',
67
67
  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"/>'
68
68
  };
69
- function Icon({ name, size = 24, stroke = 1.75, color = "currentColor", fill = "none", style }) {
70
- const d = ICON_PATHS[name];
69
+ function Icon({ name, size = 24, stroke = 1.75, color = "currentColor", fill = "none", style, svgPath }) {
70
+ const d = svgPath || ICON_PATHS[name];
71
71
  if (!d)
72
72
  return null;
73
73
  return /* @__PURE__ */ React.createElement(
@@ -111,7 +111,10 @@ Object.assign(ICON_PATHS, {
111
111
  zap: '<path d="M13 2 4 13h7l-1 9 9-11h-7l1-9Z"/>',
112
112
  clock: '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3.5 2"/>',
113
113
  bellOff: '<path d="M8.7 3.4A6 6 0 0 1 18 8.5c0 3 .5 5 1.1 6.3M6.1 6.1A6 6 0 0 0 6 8.5c0 6.5-2.5 8-2.5 8H16"/><path d="M13.7 20.5a2 2 0 0 1-3.4 0"/><path d="m3 3 18 18"/>',
114
- plug: '<path d="M9 2v6M15 2v6"/><path d="M7 8h10v3a5 5 0 0 1-10 0V8Z"/><path d="M12 16v6"/>'
114
+ plug: '<path d="M9 2v6M15 2v6"/><path d="M7 8h10v3a5 5 0 0 1-10 0V8Z"/><path d="M12 16v6"/>',
115
+ // A form with a filled-in field: the glyph for "Fill with Beagle" and any
116
+ // other host app that puts your identity into someone else's page.
117
+ form: '<rect x="4" y="3" width="16" height="18" rx="2"/><path d="M8 8h8M8 12h5"/><path d="M8 16h3"/><path d="m14.5 17.5 1.6 1.6 3.4-3.6"/>'
115
118
  });
116
119
 
117
120
  // src/ui-host.jsx
@@ -121,8 +124,16 @@ var hostPrefersRtcFile = false;
121
124
  var hostHasKeyBackup = false;
122
125
  var hostEnsWrite = true;
123
126
  var hostBrandHref = "";
127
+ var hostTabs = null;
128
+ var hostHideApps = null;
129
+ var hostApps = null;
130
+ var hostProfileSections = null;
124
131
  function setUiHost(host) {
125
132
  H = host || {};
133
+ hostTabs = Array.isArray(H.hostTabs) ? H.hostTabs : null;
134
+ hostHideApps = Array.isArray(H.hideApps) ? H.hideApps : null;
135
+ hostApps = Array.isArray(H.hostApps) ? H.hostApps : null;
136
+ hostProfileSections = Array.isArray(H.hostProfileSections) ? H.hostProfileSections : null;
126
137
  hostNeedsWelcome = !!H.hostNeedsWelcome;
127
138
  hostPrefersRtcFile = !!H.hostPrefersRtcFile;
128
139
  hostHasKeyBackup = !!H.hostHasKeyBackup;
@@ -650,6 +661,18 @@ var actions_default = {
650
661
  confirm: "list",
651
662
  scope: "assistant"
652
663
  },
664
+ {
665
+ name: "nodes-refresh",
666
+ usage: "/nodes refresh",
667
+ summary: "Look for friends that are provably service peers (the shipped exit table, this machine's dora roster) and offer to reclassify them. Nothing changes until you tick and confirm.",
668
+ params: {
669
+ type: "object",
670
+ properties: {},
671
+ required: []
672
+ },
673
+ confirm: "list",
674
+ scope: "assistant"
675
+ },
653
676
  {
654
677
  name: "find",
655
678
  usage: "/find <text>",
@@ -824,7 +847,8 @@ var actions_default = {
824
847
  file: "service-peers.json",
825
848
  loader: "loadServicePeers()",
826
849
  rule: "A peer is a service peer if and only if its userid is in this list. The Assistant never infers it from a name or from having no messages. /friends excludes them entirely; /nodes is only them.",
827
- later: "A dora / ENS / bridge / directory service returns the same shape; the bundled file is v1."
850
+ later: "A dora / ENS / bridge / directory service returns the same shape; the bundled file is v1.",
851
+ refresh: "/nodes refresh asks GET /api/assistant/service-candidates for evidence-backed candidates and writes the ticked ones to this machine's overlay (POST /api/assistant/service-peers, stored at <configDir>/service-peers-local.json). The shipped list is never edited at runtime; the overlay wins on the same userid."
828
852
  }
829
853
  };
830
854
 
@@ -983,6 +1007,8 @@ function parseCommand(line) {
983
1007
  const sub = (parts[1] || "").toLowerCase();
984
1008
  if (sub === "list" || sub === "remove")
985
1009
  return { ok: true, name: `${head}-${sub}`, args: parts.slice(2) };
1010
+ if (head === "nodes" && sub === "refresh")
1011
+ return { ok: true, name: "nodes-refresh", args: parts.slice(2) };
986
1012
  return { ok: false, name: head, args: parts.slice(1), error: `${head}-subcommand` };
987
1013
  }
988
1014
  return { ok: true, name: head, args: parts.slice(1) };
@@ -1020,6 +1046,19 @@ function servicePeerMap(list) {
1020
1046
  }
1021
1047
  return map;
1022
1048
  }
1049
+ function mergeServicePeers(bundled, local) {
1050
+ const byId = /* @__PURE__ */ new Map();
1051
+ for (const p of bundled && bundled.peers || [])
1052
+ if (p && p.userid)
1053
+ byId.set(p.userid, { ...p, source: "app" });
1054
+ for (const p of local && local.peers || [])
1055
+ if (p && p.userid)
1056
+ byId.set(p.userid, { ...p, source: "local" });
1057
+ return { version: 1, source: "merged", peers: [...byId.values()] };
1058
+ }
1059
+ function newCandidates(candidates, svcMap) {
1060
+ return (candidates != null ? candidates : []).filter((c) => c && c.userid && !(svcMap && svcMap.has(c.userid)));
1061
+ }
1023
1062
  function partitionPeers(friends, svcMap) {
1024
1063
  const people = [];
1025
1064
  const nodes = [];
@@ -1104,8 +1143,13 @@ function resolveFriend(friends, token) {
1104
1143
  }
1105
1144
 
1106
1145
  // src/assistant.jsx
1107
- function loadServicePeers() {
1108
- return service_peers_default;
1146
+ async function loadServicePeers() {
1147
+ try {
1148
+ const local = await fetch("/api/assistant/service-peers").then((r) => r.json());
1149
+ return mergeServicePeers(service_peers_default, local);
1150
+ } catch (e) {
1151
+ return service_peers_default;
1152
+ }
1109
1153
  }
1110
1154
  var ASSISTANT_PEER = {
1111
1155
  id: ASSISTANT_ID,
@@ -1161,10 +1205,12 @@ function helpText(lang) {
1161
1205
  const tail = lang === "zh" ? `\u7B5B\u9009\u6761\u4EF6\uFF08\u53EF\u7EC4\u5408\uFF0C\u53D6\u5E76\u96C6\uFF09\uFF1A
1162
1206
  ${filters}
1163
1207
 
1164
- \u6700\u8FD1 ${RECENT_DAYS} \u5929\u5185\u6DFB\u52A0\u7684\u597D\u53CB\u4E0D\u4F1A\u88AB"\u4ECE\u672A/\u6C89\u9ED8"\u7C7B\u7B5B\u9009\u9009\u4E2D\u3002\u5220\u9664\u6C38\u8FDC\u9700\u8981\u5148\u786E\u8BA4\u3002` : `Filters (combine them; a friend matching any one is listed):
1208
+ \`/rename\`\u3001\`/mute\`\u3001\`/find\` \u5BF9\u597D\u53CB\u548C\u670D\u52A1\u8282\u70B9\u90FD\u9002\u7528\uFF1B\u53EA\u6709\u5217\u8868\u548C\u5220\u9664\u5206\u6210\u4E24\u8FB9\u3002
1209
+ \u6700\u8FD1 ${RECENT_DAYS} \u5929\u5185\u6DFB\u52A0\u7684\u597D\u53CB\u4E0D\u4F1A\u88AB"\u4ECE\u672A/\u6C89\u9ED8"\u7C7B\u7B5B\u9009\u9009\u4E2D\uFF0C\u5E76\u4F1A\u5217\u51FA\u88AB\u8DF3\u8FC7\u7684\u540D\u5B57\u3002\u5220\u9664\u6C38\u8FDC\u9700\u8981\u5148\u786E\u8BA4\uFF0C\u53D6\u6D88\u52FE\u9009\u5373\u53EF\u4FDD\u7559\u3002` : `Filters (combine them; a friend matching any one is listed):
1165
1210
  ${filters}
1166
1211
 
1167
- Friends added in the last ${RECENT_DAYS} days are never matched by the "never/silent" filters. Removal always asks first.`;
1212
+ \`/rename\`, \`/mute\` and \`/find\` work on friends and service peers alike \u2014 only listing and removing are split in two.
1213
+ Friends added in the last ${RECENT_DAYS} days are never matched by the "never/silent" filters, and the ones held back are named. Removal always asks first: untick anyone you want to keep.`;
1168
1214
  return `${intro}
1169
1215
  ${cmds}
1170
1216
 
@@ -1178,8 +1224,9 @@ var asMsg = (from, text, extra) => Object.assign({
1178
1224
  time: (/* @__PURE__ */ new Date()).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }),
1179
1225
  text
1180
1226
  }, extra);
1181
- function AssistantPanel({ m, T, lang, onRemove }) {
1227
+ function AssistantPanel({ m, T, lang, onRemove, onAdopt }) {
1182
1228
  const rows = m.assistant.rows || [];
1229
+ const adopt = m.assistant.kind === "adopt";
1183
1230
  const [sel, setSel] = React.useState(() => new Set(rows.map((r) => r.userid)));
1184
1231
  const [busy, setBusy] = React.useState(false);
1185
1232
  const [done, setDone] = React.useState(null);
@@ -1190,7 +1237,7 @@ function AssistantPanel({ m, T, lang, onRemove }) {
1190
1237
  n.has(id) ? n.delete(id) : n.add(id);
1191
1238
  return n;
1192
1239
  });
1193
- const label = lang === "zh" ? `\u5220\u9664 ${sel.size} \u4F4D` : `Remove ${sel.size}`;
1240
+ const label = adopt ? lang === "zh" ? `\u52A0\u5165 ${sel.size} \u4E2A` : `Add ${sel.size} as ${sel.size === 1 ? "a node" : "nodes"}` : lang === "zh" ? `\u5220\u9664 ${sel.size} \u4F4D` : `Remove ${sel.size}`;
1194
1241
  return /* @__PURE__ */ React.createElement("div", { style: { marginTop: 8, border: "1px solid var(--line)", borderRadius: 10, overflow: "hidden" } }, rows.map((r) => /* @__PURE__ */ React.createElement("label", { key: r.userid, style: {
1195
1242
  display: "flex",
1196
1243
  alignItems: "center",
@@ -1207,12 +1254,14 @@ function AssistantPanel({ m, T, lang, onRemove }) {
1207
1254
  disabled: !!done || busy,
1208
1255
  onChange: () => toggle(r.userid)
1209
1256
  }
1210
- ), /* @__PURE__ */ React.createElement("span", { style: { flex: 1 } }, nameOf(r)), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, color: "var(--faint)" } }, r.userid.slice(0, 8), "\u2026"))), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 10, padding: "8px 10px" } }, done ? /* @__PURE__ */ React.createElement("span", { style: { fontSize: 12.5, color: "var(--faint)" } }, done) : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Btn, { size: "sm", tone: "danger", disabled: busy || !sel.size, onClick: () => {
1257
+ ), /* @__PURE__ */ React.createElement("span", { style: { flex: 1 } }, nameOf(r), r.why && /* @__PURE__ */ React.createElement("span", { style: { color: "var(--faint)", fontSize: 11.5 } }, ` \u2014 ${r.why}`)), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, color: "var(--faint)" } }, r.userid.slice(0, 8), "\u2026"))), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 10, padding: "8px 10px" } }, done ? /* @__PURE__ */ React.createElement("span", { style: { fontSize: 12.5, color: "var(--faint)" } }, done) : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Btn, { size: "sm", tone: adopt ? "solid" : "danger", disabled: busy || !sel.size, onClick: () => {
1211
1258
  setBusy(true);
1212
- onRemove([...sel]).then((r) => {
1213
- setDone(lang === "zh" ? `\u5DF2\u5220\u9664 ${r.removed} \u4F4D${r.failed ? `\uFF0C${r.failed} \u4F4D\u5931\u8D25` : ""}` : `Removed ${r.removed}${r.failed ? `, ${r.failed} failed` : ""}`);
1259
+ const picked = rows.filter((r) => sel.has(r.userid));
1260
+ const run = adopt ? onAdopt(picked) : onRemove([...sel]);
1261
+ run.then((r) => {
1262
+ setDone(adopt ? lang === "zh" ? `\u5DF2\u52A0\u5165 ${r.added} \u4E2A\u670D\u52A1\u8282\u70B9` : `Added ${r.added} as service peers` : lang === "zh" ? `\u5DF2\u5220\u9664 ${r.removed} \u4F4D${r.failed ? `\uFF0C${r.failed} \u4F4D\u5931\u8D25` : ""}` : `Removed ${r.removed}${r.failed ? `, ${r.failed} failed` : ""}`);
1214
1263
  }).finally(() => setBusy(false));
1215
- } }, busy ? T.working || "Working\u2026" : label), /* @__PURE__ */ React.createElement("span", { style: { fontSize: 12, color: "var(--faint)" } }, lang === "zh" ? "\u53D6\u6D88\u52FE\u9009\u53EF\u4FDD\u7559" : "Untick anyone you want to keep"))));
1264
+ } }, busy ? T.working || "Working\u2026" : label), /* @__PURE__ */ React.createElement("span", { style: { fontSize: 12, color: "var(--faint)" } }, adopt ? lang === "zh" ? "\u53D6\u6D88\u52FE\u9009\u5373\u4FDD\u6301\u4E3A\u597D\u53CB" : "Untick any you want to keep as a friend" : lang === "zh" ? "\u53D6\u6D88\u52FE\u9009\u53EF\u4FDD\u7559" : "Untick anyone you want to keep"))));
1216
1265
  }
1217
1266
  function useAssistant({ lang, T, muted, onToggleMute, sound, flash, setAlert, onReloadFriends }) {
1218
1267
  const [thread, setThread] = React.useState(() => [{ day: (/* @__PURE__ */ new Date()).toLocaleDateString() }, asMsg("them", helpText(lang))]);
@@ -1222,11 +1271,13 @@ function useAssistant({ lang, T, muted, onToggleMute, sound, flash, setAlert, on
1222
1271
  }, [lang]);
1223
1272
  const fetchFriends = async () => {
1224
1273
  const r = await fetch("/api/assistant/friends").then((x) => x.json());
1225
- const { people, nodes } = partitionPeers(r.friends || [], servicePeerMap(loadServicePeers()));
1274
+ const svcMap = servicePeerMap(await loadServicePeers());
1275
+ const { people, nodes } = partitionPeers(r.friends || [], svcMap);
1226
1276
  return {
1227
1277
  friends: people,
1228
1278
  nodes,
1229
1279
  all: r.friends || [],
1280
+ svcMap,
1230
1281
  historySince: r.historySince || null,
1231
1282
  now: r.now || Date.now(),
1232
1283
  ver: r.beagleVer || ""
@@ -1293,6 +1344,33 @@ ${cands.map((c) => `- ${nameOf(c)}`).join("\n")}`;
1293
1344
  - Waiting to be accepted: ${pending}
1294
1345
  - History on this device since: ${fmtDate(d.historySince, lang)}
1295
1346
  - Version: ${d.ver}`));
1347
+ }
1348
+ case "nodes-refresh": {
1349
+ const d = await fetchFriends();
1350
+ const r = await fetch("/api/assistant/service-candidates").then((x) => x.json());
1351
+ const fresh = newCandidates(r.candidates || [], d.svcMap);
1352
+ const src = r.sources || {};
1353
+ const where = lang === "zh" ? `\u67E5\u8BE2\u6765\u6E90\uFF1A\u5185\u7F6E\u51FA\u53E3\u8868\uFF08${src.exits || 0} \u6761\uFF09${src.dora == null ? "\uFF0C\u672C\u673A\u4E0D\u662F dora" : `\uFF0C\u672C\u673A dora \u540D\u518C\uFF08${src.dora} \u6761\uFF09`}` : `Looked at: the shipped exit table (${src.exits || 0})${src.dora == null ? ", and this machine is not a dora" : `, and this machine's dora roster (${src.dora})`}`;
1354
+ if (!fresh.length) {
1355
+ return append(asMsg("them", lang === "zh" ? `**\u6CA1\u6709\u65B0\u7684\u670D\u52A1\u8282\u70B9**
1356
+
1357
+ \u73B0\u6709 ${d.nodes.length} \u4E2A\u5DF2\u8BC6\u522B\u3002${where}\u3002
1358
+
1359
+ *\u53EA\u91C7\u7528\u8BC1\u636E\uFF0C\u4E0D\u9760\u540D\u5B57\u731C\u6D4B\u3002*` : `**Nothing new**
1360
+
1361
+ ${d.nodes.length} service peers are already classified. ${where}.
1362
+
1363
+ *Evidence only \u2014 names are never guessed from.*`));
1364
+ }
1365
+ const title = lang === "zh" ? `**\u53D1\u73B0 ${fresh.length} \u4E2A\u53EF\u4EE5\u6807\u4E3A\u670D\u52A1\u8282\u70B9\u7684\u597D\u53CB** \u2014 \u52FE\u9009\u540E\u624D\u4F1A\u751F\u6548
1366
+
1367
+ ${where}\u3002` : `**${fresh.length === 1 ? "1 friend looks" : `${fresh.length} friends look`} like service peers** \u2014 nothing changes until you tick and confirm
1368
+
1369
+ ${where}.`;
1370
+ const lines = fresh.map((c) => `- **${c.name}** \`${c.kind}${c.role ? " \xB7 " + c.role : ""}\` \u2014 ${c.why}`).join("\n");
1371
+ return append(asMsg("them", `${title}
1372
+
1373
+ ${lines}`, { assistant: { kind: "adopt", rows: fresh } }));
1296
1374
  }
1297
1375
  case "nodes-list":
1298
1376
  case "friends-list": {
@@ -1382,6 +1460,24 @@ ${cands.map((c) => `- ${nameOf(c)}`).join("\n")}`;
1382
1460
  return append(asMsg("them", lang === "zh" ? `\u51FA\u9519\u4E86\uFF1A${e && e.message || e}` : `That failed: ${e && e.message || e}`));
1383
1461
  }
1384
1462
  }, [append, lang, muted, onToggleMute, setAlert, onReloadFriends]);
1463
+ const adoptServicePeers = React.useCallback(async (rows) => {
1464
+ const add = rows.map((r) => ({ userid: r.userid, name: r.name, kind: r.kind, role: r.role }));
1465
+ let added = 0;
1466
+ try {
1467
+ const r = await fetch("/api/assistant/service-peers", {
1468
+ method: "POST",
1469
+ headers: { "content-type": "application/json" },
1470
+ body: JSON.stringify({ add })
1471
+ }).then((x) => x.json());
1472
+ added = r && r.ok ? add.length : 0;
1473
+ } catch (e) {
1474
+ added = 0;
1475
+ }
1476
+ if (onReloadFriends)
1477
+ onReloadFriends();
1478
+ append(asMsg("them", added ? lang === "zh" ? `\u5DF2\u628A ${added} \u4F4D\u6807\u4E3A\u670D\u52A1\u8282\u70B9\uFF0C\u5B83\u4EEC\u4E0D\u518D\u51FA\u73B0\u5728 \`/friends\` \u4E2D\u3002` : `${added} now count as service peers \u2014 they no longer appear in \`/friends\`.` : lang === "zh" ? "\u5199\u5165\u5931\u8D25\uFF0C\u6CA1\u6709\u4EFB\u4F55\u6539\u52A8\u3002" : "That write failed; nothing changed."));
1479
+ return { added };
1480
+ }, [append, lang, onReloadFriends]);
1385
1481
  const removeFriends = React.useCallback(async (ids) => {
1386
1482
  let removed = 0;
1387
1483
  const failed = [];
@@ -1401,7 +1497,7 @@ ${cands.map((c) => `- ${nameOf(c)}`).join("\n")}`;
1401
1497
  append(asMsg("them", failed.length ? lang === "zh" ? `\u5DF2\u5220\u9664 ${removed} \u4F4D\uFF0C${failed.length} \u4F4D\u5931\u8D25\u3002` : `Removed ${removed}, ${failed.length} failed.` : lang === "zh" ? `\u5DF2\u5220\u9664 ${removed} \u4F4D\u597D\u53CB\u3002` : `Removed ${removed} friend${removed === 1 ? "" : "s"}.`));
1402
1498
  return { removed, failed: failed.length };
1403
1499
  }, [append, lang, onReloadFriends]);
1404
- return { thread, handle, removeFriends };
1500
+ return { thread, handle, removeFriends, adoptServicePeers };
1405
1501
  }
1406
1502
 
1407
1503
  // src/chat.jsx
@@ -2210,7 +2306,7 @@ function DkChatForm({ form, submitted, peer, onSubmit }) {
2210
2306
  return null;
2211
2307
  }), err && /* @__PURE__ */ React.createElement("div", { style: { color: "var(--danger, #ff6b6b)", fontSize: 11.5, fontFamily: "var(--mono)" } }, err));
2212
2308
  }
2213
- function Msg({ m, peer, T, lang, onTheater, onDelete, onCancel, onRetry, onRetryMsg, onReveal, onOpenFile, onCall, onFormSubmit, onNameCard, onAssistantRemove, answered, selMode, selected, onToggleSel, busy, isGroup }) {
2309
+ function Msg({ m, peer, T, lang, onTheater, onDelete, onCancel, onRetry, onRetryMsg, onReveal, onOpenFile, onCall, onFormSubmit, onNameCard, onAssistantRemove, onAssistantAdopt, answered, selMode, selected, onToggleSel, busy, isGroup }) {
2214
2310
  const mine = m.from === "me";
2215
2311
  const nativeMedia = !m.file ? dkNativeMediaFromText(m.text) : null;
2216
2312
  const grp = isGroup && m.dir !== "out" && !m.file && !nativeMedia ? dkGroupParse(m.text) : null;
@@ -2539,7 +2635,7 @@ function Msg({ m, peer, T, lang, onTheater, onDelete, onCancel, onRetry, onRetry
2539
2635
  letterSpacing: -0.1,
2540
2636
  wordBreak: "break-word"
2541
2637
  } }, /* @__PURE__ */ React.createElement(MarkdownText, { text: read.text, onNameCard })));
2542
- })(), read.form && /* @__PURE__ */ React.createElement("div", { style: { marginTop: 6 } }, /* @__PURE__ */ React.createElement(DkChatForm, { form: read.form, submitted, peer, onSubmit: onFormSubmit })), m.assistant && onAssistantRemove && /* @__PURE__ */ React.createElement(AssistantPanel, { m, T, lang, onRemove: onAssistantRemove }), /* @__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), (() => {
2638
+ })(), read.form && /* @__PURE__ */ React.createElement("div", { style: { marginTop: 6 } }, /* @__PURE__ */ React.createElement(DkChatForm, { form: read.form, submitted, peer, onSubmit: onFormSubmit })), m.assistant && onAssistantRemove && /* @__PURE__ */ React.createElement(AssistantPanel, { m, T, lang, onRemove: onAssistantRemove, onAdopt: onAssistantAdopt }), /* @__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), (() => {
2543
2639
  const sentDone = mine && m.status && m.status !== "queued" && m.status !== "sending" && m.status !== "failed";
2544
2640
  const via = !mine ? m.via : sentDone ? m.via === "offline" ? "offline" : "online" : null;
2545
2641
  if (!via)
@@ -2584,7 +2680,7 @@ function Msg({ m, peer, T, lang, onTheater, onDelete, onCancel, onRetry, onRetry
2584
2680
  ) : mine && m.via === "offline" && m.status !== "queued" && m.status !== "sending" ? /* @__PURE__ */ React.createElement(Icon, { name: "check", size: 12, stroke: 2.2, color: "#f59e0b", title: T.viaOutOffline || "posted to the offline relay \u2014 they get it when they come online" }) : mine && m.status === "sent" && m.confirmed === false ? /* @__PURE__ */ React.createElement(Icon, { name: "check", size: 12, stroke: 2.2, color: "var(--faint)", title: T.sentUnconfirmed || "sent \u2014 not confirmed by the peer" }) : mine && m.status && /* @__PURE__ */ React.createElement(Icon, { name: "checkCheck", size: 12, stroke: 2.2, color: m.status === "read" ? "var(--accent)" : "var(--faint)" })))
2585
2681
  );
2586
2682
  }
2587
- function Conversation({ T, peer, lang, peers, onOpenChat, thread: threadProp, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, hasEarlier, onLoadEarlier, onAssistantRemove, muted, onToggleMute, phone, onBack }) {
2683
+ function Conversation({ T, peer, lang, peers, onOpenChat, thread: threadProp, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, hasEarlier, onLoadEarlier, onAssistantRemove, onAssistantAdopt, muted, onToggleMute, phone, onBack }) {
2588
2684
  const scrollRef = React.useRef(null);
2589
2685
  const prependRef = React.useRef(null);
2590
2686
  const [earlierBusy, setEarlierBusy] = React.useState(false);
@@ -2947,7 +3043,7 @@ function Conversation({ T, peer, lang, peers, onOpenChat, thread: threadProp, on
2947
3043
  // The internal id is not an address anyone can use and not a name
2948
3044
  // anyone chose; saying what this row IS is more use than showing it.
2949
3045
  /* @__PURE__ */ React.createElement("div", { style: { marginTop: 2, fontFamily: "var(--mono)", fontSize: 11.5, color: "var(--faint)" } }, T && T.assistantSub || "Local \xB7 APP")
2950
- ) : phone ? /* @__PURE__ */ React.createElement("div", { style: { marginTop: 2, display: "flex" } }, /* @__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(
3046
+ ) : phone ? /* @__PURE__ */ React.createElement("div", { style: { marginTop: 2, display: "flex" } }, /* @__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)), onOpenNet && /* @__PURE__ */ React.createElement("span", { style: { color: "var(--line)" } }, "\xB7"), onOpenNet && /* @__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(
2951
3047
  Mono,
2952
3048
  {
2953
3049
  size: 11.5,
@@ -2982,7 +3078,7 @@ ${peer.address}`
2982
3078
  } }), phone && onSendRtcFile && !peer.app && /* @__PURE__ */ React.createElement(MenuItem, { icon: "file", label: T && T.sendWebrtcFile || "Send file via WebRTC", onClick: () => {
2983
3079
  setMenu(false);
2984
3080
  rtcFileRef.current && rtcFileRef.current.click();
2985
- } }), phone && !peer.app && /* @__PURE__ */ React.createElement(MenuItem, { icon: "network", label: T && T.openNet || "Network view", onClick: () => {
3081
+ } }), phone && !peer.app && onOpenNet && /* @__PURE__ */ React.createElement(MenuItem, { icon: "network", label: T && T.openNet || "Network view", onClick: () => {
2986
3082
  setMenu(false);
2987
3083
  onOpenNet(peer);
2988
3084
  } }), !peer.app && /* @__PURE__ */ React.createElement(MenuItem, { icon: "hash", label: T.alias, onClick: () => {
@@ -3017,6 +3113,7 @@ ${peer.address}`
3017
3113
  isGroup,
3018
3114
  onTheater: setTheater,
3019
3115
  onAssistantRemove,
3116
+ onAssistantAdopt,
3020
3117
  onDelete: (id) => doDelete([id]),
3021
3118
  onCancel: doCancel,
3022
3119
  onRetry: doRetry,
@@ -3176,12 +3273,12 @@ function MenuItem({ icon, label, onClick, danger }) {
3176
3273
  function ChatEmpty({ T }) {
3177
3274
  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));
3178
3275
  }
3179
- function ChatTab({ T, lang, peers, requests, activeId, thread, onSelect, onAct, onAdd, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, hasEarlier, onLoadEarlier, onAssistantRemove, prefillAddr, onPrefillConsumed, muted, onToggleMute, phone, onBack }) {
3276
+ function ChatTab({ T, lang, peers, requests, activeId, thread, onSelect, onAct, onAdd, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, hasEarlier, onLoadEarlier, onAssistantRemove, onAssistantAdopt, prefillAddr, onPrefillConsumed, muted, onToggleMute, phone, onBack }) {
3180
3277
  const peer = peers.find((p) => p.id === activeId);
3181
3278
  if (phone) {
3182
- return peer ? /* @__PURE__ */ React.createElement(Conversation, { T, peer, lang, peers, onOpenChat: onSelect, thread, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, hasEarlier, onLoadEarlier, onAssistantRemove, muted: !!muted && muted.includes(peer.id), onToggleMute, phone: true, onBack }) : /* @__PURE__ */ React.createElement(PeerSidebar, { T, peers, requests, activeId, onSelect, onAct, onAdd, prefillAddr, onPrefillConsumed, muted, wide: true });
3279
+ return peer ? /* @__PURE__ */ React.createElement(Conversation, { T, peer, lang, peers, onOpenChat: onSelect, thread, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, hasEarlier, onLoadEarlier, onAssistantRemove, onAssistantAdopt, muted: !!muted && muted.includes(peer.id), onToggleMute, phone: true, onBack }) : /* @__PURE__ */ React.createElement(PeerSidebar, { T, peers, requests, activeId, onSelect, onAct, onAdd, prefillAddr, onPrefillConsumed, muted, wide: true });
3183
3280
  }
3184
- 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, muted }), peer ? /* @__PURE__ */ React.createElement(Conversation, { T, peer, lang, peers, onOpenChat: onSelect, thread, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, hasEarlier, onLoadEarlier, onAssistantRemove, muted: !!muted && muted.includes(peer.id), onToggleMute }) : /* @__PURE__ */ React.createElement(ChatEmpty, { T }));
3281
+ 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, muted }), peer ? /* @__PURE__ */ React.createElement(Conversation, { T, peer, lang, peers, onOpenChat: onSelect, thread, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, hasEarlier, onLoadEarlier, onAssistantRemove, onAssistantAdopt, muted: !!muted && muted.includes(peer.id), onToggleMute }) : /* @__PURE__ */ React.createElement(ChatEmpty, { T }));
3185
3282
  }
3186
3283
  Object.assign(window, { ChatTab });
3187
3284
 
@@ -3956,7 +4053,7 @@ function DkAvatarSheet({ T, me, zh, onSet, onClose }) {
3956
4053
  return /* @__PURE__ */ React.createElement(DkPunkPicker, { T, onPick: pickPunk, onClose: () => setPicking(false) });
3957
4054
  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: 16 } }, /* @__PURE__ */ React.createElement("div", { onClick: (e) => e.stopPropagation(), style: { width: 360, maxWidth: "100%", background: "var(--panel)", border: "1px solid var(--line)", borderRadius: 16, padding: 16, display: "flex", flexDirection: "column", gap: 8 } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 10, marginBottom: 4 } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 40, radius: 10, dot: false }), /* @__PURE__ */ React.createElement("span", { style: { flex: 1, fontFamily: "var(--mono)", fontSize: 13.5, fontWeight: 700, color: "var(--text)" } }, zh ? "\u66F4\u6362\u5934\u50CF" : "Change avatar"), /* @__PURE__ */ React.createElement(Btn, { icon: "x", size: "sm", onClick: onClose })), item("sparkles", zh ? "\u9009\u4E00\u4E2A CryptoPunk" : "Choose a CryptoPunk", () => setPicking(true)), canUpload && item("image", zh ? "\u4E0A\u4F20\u7167\u7247" : "Upload a photo", () => fileRef.current && fileRef.current.click()), has && item("trash", zh ? "\u79FB\u9664\u5934\u50CF" : "Remove avatar", () => save(canUpload ? { punkId: null, avatarDataUrl: null } : { punkId: null }), true), /* @__PURE__ */ React.createElement("input", { ref: fileRef, type: "file", accept: "image/*", onChange: onFile, style: { display: "none" } }), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 11.5, color: "var(--faint)", lineHeight: 1.5, marginTop: 2 } }, zh ? "\u4E0D\u9700\u8981\u6CE8\u518C beagles.eth \u540D\u5B57\u3002\u597D\u53CB\u4F1A\u5728\u4E0B\u6B21\u6536\u5230\u4F60\u7684\u8D44\u6599\u65F6\u770B\u5230\u65B0\u5934\u50CF\u3002" : "No beagles.eth name needed. Friends see it the next time your profile reaches them."), err && /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 11.5, color: "var(--danger)" } }, err)));
3958
4055
  }
3959
- function ProfileTab({ T, me, onEdit, t, setTweak, onSetAvatar }) {
4056
+ function ProfileTab({ T, me, onEdit, t, setTweak, onSetAvatar, sections, sectionProps }) {
3960
4057
  var _a;
3961
4058
  const [avatarSheet, setAvatarSheet] = React.useState(false);
3962
4059
  const [qr, setQr] = React.useState(null);
@@ -3988,12 +4085,33 @@ function ProfileTab({ T, me, onEdit, t, setTweak, onSetAvatar }) {
3988
4085
  },
3989
4086
  style: { width: 16, height: 16, accentColor: "var(--accent)" }
3990
4087
  }
3991
- ), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 13.5, fontWeight: 600, color: "var(--text)" } }, T && T.autoAcceptLabel || "Auto-accept friend requests"), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 11.5, color: "var(--dim)", marginTop: 2 } }, T && T.autoAcceptSub || "Off: requests wait in the sidebar for your approval.")))), hostHasKeyBackup && /* @__PURE__ */ React.createElement(DkKeyBackup, { t: t || {} }), t && setTweak && /* @__PURE__ */ React.createElement(DkAlertSettings, { t, setTweak }), t && setTweak && /* @__PURE__ */ React.createElement(DkDisplaySettings, { t, setTweak }), /* @__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(DkVersionRow, { T, me }), /* @__PURE__ */ React.createElement(DkUpdateCheck, { T })), /* @__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) }), avatarSheet && /* @__PURE__ */ React.createElement(DkAvatarSheet, { T, me, zh: t && t.lang === "zh", onSet: onSetAvatar, onClose: () => setAvatarSheet(false) }), editing && /* @__PURE__ */ React.createElement(DkEditModal, { T, me, onClose: () => setEditing(false), onSave: (name, description) => {
4088
+ ), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 13.5, fontWeight: 600, color: "var(--text)" } }, T && T.autoAcceptLabel || "Auto-accept friend requests"), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 11.5, color: "var(--dim)", marginTop: 2 } }, T && T.autoAcceptSub || "Off: requests wait in the sidebar for your approval.")))), hostHasKeyBackup && /* @__PURE__ */ React.createElement(DkKeyBackup, { t: t || {} }), t && setTweak && /* @__PURE__ */ React.createElement(DkAlertSettings, { t, setTweak }), t && setTweak && /* @__PURE__ */ React.createElement(DkDisplaySettings, { t, setTweak }), /* @__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(DkVersionRow, { T, me }), /* @__PURE__ */ React.createElement(DkUpdateCheck, { T })), /* @__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)))), Array.isArray(sections) && sections.length > 0 && /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 760, margin: "0 auto", padding: "0 20px 40px", display: "flex", flexDirection: "column", gap: 16 } }, sections.map((sec) => sec && typeof sec.render === "function" ? /* @__PURE__ */ React.createElement(DkHostSection, { key: sec.id, sec, props: sectionProps, lang: t && t.lang }) : null)), qr && /* @__PURE__ */ React.createElement(DkQrModal, { T, value: qr.value, label: qr.label, onClose: () => setQr(null) }), avatarSheet && /* @__PURE__ */ React.createElement(DkAvatarSheet, { T, me, zh: t && t.lang === "zh", onSet: onSetAvatar, onClose: () => setAvatarSheet(false) }), editing && /* @__PURE__ */ React.createElement(DkEditModal, { T, me, onClose: () => setEditing(false), onSave: (name, description) => {
3992
4089
  if (onEdit)
3993
4090
  onEdit(name, description);
3994
4091
  setEditing(false);
3995
4092
  } }));
3996
4093
  }
4094
+ var DkHostSection = class extends React.Component {
4095
+ constructor(props) {
4096
+ super(props);
4097
+ this.state = { err: null };
4098
+ }
4099
+ static getDerivedStateFromError(err) {
4100
+ return { err };
4101
+ }
4102
+ render() {
4103
+ var _a;
4104
+ const { sec, props, lang } = this.props;
4105
+ if (this.state.err) {
4106
+ return /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 12, color: "var(--faint)" } }, lang === "zh" ? "\u8FD9\u4E2A\u9644\u52A0\u677F\u5757\u51FA\u9519\u4E86\u3002" : "This host section failed to render.");
4107
+ }
4108
+ try {
4109
+ return (_a = sec.render(props)) != null ? _a : null;
4110
+ } catch (e) {
4111
+ return null;
4112
+ }
4113
+ }
4114
+ };
3997
4115
  Object.assign(window, { DkShareSheet, ProfileTab, DkPunkAvatar, DkEnsAvatar, DkPublicProfile });
3998
4116
 
3999
4117
  // src/ui.jsx
@@ -4210,6 +4328,84 @@ function Section({ label, count, trailing, style }) {
4210
4328
  }
4211
4329
  Object.assign(window, { useIsPhone, DkPhoneStyles, DkIdenticon, DkBot, DkAvatar, DkImgAvatar, StatusDot, Mono, shortKey, RouteTag, Tag, Unread, Btn, Section });
4212
4330
 
4331
+ // src/host-tabs.js
4332
+ var BUILTIN_TABS = ["chat", "people", "network", "apps", "profile"];
4333
+ var warned = /* @__PURE__ */ new Set();
4334
+ function warnOnce(what) {
4335
+ if (warned.has(what))
4336
+ return;
4337
+ warned.add(what);
4338
+ try {
4339
+ console.warn(`[beagle-ui] ignoring unknown host tab or app: ${what}`);
4340
+ } catch (e) {
4341
+ }
4342
+ }
4343
+ function pickText(value, lang, fallback = "") {
4344
+ if (typeof value === "string")
4345
+ return value;
4346
+ if (value && typeof value === "object")
4347
+ return value[lang] || value.en || value.zh || fallback;
4348
+ return fallback;
4349
+ }
4350
+ function resolveTabs(hostTabs2) {
4351
+ if (!Array.isArray(hostTabs2) || !hostTabs2.length) {
4352
+ return BUILTIN_TABS.map((id) => ({ id, kind: "builtin" }));
4353
+ }
4354
+ const out = [];
4355
+ const seen = /* @__PURE__ */ new Set();
4356
+ for (const entry of hostTabs2) {
4357
+ if (typeof entry === "string") {
4358
+ if (!BUILTIN_TABS.includes(entry)) {
4359
+ warnOnce(entry);
4360
+ continue;
4361
+ }
4362
+ if (seen.has(entry))
4363
+ continue;
4364
+ seen.add(entry);
4365
+ out.push({ id: entry, kind: "builtin" });
4366
+ continue;
4367
+ }
4368
+ if (entry && typeof entry === "object" && typeof entry.id === "string" && entry.id) {
4369
+ if (typeof entry.render !== "function") {
4370
+ warnOnce(`${entry.id} (no render function)`);
4371
+ continue;
4372
+ }
4373
+ if (BUILTIN_TABS.includes(entry.id)) {
4374
+ warnOnce(`${entry.id} (a built-in id cannot be redefined)`);
4375
+ continue;
4376
+ }
4377
+ if (seen.has(entry.id))
4378
+ continue;
4379
+ seen.add(entry.id);
4380
+ out.push({
4381
+ id: entry.id,
4382
+ kind: "host",
4383
+ label: entry.label,
4384
+ icon: entry.icon,
4385
+ render: entry.render,
4386
+ badge: typeof entry.badge === "function" ? entry.badge : void 0
4387
+ });
4388
+ continue;
4389
+ }
4390
+ warnOnce(String(entry && entry.id ? entry.id : entry));
4391
+ }
4392
+ return out.length ? out : BUILTIN_TABS.map((id) => ({ id, kind: "builtin" }));
4393
+ }
4394
+ function hasTab(tabs, id) {
4395
+ return (tabs != null ? tabs : []).some((t) => t.id === id);
4396
+ }
4397
+ function resolveApps(builtins, hideApps, hostApps2) {
4398
+ const hidden = new Set(Array.isArray(hideApps) ? hideApps : []);
4399
+ const host = (Array.isArray(hostApps2) ? hostApps2 : []).filter((a) => {
4400
+ if (a && typeof a.id === "string" && a.id && typeof a.open === "function")
4401
+ return true;
4402
+ warnOnce(`app ${a && a.id ? a.id : String(a)}`);
4403
+ return false;
4404
+ }).map((a) => ({ ...a, host: true }));
4405
+ const kept = (builtins != null ? builtins : []).filter((a) => !hidden.has(a.id));
4406
+ return host.concat(kept);
4407
+ }
4408
+
4213
4409
  // src/apps.jsx
4214
4410
  var APPS = [
4215
4411
  {
@@ -4250,11 +4446,24 @@ function dkB64Url(obj) {
4250
4446
  const b64 = btoa(unescape(encodeURIComponent(json)));
4251
4447
  return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
4252
4448
  }
4253
- function AppsTab({ T, t }) {
4449
+ function AppsTab({ T, t, me, peers, phone, setTab }) {
4254
4450
  const zh = t && t.lang === "zh";
4451
+ const apps = React.useMemo(
4452
+ () => resolveApps(APPS, hostHideApps, hostApps),
4453
+ []
4454
+ );
4255
4455
  const [busy, setBusy] = React.useState(null);
4256
4456
  const [err, setErr] = React.useState(null);
4257
4457
  const open = async (app) => {
4458
+ if (app.host) {
4459
+ setErr(null);
4460
+ try {
4461
+ app.open({ T, t, me, peers, phone, setTab });
4462
+ } catch (e) {
4463
+ setErr(String(e && e.message || e));
4464
+ }
4465
+ return;
4466
+ }
4258
4467
  setBusy(app.id);
4259
4468
  setErr(null);
4260
4469
  const win = window.open("", "_blank");
@@ -4276,7 +4485,7 @@ function AppsTab({ T, t }) {
4276
4485
  setBusy(null);
4277
4486
  }
4278
4487
  };
4279
- 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: 16 } }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 11, fontWeight: 700, letterSpacing: 0.6, textTransform: "uppercase", color: "var(--faint)" } }, zh ? "\u5E94\u7528" : "Apps"), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 12.5, color: "var(--faint)", marginTop: 6, lineHeight: 1.6 } }, zh ? "\u8FD9\u4E9B\u5E94\u7528\u63A5\u53D7 Beagle \u8EAB\u4EFD\u3002\u4ECE\u8FD9\u91CC\u6253\u5F00\u4E0D\u9700\u8981\u518D\u767B\u5F55\u4E00\u6B21 \u2014\u2014 \u4F1A\u5E26\u4E0A\u4E00\u4E2A\u4E24\u5206\u949F\u5185\u6709\u6548\u7684\u7B7E\u540D\u3002" : "These accept a Beagle identity. Opening one from here does not ask you to sign in again \u2014 it carries a signature that is good for two minutes.")), err && /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 11.5, color: "#f85149" } }, err), APPS.map((app) => /* @__PURE__ */ React.createElement("div", { key: app.id, style: {
4488
+ 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: 16 } }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 11, fontWeight: 700, letterSpacing: 0.6, textTransform: "uppercase", color: "var(--faint)" } }, zh ? "\u5E94\u7528" : "Apps"), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 12.5, color: "var(--faint)", marginTop: 6, lineHeight: 1.6 } }, zh ? "\u8FD9\u4E9B\u5E94\u7528\u63A5\u53D7 Beagle \u8EAB\u4EFD\u3002\u4ECE\u8FD9\u91CC\u6253\u5F00\u4E0D\u9700\u8981\u518D\u767B\u5F55\u4E00\u6B21 \u2014\u2014 \u4F1A\u5E26\u4E0A\u4E00\u4E2A\u4E24\u5206\u949F\u5185\u6709\u6548\u7684\u7B7E\u540D\u3002" : "These accept a Beagle identity. Opening one from here does not ask you to sign in again \u2014 it carries a signature that is good for two minutes.")), err && /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 11.5, color: "#f85149" } }, err), apps.map((app) => /* @__PURE__ */ React.createElement("div", { key: app.id, style: {
4280
4489
  display: "flex",
4281
4490
  alignItems: "center",
4282
4491
  gap: 14,
@@ -4293,7 +4502,7 @@ function AppsTab({ T, t }) {
4293
4502
  display: "flex",
4294
4503
  alignItems: "center",
4295
4504
  justifyContent: "center"
4296
- } }, /* @__PURE__ */ React.createElement(Icon, { name: app.icon, size: 19, color: "var(--accent)", stroke: 1.8 })), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0 } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 14, fontWeight: 600, color: "var(--text)" } }, app.name), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 12.5, color: "var(--faint)", marginTop: 2 } }, zh ? app.blurb.zh : app.blurb.en), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 11, color: "var(--dim)", marginTop: 3 } }, app.origin.replace(/^https?:\/\//, ""))), /* @__PURE__ */ React.createElement(Btn, { tone: "accent", size: "sm", onClick: busy ? void 0 : () => open(app) }, busy === app.id ? zh ? "\u6253\u5F00\u4E2D\u2026" : "opening\u2026" : zh ? "\u6253\u5F00" : "Open")))));
4505
+ } }, /* @__PURE__ */ React.createElement(Icon, { name: app.icon, size: 19, color: "var(--accent)", stroke: 1.8 })), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0 } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 14, fontWeight: 600, color: "var(--text)" } }, pickText(app.name, t && t.lang, app.id)), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 12.5, color: "var(--faint)", marginTop: 2 } }, pickText(app.blurb, t && t.lang, "")), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 11, color: "var(--dim)", marginTop: 3 } }, app.host ? zh ? "\u672C\u673A\u5E94\u7528" : "runs in this app" : String(app.origin || "").replace(/^https?:\/\//, ""))), /* @__PURE__ */ React.createElement(Btn, { tone: "accent", size: "sm", onClick: busy ? void 0 : () => open(app) }, busy === app.id ? zh ? "\u6253\u5F00\u4E2D\u2026" : "opening\u2026" : zh ? "\u6253\u5F00" : "Open")))));
4297
4506
  }
4298
4507
 
4299
4508
  // src/rtc-signaling.jsx
@@ -6298,7 +6507,7 @@ var STR = {
6298
6507
  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"
6299
6508
  }
6300
6509
  };
6301
- function RailBtn({ icon, label, active, soon, onClick }) {
6510
+ function RailBtn({ icon, label, active, soon, onClick, svgPath }) {
6302
6511
  return /* @__PURE__ */ React.createElement("button", { onClick, title: label, style: {
6303
6512
  width: 52,
6304
6513
  height: 52,
@@ -6313,9 +6522,9 @@ function RailBtn({ icon, label, active, soon, onClick }) {
6313
6522
  alignItems: "center",
6314
6523
  justifyContent: "center",
6315
6524
  gap: 3
6316
- } }, /* @__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)" } }));
6525
+ } }, /* @__PURE__ */ React.createElement(Icon, { name: icon, svgPath, 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)" } }));
6317
6526
  }
6318
- function TabBarBtn({ icon, label, active, badge, onClick }) {
6527
+ function TabBarBtn({ icon, label, active, badge, onClick, svgPath }) {
6319
6528
  return /* @__PURE__ */ React.createElement("button", { onClick, style: {
6320
6529
  flex: 1,
6321
6530
  minWidth: 0,
@@ -6330,9 +6539,36 @@ function TabBarBtn({ icon, label, active, badge, onClick }) {
6330
6539
  justifyContent: "center",
6331
6540
  gap: 3,
6332
6541
  fontFamily: "var(--mono)"
6333
- } }, /* @__PURE__ */ React.createElement(Icon, { name: icon, size: 22, stroke: active ? 2.1 : 1.8, color: active ? "var(--accent)" : "var(--dim)" }), /* @__PURE__ */ React.createElement("span", { style: { fontSize: 10, fontWeight: 600, color: active ? "var(--accent)" : "var(--faint)", whiteSpace: "nowrap" } }, label), badge ? /* @__PURE__ */ React.createElement("span", { style: { position: "absolute", top: 4, left: "50%", marginLeft: 6 } }, /* @__PURE__ */ React.createElement(Unread, { n: badge })) : null);
6542
+ } }, /* @__PURE__ */ React.createElement(Icon, { name: icon, svgPath, size: 22, stroke: active ? 2.1 : 1.8, color: active ? "var(--accent)" : "var(--dim)" }), /* @__PURE__ */ React.createElement("span", { style: { fontSize: 10, fontWeight: 600, color: active ? "var(--accent)" : "var(--faint)", whiteSpace: "nowrap" } }, label), badge ? /* @__PURE__ */ React.createElement("span", { style: { position: "absolute", top: 4, left: "50%", marginLeft: 6 } }, /* @__PURE__ */ React.createElement(Unread, { n: badge })) : null);
6334
6543
  }
6335
6544
  var DK_PEOPLE_TABS = ["here", "recommended", "registered"];
6545
+ var HostTabPane = class extends React.Component {
6546
+ constructor(props) {
6547
+ super(props);
6548
+ this.state = { err: null };
6549
+ }
6550
+ static getDerivedStateFromError(err) {
6551
+ return { err };
6552
+ }
6553
+ componentDidCatch(err) {
6554
+ try {
6555
+ console.error("[beagle-ui] host tab failed", err);
6556
+ } catch (e) {
6557
+ }
6558
+ }
6559
+ render() {
6560
+ var _a;
6561
+ const { tab, props, lang } = this.props;
6562
+ if (this.state.err) {
6563
+ return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, display: "flex", alignItems: "center", justifyContent: "center", padding: 24, textAlign: "center" } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 12.5, color: "var(--faint)", maxWidth: 420 } }, lang === "zh" ? `"${pickText(tab.label, lang, tab.id)}" \u8FD9\u4E2A\u6807\u7B7E\u9875\u51FA\u9519\u4E86\u3002\u5176\u5B83\u6807\u7B7E\u9875\u4E0D\u53D7\u5F71\u54CD\u3002` : `The "${pickText(tab.label, lang, tab.id)}" tab failed to render. The rest of the app is unaffected.`));
6564
+ }
6565
+ try {
6566
+ return (_a = tab.render(props)) != null ? _a : null;
6567
+ } catch (e) {
6568
+ return null;
6569
+ }
6570
+ }
6571
+ };
6336
6572
  function Brand({ T, children, style }) {
6337
6573
  if (!hostBrandHref)
6338
6574
  return /* @__PURE__ */ React.createElement("span", { style }, children);
@@ -6449,7 +6685,17 @@ function DkUpdateModal({ T, info, onClose }) {
6449
6685
  }
6450
6686
  function DkApp() {
6451
6687
  const [t, setTweak] = useTweaks({ ...DK_DEFAULTS, lang: dkDetectLang() });
6452
- const [tab, setTab] = React.useState(t.startTab || "chat");
6688
+ const tabs = React.useMemo(() => resolveTabs(hostTabs), []);
6689
+ const showPeople = hasTab(tabs, "people");
6690
+ const showNetwork = hasTab(tabs, "network");
6691
+ const showProfile = hasTab(tabs, "profile");
6692
+ const firstTab = tabs[0] ? tabs[0].id === "people" ? "here" : tabs[0].id : "chat";
6693
+ const [tab, setTab] = React.useState(() => {
6694
+ const want = t.startTab || "chat";
6695
+ if (want === "chat" && hasTab(tabs, "chat"))
6696
+ return "chat";
6697
+ return hasTab(tabs, want) || DK_PEOPLE_TABS.includes(want) && showPeople ? want : firstTab;
6698
+ });
6453
6699
  const [activeId, setActiveId] = React.useState(null);
6454
6700
  const data = useDaemonData();
6455
6701
  const me = data.me;
@@ -6484,11 +6730,13 @@ function DkApp() {
6484
6730
  });
6485
6731
  }, []);
6486
6732
  React.useEffect(() => {
6733
+ if (!showNetwork)
6734
+ return void 0;
6487
6735
  refreshBackend();
6488
6736
  const ms = backend && backend.state === "releasing" ? 1500 : 8e3;
6489
6737
  const t2 = setInterval(refreshBackend, ms);
6490
6738
  return () => clearInterval(t2);
6491
- }, [refreshBackend, backend && backend.state]);
6739
+ }, [refreshBackend, backend && backend.state, showNetwork]);
6492
6740
  const armLan = React.useCallback(async () => {
6493
6741
  await dkPost("/api/lan-arm", {});
6494
6742
  refreshBackend();
@@ -6757,29 +7005,61 @@ function DkApp() {
6757
7005
  setTab("chat");
6758
7006
  openConversation(id);
6759
7007
  };
6760
- const onOpenNet = () => setTab("network");
6761
- const nav = [
6762
- { id: "chat", icon: "message", label: T.chat },
6763
- { id: "here", icon: "users", label: T.here },
6764
- { id: "recommended", icon: "sparkles", label: T.recommended },
6765
- { id: "registered", icon: "at", label: T.registered },
6766
- { id: "network", icon: "network", label: T.network },
6767
- { id: "apps", icon: "grid", label: T.apps }
6768
- ];
7008
+ const onOpenNet = showNetwork ? () => setTab("network") : null;
7009
+ const RAIL_FOR = {
7010
+ chat: [{ id: "chat", icon: "message", label: T.chat }],
7011
+ people: [
7012
+ { id: "here", icon: "users", label: T.here },
7013
+ { id: "recommended", icon: "sparkles", label: T.recommended },
7014
+ { id: "registered", icon: "at", label: T.registered }
7015
+ ],
7016
+ network: [{ id: "network", icon: "network", label: T.network }],
7017
+ apps: [{ id: "apps", icon: "grid", label: T.apps }],
7018
+ profile: []
7019
+ };
7020
+ const nav = tabs.flatMap((x) => x.kind === "builtin" ? RAIL_FOR[x.id] || [] : [{ id: x.id, icon: x.icon, label: pickText(x.label, t.lang, x.id) }]);
6769
7021
  const peopleTab = DK_PEOPLE_TABS.includes(tab);
6770
7022
  const lastPeople = React.useRef("here");
6771
7023
  if (peopleTab)
6772
7024
  lastPeople.current = tab;
6773
7025
  const unreadTotal = (peers || []).reduce((n, p) => n + (Number(p.unread) || 0), 0) + (requests || []).length;
6774
- const phoneNav = [
6775
- { id: "chat", icon: "message", label: T.chat, active: tab === "chat", badge: unreadTotal, go: () => setTab("chat") },
6776
- { id: "people", icon: "users", label: T.people, active: peopleTab, go: () => setTab(lastPeople.current) },
6777
- { id: "network", icon: "network", label: T.network, active: tab === "network", go: () => setTab("network") },
6778
- { id: "apps", icon: "grid", label: T.apps, active: tab === "apps", go: () => setTab("apps") },
6779
- { id: "profile", icon: "userRound", label: T.profile, active: tab === "profile", go: () => setTab("profile") }
6780
- ];
7026
+ const NAV_FOR = {
7027
+ chat: { icon: "message", label: T.chat, active: tab === "chat", badge: unreadTotal, go: () => setTab("chat") },
7028
+ people: { icon: "users", label: T.people, active: peopleTab, go: () => setTab(lastPeople.current) },
7029
+ network: { icon: "network", label: T.network, active: tab === "network", go: () => setTab("network") },
7030
+ apps: { icon: "grid", label: T.apps, active: tab === "apps", go: () => setTab("apps") },
7031
+ profile: { icon: "userRound", label: T.profile, active: tab === "profile", go: () => setTab("profile") }
7032
+ };
7033
+ const phoneNav = tabs.map((x) => x.kind === "builtin" ? { id: x.id, ...NAV_FOR[x.id] } : {
7034
+ id: x.id,
7035
+ icon: typeof x.icon === "string" ? x.icon : void 0,
7036
+ svgPath: x.icon && typeof x.icon === "object" ? x.icon.svgPath : void 0,
7037
+ label: pickText(x.label, t.lang, x.id),
7038
+ active: tab === x.id,
7039
+ // A host badge is called on every render, like the unread count beside
7040
+ // it; a throwing one must not take the tab bar down with it.
7041
+ badge: (() => {
7042
+ try {
7043
+ return x.badge ? x.badge() : 0;
7044
+ } catch (e) {
7045
+ return 0;
7046
+ }
7047
+ })(),
7048
+ go: () => setTab(x.id)
7049
+ });
6781
7050
  const inConversation = phone && tab === "chat" && !!activeId && (peers || []).some((p) => p.id === activeId);
6782
- return /* @__PURE__ */ React.createElement("div", { style: { ...vars, "--row-pad": rowPad, position: "fixed", inset: 0, display: "flex", flexDirection: phone ? "column" : "row", background: "var(--bg)", color: "var(--text)", fontFamily: "var(--ui)" } }, /* @__PURE__ */ React.createElement(DkPhoneStyles, null), !phone && /* @__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(Brand, { T, style: { width: 38, height: 38, borderRadius: 10, background: "var(--accent)", display: "flex", alignItems: "center", justifyContent: "center", marginBottom: 8, flexShrink: 0 } }, /* @__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("button", { onClick: () => setTab("profile"), title: T.profile, style: {
7051
+ return /* @__PURE__ */ React.createElement("div", { style: { ...vars, "--row-pad": rowPad, position: "fixed", inset: 0, display: "flex", flexDirection: phone ? "column" : "row", background: "var(--bg)", color: "var(--text)", fontFamily: "var(--ui)" } }, /* @__PURE__ */ React.createElement(DkPhoneStyles, null), !phone && /* @__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(Brand, { T, style: { width: 38, height: 38, borderRadius: 10, background: "var(--accent)", display: "flex", alignItems: "center", justifyContent: "center", marginBottom: 8, flexShrink: 0 } }, /* @__PURE__ */ React.createElement(Icon, { name: "terminal", size: 20, color: "#fff", stroke: 2.2 })), nav.map((n) => /* @__PURE__ */ React.createElement(
7052
+ RailBtn,
7053
+ {
7054
+ key: n.id,
7055
+ icon: typeof n.icon === "string" ? n.icon : void 0,
7056
+ svgPath: n.icon && typeof n.icon === "object" ? n.icon.svgPath : void 0,
7057
+ label: n.label,
7058
+ active: tab === n.id,
7059
+ soon: n.soon,
7060
+ onClick: () => setTab(n.id)
7061
+ }
7062
+ )), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), showProfile && /* @__PURE__ */ React.createElement("button", { onClick: () => setTab("profile"), title: T.profile, style: {
6783
7063
  padding: 4,
6784
7064
  borderRadius: 12,
6785
7065
  cursor: "pointer",
@@ -6800,10 +7080,22 @@ function DkApp() {
6800
7080
  fontWeight: 600,
6801
7081
  background: tab === n.id ? "var(--accent)" : "transparent",
6802
7082
  color: tab === n.id ? "#fff" : "var(--dim)"
6803
- } }, /* @__PURE__ */ React.createElement(Icon, { name: n.icon, size: 14, stroke: 2, color: tab === n.id ? "#fff" : "var(--dim)" }), n.label))), tab === "chat" && /* @__PURE__ */ React.createElement(ChatTab, { T, lang: t.lang, phone, onBack: closeConversation, peers, requests, activeId, thread: activeId === ASSISTANT_ID ? assistant.thread : data.threads[activeId], onSelect, onAct, onAdd, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread: () => activeId && data.loadThread(activeId), hasEarlier: !!(activeId && activeId !== ASSISTANT_ID && data.earlier[activeId]), onAssistantRemove: activeId === ASSISTANT_ID ? assistant.removeFriends : null, onLoadEarlier: () => activeId && data.loadEarlier(activeId), prefillAddr: pendingAddr, onPrefillConsumed: () => setPendingAddr(""), muted: mutedIds, onToggleMute }), tab === "here" && /* @__PURE__ */ React.createElement(DiscoverTab, { T, kind: "bridge", peers, meId: me.userId, onAdd, onOpenChat }), tab === "recommended" && /* @__PURE__ */ React.createElement(DiscoverTab, { T, kind: "recommended", peers, meId: me.userId, onAdd, onOpenChat }), tab === "registered" && /* @__PURE__ */ React.createElement(DiscoverTab, { T, kind: "registered", peers, meId: me.userId, onAdd, onOpenChat }), tab === "network" && /* @__PURE__ */ React.createElement(NetworkTab, { T, me, peers, exits, activeExit, reqCount: requests.length, onSetExit, onOpenChat, backend, onArmLan: armLan, onCancelLan: cancelLan }), tab === "apps" && /* @__PURE__ */ React.createElement(AppsTab, { T, t }), tab === "profile" && /* @__PURE__ */ React.createElement(ProfileTab, { T, me, onEdit, t, setTweak, onSetAvatar: (p) => dkApi.setProfileFull(p).then((r) => {
6804
- data.refresh();
6805
- return r;
6806
- }) })), phone && !inConversation && /* @__PURE__ */ React.createElement("div", { style: { flexShrink: 0, display: "flex", borderTop: "1px solid var(--line)", background: "var(--rail)", paddingBottom: "env(safe-area-inset-bottom, 0px)" } }, phoneNav.map((n) => /* @__PURE__ */ React.createElement(TabBarBtn, { key: n.id, icon: n.icon, label: n.label, active: n.active, badge: n.badge, onClick: n.go }))), data.locked && /* @__PURE__ */ React.createElement(
7083
+ } }, /* @__PURE__ */ React.createElement(Icon, { name: n.icon, size: 14, stroke: 2, color: tab === n.id ? "#fff" : "var(--dim)" }), n.label))), tab === "chat" && /* @__PURE__ */ React.createElement(ChatTab, { T, lang: t.lang, phone, onBack: closeConversation, peers, requests, activeId, thread: activeId === ASSISTANT_ID ? assistant.thread : data.threads[activeId], onSelect, onAct, onAdd, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread: () => activeId && data.loadThread(activeId), hasEarlier: !!(activeId && activeId !== ASSISTANT_ID && data.earlier[activeId]), onAssistantRemove: activeId === ASSISTANT_ID ? assistant.removeFriends : null, onAssistantAdopt: activeId === ASSISTANT_ID ? assistant.adoptServicePeers : null, onLoadEarlier: () => activeId && data.loadEarlier(activeId), prefillAddr: pendingAddr, onPrefillConsumed: () => setPendingAddr(""), muted: mutedIds, onToggleMute }), tab === "here" && /* @__PURE__ */ React.createElement(DiscoverTab, { T, kind: "bridge", peers, meId: me.userId, onAdd, onOpenChat }), tab === "recommended" && /* @__PURE__ */ React.createElement(DiscoverTab, { T, kind: "recommended", peers, meId: me.userId, onAdd, onOpenChat }), tab === "registered" && /* @__PURE__ */ React.createElement(DiscoverTab, { T, kind: "registered", peers, meId: me.userId, onAdd, onOpenChat }), tab === "network" && /* @__PURE__ */ React.createElement(NetworkTab, { T, me, peers, exits, activeExit, reqCount: requests.length, onSetExit, onOpenChat, backend, onArmLan: armLan, onCancelLan: cancelLan }), tab === "apps" && /* @__PURE__ */ React.createElement(AppsTab, { T, t, me, peers, phone, setTab }), tabs.map((x) => x.kind === "host" && tab === x.id ? /* @__PURE__ */ React.createElement(HostTabPane, { key: x.id, tab: x, props: { T, t, me, peers, phone, setTab }, lang: t.lang }) : null), tab === "profile" && /* @__PURE__ */ React.createElement(
7084
+ ProfileTab,
7085
+ {
7086
+ T,
7087
+ me,
7088
+ onEdit,
7089
+ t,
7090
+ setTweak,
7091
+ onSetAvatar: (p) => dkApi.setProfileFull(p).then((r) => {
7092
+ data.refresh();
7093
+ return r;
7094
+ }),
7095
+ sections: hostProfileSections,
7096
+ sectionProps: { T, t, me, peers, phone, setTab }
7097
+ }
7098
+ )), phone && !inConversation && /* @__PURE__ */ React.createElement("div", { style: { flexShrink: 0, display: "flex", borderTop: "1px solid var(--line)", background: "var(--rail)", paddingBottom: "env(safe-area-inset-bottom, 0px)" } }, phoneNav.map((n) => /* @__PURE__ */ React.createElement(TabBarBtn, { key: n.id, icon: n.icon, svgPath: n.svgPath, label: n.label, active: n.active, badge: n.badge, onClick: n.go }))), data.locked && /* @__PURE__ */ React.createElement(
6807
7099
  DkLockedOut,
6808
7100
  {
6809
7101
  compact: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/beagle-ui",
3
- "version": "0.2.25",
3
+ "version": "0.2.27",
4
4
  "description": "The Beagle chat UI, minus the machine it runs on — shared by the desktop install and the browser build.",
5
5
  "license": "MIT",
6
6
  "type": "module",