ui(routes): split from/to header into aligned icon grid

The route card header wrapped onto two lines when from/to labels were
long. Replace the inline "from -> to" title with a two-row CSS grid
(auto | 1fr) so labels align vertically, each preceded by a dedicated
SVG icon (iconRouteFrom/iconRouteTo: arrow departing/arriving at a
dot). Long labels now ellipsize with a title tooltip.

Move the reversibility indicator (reversible <-> vs one-way ->) out of
the title into a ghost badge beside the quality badge, where it no
longer fights the grid layout.

Also nudge spacing between "Recent Packets" and its path rows (mt-1 ->
mt-2) for a little breathing room.
This commit is contained in:
Louis King
2026-07-16 22:56:48 +01:00
parent 30f18561bf
commit b8b7df0966
2 changed files with 18 additions and 6 deletions
@@ -181,3 +181,11 @@ export function iconClock(cls = 'h-5 w-5') {
export function iconFilter(cls = 'h-5 w-5') {
return html`<svg xmlns="http://www.w3.org/2000/svg" class=${cls} fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3c2.755 0 5.455.232 8.083.678.533.09.917.556.917 1.096v1.044a2.25 2.25 0 01-.659 1.591l-5.432 5.432a2.25 2.25 0 00-.659 1.591v2.927a2.25 2.25 0 01-1.244 2.013L9.75 21v-6.568a2.25 2.25 0 00-.659-1.591L3.659 7.409A2.25 2.25 0 013 5.818V4.774c0-.54.384-1.006.917-1.096A48.32 48.32 0 0112 3z" /></svg>`;
}
export function iconRouteFrom(cls = 'h-5 w-5') {
return html`<svg xmlns="http://www.w3.org/2000/svg" class=${cls} fill="none" viewBox="0 0 24 24" stroke="currentColor"><circle cx="5" cy="12" r="2.5" stroke-width="2" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7.5 12H21m0 0l-4-4m4 4l-4 4" /></svg>`;
}
export function iconRouteTo(cls = 'h-5 w-5') {
return html`<svg xmlns="http://www.w3.org/2000/svg" class=${cls} fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12h13.5m0 0l-4-4m4 4l-4 4" /><circle cx="19" cy="12" r="2.5" stroke-width="2" /></svg>`;
}
@@ -1,6 +1,6 @@
import { apiGet, apiPost, apiPut, apiDelete, isAbortError } from '../api.js';
import { html, litRender, nothing, t, errorAlert, getConfig, hasRole } from '../components.js';
import { iconPath, iconPlus, iconEdit, iconTrash, iconPackets, iconClock, iconRuler, iconNodes, iconSatelliteDish } from '../icons.js';
import { iconPath, iconPlus, iconEdit, iconTrash, iconPackets, iconClock, iconRuler, iconNodes, iconSatelliteDish, iconRouteFrom, iconRouteTo } from '../icons.js';
const VISIBILITY_ORDER = ['community', 'member', 'operator', 'admin'];
@@ -147,14 +147,18 @@ function renderRouteCard(route, { isAdmin, onDelete, onEdit, detail, navigate, p
<div class="card-body">
<div class="flex items-start justify-between gap-2">
<div class="flex-1 min-w-0">
<h2 class="card-title flex items-center gap-2 flex-wrap">
<span>${route.from_label}</span>
<span class="opacity-50">${arrow}</span>
<span>${route.to_label}</span>
<h2 class="card-title">
<div class="grid grid-cols-[auto_1fr] gap-x-2 items-center min-w-0">
<span class="flex items-center text-base-content/60">${iconRouteFrom('h-5 w-5')}</span>
<span class="truncate" title=${route.from_label || ''}>${route.from_label}</span>
<span class="flex items-center text-base-content/60">${iconRouteTo('h-5 w-5')}</span>
<span class="truncate" title=${route.to_label || ''}>${route.to_label}</span>
</div>
</h2>
${route.description ? html`<p class="text-sm opacity-70 mt-1">${route.description}</p>` : nothing}
</div>
<div class="flex items-center gap-2 flex-shrink-0">
<span class="badge badge-ghost badge-sm opacity-60 font-mono text-xl leading-none px-2 inline-flex items-center" title=${route.reversible !== false ? t('routes.reversible_label') : ''}>${arrow}</span>
${badge}
</div>
</div>
@@ -186,7 +190,7 @@ function renderDetailContent(route, detail, { navigate, packetsEnabled, history
${historySection}
${matches.length > 0 ? html`<div>
<strong class="opacity-70">${t('routes.recent_packets')}</strong>
<div class="mt-1 space-y-2">
<div class="mt-2 space-y-2">
${matches.map(m => {
const prefixLen = 2 * (route.match_width || 1);
const pathLookup = new Map(