Align search results UI
- SHA
6358fdc5e1366e2fba38bf35f9c7044cfbcc9906- Parents
-
9dc6deb - Tree
142919d
6358fdc
6358fdc5e1366e2fba38bf35f9c7044cfbcc99069dc6deb
142919d| Status | File | + | - |
|---|---|---|---|
| M |
docs/internal/search.md
|
8 | 10 |
| M |
internal/web/handlers/search/search.go
|
87 | 26 |
| M |
internal/web/handlers/search/search_test.go
|
19 | 0 |
| M |
internal/web/render/render.go
|
22 | 0 |
| M |
internal/web/render/render_test.go
|
23 | 0 |
| M |
internal/web/static/css/shithub.css
|
211 | 20 |
| M |
internal/web/templates/_layout.html
|
86 | 0 |
| M |
internal/web/templates/_nav.html
|
4 | 3 |
| M |
internal/web/templates/search/_quick_dropdown.html
|
53 | 34 |
| M |
internal/web/templates/search/results.html
|
55 | 40 |
docs/internal/search.mdmodified@@ -137,15 +137,16 @@ pathological-length queries; longer inputs are silently truncated. | ||
| 137 | 137 | |
| 138 | 138 | ## Routes |
| 139 | 139 | |
| 140 | -| Method | Path | Notes | | |
| 141 | -|--------|------------------|------------------------------------------| | |
| 142 | -| GET | `/search` | Full results page with type tabs | | |
| 143 | -| GET | `/search/quick` | htmx fragment endpoint for top-bar drop | | |
| 140 | +| Method | Path | Notes | | |
| 141 | +|--------|------------------|--------------------------------------------| | |
| 142 | +| GET | `/search` | Full results page with GitHub-style filters | | |
| 143 | +| GET | `/search/quick` | HTML fragment endpoint for top-bar drop | | |
| 144 | 144 | |
| 145 | 145 | The top-bar nav embeds a search form pointing at `/search`; the |
| 146 | -htmx-driven dropdown wiring is intentionally deferred (the | |
| 147 | -endpoint exists; the JS to invoke it on keystroke comes when we | |
| 148 | -add htmx-the-library to the static asset bundle). | |
| 146 | +same input now calls `/search/quick` as the user types and renders | |
| 147 | +the returned fragment under the nav search box. Full-page type URLs | |
| 148 | +emit GitHub-style `type=repositories` and `type=pullrequests` while | |
| 149 | +still accepting the legacy `type=repos` and `type=pulls` aliases. | |
| 149 | 150 | |
| 150 | 151 | ## What we deferred from the spec |
| 151 | 152 | |
@@ -161,9 +162,6 @@ add htmx-the-library to the static asset bundle). | ||
| 161 | 162 | S33 webhooks sprint pulls in the rest of the API surface so we |
| 162 | 163 | do them together (consistency on auth + body cap + scope shapes). |
| 163 | 164 | **Forward-deferred to S33 / S34 API consolidation.** |
| 164 | -* **Quick-dropdown htmx wiring**: the endpoint returns the right | |
| 165 | - HTML; the static HTML form posts to `/search` directly. The | |
| 166 | - dropdown lights up when we land htmx in the static asset bundle. | |
| 167 | 165 | * **`path:` operator**: parser falls through; querying `path:foo` |
| 168 | 166 | treats it as free text today. Documented above. |
| 169 | 167 | |
internal/web/handlers/search/search.gomodified@@ -1,7 +1,7 @@ | ||
| 1 | 1 | // SPDX-License-Identifier: AGPL-3.0-or-later |
| 2 | 2 | |
| 3 | 3 | // Package search wires the S28 web search surface. The full results |
| 4 | -// page lives at GET /search; the htmx quick dropdown lives at GET | |
| 4 | +// page lives at GET /search; the nav quick dropdown lives at GET | |
| 5 | 5 | // /search/quick. |
| 6 | 6 | package search |
| 7 | 7 | |
@@ -64,13 +64,7 @@ func (h *Handlers) actor(r *http.Request) policy.Actor { | ||
| 64 | 64 | // results renders the full /search page with type tabs. |
| 65 | 65 | func (h *Handlers) results(w http.ResponseWriter, r *http.Request) { |
| 66 | 66 | rawQ := r.URL.Query().Get("q") |
| 67 | - tab := r.URL.Query().Get("type") | |
| 68 | - if tab == "" { | |
| 69 | - tab = "repos" | |
| 70 | - } | |
| 71 | - if !validSearchTab(tab) { | |
| 72 | - tab = "repos" | |
| 73 | - } | |
| 67 | + tab := normalizeSearchTab(r.URL.Query().Get("type")) | |
| 74 | 68 | page := pageFromRequest(r) |
| 75 | 69 | |
| 76 | 70 | parsed := srch.ParseQuery(rawQ) |
@@ -85,6 +79,7 @@ func (h *Handlers) results(w http.ResponseWriter, r *http.Request) { | ||
| 85 | 79 | "Page": page, |
| 86 | 80 | "Parsed": parsed, |
| 87 | 81 | "PageSize": srch.PageSize, |
| 82 | + "SearchProTip": searchProTip(tab), | |
| 88 | 83 | } |
| 89 | 84 | |
| 90 | 85 | if !parsed.HasContent() { |
@@ -96,7 +91,7 @@ func (h *Handlers) results(w http.ResponseWriter, r *http.Request) { | ||
| 96 | 91 | |
| 97 | 92 | offset := (page - 1) * srch.PageSize |
| 98 | 93 | switch tab { |
| 99 | - case "repos": | |
| 94 | + case "repositories": | |
| 100 | 95 | rows, total, err := srch.SearchRepos(r.Context(), deps, actor, parsed, srch.PageSize, offset) |
| 101 | 96 | if err != nil && !errors.Is(err, srch.ErrEmptyQuery) { |
| 102 | 97 | h.d.Logger.ErrorContext(r.Context(), "search repos", "error", err) |
@@ -112,7 +107,7 @@ func (h *Handlers) results(w http.ResponseWriter, r *http.Request) { | ||
| 112 | 107 | data["Issues"] = rows |
| 113 | 108 | data["Total"] = total |
| 114 | 109 | data["HasNext"] = int64(page*srch.PageSize) < total |
| 115 | - case "pulls": | |
| 110 | + case "pullrequests": | |
| 116 | 111 | rows, total, err := srch.SearchIssues(r.Context(), deps, actor, parsed, "pr", srch.PageSize, offset) |
| 117 | 112 | if err != nil && !errors.Is(err, srch.ErrEmptyQuery) { |
| 118 | 113 | h.d.Logger.ErrorContext(r.Context(), "search pulls", "error", err) |
@@ -144,18 +139,33 @@ func (h *Handlers) results(w http.ResponseWriter, r *http.Request) { | ||
| 144 | 139 | } |
| 145 | 140 | data["HasPrev"] = page > 1 |
| 146 | 141 | data["SearchTabs"] = h.searchTabs(r, actor, parsed, rawQ, tab) |
| 142 | + data["ResultHeading"] = searchResultHeading(tab, data["Total"]) | |
| 143 | + if page > 1 { | |
| 144 | + data["PrevHref"] = searchHref(rawQ, tab, page-1) | |
| 145 | + } | |
| 146 | + if next, ok := data["HasNext"].(bool); ok && next { | |
| 147 | + data["NextHref"] = searchHref(rawQ, tab, page+1) | |
| 148 | + } | |
| 147 | 149 | |
| 148 | 150 | if err := h.d.Render.RenderPage(w, r, "search/results", data); err != nil { |
| 149 | 151 | h.d.Logger.ErrorContext(r.Context(), "search render", "error", err) |
| 150 | 152 | } |
| 151 | 153 | } |
| 152 | 154 | |
| 153 | -func validSearchTab(tab string) bool { | |
| 155 | +func normalizeSearchTab(tab string) string { | |
| 154 | 156 | switch tab { |
| 155 | - case "repos", "issues", "pulls", "users", "code": | |
| 156 | - return true | |
| 157 | + case "", "repos", "repositories": | |
| 158 | + return "repositories" | |
| 159 | + case "code": | |
| 160 | + return "code" | |
| 161 | + case "issues": | |
| 162 | + return "issues" | |
| 163 | + case "pulls", "pullrequests": | |
| 164 | + return "pullrequests" | |
| 165 | + case "users": | |
| 166 | + return "users" | |
| 157 | 167 | default: |
| 158 | - return false | |
| 168 | + return "repositories" | |
| 159 | 169 | } |
| 160 | 170 | } |
| 161 | 171 | |
@@ -170,11 +180,11 @@ type searchTab struct { | ||
| 170 | 180 | |
| 171 | 181 | func (h *Handlers) searchTabs(r *http.Request, actor policy.Actor, parsed srch.ParsedQuery, rawQ, active string) []searchTab { |
| 172 | 182 | tabs := []searchTab{ |
| 173 | - {Key: "repos", Label: "Repositories", Icon: "repo"}, | |
| 174 | 183 | {Key: "code", Label: "Code", Icon: "code"}, |
| 184 | + {Key: "repositories", Label: "Repositories", Icon: "repo"}, | |
| 175 | 185 | {Key: "issues", Label: "Issues", Icon: "issue-opened"}, |
| 176 | - {Key: "pulls", Label: "Pull requests", Icon: "git-pull-request"}, | |
| 177 | - {Key: "users", Label: "Users", Icon: "person"}, | |
| 186 | + {Key: "pullrequests", Label: "Pull requests", Icon: "git-pull-request"}, | |
| 187 | + {Key: "users", Label: "Users", Icon: "people"}, | |
| 178 | 188 | } |
| 179 | 189 | for i := range tabs { |
| 180 | 190 | tabs[i].Selected = tabs[i].Key == active |
@@ -189,13 +199,13 @@ func (h *Handlers) searchTabs(r *http.Request, actor policy.Actor, parsed srch.P | ||
| 189 | 199 | var total int64 |
| 190 | 200 | var err error |
| 191 | 201 | switch tabs[i].Key { |
| 192 | - case "repos": | |
| 202 | + case "repositories": | |
| 193 | 203 | _, total, err = srch.SearchRepos(r.Context(), deps, actor, parsed, 0, 0) |
| 194 | 204 | case "code": |
| 195 | 205 | _, total, err = srch.SearchCode(r.Context(), deps, actor, parsed, 0, 0) |
| 196 | 206 | case "issues": |
| 197 | 207 | _, total, err = srch.SearchIssues(r.Context(), deps, actor, parsed, "issue", 0, 0) |
| 198 | - case "pulls": | |
| 208 | + case "pullrequests": | |
| 199 | 209 | _, total, err = srch.SearchIssues(r.Context(), deps, actor, parsed, "pr", 0, 0) |
| 200 | 210 | case "users": |
| 201 | 211 | _, total, err = srch.SearchUsers(r.Context(), deps, parsed, 0, 0) |
@@ -219,8 +229,58 @@ func searchHref(q, tab string, page int) string { | ||
| 219 | 229 | return "/search?" + v.Encode() |
| 220 | 230 | } |
| 221 | 231 | |
| 222 | -// quick is the htmx dropdown endpoint. Returns one fragment with | |
| 223 | -// the top N results across all four types stacked vertically. | |
| 232 | +func searchResultHeading(tab string, total any) string { | |
| 233 | + count, _ := total.(int64) | |
| 234 | + switch tab { | |
| 235 | + case "code": | |
| 236 | + return plural(count, "code result", "code results") | |
| 237 | + case "issues": | |
| 238 | + return plural(count, "issue result", "issue results") | |
| 239 | + case "pullrequests": | |
| 240 | + return plural(count, "pull request result", "pull request results") | |
| 241 | + case "users": | |
| 242 | + return plural(count, "user result", "user results") | |
| 243 | + default: | |
| 244 | + return plural(count, "repository result", "repository results") | |
| 245 | + } | |
| 246 | +} | |
| 247 | + | |
| 248 | +func plural(count int64, one, many string) string { | |
| 249 | + if count == 1 { | |
| 250 | + return "1 " + one | |
| 251 | + } | |
| 252 | + return int64String(count) + " " + many | |
| 253 | +} | |
| 254 | + | |
| 255 | +func int64String(n int64) string { | |
| 256 | + if n == 0 { | |
| 257 | + return "0" | |
| 258 | + } | |
| 259 | + var buf [20]byte | |
| 260 | + i := len(buf) | |
| 261 | + for n > 0 { | |
| 262 | + i-- | |
| 263 | + buf[i] = byte('0' + n%10) | |
| 264 | + n /= 10 | |
| 265 | + } | |
| 266 | + return string(buf[i:]) | |
| 267 | +} | |
| 268 | + | |
| 269 | +func searchProTip(tab string) string { | |
| 270 | + switch tab { | |
| 271 | + case "issues", "pullrequests": | |
| 272 | + return "Restrict your search to the title by using the in:title qualifier." | |
| 273 | + case "code": | |
| 274 | + return "Use repo:owner/name to limit code search to a single repository." | |
| 275 | + case "users": | |
| 276 | + return "Search by username or display name to find people faster." | |
| 277 | + default: | |
| 278 | + return "Press / to activate the search input again and adjust your query." | |
| 279 | + } | |
| 280 | +} | |
| 281 | + | |
| 282 | +// quick is the nav dropdown endpoint. Returns one fragment with | |
| 283 | +// the top N results across the implemented quick-search types. | |
| 224 | 284 | func (h *Handlers) quick(w http.ResponseWriter, r *http.Request) { |
| 225 | 285 | rawQ := r.URL.Query().Get("q") |
| 226 | 286 | parsed := srch.ParseQuery(rawQ) |
@@ -236,12 +296,13 @@ func (h *Handlers) quick(w http.ResponseWriter, r *http.Request) { | ||
| 236 | 296 | users, _, _ := srch.SearchUsers(r.Context(), deps, parsed, srch.QuickResultsLimit, 0) |
| 237 | 297 | |
| 238 | 298 | data := map[string]any{ |
| 239 | - "Query": rawQ, | |
| 240 | - "Repos": repos, | |
| 241 | - "Issues": issues, | |
| 242 | - "Users": users, | |
| 299 | + "Query": rawQ, | |
| 300 | + "SearchHref": searchHref(rawQ, "repositories", 1), | |
| 301 | + "Repos": repos, | |
| 302 | + "Issues": issues, | |
| 303 | + "Users": users, | |
| 243 | 304 | } |
| 244 | - if err := h.d.Render.RenderPage(w, r, "search/_quick_dropdown", data); err != nil { | |
| 305 | + if err := h.d.Render.RenderFragment(w, "search/_quick_dropdown", data); err != nil { | |
| 245 | 306 | h.d.Logger.ErrorContext(r.Context(), "quick render", "error", err) |
| 246 | 307 | } |
| 247 | 308 | } |
internal/web/handlers/search/search_test.gomodified@@ -28,3 +28,22 @@ func TestSearchHrefEscapesQuery(t *testing.T) { | ||
| 28 | 28 | t.Fatalf("searchHref = %q, want %q", got, want) |
| 29 | 29 | } |
| 30 | 30 | } |
| 31 | + | |
| 32 | +func TestNormalizeSearchTabAcceptsGitHubTypesAndLegacyAliases(t *testing.T) { | |
| 33 | + cases := map[string]string{ | |
| 34 | + "": "repositories", | |
| 35 | + "repos": "repositories", | |
| 36 | + "repositories": "repositories", | |
| 37 | + "pulls": "pullrequests", | |
| 38 | + "pullrequests": "pullrequests", | |
| 39 | + "code": "code", | |
| 40 | + "issues": "issues", | |
| 41 | + "users": "users", | |
| 42 | + "unknown-value": "repositories", | |
| 43 | + } | |
| 44 | + for input, want := range cases { | |
| 45 | + if got := normalizeSearchTab(input); got != want { | |
| 46 | + t.Fatalf("normalizeSearchTab(%q) = %q, want %q", input, got, want) | |
| 47 | + } | |
| 48 | + } | |
| 49 | +} | |
internal/web/render/render.gomodified@@ -201,6 +201,28 @@ func (r *Renderer) Render(w io.Writer, name string, data any) error { | ||
| 201 | 201 | return err |
| 202 | 202 | } |
| 203 | 203 | |
| 204 | +// RenderFragment executes only a page template's "page" definition. Use it | |
| 205 | +// for HTML fragments returned to JavaScript or htmx; full browser pages should | |
| 206 | +// continue to call Render/RenderPage so nav, footer, and document chrome stay | |
| 207 | +// consistent. | |
| 208 | +func (r *Renderer) RenderFragment(w io.Writer, name string, data any) error { | |
| 209 | + t, ok := r.pages[name] | |
| 210 | + if !ok { | |
| 211 | + return fmt.Errorf("render: unknown page %q", name) | |
| 212 | + } | |
| 213 | + var buf bytes.Buffer | |
| 214 | + if err := t.ExecuteTemplate(&buf, "page", data); err != nil { | |
| 215 | + return fmt.Errorf("execute fragment %s: %w", name, err) | |
| 216 | + } | |
| 217 | + if rw, ok := w.(http.ResponseWriter); ok { | |
| 218 | + if rw.Header().Get("Content-Type") == "" { | |
| 219 | + rw.Header().Set("Content-Type", "text/html; charset=utf-8") | |
| 220 | + } | |
| 221 | + } | |
| 222 | + _, err := w.Write(buf.Bytes()) | |
| 223 | + return err | |
| 224 | +} | |
| 225 | + | |
| 204 | 226 | // RenderPage is the request-aware Render: when data is a map[string]any, it |
| 205 | 227 | // injects "Viewer" (from middleware.CurrentUserFromContext) and "CSRFToken" |
| 206 | 228 | // (the per-request token) if the caller hasn't set them. The nav partial's |
internal/web/render/render_test.gomodified@@ -52,6 +52,29 @@ func TestNew_RegistersSubdirPages(t *testing.T) { | ||
| 52 | 52 | } |
| 53 | 53 | } |
| 54 | 54 | |
| 55 | +func TestRenderFragmentExecutesPageWithoutLayout(t *testing.T) { | |
| 56 | + t.Parallel() | |
| 57 | + fsys := fstest.MapFS{ | |
| 58 | + "_layout.html": &fstest.MapFile{Data: []byte( | |
| 59 | + `{{ define "layout" }}<html>{{ template "page" . }}</html>{{ end }}`, | |
| 60 | + )}, | |
| 61 | + "fragment.html": &fstest.MapFile{Data: []byte( | |
| 62 | + `{{ define "page" }}fragment only{{ end }}`, | |
| 63 | + )}, | |
| 64 | + } | |
| 65 | + r, err := New(fsys, Options{}) | |
| 66 | + if err != nil { | |
| 67 | + t.Fatalf("New: %v", err) | |
| 68 | + } | |
| 69 | + var buf bytes.Buffer | |
| 70 | + if err := r.RenderFragment(&buf, "fragment", nil); err != nil { | |
| 71 | + t.Fatalf("render fragment: %v", err) | |
| 72 | + } | |
| 73 | + if got := buf.String(); got != "fragment only" { | |
| 74 | + t.Fatalf("RenderFragment body = %q, want fragment only", got) | |
| 75 | + } | |
| 76 | +} | |
| 77 | + | |
| 55 | 78 | // Regression test for the inbound deferral from S30 dogfood: a partial |
| 56 | 79 | // at `profile/_tabs.html` that defines `{{ define "tabs" }}` was |
| 57 | 80 | // silently registered as an unparsed page. A page that called |
internal/web/static/css/shithub.cssmodified@@ -4111,6 +4111,17 @@ button.shithub-repo-action { | ||
| 4111 | 4111 | } |
| 4112 | 4112 | |
| 4113 | 4113 | /* S28 — search */ |
| 4114 | +.sr-only { | |
| 4115 | + position: absolute; | |
| 4116 | + width: 1px; | |
| 4117 | + height: 1px; | |
| 4118 | + padding: 0; | |
| 4119 | + margin: -1px; | |
| 4120 | + overflow: hidden; | |
| 4121 | + clip: rect(0, 0, 0, 0); | |
| 4122 | + white-space: nowrap; | |
| 4123 | + border: 0; | |
| 4124 | +} | |
| 4114 | 4125 | .shithub-nav-search { |
| 4115 | 4126 | position: relative; |
| 4116 | 4127 | flex: 1 1 22rem; |
@@ -4134,10 +4145,15 @@ button.shithub-repo-action { | ||
| 4134 | 4145 | padding: 0.35rem 2rem 0.35rem 2rem; |
| 4135 | 4146 | border: 1px solid var(--border-default); |
| 4136 | 4147 | border-radius: 6px; |
| 4137 | - background: var(--canvas-default); | |
| 4148 | + background: var(--canvas-subtle); | |
| 4138 | 4149 | color: var(--fg-default); |
| 4139 | 4150 | font-size: 0.85rem; |
| 4140 | 4151 | } |
| 4152 | +.shithub-nav-search input:focus { | |
| 4153 | + outline: 2px solid var(--accent-fg); | |
| 4154 | + outline-offset: -1px; | |
| 4155 | + background: var(--canvas-default); | |
| 4156 | +} | |
| 4141 | 4157 | .shithub-nav-search-key { |
| 4142 | 4158 | position: absolute; |
| 4143 | 4159 | right: 0.55rem; |
@@ -4153,18 +4169,36 @@ button.shithub-repo-action { | ||
| 4153 | 4169 | text-align: center; |
| 4154 | 4170 | pointer-events: none; |
| 4155 | 4171 | } |
| 4172 | +.shithub-nav-search-popover { | |
| 4173 | + position: absolute; | |
| 4174 | + z-index: 80; | |
| 4175 | + top: calc(100% + 0.45rem); | |
| 4176 | + left: 0; | |
| 4177 | + right: 0; | |
| 4178 | + overflow: hidden; | |
| 4179 | + border: 1px solid var(--border-default); | |
| 4180 | + border-radius: 8px; | |
| 4181 | + background: var(--canvas-default); | |
| 4182 | + box-shadow: 0 16px 32px rgba(1, 4, 9, 0.22); | |
| 4183 | +} | |
| 4184 | +.shithub-nav-search-popover[hidden] { | |
| 4185 | + display: none; | |
| 4186 | +} | |
| 4156 | 4187 | .shithub-search-page { |
| 4157 | 4188 | padding: 1.5rem 1rem 3rem; |
| 4158 | 4189 | } |
| 4159 | 4190 | .shithub-search-shell { |
| 4160 | 4191 | display: grid; |
| 4161 | - grid-template-columns: 220px minmax(0, 1fr); | |
| 4162 | - gap: 2rem; | |
| 4192 | + grid-template-columns: 296px minmax(0, 1fr) 280px; | |
| 4193 | + gap: 1.5rem; | |
| 4163 | 4194 | max-width: 1280px; |
| 4164 | 4195 | margin: 0 auto; |
| 4165 | 4196 | } |
| 4166 | 4197 | .shithub-search-sidebar { |
| 4167 | - padding-top: 3.35rem; | |
| 4198 | + position: sticky; | |
| 4199 | + top: 1rem; | |
| 4200 | + align-self: start; | |
| 4201 | + padding-top: 0.2rem; | |
| 4168 | 4202 | } |
| 4169 | 4203 | .shithub-search-sidebar h2 { |
| 4170 | 4204 | margin: 0 0 0.55rem; |
@@ -4180,8 +4214,8 @@ button.shithub-repo-action { | ||
| 4180 | 4214 | align-items: center; |
| 4181 | 4215 | justify-content: space-between; |
| 4182 | 4216 | gap: 0.75rem; |
| 4183 | - min-height: 2.25rem; | |
| 4184 | - padding: 0.4rem 0.65rem; | |
| 4217 | + min-height: 2rem; | |
| 4218 | + padding: 0.35rem 0.55rem; | |
| 4185 | 4219 | border-radius: 6px; |
| 4186 | 4220 | color: var(--fg-default); |
| 4187 | 4221 | font-size: 0.875rem; |
@@ -4222,11 +4256,11 @@ button.shithub-repo-action { | ||
| 4222 | 4256 | .shithub-search-query-form { |
| 4223 | 4257 | display: flex; |
| 4224 | 4258 | gap: 0.5rem; |
| 4225 | - margin-bottom: 1.25rem; | |
| 4259 | + margin-bottom: 1rem; | |
| 4226 | 4260 | } |
| 4227 | 4261 | .shithub-search-query-form input[type=text] { |
| 4228 | 4262 | flex: 1; |
| 4229 | - min-height: 2.35rem; | |
| 4263 | + min-height: 2.25rem; | |
| 4230 | 4264 | padding: 0.45rem 0.75rem; |
| 4231 | 4265 | border: 1px solid var(--border-default); |
| 4232 | 4266 | border-radius: 6px; |
@@ -4246,6 +4280,16 @@ button.shithub-repo-action { | ||
| 4246 | 4280 | margin: 0; |
| 4247 | 4281 | font-size: 1.25rem; |
| 4248 | 4282 | line-height: 1.3; |
| 4283 | + font-weight: 600; | |
| 4284 | +} | |
| 4285 | +.shithub-search-results-head p { | |
| 4286 | + margin: 0.25rem 0 0; | |
| 4287 | + color: var(--fg-muted); | |
| 4288 | + font-size: 0.875rem; | |
| 4289 | +} | |
| 4290 | +.shithub-search-query-echo { | |
| 4291 | + color: var(--fg-default); | |
| 4292 | + font-weight: 600; | |
| 4249 | 4293 | } |
| 4250 | 4294 | .shithub-search-sort { |
| 4251 | 4295 | position: relative; |
@@ -4290,15 +4334,16 @@ button.shithub-repo-action { | ||
| 4290 | 4334 | } |
| 4291 | 4335 | .shithub-search-result { |
| 4292 | 4336 | display: flex; |
| 4293 | - gap: 0.75rem; | |
| 4337 | + gap: 1rem; | |
| 4294 | 4338 | justify-content: space-between; |
| 4295 | - padding: 1rem 0; | |
| 4339 | + padding: 1.15rem 0; | |
| 4296 | 4340 | border-bottom: 1px solid var(--border-default); |
| 4297 | 4341 | } |
| 4298 | 4342 | .shithub-search-result-main { |
| 4343 | + flex: 1 1 auto; | |
| 4299 | 4344 | min-width: 0; |
| 4300 | 4345 | } |
| 4301 | -.shithub-search-result h2 { | |
| 4346 | +.shithub-search-result-title { | |
| 4302 | 4347 | display: flex; |
| 4303 | 4348 | align-items: center; |
| 4304 | 4349 | gap: 0.45rem; |
@@ -4307,20 +4352,44 @@ button.shithub-repo-action { | ||
| 4307 | 4352 | line-height: 1.35; |
| 4308 | 4353 | font-weight: 600; |
| 4309 | 4354 | } |
| 4310 | -.shithub-search-result h2 svg { | |
| 4355 | +.shithub-search-result-title svg { | |
| 4311 | 4356 | flex: 0 0 auto; |
| 4312 | 4357 | color: var(--fg-muted); |
| 4313 | 4358 | } |
| 4359 | +.shithub-search-result-title a { | |
| 4360 | + min-width: 0; | |
| 4361 | +} | |
| 4362 | +.shithub-search-result-title a:hover { | |
| 4363 | + text-decoration: underline; | |
| 4364 | +} | |
| 4365 | +.shithub-search-match em, | |
| 4366 | +.shithub-search-result-title em { | |
| 4367 | + font-style: normal; | |
| 4368 | + font-weight: 700; | |
| 4369 | +} | |
| 4314 | 4370 | .shithub-search-result p { |
| 4315 | 4371 | margin: 0.35rem 0 0; |
| 4316 | 4372 | color: var(--fg-default); |
| 4317 | 4373 | font-size: 0.875rem; |
| 4318 | 4374 | } |
| 4375 | +.shithub-search-result-desc { | |
| 4376 | + max-width: 760px; | |
| 4377 | + line-height: 1.45; | |
| 4378 | +} | |
| 4379 | +.shithub-search-result-context { | |
| 4380 | + margin: 0 0 0.2rem !important; | |
| 4381 | + color: var(--fg-muted) !important; | |
| 4382 | +} | |
| 4383 | +.shithub-search-result-context a { | |
| 4384 | + color: var(--fg-muted); | |
| 4385 | +} | |
| 4319 | 4386 | .shithub-search-result-path { |
| 4320 | 4387 | color: var(--fg-muted) !important; |
| 4321 | 4388 | } |
| 4322 | 4389 | .shithub-search-avatar, |
| 4323 | -.shithub-search-user-avatar { | |
| 4390 | +.shithub-search-user-avatar, | |
| 4391 | +.shithub-search-mini-avatar, | |
| 4392 | +.shithub-quick-avatar { | |
| 4324 | 4393 | border-radius: 50%; |
| 4325 | 4394 | background: var(--canvas-subtle); |
| 4326 | 4395 | flex: 0 0 auto; |
@@ -4341,6 +4410,16 @@ button.shithub-repo-action { | ||
| 4341 | 4410 | align-items: center; |
| 4342 | 4411 | gap: 0.25rem; |
| 4343 | 4412 | } |
| 4413 | +.shithub-search-result-meta a { | |
| 4414 | + display: inline-flex; | |
| 4415 | + align-items: center; | |
| 4416 | + gap: 0.25rem; | |
| 4417 | + color: var(--fg-muted); | |
| 4418 | +} | |
| 4419 | +.shithub-search-result-meta a:hover { | |
| 4420 | + color: var(--accent-fg); | |
| 4421 | + text-decoration: none; | |
| 4422 | +} | |
| 4344 | 4423 | .shithub-search-result-meta li + li::before { |
| 4345 | 4424 | content: ""; |
| 4346 | 4425 | width: 3px; |
@@ -4367,6 +4446,11 @@ button.shithub-repo-action { | ||
| 4367 | 4446 | .shithub-search-user-result { |
| 4368 | 4447 | justify-content: flex-start; |
| 4369 | 4448 | } |
| 4449 | +.shithub-search-user-login { | |
| 4450 | + color: var(--fg-muted); | |
| 4451 | + font-size: 0.875rem; | |
| 4452 | + font-weight: 400; | |
| 4453 | +} | |
| 4370 | 4454 | .shithub-search-code-preview { |
| 4371 | 4455 | margin: 0.75rem 0 0; |
| 4372 | 4456 | padding: 0.65rem 0.75rem; |
@@ -4399,13 +4483,108 @@ button.shithub-repo-action { | ||
| 4399 | 4483 | gap: 0.5rem; |
| 4400 | 4484 | padding: 1.5rem 0 0; |
| 4401 | 4485 | } |
| 4402 | -.shithub-quick-dropdown { padding: 0.5rem; } | |
| 4403 | -.shithub-quick-section { padding: 0.25rem 0; border-bottom: 1px solid var(--border-default); } | |
| 4404 | -.shithub-quick-section:last-of-type { border-bottom: none; } | |
| 4405 | -.shithub-quick-section h3 { font-size: 0.7rem; text-transform: uppercase; color: var(--fg-muted); margin: 0.25rem 0; } | |
| 4406 | -.shithub-quick-section ul { list-style: none; padding: 0; margin: 0; } | |
| 4407 | -.shithub-quick-section li { padding: 0.25rem 0; } | |
| 4408 | -.shithub-quick-footer { padding: 0.5rem 0; border-top: 1px solid var(--border-default); text-align: center; } | |
| 4486 | +.shithub-search-rightbar { | |
| 4487 | + padding-top: 3.25rem; | |
| 4488 | +} | |
| 4489 | +.shithub-search-tip-card { | |
| 4490 | + padding: 1rem 0; | |
| 4491 | + border-bottom: 1px solid var(--border-muted); | |
| 4492 | + color: var(--fg-muted); | |
| 4493 | + font-size: 0.875rem; | |
| 4494 | +} | |
| 4495 | +.shithub-search-tip-card:first-child { | |
| 4496 | + padding-top: 0; | |
| 4497 | +} | |
| 4498 | +.shithub-search-tip-card strong { | |
| 4499 | + display: inline-flex; | |
| 4500 | + align-items: center; | |
| 4501 | + gap: 0.35rem; | |
| 4502 | + color: var(--fg-default); | |
| 4503 | +} | |
| 4504 | +.shithub-search-tip-card p { | |
| 4505 | + margin: 0.4rem 0 0; | |
| 4506 | + line-height: 1.45; | |
| 4507 | +} | |
| 4508 | +.shithub-search-tip-card ul { | |
| 4509 | + display: flex; | |
| 4510 | + flex-direction: column; | |
| 4511 | + gap: 0.35rem; | |
| 4512 | + padding: 0; | |
| 4513 | + margin: 0.55rem 0 0; | |
| 4514 | + list-style: none; | |
| 4515 | +} | |
| 4516 | +.shithub-search-tip-card code { | |
| 4517 | + padding: 0.1rem 0.3rem; | |
| 4518 | + border: 1px solid var(--border-muted); | |
| 4519 | + border-radius: 6px; | |
| 4520 | + background: var(--canvas-subtle); | |
| 4521 | + color: var(--fg-default); | |
| 4522 | + font-size: 0.8rem; | |
| 4523 | +} | |
| 4524 | +.shithub-quick-dropdown { | |
| 4525 | + padding: 0.45rem 0; | |
| 4526 | +} | |
| 4527 | +.shithub-quick-section { | |
| 4528 | + padding: 0.25rem 0; | |
| 4529 | + border-bottom: 1px solid var(--border-default); | |
| 4530 | +} | |
| 4531 | +.shithub-quick-section:last-of-type { | |
| 4532 | + border-bottom: none; | |
| 4533 | +} | |
| 4534 | +.shithub-quick-section h3 { | |
| 4535 | + margin: 0.25rem 0.75rem 0.35rem; | |
| 4536 | + color: var(--fg-muted); | |
| 4537 | + font-size: 0.75rem; | |
| 4538 | + font-weight: 600; | |
| 4539 | +} | |
| 4540 | +.shithub-quick-section ul { | |
| 4541 | + padding: 0; | |
| 4542 | + margin: 0; | |
| 4543 | + list-style: none; | |
| 4544 | +} | |
| 4545 | +.shithub-quick-section a { | |
| 4546 | + display: grid; | |
| 4547 | + grid-template-columns: 20px minmax(0, 1fr) auto; | |
| 4548 | + gap: 0.55rem; | |
| 4549 | + align-items: center; | |
| 4550 | + padding: 0.4rem 0.75rem; | |
| 4551 | + color: var(--fg-default); | |
| 4552 | +} | |
| 4553 | +.shithub-quick-section a:hover, | |
| 4554 | +.shithub-quick-section a:focus { | |
| 4555 | + background: var(--canvas-subtle); | |
| 4556 | + text-decoration: none; | |
| 4557 | +} | |
| 4558 | +.shithub-quick-leading { | |
| 4559 | + display: inline-flex; | |
| 4560 | + align-items: center; | |
| 4561 | + justify-content: center; | |
| 4562 | + color: var(--fg-muted); | |
| 4563 | +} | |
| 4564 | +.shithub-quick-title, | |
| 4565 | +.shithub-quick-context { | |
| 4566 | + min-width: 0; | |
| 4567 | + overflow: hidden; | |
| 4568 | + text-overflow: ellipsis; | |
| 4569 | + white-space: nowrap; | |
| 4570 | +} | |
| 4571 | +.shithub-quick-context { | |
| 4572 | + grid-column: 2 / -1; | |
| 4573 | + margin-top: -0.15rem; | |
| 4574 | + color: var(--fg-muted); | |
| 4575 | + font-size: 0.75rem; | |
| 4576 | +} | |
| 4577 | +.shithub-quick-empty { | |
| 4578 | + margin: 0; | |
| 4579 | + padding: 0.8rem 0.75rem; | |
| 4580 | + color: var(--fg-muted); | |
| 4581 | + font-size: 0.875rem; | |
| 4582 | +} | |
| 4583 | +.shithub-quick-footer { | |
| 4584 | + padding: 0.6rem 0.75rem 0.2rem; | |
| 4585 | + border-top: 1px solid var(--border-default); | |
| 4586 | + font-size: 0.875rem; | |
| 4587 | +} | |
| 4409 | 4588 | @media (max-width: 760px) { |
| 4410 | 4589 | .shithub-nav-search { |
| 4411 | 4590 | order: 3; |
@@ -4421,8 +4600,12 @@ button.shithub-repo-action { | ||
| 4421 | 4600 | gap: 1rem; |
| 4422 | 4601 | } |
| 4423 | 4602 | .shithub-search-sidebar { |
| 4603 | + position: static; | |
| 4424 | 4604 | padding-top: 0; |
| 4425 | 4605 | } |
| 4606 | + .shithub-search-rightbar { | |
| 4607 | + display: none; | |
| 4608 | + } | |
| 4426 | 4609 | .shithub-search-filter-list { |
| 4427 | 4610 | flex-direction: row; |
| 4428 | 4611 | gap: 0.25rem; |
@@ -4445,6 +4628,14 @@ button.shithub-repo-action { | ||
| 4445 | 4628 | align-self: flex-start; |
| 4446 | 4629 | } |
| 4447 | 4630 | } |
| 4631 | +@media (min-width: 761px) and (max-width: 1100px) { | |
| 4632 | + .shithub-search-shell { | |
| 4633 | + grid-template-columns: 256px minmax(0, 1fr); | |
| 4634 | + } | |
| 4635 | + .shithub-search-rightbar { | |
| 4636 | + display: none; | |
| 4637 | + } | |
| 4638 | +} | |
| 4448 | 4639 | |
| 4449 | 4640 | /* S34 — admin impersonation banner. Sticky-top, loud red so the |
| 4450 | 4641 | admin can't lose track of "I am viewing as someone else right |
internal/web/templates/_layout.htmlmodified@@ -65,6 +65,92 @@ | ||
| 65 | 65 | }); |
| 66 | 66 | })(); |
| 67 | 67 | |
| 68 | + (function () { | |
| 69 | + var root = document.querySelector("[data-search-root]"); | |
| 70 | + if (!root) return; | |
| 71 | + var input = root.querySelector("[data-search-input]"); | |
| 72 | + var panel = root.querySelector("[data-search-results]"); | |
| 73 | + if (!input || !panel || !window.fetch) return; | |
| 74 | + | |
| 75 | + var timer = 0; | |
| 76 | + var controller = null; | |
| 77 | + | |
| 78 | + function closePanel() { | |
| 79 | + panel.hidden = true; | |
| 80 | + panel.innerHTML = ""; | |
| 81 | + input.setAttribute("aria-expanded", "false"); | |
| 82 | + if (controller) { | |
| 83 | + controller.abort(); | |
| 84 | + controller = null; | |
| 85 | + } | |
| 86 | + } | |
| 87 | + | |
| 88 | + function showPanel(html) { | |
| 89 | + panel.innerHTML = html; | |
| 90 | + panel.hidden = false; | |
| 91 | + input.setAttribute("aria-expanded", "true"); | |
| 92 | + } | |
| 93 | + | |
| 94 | + function loadSuggestions() { | |
| 95 | + var query = input.value.trim(); | |
| 96 | + if (query.length < 2) { | |
| 97 | + closePanel(); | |
| 98 | + return; | |
| 99 | + } | |
| 100 | + if (controller) controller.abort(); | |
| 101 | + controller = new AbortController(); | |
| 102 | + fetch("/search/quick?q=" + encodeURIComponent(query), { | |
| 103 | + headers: { "X-Requested-With": "XMLHttpRequest" }, | |
| 104 | + signal: controller.signal | |
| 105 | + }).then(function (response) { | |
| 106 | + if (response.status === 204) return ""; | |
| 107 | + if (!response.ok) throw new Error("search quick failed"); | |
| 108 | + return response.text(); | |
| 109 | + }).then(function (html) { | |
| 110 | + if (!html) { | |
| 111 | + closePanel(); | |
| 112 | + return; | |
| 113 | + } | |
| 114 | + showPanel(html); | |
| 115 | + }).catch(function (error) { | |
| 116 | + if (error.name !== "AbortError") closePanel(); | |
| 117 | + }); | |
| 118 | + } | |
| 119 | + | |
| 120 | + function queueSuggestions() { | |
| 121 | + window.clearTimeout(timer); | |
| 122 | + timer = window.setTimeout(loadSuggestions, 160); | |
| 123 | + } | |
| 124 | + | |
| 125 | + input.addEventListener("input", queueSuggestions); | |
| 126 | + input.addEventListener("focus", function () { | |
| 127 | + if (input.value.trim().length >= 2) queueSuggestions(); | |
| 128 | + }); | |
| 129 | + input.addEventListener("keydown", function (event) { | |
| 130 | + if (event.key === "Escape") { | |
| 131 | + closePanel(); | |
| 132 | + input.blur(); | |
| 133 | + } | |
| 134 | + }); | |
| 135 | + root.addEventListener("submit", closePanel); | |
| 136 | + document.addEventListener("click", function (event) { | |
| 137 | + if (!root.contains(event.target)) closePanel(); | |
| 138 | + }); | |
| 139 | + document.addEventListener("keydown", function (event) { | |
| 140 | + var active = document.activeElement; | |
| 141 | + var editable = active && ( | |
| 142 | + active.tagName === "INPUT" || | |
| 143 | + active.tagName === "TEXTAREA" || | |
| 144 | + active.isContentEditable | |
| 145 | + ); | |
| 146 | + if (event.key === "/" && !event.metaKey && !event.ctrlKey && !event.altKey && !editable) { | |
| 147 | + event.preventDefault(); | |
| 148 | + input.focus(); | |
| 149 | + input.select(); | |
| 150 | + } | |
| 151 | + }); | |
| 152 | + })(); | |
| 153 | + | |
| 68 | 154 | (function () { |
| 69 | 155 | var modal = document.querySelector("[data-pins-modal]"); |
| 70 | 156 | if (!modal) return; |
internal/web/templates/search/_quick_dropdown.htmlmodified@@ -1,40 +1,59 @@ | ||
| 1 | 1 | {{ define "page" -}} |
| 2 | -<div class="shithub-quick-dropdown" role="listbox"> | |
| 3 | - {{ if .Repos }} | |
| 4 | - <div class="shithub-quick-section"> | |
| 5 | - <h3>Repositories</h3> | |
| 6 | - <ul> | |
| 7 | - {{ range .Repos }} | |
| 8 | - <li> | |
| 9 | - <a href="/{{ .OwnerUsername }}/{{ .Name }}">{{ .OwnerUsername }}/{{ .Name }}</a> | |
| 10 | - {{ if eq .Visibility "private" }}<span class="shithub-pill shithub-pill-private">private</span>{{ end }} | |
| 11 | - </li> | |
| 12 | - {{ end }} | |
| 13 | - </ul> | |
| 14 | - </div> | |
| 15 | - {{ end }} | |
| 16 | - {{ if .Issues }} | |
| 17 | - <div class="shithub-quick-section"> | |
| 18 | - <h3>Issues</h3> | |
| 19 | - <ul> | |
| 20 | - {{ range .Issues }} | |
| 21 | - <li><a href="/{{ .OwnerUsername }}/{{ .RepoName }}/issues/{{ .Number }}">{{ .OwnerUsername }}/{{ .RepoName }}#{{ .Number }} — {{ .Title }}</a></li> | |
| 22 | - {{ end }} | |
| 23 | - </ul> | |
| 24 | - </div> | |
| 25 | - {{ end }} | |
| 26 | - {{ if .Users }} | |
| 27 | - <div class="shithub-quick-section"> | |
| 28 | - <h3>Users</h3> | |
| 29 | - <ul> | |
| 30 | - {{ range .Users }} | |
| 31 | - <li><a href="/{{ .Username }}">@{{ .Username }} — {{ .DisplayName }}</a></li> | |
| 32 | - {{ end }} | |
| 33 | - </ul> | |
| 34 | - </div> | |
| 2 | +<div class="shithub-quick-dropdown" role="listbox" aria-label="Search suggestions"> | |
| 3 | + {{ if or .Repos .Issues .Users }} | |
| 4 | + {{ if .Repos }} | |
| 5 | + <section class="shithub-quick-section" aria-labelledby="quick-repositories-heading"> | |
| 6 | + <h3 id="quick-repositories-heading">Repositories</h3> | |
| 7 | + <ul> | |
| 8 | + {{ range .Repos }} | |
| 9 | + <li> | |
| 10 | + <a href="/{{ .OwnerUsername }}/{{ .Name }}" role="option"> | |
| 11 | + <span class="shithub-quick-leading">{{ octicon "repo" }}</span> | |
| 12 | + <span class="shithub-quick-title">{{ .OwnerUsername }}/{{ .Name }}</span> | |
| 13 | + {{ if eq .Visibility "private" }}<span class="shithub-pill shithub-pill-private">Private</span>{{ end }} | |
| 14 | + </a> | |
| 15 | + </li> | |
| 16 | + {{ end }} | |
| 17 | + </ul> | |
| 18 | + </section> | |
| 19 | + {{ end }} | |
| 20 | + {{ if .Issues }} | |
| 21 | + <section class="shithub-quick-section" aria-labelledby="quick-issues-heading"> | |
| 22 | + <h3 id="quick-issues-heading">Issues and pull requests</h3> | |
| 23 | + <ul> | |
| 24 | + {{ range .Issues }} | |
| 25 | + <li> | |
| 26 | + <a href="/{{ .OwnerUsername }}/{{ .RepoName }}/{{ if eq .Kind "pr" }}pulls{{ else }}issues{{ end }}/{{ .Number }}" role="option"> | |
| 27 | + <span class="shithub-quick-leading">{{ if eq .Kind "pr" }}{{ octicon "git-pull-request" }}{{ else }}{{ octicon "issue-opened" }}{{ end }}</span> | |
| 28 | + <span class="shithub-quick-title">{{ .Title }}</span> | |
| 29 | + <span class="shithub-quick-context">{{ .OwnerUsername }}/{{ .RepoName }}#{{ .Number }}</span> | |
| 30 | + </a> | |
| 31 | + </li> | |
| 32 | + {{ end }} | |
| 33 | + </ul> | |
| 34 | + </section> | |
| 35 | + {{ end }} | |
| 36 | + {{ if .Users }} | |
| 37 | + <section class="shithub-quick-section" aria-labelledby="quick-users-heading"> | |
| 38 | + <h3 id="quick-users-heading">Users</h3> | |
| 39 | + <ul> | |
| 40 | + {{ range .Users }} | |
| 41 | + <li> | |
| 42 | + <a href="/{{ .Username }}" role="option"> | |
| 43 | + <img src="/avatars/{{ .Username }}" alt="" width="20" height="20" class="shithub-quick-avatar"> | |
| 44 | + <span class="shithub-quick-title">{{ if .DisplayName }}{{ .DisplayName }}{{ else }}{{ .Username }}{{ end }}</span> | |
| 45 | + <span class="shithub-quick-context">@{{ .Username }}</span> | |
| 46 | + </a> | |
| 47 | + </li> | |
| 48 | + {{ end }} | |
| 49 | + </ul> | |
| 50 | + </section> | |
| 51 | + {{ end }} | |
| 52 | + {{ else }} | |
| 53 | + <p class="shithub-quick-empty">No results found.</p> | |
| 35 | 54 | {{ end }} |
| 36 | 55 | <div class="shithub-quick-footer"> |
| 37 | - <a href="/search?q={{ .Query }}">See all results →</a> | |
| 56 | + <a href="{{ .SearchHref }}">See all results for <strong>{{ .Query }}</strong></a> | |
| 38 | 57 | </div> |
| 39 | 58 | </div> |
| 40 | 59 | {{- end }} |
internal/web/templates/search/results.htmlmodified@@ -2,12 +2,12 @@ | ||
| 2 | 2 | <section class="shithub-search-page"> |
| 3 | 3 | <div class="shithub-search-shell"> |
| 4 | 4 | <aside class="shithub-search-sidebar" aria-label="Search filters"> |
| 5 | - <h2>Filter by</h2> | |
| 6 | - <nav class="shithub-search-filter-list"> | |
| 5 | + <h2 id="search-filters-title">Filter by</h2> | |
| 6 | + <nav class="shithub-search-filter-list" aria-labelledby="search-filters-title"> | |
| 7 | 7 | {{ range .SearchTabs }} |
| 8 | 8 | <a href="{{ .Href }}" class="shithub-search-filter{{ if .Selected }} is-selected{{ end }}"{{ if .Selected }} aria-current="page"{{ end }}> |
| 9 | 9 | <span class="shithub-search-filter-label">{{ octicon .Icon }} {{ .Label }}</span> |
| 10 | - {{ if .Count }}<span class="shithub-search-filter-count">{{ .Count }}</span>{{ end }} | |
| 10 | + <span class="shithub-search-filter-count">{{ if .Count }}{{ .Count }}{{ else }}0{{ end }}</span> | |
| 11 | 11 | </a> |
| 12 | 12 | {{ end }} |
| 13 | 13 | </nav> |
@@ -15,7 +15,8 @@ | ||
| 15 | 15 | |
| 16 | 16 | <div class="shithub-search-results"> |
| 17 | 17 | <form action="/search" method="get" class="shithub-search-query-form"> |
| 18 | - <input type="text" name="q" value="{{ .Query }}" placeholder="Search shithub" autofocus> | |
| 18 | + <label class="sr-only" for="search-page-query">Search query</label> | |
| 19 | + <input id="search-page-query" type="text" name="q" value="{{ .Query }}" placeholder="Search shithub" autofocus> | |
| 19 | 20 | <input type="hidden" name="type" value="{{ .Tab }}"> |
| 20 | 21 | <button type="submit" class="shithub-button shithub-button-primary">Search</button> |
| 21 | 22 | </form> |
@@ -27,39 +28,33 @@ | ||
| 27 | 28 | </div> |
| 28 | 29 | {{ else }} |
| 29 | 30 | <div class="shithub-search-results-head"> |
| 30 | - <h1> | |
| 31 | - {{ .Total }} | |
| 32 | - {{ if eq .Tab "repos" }}repository{{ if ne .Total 1 }} results{{ else }} result{{ end }} | |
| 33 | - {{ else if eq .Tab "code" }}code result{{ if ne .Total 1 }}s{{ end }} | |
| 34 | - {{ else if eq .Tab "issues" }}issue result{{ if ne .Total 1 }}s{{ end }} | |
| 35 | - {{ else if eq .Tab "pulls" }}pull request result{{ if ne .Total 1 }}s{{ end }} | |
| 36 | - {{ else if eq .Tab "users" }}user result{{ if ne .Total 1 }}s{{ end }} | |
| 37 | - {{ else }}result{{ if ne .Total 1 }}s{{ end }} | |
| 38 | - {{ end }} | |
| 39 | - </h1> | |
| 31 | + <div> | |
| 32 | + <h1>{{ .ResultHeading }}</h1> | |
| 33 | + <p>for <span class="shithub-search-query-echo">{{ .Query }}</span></p> | |
| 34 | + </div> | |
| 40 | 35 | <details class="shithub-search-sort"> |
| 41 | 36 | <summary>Sort: Best match</summary> |
| 42 | - <div> | |
| 43 | - <span>Best match</span> | |
| 44 | - <span>Most recently updated</span> | |
| 37 | + <div role="menu"> | |
| 38 | + <span role="menuitem">Best match</span> | |
| 39 | + <span role="menuitem" aria-disabled="true">Most recently updated</span> | |
| 45 | 40 | </div> |
| 46 | 41 | </details> |
| 47 | 42 | </div> |
| 48 | 43 | |
| 49 | - {{ if eq .Tab "repos" }} | |
| 44 | + {{ if eq .Tab "repositories" }} | |
| 50 | 45 | {{ if .Repos }} |
| 51 | 46 | <ol class="shithub-search-result-list"> |
| 52 | 47 | {{ range .Repos }} |
| 53 | 48 | <li class="shithub-search-result shithub-search-repo-result"> |
| 54 | 49 | <div class="shithub-search-result-main"> |
| 55 | - <h2> | |
| 50 | + <h2 class="shithub-search-result-title"> | |
| 56 | 51 | <img src="/avatars/{{ .OwnerUsername }}" alt="" width="20" height="20" class="shithub-search-avatar"> |
| 57 | - <a href="/{{ .OwnerUsername }}/{{ .Name }}">{{ .OwnerUsername }}/{{ .Name }}</a> | |
| 52 | + <a href="/{{ .OwnerUsername }}/{{ .Name }}"><span class="shithub-search-match">{{ .OwnerUsername }}/{{ .Name }}</span></a> | |
| 58 | 53 | {{ if eq .Visibility "private" }}<span class="shithub-pill shithub-pill-private">Private</span>{{ end }} |
| 59 | 54 | </h2> |
| 60 | - {{ if .Description }}<p>{{ .Description }}</p>{{ end }} | |
| 55 | + {{ if .Description }}<p class="shithub-search-result-desc">{{ .Description }}</p>{{ end }} | |
| 61 | 56 | <ul class="shithub-search-result-meta"> |
| 62 | - <li>{{ octicon "star" }} {{ .StarCount }}</li> | |
| 57 | + <li><a href="/{{ .OwnerUsername }}/{{ .Name }}/stargazers">{{ octicon "star" }} {{ .StarCount }}</a></li> | |
| 63 | 58 | <li>Updated {{ relativeTime .UpdatedAt }}</li> |
| 64 | 59 | </ul> |
| 65 | 60 | </div> |
@@ -75,17 +70,18 @@ | ||
| 75 | 70 | {{ range .Issues }} |
| 76 | 71 | <li class="shithub-search-result"> |
| 77 | 72 | <div class="shithub-search-result-main"> |
| 78 | - <h2> | |
| 73 | + <p class="shithub-search-result-context"><a href="/{{ .OwnerUsername }}/{{ .RepoName }}">{{ .OwnerUsername }}/{{ .RepoName }}</a></p> | |
| 74 | + <h2 class="shithub-search-result-title"> | |
| 79 | 75 | <span class="shithub-search-state shithub-search-state-{{ .State }}"> |
| 80 | 76 | {{ if eq .State "closed" }}{{ octicon "issue-closed" }}{{ else }}{{ octicon "issue-opened" }}{{ end }} |
| 81 | 77 | </span> |
| 82 | - <a href="/{{ .OwnerUsername }}/{{ .RepoName }}/issues/{{ .Number }}">{{ .Title }}</a> | |
| 78 | + <a href="/{{ .OwnerUsername }}/{{ .RepoName }}/issues/{{ .Number }}"><span class="shithub-search-match">{{ .Title }}</span></a> | |
| 83 | 79 | </h2> |
| 84 | - <p class="shithub-search-result-path">{{ .OwnerUsername }}/{{ .RepoName }}#{{ .Number }}</p> | |
| 85 | 80 | <ul class="shithub-search-result-meta"> |
| 81 | + {{ if .AuthorName }}<li><img src="/avatars/{{ .AuthorName }}" alt="" width="16" height="16" class="shithub-search-mini-avatar"> {{ .AuthorName }}</li>{{ end }} | |
| 86 | 82 | <li>{{ .State }}</li> |
| 87 | - {{ if .AuthorName }}<li>Opened by @{{ .AuthorName }}</li>{{ end }} | |
| 88 | 83 | <li>Updated {{ relativeTime .UpdatedAt }}</li> |
| 84 | + <li><a href="/{{ .OwnerUsername }}/{{ .RepoName }}/issues/{{ .Number }}">#{{ .Number }}</a></li> | |
| 89 | 85 | </ul> |
| 90 | 86 | </div> |
| 91 | 87 | </li> |
@@ -93,21 +89,22 @@ | ||
| 93 | 89 | </ol> |
| 94 | 90 | {{ else }}<p class="shithub-search-empty">No issues matched your search.</p>{{ end }} |
| 95 | 91 | |
| 96 | - {{ else if eq .Tab "pulls" }} | |
| 92 | + {{ else if eq .Tab "pullrequests" }} | |
| 97 | 93 | {{ if .Issues }} |
| 98 | 94 | <ol class="shithub-search-result-list"> |
| 99 | 95 | {{ range .Issues }} |
| 100 | 96 | <li class="shithub-search-result"> |
| 101 | 97 | <div class="shithub-search-result-main"> |
| 102 | - <h2> | |
| 98 | + <p class="shithub-search-result-context"><a href="/{{ .OwnerUsername }}/{{ .RepoName }}">{{ .OwnerUsername }}/{{ .RepoName }}</a></p> | |
| 99 | + <h2 class="shithub-search-result-title"> | |
| 103 | 100 | <span class="shithub-search-state shithub-search-state-pr">{{ octicon "git-pull-request" }}</span> |
| 104 | - <a href="/{{ .OwnerUsername }}/{{ .RepoName }}/pulls/{{ .Number }}">{{ .Title }}</a> | |
| 101 | + <a href="/{{ .OwnerUsername }}/{{ .RepoName }}/pulls/{{ .Number }}"><span class="shithub-search-match">{{ .Title }}</span></a> | |
| 105 | 102 | </h2> |
| 106 | - <p class="shithub-search-result-path">{{ .OwnerUsername }}/{{ .RepoName }}#{{ .Number }}</p> | |
| 107 | 103 | <ul class="shithub-search-result-meta"> |
| 104 | + {{ if .AuthorName }}<li><img src="/avatars/{{ .AuthorName }}" alt="" width="16" height="16" class="shithub-search-mini-avatar"> {{ .AuthorName }}</li>{{ end }} | |
| 108 | 105 | <li>{{ .State }}</li> |
| 109 | - {{ if .AuthorName }}<li>Opened by @{{ .AuthorName }}</li>{{ end }} | |
| 110 | 106 | <li>Updated {{ relativeTime .UpdatedAt }}</li> |
| 107 | + <li><a href="/{{ .OwnerUsername }}/{{ .RepoName }}/pulls/{{ .Number }}">#{{ .Number }}</a></li> | |
| 111 | 108 | </ul> |
| 112 | 109 | </div> |
| 113 | 110 | </li> |
@@ -120,11 +117,13 @@ | ||
| 120 | 117 | <ol class="shithub-search-result-list"> |
| 121 | 118 | {{ range .Users }} |
| 122 | 119 | <li class="shithub-search-result shithub-search-user-result"> |
| 123 | - <img src="/avatars/{{ .Username }}" alt="" width="40" height="40" class="shithub-search-user-avatar"> | |
| 120 | + <img src="/avatars/{{ .Username }}" alt="" width="48" height="48" class="shithub-search-user-avatar"> | |
| 124 | 121 | <div class="shithub-search-result-main"> |
| 125 | - <h2><a href="/{{ .Username }}">{{ if .DisplayName }}{{ .DisplayName }}{{ else }}{{ .Username }}{{ end }}</a></h2> | |
| 126 | - <p class="shithub-search-result-path">@{{ .Username }}</p> | |
| 127 | - {{ if .Bio }}<p>{{ .Bio }}</p>{{ end }} | |
| 122 | + <h2 class="shithub-search-result-title"> | |
| 123 | + <a href="/{{ .Username }}">{{ if .DisplayName }}{{ .DisplayName }}{{ else }}{{ .Username }}{{ end }}</a> | |
| 124 | + <span class="shithub-search-user-login">{{ .Username }}</span> | |
| 125 | + </h2> | |
| 126 | + {{ if .Bio }}<p class="shithub-search-result-desc">{{ .Bio }}</p>{{ end }} | |
| 128 | 127 | </div> |
| 129 | 128 | </li> |
| 130 | 129 | {{ end }} |
@@ -137,11 +136,12 @@ | ||
| 137 | 136 | {{ range .Code }} |
| 138 | 137 | <li class="shithub-search-result"> |
| 139 | 138 | <div class="shithub-search-result-main"> |
| 140 | - <h2> | |
| 139 | + <p class="shithub-search-result-context"><a href="/{{ .OwnerUsername }}/{{ .RepoName }}">{{ .OwnerUsername }}/{{ .RepoName }}</a></p> | |
| 140 | + <h2 class="shithub-search-result-title"> | |
| 141 | 141 | {{ octicon "file" }} |
| 142 | - <a href="/{{ .OwnerUsername }}/{{ .RepoName }}/blob/{{ .RefName }}/{{ .Path }}">{{ .Path }}</a> | |
| 142 | + <a href="/{{ .OwnerUsername }}/{{ .RepoName }}/blob/{{ .RefName }}/{{ .Path }}"><span class="shithub-search-match">{{ .Path }}</span></a> | |
| 143 | 143 | </h2> |
| 144 | - <p class="shithub-search-result-path">{{ .OwnerUsername }}/{{ .RepoName }} on {{ .RefName }}</p> | |
| 144 | + <p class="shithub-search-result-path">on {{ .RefName }}</p> | |
| 145 | 145 | {{ if .PreviewLine }}<pre class="shithub-search-code-preview"><code>{{ .PreviewLine }}</code></pre>{{ end }} |
| 146 | 146 | </div> |
| 147 | 147 | </li> |
@@ -152,12 +152,27 @@ | ||
| 152 | 152 | |
| 153 | 153 | {{ if or .HasPrev .HasNext }} |
| 154 | 154 | <nav class="shithub-search-pagination" aria-label="Pagination"> |
| 155 | - {{ if .HasPrev }}<a href="?q={{ .Query }}&type={{ .Tab }}&p={{ sub .Page 1 }}" class="shithub-button">Previous</a>{{ else }}<span class="shithub-button shithub-button-disabled">Previous</span>{{ end }} | |
| 156 | - {{ if .HasNext }}<a href="?q={{ .Query }}&type={{ .Tab }}&p={{ add .Page 1 }}" class="shithub-button">Next</a>{{ else }}<span class="shithub-button shithub-button-disabled">Next</span>{{ end }} | |
| 155 | + {{ if .HasPrev }}<a href="{{ .PrevHref }}" class="shithub-button">Previous</a>{{ else }}<span class="shithub-button shithub-button-disabled" aria-disabled="true">Previous</span>{{ end }} | |
| 156 | + {{ if .HasNext }}<a href="{{ .NextHref }}" class="shithub-button">Next</a>{{ else }}<span class="shithub-button shithub-button-disabled" aria-disabled="true">Next</span>{{ end }} | |
| 157 | 157 | </nav> |
| 158 | 158 | {{ end }} |
| 159 | 159 | {{ end }} |
| 160 | 160 | </div> |
| 161 | + | |
| 162 | + <aside class="shithub-search-rightbar" aria-label="Search tips"> | |
| 163 | + <div class="shithub-search-tip-card"> | |
| 164 | + <strong>{{ octicon "search" }} ProTip!</strong> | |
| 165 | + <p>{{ .SearchProTip }}</p> | |
| 166 | + </div> | |
| 167 | + <div class="shithub-search-tip-card"> | |
| 168 | + <strong>Search syntax tips</strong> | |
| 169 | + <ul> | |
| 170 | + <li><code>repo:owner/name</code></li> | |
| 171 | + <li><code>is:open</code></li> | |
| 172 | + <li><code>author:username</code></li> | |
| 173 | + </ul> | |
| 174 | + </div> | |
| 175 | + </aside> | |
| 161 | 176 | </div> |
| 162 | 177 | </section> |
| 163 | 178 | {{- end }} |