Commit d8eafecb60

d8eafecb60403faf614906f2da249112b94c4d13

parent: 78b20e7d69

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-20 01:04 UTC

web: the side column is a disclosure below 62rem

Below 62rem the column stacked after the content as inline-block
groups of unbounded length, so on the issue, merge request and build
lists the filters were taller than what they filtered.

The groups sit in a details.sidedrop now. Below 62rem that disclosure
is the column: one closed line before the list, headed by its word and
the filters in force. Above 62rem the summary is hidden and
::details-content is shown whatever the open state, so the column is
what it was. The @supports guard is the fail-safe -- an engine without
the selector keeps a working toggle at every width rather than losing
the column.

The nav and its aria-label stay outside the disclosure, so the
landmark is unchanged.

Ref #237
internal/httpd/sidecol_test.go added +74
@@ -0,0 +1,74 @@
1package httpd
2
3import (
4 "html/template"
5 "net/url"
6 "strings"
7 "testing"
8
9 "gitbay.org/gitbay/internal/store"
10 "gitbay.org/gitbay/internal/web"
11)
12
13// renderIssues renders issues.html around the facets given, which is the
14// shared sidecol partial every list page uses.
15func renderIssues(t *testing.T, facets []facetGroup) string {
16 t.Helper()
17 var sb strings.Builder
18 err := web.Render(&sb, "issues.html", struct {
19 repoPage
20 State string
21 Label string
22 Query string
23 Filters []listFilter
24 Facets []facetGroup
25 Issues []store.Issue
26 LabelColors map[string]template.CSS
27 Older string
28 }{repoPage: testRepoPage(), State: "open", Facets: facets})
29 if err != nil {
30 t.Fatalf("render: %v", err)
31 }
32 return sb.String()
33}
34
35// The column's groups sit inside a details.sidedrop whose summary names
36// the filters in force. Below 62rem that disclosure is the column; above
37// it style.css hides the summary and shows the content, so the markup is
38// the same either way (#237).
39func TestSidecolIsADisclosure(t *testing.T) {
40 base := url.Values{"state": {"open"}, "label": {"bug"}}
41 labels := []store.Label{{Name: "bug", Issues: 2}}
42 facets := listFacets(base, []string{"open", "closed"}, "open", labels, nil, false)
43
44 out := renderIssues(t, facets)
45 nav := strings.Index(out, `<nav class="sidecol" aria-label="Filters">`)
46 drop := strings.Index(out, `<details class="sidedrop">`)
47 sum := strings.Index(out, "<summary>")
48 grp := strings.Index(out, `<div class="grp">`)
49 if nav < 0 || drop < nav || sum < drop || grp < sum {
50 t.Fatalf("nav=%d details=%d summary=%d grp=%d\n%s", nav, drop, sum, grp, out)
51 }
52 if !strings.Contains(out, `<span class="sideword">Filters</span>`) {
53 t.Errorf("summary does not head with its word:\n%s", out)
54 }
55 // state and label are both in force, so both are named
56 for _, want := range []string{`<span class="sideon">open</span>`, `<span class="sideon">bug</span>`} {
57 if !strings.Contains(out, want) {
58 t.Errorf("summary does not name %q:\n%s", want, out)
59 }
60 }
61 if n := strings.Count(out, "</details>"); n != 1 {
62 t.Errorf("details not closed once: %d", n)
63 }
64}
65
66// The column leads the list in the markup, because below 62rem it is a
67// control above what it narrows rather than a note after it (#237).
68func TestSidecolPrecedesTheList(t *testing.T) {
69 facets := listFacets(url.Values{"state": {"open"}}, []string{"open"}, "open", nil, nil, false)
70 out := renderIssues(t, facets)
71 if i, j := strings.Index(out, `class="sidecol"`), strings.Index(out, `class="colmain"`); i < 0 || j < i {
72 t.Errorf("sidecol=%d colmain=%d", i, j)
73 }
74}
internal/web/static/style.css +45 −2
@@ -1430,6 +1430,46 @@ a.memberchip .role { color: var(--muted); }
14301430.sidecol form.searchform { margin-top: var(--sp-2); }
14311431.sidecol form.searchform input[type="text"] { min-width: 0; width: 100%; }
14321432
1433/* The column's groups sit in a disclosure. Below 62rem that is all the
1434 reader sees of the column: one closed line before the list, because a
1435 repository with two dozen labels stacked the whole vocabulary under
1436 the content it filters. Above 62rem the summary is hidden and the
1437 content shown whatever the open state, so the column is what it
1438 always was. The @supports guard is the fail-safe: an engine without
1439 ::details-content keeps a working toggle at every width instead of
1440 losing the column (#237). */
1441.sidedrop > summary {
1442 display: inline-flex;
1443 align-items: center;
1444 gap: var(--sp-2);
1445 height: 32px;
1446 padding: 0 var(--sp-3);
1447 background: var(--canvas);
1448 border: 1px solid var(--line);
1449 border-radius: var(--r-ctl);
1450 color: var(--fg);
1451 font-size: var(--fs-2);
1452 cursor: pointer;
1453 list-style: none;
1454}
1455.sidedrop[open] > summary { margin-bottom: var(--sp-3); }
1456.sidedrop > summary::-webkit-details-marker { display: none; }
1457.sidedrop > summary:hover { background: var(--hover); }
1458.sidedrop > summary::after { content: "\25be"; color: var(--muted); }
1459.sidedrop[open] > summary::after { content: "\25b4"; }
1460.sideword { font-weight: 600; }
1461/* the filters in force: where you are */
1462.sideon { color: var(--warn); }
1463/* every group came out empty, so there is nothing to open */
1464.sidedrop:not(:has(.grp)) { display: none; }
1465
1466@media (min-width: 62.01rem) {
1467 @supports selector(::details-content) {
1468 .sidedrop > summary { display: none; }
1469 .sidedrop::details-content { content-visibility: visible; }
1470 }
1471}
1472
14331473/* a sticky column taller than the viewport scrolls inside itself, or its
14341474 tail is unreachable once the page scrolls. Above 62rem only: below it
14351475 .filenav is hidden and the other two are static. .dashpins stops being
@@ -1655,9 +1695,12 @@ svg.icon { vertical-align: -0.125em; }
16551695 .blobgrid { grid-template-columns: 1fr; }
16561696 .filenav { display: none; }
16571697
1658 /* the column follows the content on a phone, the way the aside does */
1698 /* the column is a closed disclosure before the content: a filter you
1699 reach without scrolling the list, and nothing to scroll past when
1700 you do not want it. It leads rather than follows because it is what
1701 the list is narrowed by, not a note on one row (#237) */
16591702 .withcol, .withcol.narrow { grid-template-columns: 1fr; }
1660 .sidecol { position: static; order: 2; }
1703 .sidecol { position: static; margin-bottom: var(--sp-3); }
16611704 .sidecol .grp { display: inline-block; vertical-align: top; margin-right: var(--sp-5); }
16621705}
16631706
internal/web/templates/account.html +3
@@ -7,6 +7,8 @@
77
88<div class="withcol narrow">
99<nav class="sidecol" aria-label="Sections">
10<details class="sidedrop">
11<summary><span class="sideword">Sections</span></summary>
1012 <div class="grp"><h2 class="colhead">Sections</h2>
1113 <ul>
1214 <li><a href="#profile">Profile</a></li>
@@ -18,6 +20,7 @@
1820 <li><a href="#export">Export</a></li>
1921 <li><a href="#cli">On the command line</a></li>
2022 </ul></div>
23</details>
2124</nav>
2225<div class="colmain">
2326<section id="profile"><h2>Profile</h2>
internal/web/templates/admin.html +3
@@ -7,6 +7,8 @@
77
88<div class="withcol narrow">
99<nav class="sidecol" aria-label="Sections">
10<details class="sidedrop">
11<summary><span class="sideword">Sections</span></summary>
1012 <div class="grp"><h2 class="colhead">Sections</h2>
1113 <ul>
1214 <li><a href="#webhooks">Webhook deliveries</a></li>
@@ -15,6 +17,7 @@
1517 <li><a href="#builds">Builds</a></li>
1618 <li><a href="#deps">Dependency checks</a></li>
1719 </ul></div>
20</details>
1821</nav>
1922<div class="colmain">
2023
internal/web/templates/builds.html +3
@@ -3,6 +3,8 @@
33{{define "content"}}
44<div class="withcol">
55<nav class="sidecol" aria-label="Filters">
6<details class="sidedrop">
7<summary><span class="sideword">Filters</span>{{range .Facets}}{{range .Items}}{{if .Active}} <span class="sideon">{{.Label}}</span>{{end}}{{end}}{{end}}</summary>
68 {{range .Facets}}{{if .Items}}<div class="grp">
79 <h2 class="colhead">{{.Title}}</h2>
810 <ul>{{range .Items}}<li><a{{if .Active}} aria-current="page"{{end}} href="{{.Href}}">{{.Label}}</a></li>{{end}}</ul>
@@ -15,6 +17,7 @@
1517 <input type="hidden" name="job" value="{{.Filter.Job}}">
1618 <button type="submit" class="btn">Filter</button>
1719 </form>
20</details>
1821</nav>
1922<div class="colmain">
2023<div class="listhead">
internal/web/templates/globalsearch.html +3
@@ -3,6 +3,8 @@
33{{define "content"}}
44<div class="withcol">
55<nav class="sidecol" aria-label="Filters">
6<details class="sidedrop">
7<summary><span class="sideword">Filters</span>{{if .Kind}} <span class="sideon">{{.Kind}}</span>{{end}}</summary>
68 <div class="grp"><h2 class="colhead">Kind</h2>
79 <ul>
810 <li><a{{if eq .Kind ""}} aria-current="page"{{end}} href="?q={{.Query}}">everything</a></li>
@@ -10,6 +12,7 @@
1012 <li><a{{if eq .Kind "issue"}} aria-current="page"{{end}} href="?q={{.Query}}&amp;kind=issue">issues</a></li>
1113 <li><a{{if eq .Kind "mr"}} aria-current="page"{{end}} href="?q={{.Query}}&amp;kind=mr">merge requests</a></li>
1214 </ul></div>
15</details>
1316</nav>
1417<div class="colmain">
1518<div class="listhead">
internal/web/templates/layout.html +9
@@ -122,11 +122,20 @@
122122 {{end}}</ul>
123123</nav>{{end}}
124124
125{{/* Every side column's groups sit in a details.sidedrop. Below 62rem
126 that disclosure is what the reader sees of the column: one closed
127 line they open. Above it style.css hides the summary and shows the
128 content whatever the open state, so the column reads as it always
129 has (#237). The facet columns name the filters in force in the
130 summary; the section navs head with the word alone. */}}
125131{{define "sidecol"}}<nav class="sidecol" aria-label="Filters">
132<details class="sidedrop">
133<summary><span class="sideword">Filters</span>{{range .}}{{range .Items}}{{if .Active}} <span class="sideon">{{.Label}}</span>{{end}}{{end}}{{end}}</summary>
126134 {{range .}}{{if .Items}}<div class="grp">
127135 <h2 class="colhead">{{.Title}}</h2>
128136 <ul>{{range .Items}}<li><a{{if .Active}} aria-current="page"{{end}} href="{{.Href}}">{{.Label}}{{if .Count}} <i>{{.Count}}</i>{{end}}</a></li>{{end}}</ul>
129137 </div>{{end}}{{end}}
138</details>
130139</nav>{{end}}
131140
132141{{define "branchicon"}}<svg class="icon" width="12" height="12" viewBox="0 0 16 16" aria-hidden="true" fill="currentColor"><path d="M9.5 3.25a2.25 2.25 0 1 1 3 2.122V6A2.5 2.5 0 0 1 10 8.5H6a1 1 0 0 0-1 1v1.128a2.251 2.251 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.5 0v1.836A2.493 2.493 0 0 1 6 7h4a1 1 0 0 0 1-1v-.628a2.25 2.25 0 0 1-1.5-2.122ZM4.25 12a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5ZM3.5 3.25a.75.75 0 1 1 1.5 0 .75.75 0 0 1-1.5 0Zm8.25-.75a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Z"/></svg>{{end}}
internal/web/templates/settings.html +3
@@ -6,6 +6,8 @@
66{{if .Notice}}{{if .Saved}}<p class="notice" role="status">{{.Notice}}</p>{{else}}<p class="error" role="alert">{{.Notice}}</p>{{end}}{{end}}
77<div class="withcol narrow">
88<nav class="sidecol" aria-label="Sections">
9<details class="sidedrop">
10<summary><span class="sideword">Sections</span></summary>
911 <div class="grp"><h2 class="colhead">Sections</h2>
1012 <ul>
1113 <li><a href="#identity">Identity</a></li>
@@ -17,6 +19,7 @@
1719 <li><a href="#runners">Runners</a></li>
1820 <li><a href="#lifecycle">Lifecycle</a></li>
1921 </ul></div>
22</details>
2023</nav>
2124<div class="colmain">
2225