ux(rover-planning): table rows are real <a href> links

Each path-table cell now wraps its content in <.link navigate={...}>,
so the row behaves as a native anchor: hovering shows the destination
URL in the browser status bar, right-click 'open in new tab' works,
middle-click works, and the rendered HTML is meaningful to assistive
tech / scrapers / link-validators.

Replaces the phx-click={JS.navigate(...)} pattern, which gave none of
those affordances and required the user to trust that the row was
clickable. Whole-row hover highlight is preserved via a CSS
:has(a:hover) selector so cell-hover still tints the row.
This commit is contained in:
Graham McIntire 2026-05-03 16:20:45 -05:00
parent 893a02e1e2
commit 9bec5721d0
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59

View file

@ -14,7 +14,6 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Show do
alias Microwaveprop.RoverPlanning
alias Microwaveprop.RoverPlanning.Mission
alias MicrowavepropWeb.LocationResolver
alias Phoenix.LiveView.JS
@impl true
def mount(%{"id" => id}, _session, socket) do
@ -329,6 +328,29 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Show do
defp path_url(_, _), do: nil
# Wraps a single table cell's content in a real <a> when an href is
# available, so the browser shows the destination on hover and
# right-click / middle-click work natively. Falls back to a plain
# <div> when the row has no path (e.g. compute hasn't started).
attr :href, :string, default: nil
slot :inner_block, required: true
defp path_cell(%{href: href} = assigns) when is_binary(href) do
~H"""
<.link navigate={@href} class="block px-3 py-2 text-inherit no-underline">
{render_slot(@inner_block)}
</.link>
"""
end
defp path_cell(assigns) do
~H"""
<div class="px-3 py-2">
{render_slot(@inner_block)}
</div>
"""
end
@impl true
def render(assigns) do
summary = progress_summary(assigns.paths)
@ -513,38 +535,58 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Show do
</tr>
</thead>
<tbody>
<%!-- Each cell wraps its content in <.link>, so the row
behaves as a real anchor: native href hover preview,
right-click "open in new tab", middle-click, etc. The
row-hover highlight is preserved via :has(a:hover) so
hovering any cell still tints the whole row. --%>
<tr
:for={path <- group_paths}
phx-click={path_url(@mission, path) && JS.navigate(path_url(@mission, path))}
class={[
path_url(@mission, path) && "cursor-pointer hover:bg-base-200"
path_url(@mission, path) &&
"[&:has(a:hover)]:bg-base-200 transition-colors"
]}
title={path_url(@mission, path) && "Open in Path Calculator"}
>
<td class="font-mono text-xs">
<td class="font-mono text-xs p-0">
<% {primary, secondary} = station_lines(path) %>
<.path_cell href={path_url(@mission, path)}>
<div>{primary}</div>
<div :if={secondary} class="opacity-60">{secondary}</div>
</.path_cell>
</td>
<td class="font-mono text-xs">
<td class="font-mono text-xs p-0">
<.path_cell href={path_url(@mission, path)}>
{band_label(path.band_mhz || @mission.band_mhz)}
</.path_cell>
</td>
<td>{status_badge(path.status)}</td>
<td class="text-xs">
<td class="p-0">
<.path_cell href={path_url(@mission, path)}>
{status_badge(path.status)}
</.path_cell>
</td>
<td class="text-xs p-0">
<.path_cell href={path_url(@mission, path)}>
{if path.result, do: format_distance(path.result["distance_km"]), else: ""}
</.path_cell>
</td>
<td class="text-xs">
<td class="text-xs p-0">
<.path_cell href={path_url(@mission, path)}>
{if path.result,
do: format_loss(path.result["total_baseline_loss_db"]),
else: ""}
</.path_cell>
</td>
<td>
<td class="p-0">
<.path_cell href={path_url(@mission, path)}>
{if path.result, do: verdict_badge(path.result["verdict"]), else: ""}
</.path_cell>
</td>
<td>
<td class="p-0">
<.path_cell href={path_url(@mission, path)}>
{if path.result,
do: propagation_badge(path.result["propagation_score"]),
else: ""}
</.path_cell>
</td>
</tr>
</tbody>