feat(ui): dual-ecash wallet settings, buy-peer-files, seed backup, assorted fixes

- Tabbed Wallet Settings modal (Cashu + Fedimint) and dual-balance wallet card
- Buy a peer's paid file (ecash / node Lightning / on-chain / external QR)
- Recovery-phrase reveal + backup section; onboarding seed retry resilience
- NetBird HTTPS launch, remote-control two-finger scroll + external-open
- Shared BackButton, single-v version label, mesh Bitcoin header toggles

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-06-17 19:21:42 -04:00
co-authored by Claude Opus 4.8
parent bd567cd165
commit 87769cbfbf
46 changed files with 1527 additions and 283 deletions
+72 -4
View File
@@ -139,6 +139,41 @@ function deepElementFromPoint(x: number, y: number): Element | null {
return el
}
/**
* Find the nearest scrollable ancestor of `el` for the given delta, hopping out
* of same-origin iframes when needed. Synthetic WheelEvents are untrusted and
* never actually scroll the page, so two-finger scroll must call scrollBy on a
* real scroll container — this locates it (e.g. the right-hand app frame). (#7)
*/
function findScrollable(el: Element | null, dx: number, dy: number): Element | null {
let node: Element | null = el
let guard = 0
while (node && guard++ < 60) {
const win = node.ownerDocument?.defaultView
const style = win?.getComputedStyle(node)
if (style) {
const oy = style.overflowY
const ox = style.overflowX
const isRoot = node === node.ownerDocument?.scrollingElement
const canY =
(oy === 'auto' || oy === 'scroll' || isRoot) &&
node.scrollHeight > node.clientHeight + 1
const canX =
(ox === 'auto' || ox === 'scroll' || isRoot) &&
node.scrollWidth > node.clientWidth + 1
if ((dy !== 0 && canY) || (dx !== 0 && canX)) return node
}
if (node.parentElement) {
node = node.parentElement
} else if (win?.frameElement) {
node = win.frameElement as Element // same-origin iframe → continue in parent doc
} else {
break
}
}
return null
}
/** The actually-focused element, descending through same-origin iframes. */
function deepActiveElement(): Element | null {
let el: Element | null = document.activeElement
@@ -267,10 +302,19 @@ function handleMessage(data: string) {
break
}
case 's': {
const dy = msg.y ?? 0
document.dispatchEvent(new WheelEvent('wheel', {
bubbles: true, deltaY: dy * 100, deltaMode: WheelEvent.DOM_DELTA_PIXEL,
}))
// Scroll the element under the virtual cursor (incl. inside same-origin
// app frames like the right-hand panel), not the top document. A synthetic
// wheel event won't scroll — call scrollBy on a real scroll container. (#7)
const dy = (msg.y ?? 0) * 100
const dx = (msg.x ?? 0) * 100
const start = deepElementFromPoint(cursorX, cursorY)
const scroller = findScrollable(start, dx, dy)
if (scroller) {
scroller.scrollBy({ left: dx, top: dy })
} else {
const win = start?.ownerDocument?.defaultView ?? window
win.scrollBy(dx, dy)
}
break
}
}
@@ -308,6 +352,30 @@ function doConnect() {
}
}
/**
* Ask the companion (phone) to open a URL in its own browser.
*
* "Open in external browser" apps can't be usefully opened on the kiosk when a
* companion is driving it — `window.open` lands on the kiosk, which the phone
* user never sees. When a companion is active we forward the URL over the relay
* socket ({"t":"o","url"}); the backend routes it to the phone, which opens it.
*
* Returns true if the request was forwarded (caller should NOT open locally),
* false if there's no active companion (caller should open normally).
*/
export function requestExternalOpen(url: string): boolean {
if (!url || !/^https?:\/\//i.test(url)) return false
if (!companionActive.value) return false
if (!ws || ws.readyState !== WebSocket.OPEN) return false
try {
ws.send(JSON.stringify({ t: 'o', url }))
if (import.meta.env.DEV) console.log('[RemoteRelay] Forwarded external-open to companion:', url)
return true
} catch {
return false
}
}
/** Start the remote relay listener. Connects to /ws/remote-relay. */
export function startRemoteRelay() {
shouldReconnect = true