From 87357d5297f433b7dcf1224bd90cff16bfb86e39 Mon Sep 17 00:00:00 2001 From: Junwei Zhao Date: Sat, 20 Jun 2026 16:55:29 +1000 Subject: [PATCH] Update UI --- src/components/LinkTable.tsx | 118 ++++++++++++++++++++++++++++++----- src/styles.css | 12 ++++ 2 files changed, 114 insertions(+), 16 deletions(-) diff --git a/src/components/LinkTable.tsx b/src/components/LinkTable.tsx index c657587..a447016 100644 --- a/src/components/LinkTable.tsx +++ b/src/components/LinkTable.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useState, type KeyboardEvent as ReactKeyboardEvent, type SyntheticEvent } from 'react'; import type { Link } from '../lib/api'; import { safeLinkTargetUrl } from '../lib/url'; @@ -64,6 +64,25 @@ function SortIndicator({ field, sortField, sortDir }: { field: SortField; sortFi return {sortDir === 'asc' ? '↑' : '↓'}; } +function navigateToLinkDetail(link: Pick) { + window.location.hash = `/links/${link.scope}/${encodeURIComponent(link.id)}`; +} + +function stopRowNavigation(event: SyntheticEvent) { + event.stopPropagation(); +} + +function handleRowKeyDown(event: ReactKeyboardEvent, link: Pick) { + if (event.target !== event.currentTarget) { + return; + } + + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + navigateToLinkDetail(link); + } +} + export default function LinkTable({ links, loading = false, @@ -125,6 +144,7 @@ export default function LinkTable({ const visible = sorted.slice(0, visibleCount); const hasMore = sorted.length > visibleCount; const remaining = sorted.length - visibleCount; + const showActionColumn = actions && (onEdit != null || onDelete != null); return (
@@ -203,7 +223,7 @@ export default function LinkTable({ ))} - Actions + {showActionColumn ? Actions : null} @@ -211,13 +231,22 @@ export default function LinkTable({ const selected = selectable && selectedIds?.has(link.id) === true; const safeUrl = link.linkType === 'redirect' ? safeLinkTargetUrl(link.targetUrl) : null; return ( - + navigateToLinkDetail(link)} + onKeyDown={(event) => handleRowKeyDown(event, link)} + tabIndex={0} + role="link" + aria-label={`Open details for ${link.alias}`} + > {selectable ? ( onToggleSelect?.(link.id)} /> @@ -229,6 +258,7 @@ export default function LinkTable({ rel="noreferrer noopener" className="alias-pill" style={{ textDecoration: 'none' }} + onClick={stopRowNavigation} > #{link.alias} @@ -239,7 +269,7 @@ export default function LinkTable({ {link.linkType === 'redirect' ? ( link.targetUrl ? ( safeUrl - ? {link.targetUrl} + ? {link.targetUrl} : Invalid target ) : ) : markdown} @@ -248,11 +278,34 @@ export default function LinkTable({ - - {actions && onEdit ? : null} - - {actions && onDelete ? : null} - + {showActionColumn ? ( + + {actions && onEdit ? ( + + ) : null} + {actions && onDelete ? ( + + ) : null} + + ) : null} ); })} @@ -267,13 +320,22 @@ export default function LinkTable({ const selected = selectable && selectedIds?.has(link.id) === true; const safeUrl = link.linkType === 'redirect' ? safeLinkTargetUrl(link.targetUrl) : null; return ( -
+
navigateToLinkDetail(link)} + onKeyDown={(event) => handleRowKeyDown(event, link)} + tabIndex={0} + role="link" + aria-label={`Open details for ${link.alias}`} + >
{selectable ? ( onToggleSelect?.(link.id)} /> ) : null} @@ -283,6 +345,7 @@ export default function LinkTable({ rel="noreferrer noopener" className="alias-pill" style={{ textDecoration: 'none', fontSize: '0.8125rem' }} + onClick={stopRowNavigation} > #{link.alias} @@ -292,7 +355,7 @@ export default function LinkTable({ {link.linkType === 'redirect' && link.targetUrl ? (

{safeUrl - ? {link.targetUrl} + ? {link.targetUrl} : {link.targetUrl}}

) : null} @@ -302,11 +365,34 @@ export default function LinkTable({ {new Date(link.updatedAt).toLocaleDateString()}
-
- {actions && onEdit ? : null} - - {actions && onDelete ? : null} -
+ {showActionColumn ? ( +
+ {actions && onEdit ? ( + + ) : null} + {actions && onDelete ? ( + + ) : null} +
+ ) : null}
); })} diff --git a/src/styles.css b/src/styles.css index 4f77b10..c8c6cf1 100644 --- a/src/styles.css +++ b/src/styles.css @@ -509,6 +509,12 @@ button.link-action--confirm:hover { background: var(--accent-soft); } transition: background 0.1s; } .link-table tbody tr:hover { background: #F8FAFF; } +.link-table-row--clickable { cursor: pointer; } +.link-table-row--clickable:focus-visible { + background: #F8FAFF; + outline: 2px solid #818CF8; + outline-offset: -2px; +} .link-table tbody tr:last-child td { border-bottom: none; } .link-table tr.is-selected { background: var(--accent-soft); } .link-table tr.is-selected:hover { background: #E0E7FF; } @@ -588,10 +594,16 @@ button.link-action--confirm:hover { background: var(--accent-soft); } padding: 0.75rem; transition: border-color 0.15s, box-shadow 0.15s; } +.link-card--clickable { cursor: pointer; } .link-card:hover { border-color: #C7D2FE; box-shadow: 0 4px 16px rgba(99,102,241,0.08); } +.link-card--clickable:focus-visible { + border-color: #818CF8; + box-shadow: 0 0 0 3px rgba(99,102,241,0.18), 0 4px 16px rgba(99,102,241,0.08); + outline: none; +} .link-card.is-selected { background: var(--accent-soft); border-color: #A5B4FC; } .link-card-header {