@decentnetwork/beagle-ui 0.2.26 → 0.2.28

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 +424 -55
  2. package/package.json +1 -1
package/dist/beagle-ui.js CHANGED
@@ -1,4 +1,4 @@
1
- export const BEAGLE_UI_VERSION = "0.2.26";
1
+ export const BEAGLE_UI_VERSION = "0.2.28";
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,12 @@ 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
+ // A pane pinned to the right edge — "keep this beside the page".
118
+ dock: '<rect x="3" y="4" width="18" height="16" rx="2"/><path d="M14 4v16"/><path d="M17 9.5h1.5M17 13h1.5"/>',
119
+ 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
120
  });
116
121
 
117
122
  // src/ui-host.jsx
@@ -121,8 +126,20 @@ var hostPrefersRtcFile = false;
121
126
  var hostHasKeyBackup = false;
122
127
  var hostEnsWrite = true;
123
128
  var hostBrandHref = "";
129
+ var hostTabs = null;
130
+ var hostHideApps = null;
131
+ var hostApps = null;
132
+ var hostProfileSections = null;
133
+ var hostHeaderActions = null;
134
+ var hostIsProtocolMessage = null;
124
135
  function setUiHost(host) {
125
136
  H = host || {};
137
+ hostTabs = Array.isArray(H.hostTabs) ? H.hostTabs : null;
138
+ hostHideApps = Array.isArray(H.hideApps) ? H.hideApps : null;
139
+ hostApps = Array.isArray(H.hostApps) ? H.hostApps : null;
140
+ hostProfileSections = Array.isArray(H.hostProfileSections) ? H.hostProfileSections : null;
141
+ hostHeaderActions = Array.isArray(H.hostHeaderActions) ? H.hostHeaderActions : null;
142
+ hostIsProtocolMessage = typeof H.isProtocolMessage === "function" ? H.isProtocolMessage : null;
126
143
  hostNeedsWelcome = !!H.hostNeedsWelcome;
127
144
  hostPrefersRtcFile = !!H.hostPrefersRtcFile;
128
145
  hostHasKeyBackup = !!H.hostHasKeyBackup;
@@ -1489,6 +1506,100 @@ ${lines}`, { assistant: { kind: "adopt", rows: fresh } }));
1489
1506
  return { thread, handle, removeFriends, adoptServicePeers };
1490
1507
  }
1491
1508
 
1509
+ // src/host-messages.js
1510
+ function protocolTest(rule) {
1511
+ if (typeof rule !== "function")
1512
+ return () => false;
1513
+ return (msg) => {
1514
+ if (!msg)
1515
+ return false;
1516
+ try {
1517
+ const v = rule(msg);
1518
+ return typeof v === "string" ? v || true : !!v;
1519
+ } catch (e) {
1520
+ return false;
1521
+ }
1522
+ };
1523
+ }
1524
+ function stripOwnPrefix(text) {
1525
+ return String(text == null ? "" : text).replace(/^you:\s*/i, "");
1526
+ }
1527
+ function lastMessageOf(peer) {
1528
+ const raw = String(peer && peer.lastMsg || "");
1529
+ return {
1530
+ text: stripOwnPrefix(raw),
1531
+ dir: peer && peer.lastDir || (/^you:\s*/i.test(raw) ? "out" : "in"),
1532
+ ts: peer && peer.lastTs || 0
1533
+ };
1534
+ }
1535
+ function lastIsProtocol(peer, isProto) {
1536
+ const last = lastMessageOf(peer);
1537
+ if (!last.text)
1538
+ return false;
1539
+ return isProto(last);
1540
+ }
1541
+ function rosterRow(peer, isProto) {
1542
+ const hidden = lastIsProtocol(peer, isProto);
1543
+ return {
1544
+ preview: hidden ? "" : peer && peer.lastMsg || "",
1545
+ lastTime: hidden ? "" : peer && peer.lastTime || "",
1546
+ unread: hidden ? 0 : Number(peer && peer.unread || 0),
1547
+ activityTs: hidden ? 0 : Number(peer && peer.lastTs || 0),
1548
+ hidden
1549
+ };
1550
+ }
1551
+ function rosterUnreadTotal(peers, isProto) {
1552
+ return (peers || []).reduce((n, p) => n + rosterRow(p, isProto).unread, 0);
1553
+ }
1554
+ function collapseProtocol(thread, isProto, lang) {
1555
+ const out = [];
1556
+ let run = null;
1557
+ const flush = () => {
1558
+ if (!run)
1559
+ return;
1560
+ out.push({
1561
+ protocol: true,
1562
+ id: `proto-${run.first.id || run.startIndex}`,
1563
+ count: run.items.length,
1564
+ label: run.label,
1565
+ dir: run.first.from === "me" ? "out" : "in",
1566
+ text: run.items.map((m) => m.text).join("\n"),
1567
+ line: protocolLine(run, lang)
1568
+ });
1569
+ run = null;
1570
+ };
1571
+ (thread || []).forEach((m, i) => {
1572
+ const hit = m && !m.day ? isProto({ text: m.text, dir: m && m.from === "me" ? "out" : "in", ts: m && m.ts }) : false;
1573
+ if (!hit || m && m.file) {
1574
+ flush();
1575
+ out.push(m);
1576
+ return;
1577
+ }
1578
+ if (!run)
1579
+ run = { first: m, startIndex: i, items: [], label: typeof hit === "string" ? hit : "" };
1580
+ if (!run.label && typeof hit === "string")
1581
+ run.label = hit;
1582
+ run.items.push(m);
1583
+ });
1584
+ flush();
1585
+ return out;
1586
+ }
1587
+ function protocolLine(run, lang) {
1588
+ const arrow = run.first.from === "me" ? "\u2192" : "\u2190";
1589
+ const n = run.items.length;
1590
+ if (run.label) {
1591
+ return `\u2699 ${arrow} ${run.label}${n > 1 ? ` \xB7 ${n}` : ""}`;
1592
+ }
1593
+ if (n > 1) {
1594
+ return lang === "zh" ? `\u2699 ${arrow} ${n} \u6761\u534F\u8BAE\u6D88\u606F` : `\u2699 ${arrow} ${n} protocol messages`;
1595
+ }
1596
+ return `\u2699 ${arrow} ${protocolSummary(run.first.text)}`;
1597
+ }
1598
+ function protocolSummary(text, max = 72) {
1599
+ const s = String(text == null ? "" : text).replace(/\s+/g, " ").trim();
1600
+ return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
1601
+ }
1602
+
1492
1603
  // src/chat.jsx
1493
1604
  var DK_CMD_HIST_KEY = "beagle.assistant.history";
1494
1605
  var DK_CMD_HIST_MAX = 50;
@@ -1550,8 +1661,9 @@ function RequestsBlock({ T, requests, onAct }) {
1550
1661
  paddingLeft: 8
1551
1662
  } }, r.descr), failed[r.id] && /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 11, color: "var(--danger)" } }, failed[r.id]), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: 6 } }, /* @__PURE__ */ React.createElement(Btn, { tone: "ok", icon: "check", size: "sm", onClick: () => act(r, "accept"), style: { flex: 1 } }, T.accept), /* @__PURE__ */ React.createElement(Btn, { tone: "danger", icon: "x", size: "sm", onClick: () => act(r, "reject"), style: { flex: 1 } }, T.reject))))));
1552
1663
  }
1553
- function PeerRow({ peer, T, active, onClick, muted }) {
1664
+ function PeerRow({ peer, T, active, onClick, muted, isProtocol }) {
1554
1665
  const name = peer.alias || peer.userId;
1666
+ const row = rosterRow(peer, isProtocol || (() => false));
1555
1667
  return /* @__PURE__ */ React.createElement("button", { onClick, style: {
1556
1668
  display: "flex",
1557
1669
  alignItems: "center",
@@ -1580,12 +1692,13 @@ function PeerRow({ peer, T, active, onClick, muted }) {
1580
1692
  textOverflow: "ellipsis",
1581
1693
  flex: 1,
1582
1694
  minWidth: 0
1583
- } }, dkRowPreview(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 }) : peer.pending ? /* @__PURE__ */ React.createElement(Icon, { name: "clock", size: 11, stroke: 2.2, color: "var(--warn, #d29922)", title: "friend request pending" }) : /* @__PURE__ */ React.createElement(StatusDot, { online: peer.online })));
1695
+ } }, dkRowPreview(row.preview)))), /* @__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)" } }, row.lastTime), row.unread ? /* @__PURE__ */ React.createElement(Unread, { n: row.unread }) : peer.pending ? /* @__PURE__ */ React.createElement(Icon, { name: "clock", size: 11, stroke: 2.2, color: "var(--warn, #d29922)", title: "friend request pending" }) : /* @__PURE__ */ React.createElement(StatusDot, { online: peer.online })));
1584
1696
  }
1585
- function dkActivityTs(p) {
1586
- return Math.max(p.lastTs || 0, p.acceptedAt || 0, p.requestedAt || 0);
1697
+ function dkActivityTs(p, isProtocol) {
1698
+ const row = rosterRow(p, isProtocol || (() => false));
1699
+ return Math.max(row.activityTs, p.acceptedAt || 0, p.requestedAt || 0);
1587
1700
  }
1588
- function PeerSidebar({ T, peers, requests, activeId, onSelect, onAct, onAdd, prefillAddr, onPrefillConsumed, muted, wide }) {
1701
+ function PeerSidebar({ T, peers, requests, activeId, onSelect, onAct, onAdd, prefillAddr, onPrefillConsumed, muted, wide, isProtocol }) {
1589
1702
  const [q, setQ] = React.useState("");
1590
1703
  const [addr, setAddr] = React.useState("");
1591
1704
  const [addState, setAddState] = React.useState(null);
@@ -1630,7 +1743,7 @@ function PeerSidebar({ T, peers, requests, activeId, onSelect, onAct, onAdd, pre
1630
1743
  }).slice().sort((a, b) => {
1631
1744
  if (!!a.pinned !== !!b.pinned)
1632
1745
  return a.pinned ? -1 : 1;
1633
- const ta = dkActivityTs(a), tb = dkActivityTs(b);
1746
+ const ta = dkActivityTs(a, isProtocol), tb = dkActivityTs(b, isProtocol);
1634
1747
  if (ta !== tb)
1635
1748
  return tb - ta;
1636
1749
  return String(a.alias || a.userId || "").localeCompare(String(b.alias || b.userId || ""));
@@ -1669,7 +1782,7 @@ function PeerSidebar({ T, peers, requests, activeId, onSelect, onAct, onAdd, pre
1669
1782
  fontSize: 11.5,
1670
1783
  lineHeight: 1.35,
1671
1784
  color: addState.kind === "err" ? "var(--bad, #e5484d)" : addState.kind === "ok" ? "var(--good, #46a758)" : "var(--faint)"
1672
- } }, 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), muted: !!muted && muted.includes(p.id) })), !filtered.length && /* @__PURE__ */ React.createElement("div", { style: { padding: 16, textAlign: "center", fontFamily: "var(--mono)", fontSize: 12, color: "var(--faint)" } }, "no matches"))));
1785
+ } }, 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), muted: !!muted && muted.includes(p.id), isProtocol })), !filtered.length && /* @__PURE__ */ React.createElement("div", { style: { padding: 16, textAlign: "center", fontFamily: "var(--mono)", fontSize: 12, color: "var(--faint)" } }, "no matches"))));
1673
1786
  }
1674
1787
  var inputStyle = {
1675
1788
  flex: 1,
@@ -2669,7 +2782,7 @@ function Msg({ m, peer, T, lang, onTheater, onDelete, onCancel, onRetry, onRetry
2669
2782
  ) : 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)" })))
2670
2783
  );
2671
2784
  }
2672
- 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 }) {
2785
+ function Conversation({ T, peer, lang, peers, onOpenChat, thread: threadProp, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, hasEarlier, onLoadEarlier, onAssistantRemove, onAssistantAdopt, isProtocol, muted, onToggleMute, phone, onBack }) {
2673
2786
  const scrollRef = React.useRef(null);
2674
2787
  const prependRef = React.useRef(null);
2675
2788
  const [earlierBusy, setEarlierBusy] = React.useState(false);
@@ -3032,7 +3145,7 @@ function Conversation({ T, peer, lang, peers, onOpenChat, thread: threadProp, on
3032
3145
  // The internal id is not an address anyone can use and not a name
3033
3146
  // anyone chose; saying what this row IS is more use than showing it.
3034
3147
  /* @__PURE__ */ React.createElement("div", { style: { marginTop: 2, fontFamily: "var(--mono)", fontSize: 11.5, color: "var(--faint)" } }, T && T.assistantSub || "Local \xB7 APP")
3035
- ) : 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(
3148
+ ) : 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(
3036
3149
  Mono,
3037
3150
  {
3038
3151
  size: 11.5,
@@ -3067,7 +3180,7 @@ ${peer.address}`
3067
3180
  } }), phone && onSendRtcFile && !peer.app && /* @__PURE__ */ React.createElement(MenuItem, { icon: "file", label: T && T.sendWebrtcFile || "Send file via WebRTC", onClick: () => {
3068
3181
  setMenu(false);
3069
3182
  rtcFileRef.current && rtcFileRef.current.click();
3070
- } }), phone && !peer.app && /* @__PURE__ */ React.createElement(MenuItem, { icon: "network", label: T && T.openNet || "Network view", onClick: () => {
3183
+ } }), phone && !peer.app && onOpenNet && /* @__PURE__ */ React.createElement(MenuItem, { icon: "network", label: T && T.openNet || "Network view", onClick: () => {
3071
3184
  setMenu(false);
3072
3185
  onOpenNet(peer);
3073
3186
  } }), !peer.app && /* @__PURE__ */ React.createElement(MenuItem, { icon: "hash", label: T.alias, onClick: () => {
@@ -3091,7 +3204,15 @@ ${peer.address}`
3091
3204
  fontSize: 12.5,
3092
3205
  color: "var(--text)"
3093
3206
  } }, /* @__PURE__ */ React.createElement(Icon, { name: "clock", size: 13, stroke: 2.2, color: "#d29922" }), /* @__PURE__ */ React.createElement("span", null, T.pendingFriend || "Waiting for them to accept your friend request \u2014 messages will queue and deliver once they do.")),
3094
- /* @__PURE__ */ React.createElement("div", { ref: scrollRef, onScroll: updateFollowScroll, style: { flex: 1, overflow: "auto", padding: phone ? "12px 10px" : "18px 22px", WebkitOverflowScrolling: "touch" } }, /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 1200, margin: "0 auto" } }, hasEarlier && /* @__PURE__ */ React.createElement("div", { style: { display: "flex", justifyContent: "center", margin: "4px 0 10px" } }, /* @__PURE__ */ React.createElement(Btn, { size: "sm", onClick: loadEarlier, disabled: earlierBusy }, earlierBusy ? T.loadingEarlier || "Loading\u2026" : T.loadEarlier || "Load earlier messages")), 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(
3207
+ /* @__PURE__ */ React.createElement("div", { ref: scrollRef, onScroll: updateFollowScroll, style: { flex: 1, overflow: "auto", padding: phone ? "12px 10px" : "18px 22px", WebkitOverflowScrolling: "touch" } }, /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 1200, margin: "0 auto" } }, hasEarlier && /* @__PURE__ */ React.createElement("div", { style: { display: "flex", justifyContent: "center", margin: "4px 0 10px" } }, /* @__PURE__ */ React.createElement(Btn, { size: "sm", onClick: loadEarlier, disabled: earlierBusy }, earlierBusy ? T.loadingEarlier || "Loading\u2026" : T.loadEarlier || "Load earlier messages")), collapseProtocol(thread, isProtocol || (() => false), lang).filter((m) => m.day || m.protocol || !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)) : m.protocol ? /* @__PURE__ */ React.createElement("div", { key: m.id || i, style: { display: "flex", justifyContent: "center", margin: "4px 0" } }, /* @__PURE__ */ React.createElement("span", { title: m.text, style: {
3208
+ fontFamily: "var(--mono)",
3209
+ fontSize: 10.5,
3210
+ color: "var(--faint)",
3211
+ maxWidth: "86%",
3212
+ whiteSpace: "nowrap",
3213
+ overflow: "hidden",
3214
+ textOverflow: "ellipsis"
3215
+ } }, m.line)) : /* @__PURE__ */ React.createElement(
3095
3216
  Msg,
3096
3217
  {
3097
3218
  key: m.id || i,
@@ -3262,12 +3383,12 @@ function MenuItem({ icon, label, onClick, danger }) {
3262
3383
  function ChatEmpty({ T }) {
3263
3384
  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));
3264
3385
  }
3265
- 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 }) {
3386
+ function ChatTab({ T, lang, peers, requests, activeId, thread, isProtocol, onSelect, onAct, onAdd, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, hasEarlier, onLoadEarlier, onAssistantRemove, onAssistantAdopt, prefillAddr, onPrefillConsumed, muted, onToggleMute, phone, onBack }) {
3266
3387
  const peer = peers.find((p) => p.id === activeId);
3267
3388
  if (phone) {
3268
- 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 });
3389
+ return peer ? /* @__PURE__ */ React.createElement(Conversation, { T, peer, lang, peers, onOpenChat: onSelect, thread, isProtocol, 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, isProtocol, wide: true });
3269
3390
  }
3270
- 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 }));
3391
+ 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, isProtocol }), peer ? /* @__PURE__ */ React.createElement(Conversation, { T, peer, lang, peers, onOpenChat: onSelect, thread, isProtocol, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, hasEarlier, onLoadEarlier, onAssistantRemove, onAssistantAdopt, muted: !!muted && muted.includes(peer.id), onToggleMute }) : /* @__PURE__ */ React.createElement(ChatEmpty, { T }));
3271
3392
  }
3272
3393
  Object.assign(window, { ChatTab });
3273
3394
 
@@ -4042,7 +4163,7 @@ function DkAvatarSheet({ T, me, zh, onSet, onClose }) {
4042
4163
  return /* @__PURE__ */ React.createElement(DkPunkPicker, { T, onPick: pickPunk, onClose: () => setPicking(false) });
4043
4164
  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)));
4044
4165
  }
4045
- function ProfileTab({ T, me, onEdit, t, setTweak, onSetAvatar }) {
4166
+ function ProfileTab({ T, me, onEdit, t, setTweak, onSetAvatar, sections, sectionProps }) {
4046
4167
  var _a;
4047
4168
  const [avatarSheet, setAvatarSheet] = React.useState(false);
4048
4169
  const [qr, setQr] = React.useState(null);
@@ -4074,12 +4195,33 @@ function ProfileTab({ T, me, onEdit, t, setTweak, onSetAvatar }) {
4074
4195
  },
4075
4196
  style: { width: 16, height: 16, accentColor: "var(--accent)" }
4076
4197
  }
4077
- ), /* @__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) => {
4198
+ ), /* @__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) => {
4078
4199
  if (onEdit)
4079
4200
  onEdit(name, description);
4080
4201
  setEditing(false);
4081
4202
  } }));
4082
4203
  }
4204
+ var DkHostSection = class extends React.Component {
4205
+ constructor(props) {
4206
+ super(props);
4207
+ this.state = { err: null };
4208
+ }
4209
+ static getDerivedStateFromError(err) {
4210
+ return { err };
4211
+ }
4212
+ render() {
4213
+ var _a;
4214
+ const { sec, props, lang } = this.props;
4215
+ if (this.state.err) {
4216
+ 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.");
4217
+ }
4218
+ try {
4219
+ return (_a = sec.render(props)) != null ? _a : null;
4220
+ } catch (e) {
4221
+ return null;
4222
+ }
4223
+ }
4224
+ };
4083
4225
  Object.assign(window, { DkShareSheet, ProfileTab, DkPunkAvatar, DkEnsAvatar, DkPublicProfile });
4084
4226
 
4085
4227
  // src/ui.jsx
@@ -4296,6 +4438,84 @@ function Section({ label, count, trailing, style }) {
4296
4438
  }
4297
4439
  Object.assign(window, { useIsPhone, DkPhoneStyles, DkIdenticon, DkBot, DkAvatar, DkImgAvatar, StatusDot, Mono, shortKey, RouteTag, Tag, Unread, Btn, Section });
4298
4440
 
4441
+ // src/host-tabs.js
4442
+ var BUILTIN_TABS = ["chat", "people", "network", "apps", "profile"];
4443
+ var warned = /* @__PURE__ */ new Set();
4444
+ function warnOnce(what) {
4445
+ if (warned.has(what))
4446
+ return;
4447
+ warned.add(what);
4448
+ try {
4449
+ console.warn(`[beagle-ui] ignoring unknown host tab or app: ${what}`);
4450
+ } catch (e) {
4451
+ }
4452
+ }
4453
+ function pickText(value, lang, fallback = "") {
4454
+ if (typeof value === "string")
4455
+ return value;
4456
+ if (value && typeof value === "object")
4457
+ return value[lang] || value.en || value.zh || fallback;
4458
+ return fallback;
4459
+ }
4460
+ function resolveTabs(hostTabs2) {
4461
+ if (!Array.isArray(hostTabs2) || !hostTabs2.length) {
4462
+ return BUILTIN_TABS.map((id) => ({ id, kind: "builtin" }));
4463
+ }
4464
+ const out = [];
4465
+ const seen = /* @__PURE__ */ new Set();
4466
+ for (const entry of hostTabs2) {
4467
+ if (typeof entry === "string") {
4468
+ if (!BUILTIN_TABS.includes(entry)) {
4469
+ warnOnce(entry);
4470
+ continue;
4471
+ }
4472
+ if (seen.has(entry))
4473
+ continue;
4474
+ seen.add(entry);
4475
+ out.push({ id: entry, kind: "builtin" });
4476
+ continue;
4477
+ }
4478
+ if (entry && typeof entry === "object" && typeof entry.id === "string" && entry.id) {
4479
+ if (typeof entry.render !== "function") {
4480
+ warnOnce(`${entry.id} (no render function)`);
4481
+ continue;
4482
+ }
4483
+ if (BUILTIN_TABS.includes(entry.id)) {
4484
+ warnOnce(`${entry.id} (a built-in id cannot be redefined)`);
4485
+ continue;
4486
+ }
4487
+ if (seen.has(entry.id))
4488
+ continue;
4489
+ seen.add(entry.id);
4490
+ out.push({
4491
+ id: entry.id,
4492
+ kind: "host",
4493
+ label: entry.label,
4494
+ icon: entry.icon,
4495
+ render: entry.render,
4496
+ badge: typeof entry.badge === "function" ? entry.badge : void 0
4497
+ });
4498
+ continue;
4499
+ }
4500
+ warnOnce(String(entry && entry.id ? entry.id : entry));
4501
+ }
4502
+ return out.length ? out : BUILTIN_TABS.map((id) => ({ id, kind: "builtin" }));
4503
+ }
4504
+ function hasTab(tabs, id) {
4505
+ return (tabs != null ? tabs : []).some((t) => t.id === id);
4506
+ }
4507
+ function resolveApps(builtins, hideApps, hostApps2) {
4508
+ const hidden = new Set(Array.isArray(hideApps) ? hideApps : []);
4509
+ const host = (Array.isArray(hostApps2) ? hostApps2 : []).filter((a) => {
4510
+ if (a && typeof a.id === "string" && a.id && typeof a.open === "function")
4511
+ return true;
4512
+ warnOnce(`app ${a && a.id ? a.id : String(a)}`);
4513
+ return false;
4514
+ }).map((a) => ({ ...a, host: true }));
4515
+ const kept = (builtins != null ? builtins : []).filter((a) => !hidden.has(a.id));
4516
+ return host.concat(kept);
4517
+ }
4518
+
4299
4519
  // src/apps.jsx
4300
4520
  var APPS = [
4301
4521
  {
@@ -4336,11 +4556,24 @@ function dkB64Url(obj) {
4336
4556
  const b64 = btoa(unescape(encodeURIComponent(json)));
4337
4557
  return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
4338
4558
  }
4339
- function AppsTab({ T, t }) {
4559
+ function AppsTab({ T, t, me, peers, phone, setTab }) {
4340
4560
  const zh = t && t.lang === "zh";
4561
+ const apps = React.useMemo(
4562
+ () => resolveApps(APPS, hostHideApps, hostApps),
4563
+ []
4564
+ );
4341
4565
  const [busy, setBusy] = React.useState(null);
4342
4566
  const [err, setErr] = React.useState(null);
4343
4567
  const open = async (app) => {
4568
+ if (app.host) {
4569
+ setErr(null);
4570
+ try {
4571
+ app.open({ T, t, me, peers, phone, setTab });
4572
+ } catch (e) {
4573
+ setErr(String(e && e.message || e));
4574
+ }
4575
+ return;
4576
+ }
4344
4577
  setBusy(app.id);
4345
4578
  setErr(null);
4346
4579
  const win = window.open("", "_blank");
@@ -4362,7 +4595,7 @@ function AppsTab({ T, t }) {
4362
4595
  setBusy(null);
4363
4596
  }
4364
4597
  };
4365
- 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: {
4598
+ 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: {
4366
4599
  display: "flex",
4367
4600
  alignItems: "center",
4368
4601
  gap: 14,
@@ -4379,7 +4612,7 @@ function AppsTab({ T, t }) {
4379
4612
  display: "flex",
4380
4613
  alignItems: "center",
4381
4614
  justifyContent: "center"
4382
- } }, /* @__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")))));
4615
+ } }, /* @__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")))));
4383
4616
  }
4384
4617
 
4385
4618
  // src/rtc-signaling.jsx
@@ -5799,7 +6032,7 @@ function dkFlashStop() {
5799
6032
  function dkLooking() {
5800
6033
  return document.visibilityState === "visible" && document.hasFocus();
5801
6034
  }
5802
- function useIncomingAlerts({ T, peers, requests, loaded, activeId, sound = true, flash = true, muted }) {
6035
+ function useIncomingAlerts({ T, peers, requests, loaded, activeId, sound = true, flash = true, muted, isProtocol }) {
5803
6036
  const prev = React.useRef(null);
5804
6037
  const mutedRef = React.useRef(/* @__PURE__ */ new Set());
5805
6038
  mutedRef.current = new Set(muted || []);
@@ -5829,7 +6062,8 @@ function useIncomingAlerts({ T, peers, requests, loaded, activeId, sound = true,
5829
6062
  const byId = /* @__PURE__ */ new Map();
5830
6063
  for (const p of peers || []) {
5831
6064
  const dir = p.lastDir || (String(p.lastMsg || "").startsWith("you: ") ? "out" : "in");
5832
- byId.set(p.id, { ts: p.lastTs || 0, dir });
6065
+ const quiet = typeof isProtocol === "function" && lastIsProtocol(p, isProtocol);
6066
+ byId.set(p.id, { ts: p.lastTs || 0, dir, quiet });
5833
6067
  }
5834
6068
  const reqIds = new Set((requests || []).map((r) => r.id));
5835
6069
  const was = prev.current;
@@ -5840,6 +6074,8 @@ function useIncomingAlerts({ T, peers, requests, loaded, activeId, sound = true,
5840
6074
  for (const [id, cur] of byId) {
5841
6075
  if (cur.dir !== "in" || !cur.ts)
5842
6076
  continue;
6077
+ if (cur.quiet)
6078
+ continue;
5843
6079
  const old = was.byId.get(id);
5844
6080
  if (old ? cur.ts <= old.ts : Date.now() - cur.ts > 3e4)
5845
6081
  continue;
@@ -6384,7 +6620,7 @@ var STR = {
6384
6620
  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"
6385
6621
  }
6386
6622
  };
6387
- function RailBtn({ icon, label, active, soon, onClick }) {
6623
+ function RailBtn({ icon, label, active, soon, onClick, svgPath }) {
6388
6624
  return /* @__PURE__ */ React.createElement("button", { onClick, title: label, style: {
6389
6625
  width: 52,
6390
6626
  height: 52,
@@ -6399,9 +6635,9 @@ function RailBtn({ icon, label, active, soon, onClick }) {
6399
6635
  alignItems: "center",
6400
6636
  justifyContent: "center",
6401
6637
  gap: 3
6402
- } }, /* @__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)" } }));
6638
+ } }, /* @__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)" } }));
6403
6639
  }
6404
- function TabBarBtn({ icon, label, active, badge, onClick }) {
6640
+ function TabBarBtn({ icon, label, active, badge, onClick, svgPath }) {
6405
6641
  return /* @__PURE__ */ React.createElement("button", { onClick, style: {
6406
6642
  flex: 1,
6407
6643
  minWidth: 0,
@@ -6416,9 +6652,36 @@ function TabBarBtn({ icon, label, active, badge, onClick }) {
6416
6652
  justifyContent: "center",
6417
6653
  gap: 3,
6418
6654
  fontFamily: "var(--mono)"
6419
- } }, /* @__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);
6655
+ } }, /* @__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);
6420
6656
  }
6421
6657
  var DK_PEOPLE_TABS = ["here", "recommended", "registered"];
6658
+ var HostTabPane = class extends React.Component {
6659
+ constructor(props) {
6660
+ super(props);
6661
+ this.state = { err: null };
6662
+ }
6663
+ static getDerivedStateFromError(err) {
6664
+ return { err };
6665
+ }
6666
+ componentDidCatch(err) {
6667
+ try {
6668
+ console.error("[beagle-ui] host tab failed", err);
6669
+ } catch (e) {
6670
+ }
6671
+ }
6672
+ render() {
6673
+ var _a;
6674
+ const { tab, props, lang } = this.props;
6675
+ if (this.state.err) {
6676
+ 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.`));
6677
+ }
6678
+ try {
6679
+ return (_a = tab.render(props)) != null ? _a : null;
6680
+ } catch (e) {
6681
+ return null;
6682
+ }
6683
+ }
6684
+ };
6422
6685
  function Brand({ T, children, style }) {
6423
6686
  if (!hostBrandHref)
6424
6687
  return /* @__PURE__ */ React.createElement("span", { style }, children);
@@ -6535,7 +6798,17 @@ function DkUpdateModal({ T, info, onClose }) {
6535
6798
  }
6536
6799
  function DkApp() {
6537
6800
  const [t, setTweak] = useTweaks({ ...DK_DEFAULTS, lang: dkDetectLang() });
6538
- const [tab, setTab] = React.useState(t.startTab || "chat");
6801
+ const tabs = React.useMemo(() => resolveTabs(hostTabs), []);
6802
+ const showPeople = hasTab(tabs, "people");
6803
+ const showNetwork = hasTab(tabs, "network");
6804
+ const showProfile = hasTab(tabs, "profile");
6805
+ const firstTab = tabs[0] ? tabs[0].id === "people" ? "here" : tabs[0].id : "chat";
6806
+ const [tab, setTab] = React.useState(() => {
6807
+ const want = t.startTab || "chat";
6808
+ if (want === "chat" && hasTab(tabs, "chat"))
6809
+ return "chat";
6810
+ return hasTab(tabs, want) || DK_PEOPLE_TABS.includes(want) && showPeople ? want : firstTab;
6811
+ });
6539
6812
  const [activeId, setActiveId] = React.useState(null);
6540
6813
  const data = useDaemonData();
6541
6814
  const me = data.me;
@@ -6570,11 +6843,13 @@ function DkApp() {
6570
6843
  });
6571
6844
  }, []);
6572
6845
  React.useEffect(() => {
6846
+ if (!showNetwork)
6847
+ return void 0;
6573
6848
  refreshBackend();
6574
6849
  const ms = backend && backend.state === "releasing" ? 1500 : 8e3;
6575
6850
  const t2 = setInterval(refreshBackend, ms);
6576
6851
  return () => clearInterval(t2);
6577
- }, [refreshBackend, backend && backend.state]);
6852
+ }, [refreshBackend, backend && backend.state, showNetwork]);
6578
6853
  const armLan = React.useCallback(async () => {
6579
6854
  await dkPost("/api/lan-arm", {});
6580
6855
  refreshBackend();
@@ -6648,6 +6923,7 @@ function DkApp() {
6648
6923
  const rowPad = t.density === "comfortable" ? "11px 12px" : "7px 10px";
6649
6924
  const mutedIds = Array.isArray(t.alertMuted) ? t.alertMuted : [];
6650
6925
  const onToggleMute = (id) => setTweak("alertMuted", mutedIds.includes(id) ? mutedIds.filter((x) => x !== id) : mutedIds.concat(id));
6926
+ const isProtocol = React.useMemo(() => protocolTest(hostIsProtocolMessage), []);
6651
6927
  useIncomingAlerts({
6652
6928
  T,
6653
6929
  peers,
@@ -6656,7 +6932,8 @@ function DkApp() {
6656
6932
  activeId,
6657
6933
  sound: t.alertSound !== false,
6658
6934
  flash: t.alertFlash !== false,
6659
- muted: mutedIds
6935
+ muted: mutedIds,
6936
+ isProtocol
6660
6937
  });
6661
6938
  const assistant = useAssistant({
6662
6939
  lang: t.lang,
@@ -6690,6 +6967,54 @@ function DkApp() {
6690
6967
  });
6691
6968
  const phone = useIsPhone();
6692
6969
  const [sharing, setSharing] = React.useState(false);
6970
+ const headerActions = (hostHeaderActions || []).filter((a) => a && a.id && typeof a.onClick === "function");
6971
+ const headerActionBtns = headerActions.map((a) => {
6972
+ let on = false;
6973
+ try {
6974
+ on = typeof a.active === "function" ? !!a.active() : false;
6975
+ } catch (e) {
6976
+ on = false;
6977
+ }
6978
+ const label = pickText(a.label, t.lang, a.id);
6979
+ return /* @__PURE__ */ React.createElement(
6980
+ "button",
6981
+ {
6982
+ key: a.id,
6983
+ title: label,
6984
+ "aria-label": label,
6985
+ "aria-pressed": on,
6986
+ onClick: () => {
6987
+ try {
6988
+ a.onClick({ T, t, me, activeId, phone });
6989
+ } catch (e) {
6990
+ }
6991
+ },
6992
+ style: {
6993
+ width: 32,
6994
+ height: 32,
6995
+ flexShrink: 0,
6996
+ borderRadius: 8,
6997
+ cursor: "pointer",
6998
+ padding: 0,
6999
+ border: "1px solid " + (on ? "var(--accent)" : "var(--line)"),
7000
+ background: on ? "color-mix(in oklab, var(--accent), transparent 86%)" : "transparent",
7001
+ display: "flex",
7002
+ alignItems: "center",
7003
+ justifyContent: "center"
7004
+ }
7005
+ },
7006
+ /* @__PURE__ */ React.createElement(
7007
+ Icon,
7008
+ {
7009
+ name: typeof a.icon === "string" ? a.icon : void 0,
7010
+ svgPath: a.icon && typeof a.icon === "object" ? a.icon.svgPath : void 0,
7011
+ size: 16,
7012
+ stroke: 2,
7013
+ color: on ? "var(--accent)" : "var(--dim)"
7014
+ }
7015
+ )
7016
+ );
7017
+ });
6693
7018
  const shareBtn = /* @__PURE__ */ React.createElement(
6694
7019
  "button",
6695
7020
  {
@@ -6843,36 +7168,68 @@ function DkApp() {
6843
7168
  setTab("chat");
6844
7169
  openConversation(id);
6845
7170
  };
6846
- const onOpenNet = () => setTab("network");
6847
- const nav = [
6848
- { id: "chat", icon: "message", label: T.chat },
6849
- { id: "here", icon: "users", label: T.here },
6850
- { id: "recommended", icon: "sparkles", label: T.recommended },
6851
- { id: "registered", icon: "at", label: T.registered },
6852
- { id: "network", icon: "network", label: T.network },
6853
- { id: "apps", icon: "grid", label: T.apps }
6854
- ];
7171
+ const onOpenNet = showNetwork ? () => setTab("network") : null;
7172
+ const RAIL_FOR = {
7173
+ chat: [{ id: "chat", icon: "message", label: T.chat }],
7174
+ people: [
7175
+ { id: "here", icon: "users", label: T.here },
7176
+ { id: "recommended", icon: "sparkles", label: T.recommended },
7177
+ { id: "registered", icon: "at", label: T.registered }
7178
+ ],
7179
+ network: [{ id: "network", icon: "network", label: T.network }],
7180
+ apps: [{ id: "apps", icon: "grid", label: T.apps }],
7181
+ profile: []
7182
+ };
7183
+ 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) }]);
6855
7184
  const peopleTab = DK_PEOPLE_TABS.includes(tab);
6856
7185
  const lastPeople = React.useRef("here");
6857
7186
  if (peopleTab)
6858
7187
  lastPeople.current = tab;
6859
- const unreadTotal = (peers || []).reduce((n, p) => n + (Number(p.unread) || 0), 0) + (requests || []).length;
6860
- const phoneNav = [
6861
- { id: "chat", icon: "message", label: T.chat, active: tab === "chat", badge: unreadTotal, go: () => setTab("chat") },
6862
- { id: "people", icon: "users", label: T.people, active: peopleTab, go: () => setTab(lastPeople.current) },
6863
- { id: "network", icon: "network", label: T.network, active: tab === "network", go: () => setTab("network") },
6864
- { id: "apps", icon: "grid", label: T.apps, active: tab === "apps", go: () => setTab("apps") },
6865
- { id: "profile", icon: "userRound", label: T.profile, active: tab === "profile", go: () => setTab("profile") }
6866
- ];
7188
+ const unreadTotal = rosterUnreadTotal(peers, isProtocol) + (requests || []).length;
7189
+ const NAV_FOR = {
7190
+ chat: { icon: "message", label: T.chat, active: tab === "chat", badge: unreadTotal, go: () => setTab("chat") },
7191
+ people: { icon: "users", label: T.people, active: peopleTab, go: () => setTab(lastPeople.current) },
7192
+ network: { icon: "network", label: T.network, active: tab === "network", go: () => setTab("network") },
7193
+ apps: { icon: "grid", label: T.apps, active: tab === "apps", go: () => setTab("apps") },
7194
+ profile: { icon: "userRound", label: T.profile, active: tab === "profile", go: () => setTab("profile") }
7195
+ };
7196
+ const phoneNav = tabs.map((x) => x.kind === "builtin" ? { id: x.id, ...NAV_FOR[x.id] } : {
7197
+ id: x.id,
7198
+ icon: typeof x.icon === "string" ? x.icon : void 0,
7199
+ svgPath: x.icon && typeof x.icon === "object" ? x.icon.svgPath : void 0,
7200
+ label: pickText(x.label, t.lang, x.id),
7201
+ active: tab === x.id,
7202
+ // A host badge is called on every render, like the unread count beside
7203
+ // it; a throwing one must not take the tab bar down with it.
7204
+ badge: (() => {
7205
+ try {
7206
+ return x.badge ? x.badge() : 0;
7207
+ } catch (e) {
7208
+ return 0;
7209
+ }
7210
+ })(),
7211
+ go: () => setTab(x.id)
7212
+ });
6867
7213
  const inConversation = phone && tab === "chat" && !!activeId && (peers || []).some((p) => p.id === activeId);
6868
- 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: {
7214
+ 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(
7215
+ RailBtn,
7216
+ {
7217
+ key: n.id,
7218
+ icon: typeof n.icon === "string" ? n.icon : void 0,
7219
+ svgPath: n.icon && typeof n.icon === "object" ? n.icon.svgPath : void 0,
7220
+ label: n.label,
7221
+ active: tab === n.id,
7222
+ soon: n.soon,
7223
+ onClick: () => setTab(n.id)
7224
+ }
7225
+ )), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), showProfile && /* @__PURE__ */ React.createElement("button", { onClick: () => setTab("profile"), title: T.profile, style: {
6869
7226
  padding: 4,
6870
7227
  borderRadius: 12,
6871
7228
  cursor: "pointer",
6872
7229
  display: "flex",
6873
7230
  border: "1px solid " + (tab === "profile" ? "var(--line)" : "transparent"),
6874
7231
  background: tab === "profile" ? "var(--panel-2)" : "transparent"
6875
- } }, /* @__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" } }, !inConversation && /* @__PURE__ */ React.createElement(DkBrowserNotice, { lang: t.lang }), !inConversation && /* @__PURE__ */ React.createElement("div", { style: { height: 46, boxSizing: "content-box", paddingTop: phone ? "env(safe-area-inset-top, 0px)" : 0, flexShrink: 0, borderBottom: "1px solid var(--line)", background: "var(--panel)", display: "flex", alignItems: "center", gap: phone ? 8 : 12, paddingLeft: phone ? 12 : 16, paddingRight: phone ? 12 : 16 } }, /* @__PURE__ */ React.createElement(Brand, { T, style: { gap: 12, flexShrink: 0 } }, /* @__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)", whiteSpace: "nowrap" } }, "\xB7 ", (nav.find((n) => n.id === tab) || { label: T.profile }).label.toLowerCase()), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), phone ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(StatusDot, { online: me.online }), shareBtn, /* @__PURE__ */ React.createElement("button", { onClick: () => setTab("profile"), title: T.profile, style: { padding: 0, border: "none", background: "transparent", cursor: "pointer", display: "flex" } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 28, radius: 7, dot: false }))) : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__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)" } }), shareBtn, /* @__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)))), phone && peopleTab && /* @__PURE__ */ React.createElement("div", { style: { flexShrink: 0, display: "flex", gap: 6, padding: "8px 12px", borderBottom: "1px solid var(--line)", background: "var(--panel)", overflowX: "auto" } }, nav.filter((n) => DK_PEOPLE_TABS.includes(n.id)).map((n) => /* @__PURE__ */ React.createElement("button", { key: n.id, onClick: () => setTab(n.id), style: {
7232
+ } }, /* @__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" } }, !inConversation && /* @__PURE__ */ React.createElement(DkBrowserNotice, { lang: t.lang }), !inConversation && /* @__PURE__ */ React.createElement("div", { style: { height: 46, boxSizing: "content-box", paddingTop: phone ? "env(safe-area-inset-top, 0px)" : 0, flexShrink: 0, borderBottom: "1px solid var(--line)", background: "var(--panel)", display: "flex", alignItems: "center", gap: phone ? 8 : 12, paddingLeft: phone ? 12 : 16, paddingRight: phone ? 12 : 16 } }, /* @__PURE__ */ React.createElement(Brand, { T, style: { gap: 12, flexShrink: 0 } }, /* @__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)", whiteSpace: "nowrap" } }, "\xB7 ", (nav.find((n) => n.id === tab) || { label: T.profile }).label.toLowerCase()), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), phone ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(StatusDot, { online: me.online }), headerActionBtns, shareBtn, /* @__PURE__ */ React.createElement("button", { onClick: () => setTab("profile"), title: T.profile, style: { padding: 0, border: "none", background: "transparent", cursor: "pointer", display: "flex" } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 28, radius: 7, dot: false }))) : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__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)" } }), headerActionBtns, shareBtn, /* @__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)))), phone && peopleTab && /* @__PURE__ */ React.createElement("div", { style: { flexShrink: 0, display: "flex", gap: 6, padding: "8px 12px", borderBottom: "1px solid var(--line)", background: "var(--panel)", overflowX: "auto" } }, nav.filter((n) => DK_PEOPLE_TABS.includes(n.id)).map((n) => /* @__PURE__ */ React.createElement("button", { key: n.id, onClick: () => setTab(n.id), style: {
6876
7233
  display: "inline-flex",
6877
7234
  alignItems: "center",
6878
7235
  gap: 6,
@@ -6886,10 +7243,22 @@ function DkApp() {
6886
7243
  fontWeight: 600,
6887
7244
  background: tab === n.id ? "var(--accent)" : "transparent",
6888
7245
  color: tab === n.id ? "#fff" : "var(--dim)"
6889
- } }, /* @__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 }), tab === "profile" && /* @__PURE__ */ React.createElement(ProfileTab, { T, me, onEdit, t, setTweak, onSetAvatar: (p) => dkApi.setProfileFull(p).then((r) => {
6890
- data.refresh();
6891
- return r;
6892
- }) })), 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(
7246
+ } }, /* @__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, isProtocol, 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(
7247
+ ProfileTab,
7248
+ {
7249
+ T,
7250
+ me,
7251
+ onEdit,
7252
+ t,
7253
+ setTweak,
7254
+ onSetAvatar: (p) => dkApi.setProfileFull(p).then((r) => {
7255
+ data.refresh();
7256
+ return r;
7257
+ }),
7258
+ sections: hostProfileSections,
7259
+ sectionProps: { T, t, me, peers, phone, setTab }
7260
+ }
7261
+ )), 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(
6893
7262
  DkLockedOut,
6894
7263
  {
6895
7264
  compact: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/beagle-ui",
3
- "version": "0.2.26",
3
+ "version": "0.2.28",
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",