Commit ce4ca463ba

ce4ca463ba12dff63b70b324dc23edb4615e8f52

parent: ab515bab17

Verified · cmc ci/build: success ci/test: success ci/vuln: success

cmc <hello@cleberg.net> · 2026-09-04 03:02 UTC

web: diff lines, files and review threads can be linked to

Diff rows carried no id, so a review comment or a line under
discussion could not be pointed at, and a file's #L anchors had no
highlight. Each diff file is #f<n>, each row #f<n>-n<line> (or
-o<line> for a deleted line), each thread #thread-<id>, and a targeted
row, file line or thread is highlighted.

Ref #118

Layout: unified · split

internal/web/static/style.css +4
@@ -773,6 +773,10 @@ button.linklike:hover { text-decoration: underline; filter: none; }
773773
774774/* rendered markdown / org / html */
775775.rendered { max-width: 100%; overflow-wrap: anywhere; }
776/* A linked line: #f2-n17 on a diff, #L17 on a file (#118). */
777.difftable tr:target td, .code .line:has(a:target) { background: color-mix(in srgb, var(--mark) 18%, transparent); }
778.difftable tr:target td.ln, .code .line:has(a:target) .ln { border-left: 3px solid var(--mark); }
779.thread:target { outline: 2px solid var(--mark); outline-offset: 2px; }
776780/* GFM task lists: the box sits in the line, sized to the prose. */
777781.rendered li input[type="checkbox"] { width: 0.9em; height: 0.9em; margin: 0 0.35em 0 0; vertical-align: -0.05em; accent-color: var(--accent); }
778782.rendered li:has(> input[type="checkbox"]) { list-style: none; margin-left: -1.2em; }
internal/web/templates/layout.html +3 −3
@@ -119,7 +119,7 @@
119119 endpoint and Viewer the signed-in account; the commit page passes
120120 neither and gets the same diff without review controls. */}}
121121{{define "difffiles"}}{{$base := .Base}}{{$viewer := .Viewer}}
122{{range .Files}}<details class="difffold"{{if .Open}} open{{end}}>
122{{range $fi, $f := .Files}}<details class="difffold" id="f{{$fi}}"{{if .Open}} open{{end}}>
123123 <summary>
124124 <span class="fpath">{{if eq .Status "renamed"}}<span class="was">{{.OldPath}} →</span> {{end}}{{.Path}}</span>
125125 {{if ne .Status "modified"}}<span class="chip">{{.Status}}</span>{{end}}
@@ -129,7 +129,7 @@
129129 {{else}}<div class="tablewrap"><table class="difftable">
130130 {{range .Lines}}{{if eq .Class "hunk"}}<tr class="hunk"><td class="ln" colspan="2"></td><td class="src">{{.Text}}</td></tr>
131131 {{else if eq .Class "meta"}}<tr class="dmeta"><td class="ln" colspan="2"></td><td class="src">{{.Text}}</td></tr>
132 {{else}}<tr class="{{.Class}}"><td class="ln">{{if .OldLine}}{{if eq .Class "del"}}{{template "cmtln" dict "N" .OldLine "Path" .Path "Side" "old" "Base" $base "Viewer" $viewer}}{{else}}{{.OldLine}}{{end}}{{end}}</td><td class="ln">{{if .NewLine}}{{template "cmtln" dict "N" .NewLine "Path" .Path "Side" "new" "Base" $base "Viewer" $viewer}}{{end}}</td><td class="src chroma">{{if .Code}}{{.Code}}{{else}}{{.Content}}{{end}}</td></tr>
132 {{else}}<tr class="{{.Class}}" id="f{{$fi}}-{{if eq .Class "del"}}o{{.OldLine}}{{else}}n{{.NewLine}}{{end}}"><td class="ln">{{if .OldLine}}{{if eq .Class "del"}}{{template "cmtln" dict "N" .OldLine "Path" .Path "Side" "old" "Base" $base "Viewer" $viewer}}{{else}}{{.OldLine}}{{end}}{{end}}</td><td class="ln">{{if .NewLine}}{{template "cmtln" dict "N" .NewLine "Path" .Path "Side" "new" "Base" $base "Viewer" $viewer}}{{end}}</td><td class="src chroma">{{if .Code}}{{.Code}}{{else}}{{.Content}}{{end}}</td></tr>
133133 {{if .Compose}}<tr class="threadrow"><td colspan="3"><form id="compose" method="post" action="{{$base}}/diff-comment" class="thread composing">
134134 <input type="hidden" name="path" value="{{.Path}}">
135135 <input type="hidden" name="line" value="{{if eq .Class "del"}}{{.OldLine}}{{else}}{{.NewLine}}{{end}}">
@@ -149,7 +149,7 @@
149149
150150{{/* thread renders one review thread with its reply and resolve controls.
151151 Class carries "stale" for threads whose anchor is gone. */}}
152{{define "thread"}}{{$t := .T}}<div class="thread{{if $t.Resolved}} resolved{{end}}{{if .Class}} {{.Class}}{{end}}">
152{{define "thread"}}{{$t := .T}}<div class="thread{{if $t.Resolved}} resolved{{end}}{{if .Class}} {{.Class}}{{end}}" id="thread-{{$t.ID}}">
153153{{if or $t.Resolved (and .Class $t.Stale)}}<p class="threadstate">{{if and .Class $t.Stale}}stale{{end}}{{if $t.Resolved}}{{if and .Class $t.Stale}} · {{end}}resolved by {{$t.Resolved}}{{end}}</p>{{end}}
154154{{range $t.Comments}}<p class="commenthead"><strong>{{.Author}}</strong> <span class="when">{{when .CreatedAt}}</span></p><div class="rendered">{{.BodyHTML}}</div>{{end}}
155155{{if .Viewer}}<details class="threadreply"><summary>Reply</summary>