web: the facet column is a disclosure below 62rem !439
14 files changed, +260 −19
Layout: unified · split
CHANGELOG.org +10 −5
| @@ -23,11 +23,16 @@ The desktop layout (#226): the web UI uses a wide screen. | ||
| 23 | 23 | - Side columns: state, labels and open milestones on the issue and |
| 24 | 24 | merge request lists; status, jobs and branches on builds; topics on |
| 25 | 25 | explore; kinds on search; sections on repository settings, account |
| 26 | settings and admin. | |
| 27 | - Below 62rem the facet and section columns stack after their content | |
| 28 | and the navigator hides, since the tree page is the navigator on a | |
| 29 | phone. Below 80rem the dashboard's pinned repositories return to a | |
| 30 | chip row above the queues. | |
| 26 | settings and admin. A label or milestone nothing is filed under is | |
| 27 | left out, so the merge request list offers the labels that have | |
| 28 | merge requests rather than every label the repository holds; the | |
| 29 | branch group caps at ten, and the branch field below it still takes | |
| 30 | any ref. | |
| 31 | - Below 62rem the facet and section columns become a closed disclosure | |
| 32 | before their content, headed by the filters in force, and the | |
| 33 | navigator hides, since the tree page is the navigator on a phone. | |
| 34 | Below 80rem the dashboard's pinned repositories return to a chip row | |
| 35 | above the queues. | |
| 31 | 36 | |
| 32 | 37 | Three accessibility defects the wider scan found, on pages the |
| 33 | 38 | sixteen-page one never covered (#226): |
docs/specs/2026-09-19-desktop-layout-design.md +12 −4
| @@ -63,10 +63,18 @@ feed rows are the row format the lists should adopt. | ||
| 63 | 63 | description with topics and website inline in muted text, the |
| 64 | 64 | Pin/Watch/Bookmark/Fork buttons at the right. Row two: the tabs. The |
| 65 | 65 | toggles hint moves to `title` text on the three buttons. |
| 66 | 7. **Below 62rem** a facet or section column stacks after the content, | |
| 67 | the way the issue aside does; the file navigator disappears (the | |
| 68 | tree page exists). **Below 80rem** the dashboard's pinned column | |
| 69 | returns to the chip row. Nothing the phone layout fixed moves. | |
| 66 | 7. **Below 62rem** a facet or section column becomes a closed | |
| 67 | disclosure before the content, headed by its word and the filters in | |
| 68 | force; the file navigator disappears (the tree page exists). | |
| 69 | **Below 80rem** the dashboard's pinned column returns to the chip | |
| 70 | row. Nothing the phone layout fixed moves. | |
| 71 | ||
| 72 | *Amended 2026-09-19 (#237).* The rule read "stacks after the | |
| 73 | content, the way the issue aside does". An aside carries a note on | |
| 74 | one thing; a facet column carries the vocabulary the list is | |
| 75 | narrowed by, and a repository with two dozen labels stacked all of | |
| 76 | them under the content, taller than it. A column that stops being a | |
| 77 | column is a control, so it collapses to one and leads the list. | |
| 70 | 78 | |
| 71 | 79 | ## Pages |
| 72 | 80 | |
internal/httpd/builds.go +18 −1
| @@ -3,6 +3,7 @@ package httpd | ||
| 3 | 3 | import ( |
| 4 | 4 | "net/http" |
| 5 | 5 | "net/url" |
| 6 | "slices" | |
| 6 | 7 | "strconv" |
| 7 | 8 | |
| 8 | 9 | "gitbay.org/gitbay/internal/control" |
| @@ -61,6 +62,11 @@ func filterLinks(f buildFilter, jobs []control.JobOut) []buildFilterLink { | ||
| 61 | 62 | return links |
| 62 | 63 | } |
| 63 | 64 | |
| 65 | // maxBranchFacets caps the Branches group, the way topicFacets caps | |
| 66 | // topics: a page of builds names as many refs as it likes and the column | |
| 67 | // is not a branch listing. The field below the column takes any ref. | |
| 68 | const maxBranchFacets = 10 | |
| 69 | ||
| 64 | 70 | // buildFacets is the builds page's side column: filterLinks' rows split |
| 65 | 71 | // into their groups, plus one link per branch seen, which keeps status |
| 66 | 72 | // and job and clears itself when active. |
| @@ -77,7 +83,18 @@ func buildFacets(f buildFilter, jobs []control.JobOut, refs []string) []facetGro | ||
| 77 | 83 | } |
| 78 | 84 | branch := facetGroup{Title: "Branches"} |
| 79 | 85 | base := url.Values{"ref": {f.Ref}, "status": {f.Status}, "job": {f.Job}} |
| 80 | for _, ref := range refs { | |
| 86 | shown := refs | |
| 87 | if len(shown) > maxBranchFacets { | |
| 88 | shown = shown[:maxBranchFacets] | |
| 89 | // the ref in force belongs in the group wherever it sits, or the | |
| 90 | // filter it set cannot be cleared from the column. The reslice | |
| 91 | // caps the capacity so the append copies instead of writing | |
| 92 | // through to refs. | |
| 93 | if i := slices.Index(refs, f.Ref); i >= maxBranchFacets { | |
| 94 | shown = append(shown[:maxBranchFacets-1:maxBranchFacets-1], refs[i]) | |
| 95 | } | |
| 96 | } | |
| 97 | for _, ref := range shown { | |
| 81 | 98 | active := ref == f.Ref |
| 82 | 99 | href := facetHref(base, "ref", ref) |
| 83 | 100 | if active { |
internal/httpd/builds_test.go +34
| @@ -1,6 +1,7 @@ | ||
| 1 | 1 | package httpd |
| 2 | 2 | |
| 3 | 3 | import ( |
| 4 | "fmt" | |
| 4 | 5 | "reflect" |
| 5 | 6 | "testing" |
| 6 | 7 | |
| @@ -174,3 +175,36 @@ func TestBuildFacetsGroups(t *testing.T) { | ||
| 174 | 175 | t.Errorf("dev: %+v", b) |
| 175 | 176 | } |
| 176 | 177 | } |
| 178 | ||
| 179 | // The branch group caps, the way topicFacets caps topics: a page of | |
| 180 | // builds can name dozens of refs and the column is not a branch | |
| 181 | // listing. The ref in force is kept whatever its position (#237). | |
| 182 | func TestBuildFacetsCapsBranches(t *testing.T) { | |
| 183 | var refs []string | |
| 184 | for i := 0; i < 30; i++ { | |
| 185 | refs = append(refs, fmt.Sprintf("b%02d", i)) | |
| 186 | } | |
| 187 | groups := buildFacets(buildFilter{}, nil, refs) | |
| 188 | branches := groups[2].Items | |
| 189 | if len(branches) != maxBranchFacets { | |
| 190 | t.Fatalf("branches: %d", len(branches)) | |
| 191 | } | |
| 192 | if branches[0].Label != "b00" || branches[len(branches)-1].Label != "b09" { | |
| 193 | t.Errorf("kept the wrong refs: %+v", branches) | |
| 194 | } | |
| 195 | ||
| 196 | groups = buildFacets(buildFilter{Ref: "b29"}, nil, refs) | |
| 197 | branches = groups[2].Items | |
| 198 | if len(branches) != maxBranchFacets { | |
| 199 | t.Fatalf("branches with an active ref: %d", len(branches)) | |
| 200 | } | |
| 201 | var active *facetItem | |
| 202 | for i := range branches { | |
| 203 | if branches[i].Label == "b29" { | |
| 204 | active = &branches[i] | |
| 205 | } | |
| 206 | } | |
| 207 | if active == nil || !active.Active || active.Href != "?" { | |
| 208 | t.Errorf("active ref past the cap: %+v", branches) | |
| 209 | } | |
| 210 | } | |
internal/httpd/facets.go +9 −1
| @@ -41,7 +41,9 @@ func facetHref(base url.Values, key, value string) string { | ||
| 41 | 41 | // listFacets builds the issue or merge request list's column from the |
| 42 | 42 | // active parameters, the states the page offers, and the repository's |
| 43 | 43 | // labels and open milestones. Counts are the rows' own: a label's issue |
| 44 | // count on the issue list, its MR count on the MR list. | |
| 44 | // count on the issue list, its MR count on the MR list. A count of zero | |
| 45 | // is left out — it links to an empty list — unless it is the filter in | |
| 46 | // force, which stays so it can be cleared (#237). | |
| 45 | 47 | func listFacets(base url.Values, states []string, state string, labels []store.Label, ms []store.Milestone, forMRs bool) []facetGroup { |
| 46 | 48 | var st facetGroup |
| 47 | 49 | st.Title = "State" |
| @@ -55,6 +57,9 @@ func listFacets(base url.Values, states []string, state string, labels []store.L | ||
| 55 | 57 | n = l.MRs |
| 56 | 58 | } |
| 57 | 59 | active := base.Get("label") == l.Name |
| 60 | if n == 0 && !active { | |
| 61 | continue | |
| 62 | } | |
| 58 | 63 | href := facetHref(base, "label", l.Name) |
| 59 | 64 | if active { |
| 60 | 65 | href = facetHref(base, "label", "") |
| @@ -64,6 +69,9 @@ func listFacets(base url.Values, states []string, state string, labels []store.L | ||
| 64 | 69 | mg := facetGroup{Title: "Milestones"} |
| 65 | 70 | for _, m := range ms { |
| 66 | 71 | active := base.Get("milestone") == m.Title |
| 72 | if m.OpenItems == 0 && !active { | |
| 73 | continue | |
| 74 | } | |
| 67 | 75 | href := facetHref(base, "milestone", m.Title) |
| 68 | 76 | if active { |
| 69 | 77 | href = facetHref(base, "milestone", "") |
internal/httpd/facets_test.go +34 −6
| @@ -35,18 +35,46 @@ func TestListFacetsGroups(t *testing.T) { | ||
| 35 | 35 | t.Errorf("state items: %+v", st) |
| 36 | 36 | } |
| 37 | 37 | lb := groups[1].Items |
| 38 | // docs has no issues, so the issue list does not offer it (#237) | |
| 39 | if len(lb) != 1 { | |
| 40 | t.Fatalf("labels: %+v", lb) | |
| 41 | } | |
| 38 | 42 | if lb[0].Label != "bug" || lb[0].Count != 2 || !lb[0].Active || lb[0].Href != "?state=open" { |
| 39 | 43 | t.Errorf("active label clears itself: %+v", lb[0]) |
| 40 | 44 | } |
| 41 | if lb[1].Label != "docs" || lb[1].Count != 0 || lb[1].Active || lb[1].Href != "?label=docs&state=open" { | |
| 42 | t.Errorf("inactive label: %+v", lb[1]) | |
| 43 | } | |
| 44 | 45 | if m := groups[2].Items[0]; m.Label != "v2" || m.Count != 4 || m.Href != "?label=bug&milestone=v2&state=open" { |
| 45 | 46 | t.Errorf("milestone: %+v", m) |
| 46 | 47 | } |
| 47 | // on the MR list a label's count is its MR count | |
| 48 | // on the MR list a label's count is its MR count, and both labels | |
| 49 | // have merge requests, so both are offered | |
| 48 | 50 | mr := listFacets(base, []string{"open"}, "open", labels, nil, true) |
| 49 | if mr[1].Items[1].Count != 3 { | |
| 50 | t.Errorf("mr count: %+v", mr[1].Items[1]) | |
| 51 | if len(mr[1].Items) != 2 || mr[1].Items[1].Count != 3 { | |
| 52 | t.Errorf("mr counts: %+v", mr[1].Items) | |
| 53 | } | |
| 54 | } | |
| 55 | ||
| 56 | // A count of zero is a link to an empty list, so it is left out — unless | |
| 57 | // it is the filter in force, which has to stay clickable to clear (#237). | |
| 58 | func TestListFacetsDropsEmpty(t *testing.T) { | |
| 59 | labels := []store.Label{{Name: "bug", Issues: 0}, {Name: "docs", Issues: 0}} | |
| 60 | ms := []store.Milestone{{Title: "v2", OpenItems: 0}, {Title: "v3", OpenItems: 1}} | |
| 61 | ||
| 62 | groups := listFacets(url.Values{"state": {"open"}}, []string{"open"}, "open", labels, ms, false) | |
| 63 | if n := len(groups[1].Items); n != 0 { | |
| 64 | t.Errorf("empty labels offered: %+v", groups[1].Items) | |
| 65 | } | |
| 66 | if n := len(groups[2].Items); n != 1 || groups[2].Items[0].Label != "v3" { | |
| 67 | t.Errorf("milestones: %+v", groups[2].Items) | |
| 68 | } | |
| 69 | ||
| 70 | base := url.Values{"state": {"open"}, "label": {"bug"}, "milestone": {"v2"}} | |
| 71 | groups = listFacets(base, []string{"open"}, "open", labels, ms, false) | |
| 72 | lb := groups[1].Items | |
| 73 | if len(lb) != 1 || lb[0].Label != "bug" || !lb[0].Active || lb[0].Href != "?milestone=v2&state=open" { | |
| 74 | t.Errorf("active empty label dropped: %+v", lb) | |
| 75 | } | |
| 76 | mg := groups[2].Items | |
| 77 | if len(mg) != 2 || mg[0].Label != "v2" || !mg[0].Active || mg[0].Href != "?label=bug&state=open" { | |
| 78 | t.Errorf("active empty milestone dropped: %+v", mg) | |
| 51 | 79 | } |
| 52 | 80 | } |
internal/httpd/sidecol_test.go added +74
| @@ -0,0 +1,74 @@ | ||
| 1 | package httpd | |
| 2 | ||
| 3 | import ( | |
| 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. | |
| 15 | func 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). | |
| 39 | func 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). | |
| 68 | func 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); } | ||
| 1430 | 1430 | .sidecol form.searchform { margin-top: var(--sp-2); } |
| 1431 | 1431 | .sidecol form.searchform input[type="text"] { min-width: 0; width: 100%; } |
| 1432 | 1432 | |
| 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 | ||
| 1433 | 1473 | /* a sticky column taller than the viewport scrolls inside itself, or its |
| 1434 | 1474 | tail is unreachable once the page scrolls. Above 62rem only: below it |
| 1435 | 1475 | .filenav is hidden and the other two are static. .dashpins stops being |
| @@ -1655,9 +1695,12 @@ svg.icon { vertical-align: -0.125em; } | ||
| 1655 | 1695 | .blobgrid { grid-template-columns: 1fr; } |
| 1656 | 1696 | .filenav { display: none; } |
| 1657 | 1697 | |
| 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) */ | |
| 1659 | 1702 | .withcol, .withcol.narrow { grid-template-columns: 1fr; } |
| 1660 | .sidecol { position: static; order: 2; } | |
| 1703 | .sidecol { position: static; margin-bottom: var(--sp-3); } | |
| 1661 | 1704 | .sidecol .grp { display: inline-block; vertical-align: top; margin-right: var(--sp-5); } |
| 1662 | 1705 | } |
| 1663 | 1706 | |
internal/web/templates/account.html +3
| @@ -7,6 +7,8 @@ | ||
| 7 | 7 | |
| 8 | 8 | <div class="withcol narrow"> |
| 9 | 9 | <nav class="sidecol" aria-label="Sections"> |
| 10 | <details class="sidedrop"> | |
| 11 | <summary><span class="sideword">Sections</span></summary> | |
| 10 | 12 | <div class="grp"><h2 class="colhead">Sections</h2> |
| 11 | 13 | <ul> |
| 12 | 14 | <li><a href="#profile">Profile</a></li> |
| @@ -18,6 +20,7 @@ | ||
| 18 | 20 | <li><a href="#export">Export</a></li> |
| 19 | 21 | <li><a href="#cli">On the command line</a></li> |
| 20 | 22 | </ul></div> |
| 23 | </details> | |
| 21 | 24 | </nav> |
| 22 | 25 | <div class="colmain"> |
| 23 | 26 | <section id="profile"><h2>Profile</h2> |
internal/web/templates/admin.html +3
| @@ -7,6 +7,8 @@ | ||
| 7 | 7 | |
| 8 | 8 | <div class="withcol narrow"> |
| 9 | 9 | <nav class="sidecol" aria-label="Sections"> |
| 10 | <details class="sidedrop"> | |
| 11 | <summary><span class="sideword">Sections</span></summary> | |
| 10 | 12 | <div class="grp"><h2 class="colhead">Sections</h2> |
| 11 | 13 | <ul> |
| 12 | 14 | <li><a href="#webhooks">Webhook deliveries</a></li> |
| @@ -15,6 +17,7 @@ | ||
| 15 | 17 | <li><a href="#builds">Builds</a></li> |
| 16 | 18 | <li><a href="#deps">Dependency checks</a></li> |
| 17 | 19 | </ul></div> |
| 20 | </details> | |
| 18 | 21 | </nav> |
| 19 | 22 | <div class="colmain"> |
| 20 | 23 | |
internal/web/templates/builds.html +3
| @@ -3,6 +3,8 @@ | ||
| 3 | 3 | {{define "content"}} |
| 4 | 4 | <div class="withcol"> |
| 5 | 5 | <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> | |
| 6 | 8 | {{range .Facets}}{{if .Items}}<div class="grp"> |
| 7 | 9 | <h2 class="colhead">{{.Title}}</h2> |
| 8 | 10 | <ul>{{range .Items}}<li><a{{if .Active}} aria-current="page"{{end}} href="{{.Href}}">{{.Label}}</a></li>{{end}}</ul> |
| @@ -15,6 +17,7 @@ | ||
| 15 | 17 | <input type="hidden" name="job" value="{{.Filter.Job}}"> |
| 16 | 18 | <button type="submit" class="btn">Filter</button> |
| 17 | 19 | </form> |
| 20 | </details> | |
| 18 | 21 | </nav> |
| 19 | 22 | <div class="colmain"> |
| 20 | 23 | <div class="listhead"> |
internal/web/templates/globalsearch.html +3
| @@ -3,6 +3,8 @@ | ||
| 3 | 3 | {{define "content"}} |
| 4 | 4 | <div class="withcol"> |
| 5 | 5 | <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> | |
| 6 | 8 | <div class="grp"><h2 class="colhead">Kind</h2> |
| 7 | 9 | <ul> |
| 8 | 10 | <li><a{{if eq .Kind ""}} aria-current="page"{{end}} href="?q={{.Query}}">everything</a></li> |
| @@ -10,6 +12,7 @@ | ||
| 10 | 12 | <li><a{{if eq .Kind "issue"}} aria-current="page"{{end}} href="?q={{.Query}}&kind=issue">issues</a></li> |
| 11 | 13 | <li><a{{if eq .Kind "mr"}} aria-current="page"{{end}} href="?q={{.Query}}&kind=mr">merge requests</a></li> |
| 12 | 14 | </ul></div> |
| 15 | </details> | |
| 13 | 16 | </nav> |
| 14 | 17 | <div class="colmain"> |
| 15 | 18 | <div class="listhead"> |
internal/web/templates/layout.html +9
| @@ -122,11 +122,20 @@ | ||
| 122 | 122 | {{end}}</ul> |
| 123 | 123 | </nav>{{end}} |
| 124 | 124 | |
| 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. */}} | |
| 125 | 131 | {{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> | |
| 126 | 134 | {{range .}}{{if .Items}}<div class="grp"> |
| 127 | 135 | <h2 class="colhead">{{.Title}}</h2> |
| 128 | 136 | <ul>{{range .Items}}<li><a{{if .Active}} aria-current="page"{{end}} href="{{.Href}}">{{.Label}}{{if .Count}} <i>{{.Count}}</i>{{end}}</a></li>{{end}}</ul> |
| 129 | 137 | </div>{{end}}{{end}} |
| 138 | </details> | |
| 130 | 139 | </nav>{{end}} |
| 131 | 140 | |
| 132 | 141 | {{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 @@ | ||
| 6 | 6 | {{if .Notice}}{{if .Saved}}<p class="notice" role="status">{{.Notice}}</p>{{else}}<p class="error" role="alert">{{.Notice}}</p>{{end}}{{end}} |
| 7 | 7 | <div class="withcol narrow"> |
| 8 | 8 | <nav class="sidecol" aria-label="Sections"> |
| 9 | <details class="sidedrop"> | |
| 10 | <summary><span class="sideword">Sections</span></summary> | |
| 9 | 11 | <div class="grp"><h2 class="colhead">Sections</h2> |
| 10 | 12 | <ul> |
| 11 | 13 | <li><a href="#identity">Identity</a></li> |
| @@ -17,6 +19,7 @@ | ||
| 17 | 19 | <li><a href="#runners">Runners</a></li> |
| 18 | 20 | <li><a href="#lifecycle">Lifecycle</a></li> |
| 19 | 21 | </ul></div> |
| 22 | </details> | |
| 20 | 23 | </nav> |
| 21 | 24 | <div class="colmain"> |
| 22 | 25 | |