fix(repeaters): let a long path wrap in the list instead of being cut off

Same defect as the management header, different symptom: the row put the path
in a .text-truncate line, so on a phone a multi-hop path ended in an ellipsis
and the remaining hops were unreadable. The path line now wraps (per hop, via
the same zero-width space) and keeps its own row width; the logging-in and
not-on-device variants are short and stay truncated as before.

The add-repeater picker and the current-path line in the path modal show the
same string and got the same treatment.

Verified at 360 px against the running container with stubbed API data: list
row, picker item and modal each wrap to two lines with zero overflow, and the
page reports no console errors.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
MarekWoandClaude Opus 5 committed 2026-08-08 15:48:14 +02:00
1 parent 31729fd3f3
commit 8224128da7
2 files changed
+26 -7

No files matched your search

+18 -7
View File
@@ -91,6 +91,15 @@ function esc(s) {
.replaceAll('"', '&quot;');
}
// 'AB→CD→EF' offers the browser no line-break opportunity, so a multi-hop path
// runs off a narrow screen. A zero-width space after each arrow lets it wrap by
// hop. Apply after esc() — the result is HTML.
const RPT_ZWSP = String.fromCharCode(0x200B);
function wrapPath(s) {
return String(s ?? '').replaceAll('→', '→' + RPT_ZWSP);
}
// Relative time comes from datetime-utils.js as formatTimeAgo(). The local copy this
// replaced also had a "Never" branch for a falsy timestamp, which was unreachable — the
// single call site already guards it. contacts.js and dm.js have since been folded in
@@ -165,11 +174,13 @@ function renderRepeaterRows() {
? '<span class="spinner-border spinner-border-sm text-success" role="status"></span>'
: '<i class="bi bi-diagram-3 text-success fs-4"></i>';
// The path line wraps (no text-truncate) so a long path stays readable
// on a phone; the other two variants are short and stay on one line.
const statusLine = isLoggingIn
? `<span class="text-primary">${tHtml('repeaters.row.logging_in')}</span>`
? `<small class="text-muted d-block text-truncate"><span class="text-primary">${tHtml('repeaters.row.logging_in')}</span></small>`
: (r.on_device
? `<span class="rpt-path font-monospace">${esc(r.path_or_mode || '—')}</span>`
: `<span class="text-warning">${tHtml('repeaters.row.not_on_device')}</span>`);
? `<small class="text-muted d-block"><span class="rpt-path font-monospace">${wrapPath(esc(r.path_or_mode || '—'))}</span></small>`
: `<small class="text-muted d-block text-truncate"><span class="text-warning">${tHtml('repeaters.row.not_on_device')}</span></small>`);
// "last login" goes on its own line so a long path keeps the full row
// width to itself (on narrow phones it otherwise gets truncated hard).
const roleLine = (r.on_device && !isLoggingIn && r.last_login_at)
@@ -181,7 +192,7 @@ function renderRepeaterRows() {
<span class="rpt-icon flex-shrink-0">${icon}</span>
<div class="flex-grow-1 rpt-main">
<div class="fw-semibold text-truncate">${esc(r.name || r.public_key.substring(0, 12))}</div>
<small class="text-muted d-block text-truncate">${statusLine}</small>
${statusLine}
${roleLine}
</div>
<div class="btn-group btn-group-sm flex-shrink-0">
@@ -461,7 +472,7 @@ function renderDeviceRepeaterList() {
<i class="bi bi-diagram-3 text-success"></i>
<div class="flex-grow-1" style="min-width: 0;">
<div class="text-truncate">${esc(c.name || c.public_key_prefix)}</div>
<small class="text-muted font-monospace">${esc(c.public_key_prefix)} · ${esc(c.path_or_mode || '')}</small>
<small class="text-muted font-monospace rpt-path-wrap">${esc(c.public_key_prefix)} · ${wrapPath(esc(c.path_or_mode || ''))}</small>
</div>
${added
? `<span class="badge bg-secondary flex-shrink-0">${tHtml('repeaters.added_badge')}</span>`
@@ -503,7 +514,7 @@ function openPathModal(pubkey) {
if (!r) return;
_pathPubkey = pubkey;
document.getElementById('pathModalName').textContent = r.name || pubkey.substring(0, 12);
document.getElementById('pathModalCurrent').textContent = r.path_or_mode || '—';
document.getElementById('pathModalCurrent').textContent = wrapPath(r.path_or_mode || '—');
_pathModal.show();
renderPathList(pubkey);
}
@@ -514,7 +525,7 @@ async function refreshDevicePathDisplay() {
await loadRepeaters();
const r = findRepeater(_pathPubkey);
const el = document.getElementById('pathModalCurrent');
if (el && r) el.textContent = r.path_or_mode || '—';
if (el && r) el.textContent = wrapPath(r.path_or_mode || '—');
}
async function renderPathList(pubkey) {
+8
View File
@@ -87,10 +87,18 @@
opacity: 0.65;
}
/* Paths wrap per hop (zero-width space from wrapPath()); this is the
fallback for a single hop token too long for the line. */
.rpt-path {
font-size: 0.8rem;
}
.rpt-path,
.rpt-path-wrap,
#pathModalCurrent {
overflow-wrap: anywhere;
}
.empty-state {
text-align: center;
color: var(--text-secondary, #6c757d);