feat(viewer): add back navigation to improve page traversal UX (#1217)

* feat(viewer): add back navigation

* fix(viewer): refine back navigation layout and test coverage

* feat(viewer): add back navigation to the compare page

* style(viewer): use icon-only back navigation
This commit is contained in:
hyy authored and GitHub committed 2026-09-15 10:39:04 +08:00
1 parent 6e5cd8cf25
commit 2ce53f5a98
6 files changed
+112 -14

No files matched your search

+8
View File
@@ -208,6 +208,14 @@ func TestHandleCompare(t *testing.T) {
contains: []string{"New (1)", "Persisting (1)", "Resolved (1)", "Not reviewed (0)",
"newly broken", "still broken", "was broken", "none"},
},
{
name: "back link returns to sessions",
query: "before=s1&after=s2",
status: http.StatusOK,
contains: []string{
`<a class="back-link" href="/r/myrepo" aria-label="Back to sessions">`,
},
},
{
// The CLI renders a suggested patch as a diff (renderComment ->
// buildDiffLines); dropping it here would leave the page showing
+39 -11
View File
@@ -105,18 +105,39 @@ func TestRenderTemplate_BadTemplate(t *testing.T) {
}
func TestRenderTemplate_Sessions(t *testing.T) {
rr := httptest.NewRecorder()
renderTemplate(rr, "sessions.html", sessionsData{
EncodedRepo: "test-repo",
RepoName: "MyProject",
Sessions: []SessionSummary{},
})
if rr.Code != http.StatusOK {
t.Errorf("status = %d, want 200", rr.Code)
tests := []struct {
name string
sessions []SessionSummary
}{
{name: "empty", sessions: []SessionSummary{}},
{name: "populated", sessions: []SessionSummary{{SessionID: "session-123", GitBranch: "main"}}},
}
if !strings.Contains(rr.Body.String(), "MyProject") {
t.Errorf("expected repo name in sessions template")
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
rr := httptest.NewRecorder()
renderTemplate(rr, "sessions.html", sessionsData{
EncodedRepo: "test-repo",
RepoName: "MyProject",
Sessions: tt.sessions,
})
if rr.Code != http.StatusOK {
t.Errorf("status = %d, want 200", rr.Code)
}
body := rr.Body.String()
if len(tt.sessions) > 0 && !strings.Contains(body, `href="/r/test-repo/session-123"`) {
t.Errorf("expected populated session link in rendered output")
}
if !strings.Contains(body, "MyProject") {
t.Errorf("expected repo name in sessions template")
}
if !strings.Contains(body, `<a class="back-link" href="/" aria-label="Back to repositories">`) {
t.Errorf("expected back link to repositories in sessions template")
}
if !strings.Contains(body, `<a href="/" class="nav-brand">`) {
t.Errorf("expected breadcrumb navigation to remain in sessions template")
}
})
}
}
@@ -154,6 +175,13 @@ func TestRenderTemplate_SessionPage(t *testing.T) {
if rr.Code != http.StatusOK {
t.Errorf("status = %d, want 200", rr.Code)
}
body := rr.Body.String()
if !strings.Contains(body, `<a class="back-link" href="/r/repo" aria-label="Back to sessions">`) {
t.Errorf("expected back link to repository sessions in session template")
}
if !strings.Contains(body, `<a href="/r/repo">MyRepo</a>`) {
t.Errorf("expected breadcrumb navigation to remain in session template")
}
}
func TestRenderTemplate_SecondarySectionsCollapsedByDefault(t *testing.T) {
+53
View File
@@ -344,6 +344,58 @@ h3 {
font-size: 0.82em;
}
/* ── Page heading ── */
/* Keep the back link reachable beside long repository and session titles. */
.page-heading {
display: flex;
align-items: flex-start;
gap: 0.75rem;
min-width: 0;
margin-bottom: 1.25rem;
}
.page-heading > h2 {
flex: 1 1 auto;
min-width: 0;
margin-bottom: 0;
overflow-wrap: anywhere;
}
.back-link {
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
width: 44px;
height: 44px;
padding: 0;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: var(--surface-alt);
color: var(--link);
text-decoration: none;
transition: color var(--transition), background var(--transition), border-color var(--transition);
}
.back-link::before {
content: "";
width: 20px;
height: 20px;
background: currentColor;
-webkit-mask: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cline%20x1%3D%2219%22%20y1%3D%2212%22%20x2%3D%225%22%20y2%3D%2212%22%2F%3E%3Cpolyline%20points%3D%2212%2019%205%2012%2012%205%22%2F%3E%3C%2Fsvg%3E") center / contain no-repeat;
mask: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cline%20x1%3D%2219%22%20y1%3D%2212%22%20x2%3D%225%22%20y2%3D%2212%22%2F%3E%3Cpolyline%20points%3D%2212%2019%205%2012%2012%205%22%2F%3E%3C%2Fsvg%3E") center / contain no-repeat;
}
.back-link:hover {
color: var(--accent-hover);
background: var(--accent-soft);
border-color: var(--link);
}
.back-link:focus-visible {
outline: 2px solid var(--link);
outline-offset: 2px;
}
.session-header .page-heading {
margin-bottom: 0.75rem;
}
/* ── Token Usage Summary ── */
.token-summary {
background: var(--surface);
@@ -1232,6 +1284,7 @@ p {
main { padding: 0 1rem; margin: 1.25rem auto; }
nav.breadcrumb { padding: 0 1rem; }
.session-header { padding: 1.25rem; }
.page-heading { gap: 0.5rem; }
.token-stats { grid-template-columns: repeat(2, 1fr); }
.meta { gap: 0.5rem 1rem; font-size: 0.8rem; }
.table th, .table td { padding: 0.6rem 0.75rem; font-size: 0.82rem; }
+4 -1
View File
@@ -11,7 +11,10 @@
<nav class="breadcrumb"><a href="/" class="nav-brand"><span class="brand-icon"></span>Open Code Review Viewer</a><span class="sep">/</span><a href="/r/{{.EncodedRepo}}">{{.RepoName}}</a><span class="sep">/</span><span class="current">compare</span></nav>
<main>
<div class="session-header">
<h2>Compare sessions</h2>
<div class="page-heading">
<a class="back-link" href="/r/{{.EncodedRepo}}" aria-label="Back to sessions"></a>
<h2>Compare sessions</h2>
</div>
<div class="meta">
<span><strong>Before:</strong> <a href="/r/{{.EncodedRepo}}/{{.Before.SessionID}}"><code>{{truncate 12 .Before.SessionID}}</code></a></span>
<span><strong>After:</strong> <a href="/r/{{.EncodedRepo}}/{{.After.SessionID}}"><code>{{truncate 12 .After.SessionID}}</code></a></span>
+4 -1
View File
@@ -11,7 +11,10 @@
<nav class="breadcrumb"><a href="/" class="nav-brand"><span class="brand-icon"></span>Open Code Review Viewer</a><span class="sep">/</span><a href="/r/{{.EncodedRepo}}">{{.RepoName}}</a><span class="sep">/</span><span class="current">{{.Session.Summary.SessionID | truncate 12}}</span></nav>
<main>
<div class="session-header">
<h2>Session: {{.Session.Summary.SessionID}}</h2>
<div class="page-heading">
<a class="back-link" href="/r/{{.EncodedRepo}}" aria-label="Back to sessions"></a>
<h2>Session: {{.Session.Summary.SessionID}}</h2>
</div>
<div class="meta">
<span><strong>CWD:</strong> {{.Session.Summary.CWD}}</span>
<span><strong>Branch:</strong> {{.Session.Summary.GitBranch}}</span>
+4 -1
View File
@@ -10,7 +10,10 @@
<body>
<nav class="breadcrumb"><a href="/" class="nav-brand"><span class="brand-icon"></span>Open Code Review Viewer</a><span class="sep">/</span><span class="current">{{.RepoName}}</span></nav>
<main>
<h2>Sessions: {{.RepoName}}</h2>
<div class="page-heading">
<a class="back-link" href="/" aria-label="Back to repositories"></a>
<h2>Sessions: {{.RepoName}}</h2>
</div>
{{if .Sessions}}
<table class="table">
<thead><tr><th>Session ID</th><th>Branch</th><th>Mode</th><th>Model</th><th>Files</th><th>Status</th><th>Comments</th><th>Duration</th><th>Started At</th><th>Compare</th></tr></thead>