Collapse repo page tools behind a hamburger on mobile
On a phone the repo tabs, branch and clone menus, and feed/website links
bunched up and scrolled away with the page, and the doc-tab row wrapped.
Collapse the secondary tools behind a hamburger on the sticky bar: an
accordion of the branch/clone dropdowns and the feed/website/upstream
links, sized to its contents and closable by tapping outside (no JS).
The content-frame doc tabs fold the same way, keeping the active doc as
the title. Desktop layout is unchanged.
Also restore the clone button to a filled green button, expose the
current ref to assistive tech (aria-current, unmasked summary text), and
share one dropdown-chrome and hamburger-toggle ruleset across the three
menus.
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
Joachim Wiberg · 59 days agocommit 595dcb7 · parent 5eda372 · child d42bced · patch
diff --git a/internal/web/pages.go b/internal/web/pages.goindex 185107a..c2d2b39 100644--- a/internal/web/pages.go+++ b/internal/web/pages.go@@ -54,12 +54,12 @@ type repoHead struct { // refsel is the branch selector; a sub-struct so embedding repoHead // cannot collide with page fields like refsData.Branches. type refsel struct {+ Label string // current ref, shown on the dropdown summary Branches []refOption Tags []refOption }-// refOption is one branch selector entry; an empty URL renders as a-// disabled placeholder (e.g. when browsing at a bare commit hash).+// refOption is one branch/tag entry in the ref dropdown. type refOption struct { Name string URL string@@ -89,8 +89,10 @@ func (s *Server) head(repo *git.Repo, ref, view, path string) repoHead { h.Refsel.Branches = refOptions(repo.Name, branches, ref, view, path) h.Refsel.Tags = refOptions(repo.Name, tags, ref, view, path) current := func(o refOption) bool { return o.Current }- if !slices.ContainsFunc(h.Refsel.Branches, current) && !slices.ContainsFunc(h.Refsel.Tags, current) {- h.Refsel.Branches = append([]refOption{{Name: shortsha(ref), Current: true}}, h.Refsel.Branches...)+ if slices.ContainsFunc(h.Refsel.Branches, current) || slices.ContainsFunc(h.Refsel.Tags, current) {+ h.Refsel.Label = ref+ } else {+ h.Refsel.Label = shortsha(ref) // browsing a bare commit } return h }diff --git a/static/doctabs.js b/static/doctabs.jsindex d7360b2..90d2d01 100644--- a/static/doctabs.js+++ b/static/doctabs.js@@ -7,6 +7,8 @@ var tabs = document.querySelectorAll(".filehead a.doctab"); var bodies = document.querySelectorAll(".readme .filebody"); var src = document.getElementById("docsrc");+ var title = document.getElementById("doctitle");+ var toggle = document.getElementById("doctoggle"); if (tabs.length < 2) return;@@ -18,6 +20,10 @@ } if (src) src.href = tabs[n].getAttribute("data-blob");+ if (title)+ title.textContent = tabs[n].textContent;+ if (toggle)+ toggle.checked = false; /* close the mobile dropdown */ } for (var i = 0; i < tabs.length; i++)diff --git a/static/style.css b/static/style.cssindex 8b099df..f02db7e 100644--- a/static/style.css+++ b/static/style.css@@ -175,39 +175,75 @@ nav.tabs a { nav.tabs a:hover { color: var(--fg); text-decoration: none; } nav.tabs a.active { color: var(--fg); border-bottom-color: var(--accent); } nav.tabs .right {+ position: relative; margin-left: auto; display: flex; align-items: center; gap: 0.5rem; }-details.clone-menu { position: relative; }-details.clone-menu summary {+/* Secondary tools show inline on wide screens; the mobile breakpoint+ collapses them behind the ☰. A checkbox toggle (not <details>) so+ CSS fully controls visibility across browsers. */+.more-toggle, .more-summary, .more-backdrop { display: none; }+.more-panel { display: flex; align-items: center; gap: 0.5rem; }+/* Branch and clone menus share one dropdown style. */+details.refmenu, details.clone-menu { position: relative; }+details.refmenu > summary, details.clone-menu > summary { list-style: none; cursor: pointer;- background: var(--accent);- color: var(--bg); /* contrast-safe on the accent in both themes */- padding: 0.15rem 0.75rem;+ background: var(--code-bg);+ color: var(--fg);+ border: 1px solid var(--border); border-radius: 6px;+ padding: 0.1rem 0.55rem; font-size: 0.875rem;- margin-bottom: 0.25rem; }-details.clone-menu summary::-webkit-details-marker { display: none; }-details.clone-menu summary::after { content: " ▾"; }-.clone-panel {+/* Clone is the primary action — a filled green button, GitHub-style. */+details.clone-menu > summary {+ background: var(--add);+ border-color: var(--add);+ color: var(--bg); /* contrast-safe on the green in both themes */+}+details.refmenu > summary::-webkit-details-marker,+details.clone-menu > summary::-webkit-details-marker { display: none; }+details.refmenu > summary::after,+details.clone-menu > summary::after { content: " \25be"; color: var(--muted); }+details.clone-menu > summary::after { color: var(--bg); }++/* Floating-panel chrome, shared by the desktop ref/clone dropdowns and+ the mobile ☰ panels. The :checked selectors are inert on desktop —+ the toggles are display:none there, so they never match. */+.ref-panel, .clone-panel, .more-toggle:checked ~ .more-panel, .doc-toggle:checked ~ .doctabs { position: absolute; right: 0; top: calc(100% + 4px);- z-index: 10;- width: 24rem;- max-width: 90vw;+ z-index: 15; background: var(--bg); border: 1px solid var(--border); border-radius: 6px;- padding: 0.25rem 0.75rem 0.75rem; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2); font-size: 0.875rem; text-align: left; }+.clone-panel { width: 24rem; max-width: 90vw; padding: 0.25rem 0.75rem 0.75rem; }+.ref-panel {+ display: flex;+ flex-direction: column;+ min-width: 9rem;+ max-height: 20rem;+ overflow-y: auto;+ padding: 0.5rem 0.75rem;+}+.ref-panel strong {+ color: var(--muted);+ font-size: 0.7rem;+ text-transform: uppercase;+ margin-top: 0.5rem;+}+.ref-panel strong:first-child { margin-top: 0; }+.ref-panel a { color: var(--fg); padding: 0.12rem 0; }+.ref-panel a.current { color: var(--accent); font-weight: 600; }+.ref-panel a:hover { color: var(--accent); text-decoration: none; } .clone-panel label { display: block; margin: 0.5rem 0 0.2rem;@@ -236,17 +272,6 @@ details.clone-menu summary::after { content: " ▾"; } .copyrow button:hover { color: var(--fg); } .clone-panel a.dl { display: inline-block; margin-top: 0.6rem; }-select.refsel {- background: var(--code-bg);- color: var(--fg);- border: 1px solid var(--border);- border-radius: 6px;- font: inherit;- font-size: 0.875rem;- padding: 0.1rem 0.3rem;- margin-bottom: 0.25rem;-}- table { border-collapse: collapse; width: 100%; } td { padding: 0.4rem 0.75rem 0.4rem 0;@@ -317,11 +342,16 @@ table.log tr[hidden] { display: none; } gap: 1.25rem; align-items: baseline; flex-wrap: wrap;+ position: relative; /* anchor for the mobile doc dropdown */ } .readme .filehead a { color: var(--muted); } .readme .filehead a:hover { color: var(--fg); text-decoration: none; } .readme .filehead a.active { color: var(--fg); font-weight: 600; } .readme .filehead a.src { margin-left: auto; }+/* Desktop: the wrapper is transparent so the tabs + source lay out+ as direct flex children. Mobile turns it into a dropdown. */+.doctabs { display: contents; }+.doc-title, .doc-toggle, .doc-summary, .doc-backdrop { display: none; } .readme .filebody { padding: 0.5rem 2rem 1rem; } .readme img { max-width: 100%; } .readme pre {@@ -482,12 +512,94 @@ pre.diff { padding-right: 0.8rem; } .spacer { display: none; }- nav.tabs .right {- margin-left: 0;- flex-wrap: wrap;- width: 100%;- padding: 0.5rem 0 0.25rem;+ /* Shared ☰ mechanics — the nav "more" menu and the content-frame doc+ switcher are the same disclosure: a visually-hidden but focusable+ checkbox, a label acting as the ☰ button, an opened column panel,+ and a full-viewport backdrop to tap-close (no JS). */+ .more-toggle, .doc-toggle {+ position: absolute; /* off-screen but focusable for keyboard */+ width: 1px;+ height: 1px;+ opacity: 0;+ }+ .more-summary, .doc-summary {+ display: inline-flex;+ align-items: center;+ cursor: pointer;+ font-size: 1.1rem;+ color: var(--muted);+ }+ .more-summary { padding: 0.35rem 0.5rem; }+ .doc-summary { margin-left: auto; padding: 0 0.25rem; }+ .more-toggle:focus-visible + .more-summary,+ .doc-toggle:focus-visible + .doc-summary {+ outline: 2px solid var(--accent);+ outline-offset: 2px;+ }+ .more-toggle:checked + .more-summary,+ .doc-toggle:checked + .doc-summary { color: var(--fg); }+ /* Opened panel: a uniform column that fits its contents (chrome comes+ from the shared floating-panel rule). */+ .more-toggle:checked ~ .more-panel,+ .doc-toggle:checked ~ .doctabs {+ display: flex;+ flex-direction: column;+ align-items: stretch;+ width: max-content; /* fits what's shown; grows when a section opens */+ max-width: calc(100vw - 1.5rem);+ padding: 0.25rem 0;+ }+ .more-toggle:checked ~ .more-backdrop,+ .doc-toggle:checked ~ .doc-backdrop {+ display: block;+ position: fixed;+ inset: 0;+ z-index: 14; /* tap-close layer: below the panel (z 15), above the page */+ }++ /* Nav "more" menu: the branch/clone <details> flatten into accordion+ sections rather than the desktop's bordered dropdowns. */+ .more-panel { display: none; }+ .more-toggle:checked ~ .more-panel { max-height: 75vh; overflow-y: auto; font-size: 0.9rem; }+ .more-panel > a,+ .more-panel details > summary { padding: 0.5rem 0.9rem; color: var(--fg); }+ .more-panel details { position: static; }+ .more-panel details > summary {+ display: block;+ background: none;+ border: none;+ border-radius: 0;+ font-size: inherit;+ }+ .more-panel details > summary::after { content: " \25b8"; color: var(--muted); }+ .more-panel details[open] > summary::after { content: " \25be"; }+ /* Section body: indented plain rows, no inner box or scrollbar —+ the whole menu scrolls as one. */+ .more-panel .ref-panel,+ .more-panel .clone-panel {+ position: static;+ width: auto;+ min-width: 0;+ max-width: none;+ max-height: none;+ overflow: visible;+ background: none;+ border: none;+ border-radius: 0;+ box-shadow: none;+ padding: 0 0.9rem 0.4rem 1.5rem; /* indent sub-items */ }+ .more-panel .ref-panel a { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }+ .more-panel .clone-panel a.dl { display: block; margin-top: 0.4rem; }+ /* let the URL field shrink so the copy button stays on-screen */+ .more-panel .copyrow input { min-width: 0; }+ /* Content-frame doc switcher: keep the active doc name as the title+ and fold the rest (+ source) behind a ☰, mirroring the nav. */+ .filehead.foldable { align-items: center; }+ .doc-title { display: block; color: var(--fg); font-weight: 600; }+ .foldable .doctabs { display: none; } /* open rule out-specifies this */+ .doc-toggle:checked ~ .doctabs a { padding: 0.5rem 0.9rem; }+ .doc-toggle:checked ~ .doctabs a.src { margin-left: 0; } .logsearch { margin-left: 0; width: 100%; } .logsearch input { width: 100%; } .keyhelp > div { min-width: 0; width: calc(100% - 2rem); }diff --git a/templates/layout.html b/templates/layout.htmlindex 2842fb2..94cde82 100644--- a/templates/layout.html+++ b/templates/layout.html@@ -49,39 +49,47 @@ <a{{if eq .View "log"}} class="active" aria-current="page"{{end}} data-nav="log" href="{{url .Name "log" .Ref}}">log</a> <a{{if eq .View "refs"}} class="active" aria-current="page"{{end}} href="{{url .Name "refs" .Ref}}">refs</a> <span class="right">- {{if feature "feeds"}}<a href="{{url .Name "atom"}}" title="Atom feed">feed</a>{{end}}- {{with .Website}}<a href="{{.}}">website ↗</a>{{end}}- {{with .Upstream}}<a href="{{.}}">upstream ↗</a>{{end}}- <select class="refsel" aria-label="Switch branch or tag" onchange="if (this.value) location.href = this.value">- <optgroup label="branches">{{range .Refsel.Branches}}{{template "refopt" .}}{{end}}</optgroup>- {{if .Refsel.Tags}}<optgroup label="tags">{{range .Refsel.Tags}}{{template "refopt" .}}{{end}}</optgroup>{{end}}- </select>- {{if or .CloneURL .SSHURL}}- <details class="clone-menu">- <summary>clone</summary>- <div class="clone-panel">- {{with .CloneURL}}- <label>HTTPS</label>- <div class="copyrow"><input readonly value="{{.}}"><button type="button" data-copy title="copy" aria-label="Copy to clipboard">⎘</button></div>- {{end}}- {{with .SSHURL}}- <label>SSH</label>- <div class="copyrow"><input readonly value="{{.}}"><button type="button" data-copy title="copy" aria-label="Copy to clipboard">⎘</button></div>- {{end}}- {{if and .Ref (feature "archive")}}- <a class="dl" href="{{url .Name "archive" (printf "%s.tar.gz" .Ref)}}">download {{basename .Ref}}.tar.gz</a>- <a class="dl" href="{{url .Name "archive" (printf "%s.zip" .Ref)}}">download {{basename .Ref}}.zip</a>+ <input type="checkbox" id="moremenu" class="more-toggle">+ <label for="moremenu" class="more-summary" aria-label="More">☰</label>+ <div class="more-panel">+ {{if feature "feeds"}}<a href="{{url .Name "atom"}}" title="Atom feed">feed</a>{{end}}+ {{with .Website}}<a href="{{.}}">website ↗</a>{{end}}+ {{with .Upstream}}<a href="{{.}}">upstream ↗</a>{{end}}+ <details class="refmenu">+ <summary><span class="sr-only">Switch branch or tag: </span>{{.Refsel.Label}}</summary>+ <div class="ref-panel">+ <strong>branches</strong>+ {{range .Refsel.Branches}}<a href="{{.URL}}"{{if .Current}} class="current" aria-current="true"{{end}}>{{.Name}}</a>{{end}}+ {{if .Refsel.Tags}}<strong>tags</strong>+ {{range .Refsel.Tags}}<a href="{{.URL}}"{{if .Current}} class="current" aria-current="true"{{end}}>{{.Name}}</a>{{end}}{{end}}+ </div>+ </details>+ {{if or .CloneURL .SSHURL}}+ <details class="clone-menu">+ <summary>clone</summary>+ <div class="clone-panel">+ {{with .CloneURL}}+ <label>HTTPS</label>+ <div class="copyrow"><input readonly value="{{.}}"><button type="button" data-copy title="copy" aria-label="Copy to clipboard">⎘</button></div>+ {{end}}+ {{with .SSHURL}}+ <label>SSH</label>+ <div class="copyrow"><input readonly value="{{.}}"><button type="button" data-copy title="copy" aria-label="Copy to clipboard">⎘</button></div>+ {{end}}+ {{if and .Ref (feature "archive")}}+ <a class="dl" href="{{url .Name "archive" (printf "%s.tar.gz" .Ref)}}">download {{basename .Ref}}.tar.gz</a>+ <a class="dl" href="{{url .Name "archive" (printf "%s.zip" .Ref)}}">download {{basename .Ref}}.zip</a>+ {{end}}+ </div>+ </details> {{end}} </div>- </details>- {{end}}+ <label for="moremenu" class="more-backdrop" aria-hidden="true"></label> </span> </nav> {{end}} {{end}}-{{define "refopt"}}{{if .URL}}<option value="{{.URL}}"{{if .Current}} selected{{end}}>{{.Name}}</option>{{else}}<option selected disabled>{{.Name}}</option>{{end}}{{end}}- {{define "committable"}} <table class="log"> <caption class="sr-only">Commit log</caption>diff --git a/templates/repo.html b/templates/repo.htmlindex 47781e6..ff1d77c 100644--- a/templates/repo.html+++ b/templates/repo.html@@ -9,9 +9,18 @@ {{template "committable" .}} {{if .Docs}} <article class="readme">- <div class="filehead">- {{range $i, $t := .Docs}}<a class="doctab{{if not $i}} active{{end}}" href="{{$t.URL}}" data-blob="{{$t.BlobURL}}">{{$t.Label}}</a>{{end}}- <a class="src" id="docsrc" href="{{(index .Docs 0).BlobURL}}">source</a>+ {{$multi := gt (len .Docs) 1}}+ <div class="filehead{{if $multi}} foldable{{end}}">+ {{if $multi}}+ <span class="doc-title" id="doctitle">{{(index .Docs 0).Label}}</span>+ <input type="checkbox" id="doctoggle" class="doc-toggle">+ <label for="doctoggle" class="doc-summary" aria-label="Documents">☰</label>+ {{end}}+ <div class="doctabs">+ {{range $i, $t := .Docs}}<a class="doctab{{if not $i}} active{{end}}" href="{{$t.URL}}" data-blob="{{$t.BlobURL}}">{{$t.Label}}</a>{{end}}+ <a class="src" id="docsrc" href="{{(index .Docs 0).BlobURL}}">source</a>+ </div>+ {{if $multi}}<label for="doctoggle" class="doc-backdrop" aria-hidden="true"></label>{{end}} </div> {{range $i, $t := .Docs}} <div class="filebody"{{if $i}} hidden{{end}}>{{$t.HTML}}</div>