tenseleyflow/shithub / 5ad7821

Browse files

Align search results UI

Authored by espadonne
SHA
5ad7821416da0632cc1b17dd0a2addb535e09b0f
Parents
128ce28
Tree
e6d38c5

6 changed files

StatusFile+-
M internal/web/handlers/search/search.go 108 7
A internal/web/handlers/search/search_test.go 30 0
M internal/web/render/octicons.go 2 0
M internal/web/static/css/shithub.css 319 15
M internal/web/templates/_nav.html 3 1
M internal/web/templates/search/results.html 154 83
internal/web/handlers/search/search.gomodified
@@ -9,6 +9,7 @@ import (
99
 	"errors"
1010
 	"log/slog"
1111
 	"net/http"
12
+	"net/url"
1213
 
1314
 	"github.com/go-chi/chi/v5"
1415
 	"github.com/jackc/pgx/v5/pgxpool"
@@ -67,6 +68,9 @@ func (h *Handlers) results(w http.ResponseWriter, r *http.Request) {
6768
 	if tab == "" {
6869
 		tab = "repos"
6970
 	}
71
+	if !validSearchTab(tab) {
72
+		tab = "repos"
73
+	}
7074
 	page := pageFromRequest(r)
7175
 
7276
 	parsed := srch.ParseQuery(rawQ)
@@ -74,16 +78,18 @@ func (h *Handlers) results(w http.ResponseWriter, r *http.Request) {
7478
 	deps := h.deps()
7579
 
7680
 	data := map[string]any{
77
-		"Title":    "Search",
78
-		"Query":    rawQ,
79
-		"Tab":      tab,
80
-		"Page":     page,
81
-		"Parsed":   parsed,
82
-		"PageSize": srch.PageSize,
81
+		"Title":             "Search",
82
+		"Query":             rawQ,
83
+		"GlobalSearchQuery": rawQ,
84
+		"Tab":               tab,
85
+		"Page":              page,
86
+		"Parsed":            parsed,
87
+		"PageSize":          srch.PageSize,
8388
 	}
8489
 
8590
 	if !parsed.HasContent() {
8691
 		data["EmptyQuery"] = true
92
+		data["SearchTabs"] = h.searchTabs(r, actor, parsed, rawQ, tab)
8793
 		_ = h.d.Render.RenderPage(w, r, "search/results", data)
8894
 		return
8995
 	}
@@ -106,6 +112,14 @@ func (h *Handlers) results(w http.ResponseWriter, r *http.Request) {
106112
 		data["Issues"] = rows
107113
 		data["Total"] = total
108114
 		data["HasNext"] = int64(page*srch.PageSize) < total
115
+	case "pulls":
116
+		rows, total, err := srch.SearchIssues(r.Context(), deps, actor, parsed, "pr", srch.PageSize, offset)
117
+		if err != nil && !errors.Is(err, srch.ErrEmptyQuery) {
118
+			h.d.Logger.ErrorContext(r.Context(), "search pulls", "error", err)
119
+		}
120
+		data["Issues"] = rows
121
+		data["Total"] = total
122
+		data["HasNext"] = int64(page*srch.PageSize) < total
109123
 	case "users":
110124
 		rows, total, err := srch.SearchUsers(r.Context(), deps, parsed, srch.PageSize, offset)
111125
 		if err != nil && !errors.Is(err, srch.ErrEmptyQuery) {
@@ -129,12 +143,82 @@ func (h *Handlers) results(w http.ResponseWriter, r *http.Request) {
129143
 		data["EmptyQuery"] = true
130144
 	}
131145
 	data["HasPrev"] = page > 1
146
+	data["SearchTabs"] = h.searchTabs(r, actor, parsed, rawQ, tab)
132147
 
133148
 	if err := h.d.Render.RenderPage(w, r, "search/results", data); err != nil {
134149
 		h.d.Logger.ErrorContext(r.Context(), "search render", "error", err)
135150
 	}
136151
 }
137152
 
153
+func validSearchTab(tab string) bool {
154
+	switch tab {
155
+	case "repos", "issues", "pulls", "users", "code":
156
+		return true
157
+	default:
158
+		return false
159
+	}
160
+}
161
+
162
+type searchTab struct {
163
+	Key      string
164
+	Label    string
165
+	Icon     string
166
+	Count    int64
167
+	Href     string
168
+	Selected bool
169
+}
170
+
171
+func (h *Handlers) searchTabs(r *http.Request, actor policy.Actor, parsed srch.ParsedQuery, rawQ, active string) []searchTab {
172
+	tabs := []searchTab{
173
+		{Key: "repos", Label: "Repositories", Icon: "repo"},
174
+		{Key: "code", Label: "Code", Icon: "code"},
175
+		{Key: "issues", Label: "Issues", Icon: "issue-opened"},
176
+		{Key: "pulls", Label: "Pull requests", Icon: "git-pull-request"},
177
+		{Key: "users", Label: "Users", Icon: "person"},
178
+	}
179
+	for i := range tabs {
180
+		tabs[i].Selected = tabs[i].Key == active
181
+		tabs[i].Href = searchHref(rawQ, tabs[i].Key, 1)
182
+	}
183
+	if !parsed.HasContent() {
184
+		return tabs
185
+	}
186
+
187
+	deps := h.deps()
188
+	for i := range tabs {
189
+		var total int64
190
+		var err error
191
+		switch tabs[i].Key {
192
+		case "repos":
193
+			_, total, err = srch.SearchRepos(r.Context(), deps, actor, parsed, 0, 0)
194
+		case "code":
195
+			_, total, err = srch.SearchCode(r.Context(), deps, actor, parsed, 0, 0)
196
+		case "issues":
197
+			_, total, err = srch.SearchIssues(r.Context(), deps, actor, parsed, "issue", 0, 0)
198
+		case "pulls":
199
+			_, total, err = srch.SearchIssues(r.Context(), deps, actor, parsed, "pr", 0, 0)
200
+		case "users":
201
+			_, total, err = srch.SearchUsers(r.Context(), deps, parsed, 0, 0)
202
+		}
203
+		if err != nil && !errors.Is(err, srch.ErrEmptyQuery) {
204
+			h.d.Logger.ErrorContext(r.Context(), "search tab count", "tab", tabs[i].Key, "error", err)
205
+			continue
206
+		}
207
+		tabs[i].Count = total
208
+	}
209
+	return tabs
210
+}
211
+
212
+func searchHref(q, tab string, page int) string {
213
+	v := url.Values{}
214
+	v.Set("q", q)
215
+	v.Set("type", tab)
216
+	if page > 1 {
217
+		v.Set("p", intString(page))
218
+	}
219
+	return "/search?" + v.Encode()
220
+}
221
+
138222
 // quick is the htmx dropdown endpoint. Returns one fragment with
139223
 // the top N results across all four types stacked vertically.
140224
 func (h *Handlers) quick(w http.ResponseWriter, r *http.Request) {
@@ -164,7 +248,10 @@ func (h *Handlers) quick(w http.ResponseWriter, r *http.Request) {
164248
 
165249
 // pageFromRequest pulls ?page=N, defaulting to 1 on missing/invalid.
166250
 func pageFromRequest(r *http.Request) int {
167
-	p := r.URL.Query().Get("page")
251
+	p := r.URL.Query().Get("p")
252
+	if p == "" {
253
+		p = r.URL.Query().Get("page")
254
+	}
168255
 	if p == "" {
169256
 		return 1
170257
 	}
@@ -183,3 +270,17 @@ func pageFromRequest(r *http.Request) int {
183270
 	}
184271
 	return n
185272
 }
273
+
274
+func intString(n int) string {
275
+	if n == 0 {
276
+		return "0"
277
+	}
278
+	var buf [20]byte
279
+	i := len(buf)
280
+	for n > 0 {
281
+		i--
282
+		buf[i] = byte('0' + n%10)
283
+		n /= 10
284
+	}
285
+	return string(buf[i:])
286
+}
internal/web/handlers/search/search_test.goadded
@@ -0,0 +1,30 @@
1
+// SPDX-License-Identifier: AGPL-3.0-or-later
2
+
3
+package search
4
+
5
+import (
6
+	"net/http/httptest"
7
+	"testing"
8
+)
9
+
10
+func TestPageFromRequestUsesGitHubStylePParam(t *testing.T) {
11
+	req := httptest.NewRequest("GET", "/search?q=repo&p=3", nil)
12
+	if got := pageFromRequest(req); got != 3 {
13
+		t.Fatalf("pageFromRequest = %d, want 3", got)
14
+	}
15
+}
16
+
17
+func TestPageFromRequestAcceptsLegacyPageParam(t *testing.T) {
18
+	req := httptest.NewRequest("GET", "/search?q=repo&page=2", nil)
19
+	if got := pageFromRequest(req); got != 2 {
20
+		t.Fatalf("pageFromRequest = %d, want 2", got)
21
+	}
22
+}
23
+
24
+func TestSearchHrefEscapesQuery(t *testing.T) {
25
+	got := searchHref("repo:alice/demo pub", "issues", 4)
26
+	want := "/search?p=4&q=repo%3Aalice%2Fdemo+pub&type=issues"
27
+	if got != want {
28
+		t.Fatalf("searchHref = %q, want %q", got, want)
29
+	}
30
+}
internal/web/render/octicons.gomodified
@@ -31,6 +31,8 @@ func BuiltinOcticons() OcticonResolver {
3131
 			`><path d="M9.598 1.591a.75.75 0 0 1 .785-.175 7 7 0 1 1-8.967 8.967.75.75 0 0 1 .961-.96 5.5 5.5 0 0 0 7.046-7.046.75.75 0 0 1 .175-.786z"/></svg>`),
3232
 		"alert": trustedSVG(`<svg xmlns="http://www.w3.org/2000/svg" ` + cls +
3333
 			`><path d="M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575zM8 5a.75.75 0 0 0-.75.75v2.5a.75.75 0 0 0 1.5 0v-2.5A.75.75 0 0 0 8 5zm1 6a1 1 0 1 1-2 0 1 1 0 0 1 2 0z"/></svg>`),
34
+		"search": trustedSVG(`<svg xmlns="http://www.w3.org/2000/svg" ` + cls +
35
+			`><path d="M10.68 11.74a6 6 0 1 1 1.06-1.06l3.04 3.04a.749.749 0 1 1-1.06 1.06ZM6.5 11.5a5 5 0 1 0 0-10 5 5 0 0 0 0 10Z"/></svg>`),
3436
 		"repo": trustedSVG(`<svg xmlns="http://www.w3.org/2000/svg" ` + cls +
3537
 			`><path d="M2 2.5A2.5 2.5 0 0 1 4.5 0h8.75A.75.75 0 0 1 14 .75v12.5a.75.75 0 0 1-.75.75H4.5a1 1 0 0 0 0 2h8.75a.75.75 0 0 1 0 1.5H4.5A2.5 2.5 0 0 1 2 15V2.5Zm2.5-1A1 1 0 0 0 3.5 2.5v10.21c.31-.13.648-.21 1-.21h8V1.5Z"/></svg>`),
3638
 		"code": trustedSVG(`<svg xmlns="http://www.w3.org/2000/svg" ` + cls +
internal/web/static/css/shithub.cssmodified
@@ -206,6 +206,8 @@ code {
206206
 .shithub-button {
207207
   display: inline-flex;
208208
   align-items: center;
209
+  justify-content: center;
210
+  gap: 0.35rem;
209211
   padding: 0.4rem 0.85rem;
210212
   border-radius: 6px;
211213
   font-size: 0.875rem;
@@ -213,6 +215,10 @@ code {
213215
   border: 1px solid transparent;
214216
   cursor: pointer;
215217
 }
218
+.shithub-button-small {
219
+  padding: 0.25rem 0.7rem;
220
+  font-size: 0.75rem;
221
+}
216222
 .shithub-button-ghost {
217223
   color: var(--fg-default);
218224
   border-color: var(--border-default);
@@ -1947,34 +1953,293 @@ code {
19471953
 
19481954
 /* S28 — search */
19491955
 .shithub-nav-search {
1950
-  flex: 1;
1956
+  position: relative;
1957
+  flex: 1 1 22rem;
19511958
   display: flex;
1959
+  align-items: center;
19521960
   max-width: 32rem;
19531961
   margin: 0 1rem;
19541962
 }
1963
+.shithub-nav-search-icon {
1964
+  position: absolute;
1965
+  left: 0.65rem;
1966
+  top: 50%;
1967
+  transform: translateY(-50%);
1968
+  color: var(--fg-muted);
1969
+  line-height: 0;
1970
+  pointer-events: none;
1971
+}
19551972
 .shithub-nav-search input {
19561973
   width: 100%;
1957
-  padding: 0.4rem 0.7rem;
1974
+  min-height: 2rem;
1975
+  padding: 0.35rem 2rem 0.35rem 2rem;
19581976
   border: 1px solid var(--border-default);
19591977
   border-radius: 6px;
1960
-  background: var(--canvas-subtle);
1978
+  background: var(--canvas-default);
19611979
   color: var(--fg-default);
19621980
   font-size: 0.85rem;
19631981
 }
1964
-.shithub-search { padding: 1rem 0; }
1965
-.shithub-search-head { padding-bottom: 0.5rem; border-bottom: 1px solid var(--border-default); margin-bottom: 1rem; }
1966
-.shithub-search-form { display: flex; gap: 0.5rem; margin: 0.5rem 0; }
1967
-.shithub-search-form input[type=text] { flex: 1; padding: 0.5rem 0.7rem; }
1968
-.shithub-search-tabs { display: flex; gap: 0.5rem; margin: 0.5rem 0; }
1969
-.shithub-search-list { list-style: none; padding: 0; margin: 0.5rem 0; }
1970
-.shithub-search-list li {
1971
-  display: flex; gap: 0.5rem; align-items: baseline;
1972
-  flex-wrap: wrap;
1973
-  padding: 0.5rem 0;
1982
+.shithub-nav-search-key {
1983
+  position: absolute;
1984
+  right: 0.55rem;
1985
+  top: 50%;
1986
+  transform: translateY(-50%);
1987
+  min-width: 1rem;
1988
+  padding: 0 0.25rem;
1989
+  border: 1px solid var(--border-default);
1990
+  border-radius: 4px;
1991
+  color: var(--fg-muted);
1992
+  font-size: 0.75rem;
1993
+  line-height: 1.15rem;
1994
+  text-align: center;
1995
+  pointer-events: none;
1996
+}
1997
+.shithub-search-page {
1998
+  padding: 1.5rem 1rem 3rem;
1999
+}
2000
+.shithub-search-shell {
2001
+  display: grid;
2002
+  grid-template-columns: 220px minmax(0, 1fr);
2003
+  gap: 2rem;
2004
+  max-width: 1280px;
2005
+  margin: 0 auto;
2006
+}
2007
+.shithub-search-sidebar {
2008
+  padding-top: 3.35rem;
2009
+}
2010
+.shithub-search-sidebar h2 {
2011
+  margin: 0 0 0.55rem;
2012
+  font-size: 0.875rem;
2013
+  font-weight: 600;
2014
+}
2015
+.shithub-search-filter-list {
2016
+  display: flex;
2017
+  flex-direction: column;
2018
+}
2019
+.shithub-search-filter {
2020
+  display: flex;
2021
+  align-items: center;
2022
+  justify-content: space-between;
2023
+  gap: 0.75rem;
2024
+  min-height: 2.25rem;
2025
+  padding: 0.4rem 0.65rem;
2026
+  border-radius: 6px;
2027
+  color: var(--fg-default);
2028
+  font-size: 0.875rem;
2029
+}
2030
+.shithub-search-filter:hover {
2031
+  background: var(--canvas-subtle);
2032
+  text-decoration: none;
2033
+}
2034
+.shithub-search-filter.is-selected {
2035
+  background: var(--canvas-subtle);
2036
+  font-weight: 600;
2037
+}
2038
+.shithub-search-filter.is-selected::before {
2039
+  content: "";
2040
+  width: 4px;
2041
+  align-self: stretch;
2042
+  margin: -0.4rem 0 -0.4rem -0.65rem;
2043
+  border-radius: 6px 0 0 6px;
2044
+  background: var(--accent-emphasis);
2045
+}
2046
+.shithub-search-filter-label {
2047
+  display: inline-flex;
2048
+  align-items: center;
2049
+  gap: 0.45rem;
2050
+  min-width: 0;
2051
+}
2052
+.shithub-search-filter-label svg {
2053
+  color: var(--fg-muted);
2054
+  flex: 0 0 auto;
2055
+}
2056
+.shithub-search-filter-count {
2057
+  color: var(--fg-muted);
2058
+  font-size: 0.75rem;
2059
+}
2060
+.shithub-search-results {
2061
+  min-width: 0;
2062
+}
2063
+.shithub-search-query-form {
2064
+  display: flex;
2065
+  gap: 0.5rem;
2066
+  margin-bottom: 1.25rem;
2067
+}
2068
+.shithub-search-query-form input[type=text] {
2069
+  flex: 1;
2070
+  min-height: 2.35rem;
2071
+  padding: 0.45rem 0.75rem;
2072
+  border: 1px solid var(--border-default);
2073
+  border-radius: 6px;
2074
+  background: var(--canvas-default);
2075
+  color: var(--fg-default);
2076
+  font-size: 0.875rem;
2077
+}
2078
+.shithub-search-results-head {
2079
+  display: flex;
2080
+  align-items: center;
2081
+  justify-content: space-between;
2082
+  gap: 1rem;
2083
+  padding-bottom: 0.9rem;
2084
+  border-bottom: 1px solid var(--border-default);
2085
+}
2086
+.shithub-search-results-head h1 {
2087
+  margin: 0;
2088
+  font-size: 1.25rem;
2089
+  line-height: 1.3;
2090
+}
2091
+.shithub-search-sort {
2092
+  position: relative;
2093
+  flex: 0 0 auto;
2094
+}
2095
+.shithub-search-sort > summary {
2096
+  list-style: none;
2097
+  display: inline-flex;
2098
+  align-items: center;
2099
+  min-height: 2rem;
2100
+  padding: 0.35rem 0.75rem;
2101
+  border: 1px solid var(--border-default);
2102
+  border-radius: 6px;
2103
+  background: var(--canvas-subtle);
2104
+  color: var(--fg-default);
2105
+  cursor: pointer;
2106
+  font-size: 0.875rem;
2107
+  font-weight: 500;
2108
+}
2109
+.shithub-search-sort > summary::-webkit-details-marker { display: none; }
2110
+.shithub-search-sort > div {
2111
+  position: absolute;
2112
+  right: 0;
2113
+  top: calc(100% + 0.4rem);
2114
+  z-index: 20;
2115
+  min-width: 190px;
2116
+  padding: 0.35rem 0;
2117
+  border: 1px solid var(--border-default);
2118
+  border-radius: 6px;
2119
+  background: var(--canvas-default);
2120
+  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
2121
+}
2122
+.shithub-search-sort span {
2123
+  display: block;
2124
+  padding: 0.45rem 0.75rem;
2125
+  font-size: 0.875rem;
2126
+}
2127
+.shithub-search-result-list {
2128
+  list-style: none;
2129
+  padding: 0;
2130
+  margin: 0;
2131
+}
2132
+.shithub-search-result {
2133
+  display: flex;
2134
+  gap: 0.75rem;
2135
+  justify-content: space-between;
2136
+  padding: 1rem 0;
19742137
   border-bottom: 1px solid var(--border-default);
19752138
 }
1976
-.shithub-search-list li:last-child { border-bottom: none; }
1977
-.shithub-search-list small { color: var(--fg-muted); }
2139
+.shithub-search-result-main {
2140
+  min-width: 0;
2141
+}
2142
+.shithub-search-result h2 {
2143
+  display: flex;
2144
+  align-items: center;
2145
+  gap: 0.45rem;
2146
+  margin: 0;
2147
+  font-size: 1rem;
2148
+  line-height: 1.35;
2149
+  font-weight: 600;
2150
+}
2151
+.shithub-search-result h2 svg {
2152
+  flex: 0 0 auto;
2153
+  color: var(--fg-muted);
2154
+}
2155
+.shithub-search-result p {
2156
+  margin: 0.35rem 0 0;
2157
+  color: var(--fg-default);
2158
+  font-size: 0.875rem;
2159
+}
2160
+.shithub-search-result-path {
2161
+  color: var(--fg-muted) !important;
2162
+}
2163
+.shithub-search-avatar,
2164
+.shithub-search-user-avatar {
2165
+  border-radius: 50%;
2166
+  background: var(--canvas-subtle);
2167
+  flex: 0 0 auto;
2168
+}
2169
+.shithub-search-result-meta {
2170
+  display: flex;
2171
+  gap: 0.5rem;
2172
+  flex-wrap: wrap;
2173
+  align-items: center;
2174
+  padding: 0;
2175
+  margin: 0.65rem 0 0;
2176
+  color: var(--fg-muted);
2177
+  list-style: none;
2178
+  font-size: 0.75rem;
2179
+}
2180
+.shithub-search-result-meta li {
2181
+  display: inline-flex;
2182
+  align-items: center;
2183
+  gap: 0.25rem;
2184
+}
2185
+.shithub-search-result-meta li + li::before {
2186
+  content: "";
2187
+  width: 3px;
2188
+  height: 3px;
2189
+  margin-right: 0.15rem;
2190
+  border-radius: 50%;
2191
+  background: var(--fg-muted);
2192
+  opacity: 0.7;
2193
+}
2194
+.shithub-search-star {
2195
+  align-self: flex-start;
2196
+  color: var(--fg-default);
2197
+  border-color: var(--border-default);
2198
+  background: var(--canvas-subtle);
2199
+}
2200
+.shithub-search-state {
2201
+  display: inline-flex;
2202
+  align-items: center;
2203
+  line-height: 0;
2204
+}
2205
+.shithub-search-state-open svg { color: var(--success-fg); }
2206
+.shithub-search-state-closed svg { color: #8250df; }
2207
+.shithub-search-state-pr svg { color: var(--accent-fg); }
2208
+.shithub-search-user-result {
2209
+  justify-content: flex-start;
2210
+}
2211
+.shithub-search-code-preview {
2212
+  margin: 0.75rem 0 0;
2213
+  padding: 0.65rem 0.75rem;
2214
+  border: 1px solid var(--border-default);
2215
+  border-radius: 6px;
2216
+  background: var(--canvas-subtle);
2217
+  color: var(--fg-default);
2218
+  overflow-x: auto;
2219
+}
2220
+.shithub-search-code-preview code {
2221
+  padding: 0;
2222
+  background: transparent;
2223
+}
2224
+.shithub-search-empty,
2225
+.shithub-search-blank {
2226
+  padding: 2rem 0;
2227
+  color: var(--fg-muted);
2228
+}
2229
+.shithub-search-blank h1 {
2230
+  margin: 0 0 0.4rem;
2231
+  color: var(--fg-default);
2232
+  font-size: 1.25rem;
2233
+}
2234
+.shithub-search-blank p {
2235
+  margin: 0;
2236
+}
2237
+.shithub-search-pagination {
2238
+  display: flex;
2239
+  justify-content: center;
2240
+  gap: 0.5rem;
2241
+  padding: 1.5rem 0 0;
2242
+}
19782243
 .shithub-quick-dropdown { padding: 0.5rem; }
19792244
 .shithub-quick-section { padding: 0.25rem 0; border-bottom: 1px solid var(--border-default); }
19802245
 .shithub-quick-section:last-of-type { border-bottom: none; }
@@ -1982,6 +2247,45 @@ code {
19822247
 .shithub-quick-section ul { list-style: none; padding: 0; margin: 0; }
19832248
 .shithub-quick-section li { padding: 0.25rem 0; }
19842249
 .shithub-quick-footer { padding: 0.5rem 0; border-top: 1px solid var(--border-default); text-align: center; }
2250
+@media (max-width: 760px) {
2251
+  .shithub-nav-search {
2252
+    order: 3;
2253
+    flex-basis: 100%;
2254
+    max-width: none;
2255
+    margin: 0;
2256
+  }
2257
+  .shithub-search-page {
2258
+    padding: 1rem;
2259
+  }
2260
+  .shithub-search-shell {
2261
+    grid-template-columns: 1fr;
2262
+    gap: 1rem;
2263
+  }
2264
+  .shithub-search-sidebar {
2265
+    padding-top: 0;
2266
+  }
2267
+  .shithub-search-filter-list {
2268
+    flex-direction: row;
2269
+    gap: 0.25rem;
2270
+    overflow-x: auto;
2271
+    padding-bottom: 0.25rem;
2272
+  }
2273
+  .shithub-search-filter {
2274
+    flex: 0 0 auto;
2275
+  }
2276
+  .shithub-search-filter.is-selected::before {
2277
+    display: none;
2278
+  }
2279
+  .shithub-search-query-form,
2280
+  .shithub-search-results-head,
2281
+  .shithub-search-result {
2282
+    align-items: stretch;
2283
+    flex-direction: column;
2284
+  }
2285
+  .shithub-search-star {
2286
+    align-self: flex-start;
2287
+  }
2288
+}
19852289
 
19862290
 /* S34 — admin impersonation banner. Sticky-top, loud red so the
19872291
    admin can't lose track of "I am viewing as someone else right
internal/web/templates/_nav.htmlmodified
@@ -5,7 +5,9 @@
55
     <span>shithub</span>
66
   </a>
77
   <form action="/search" method="get" class="shithub-nav-search" role="search">
8
-    <input type="text" name="q" placeholder="Search…" aria-label="Search" autocomplete="off">
8
+    <span class="shithub-nav-search-icon">{{ octicon "search" }}</span>
9
+    <input type="text" name="q"{{ with .GlobalSearchQuery }} value="{{ . }}"{{ end }} placeholder="Type / to search" aria-label="Search" autocomplete="off">
10
+    <span class="shithub-nav-search-key" aria-hidden="true">/</span>
911
     <input type="hidden" name="type" value="repos">
1012
   </form>
1113
   <nav class="shithub-nav-links" aria-label="Primary">
internal/web/templates/search/results.htmlmodified
@@ -1,92 +1,163 @@
11
 {{ define "page" -}}
2
-<section class="shithub-search">
3
-  <header class="shithub-search-head">
4
-    <h1>Search</h1>
5
-    <form action="/search" method="get" class="shithub-search-form">
6
-      <input type="text" name="q" value="{{ .Query }}" placeholder="search repositories, issues, users, code…" autofocus>
7
-      <input type="hidden" name="type" value="{{ .Tab }}">
8
-      <button type="submit" class="shithub-button shithub-button-primary">Search</button>
9
-    </form>
10
-    <nav class="shithub-search-tabs">
11
-      <a href="?q={{ .Query }}&type=repos" class="shithub-button {{ if eq .Tab "repos" }}shithub-button-primary{{ end }}">Repositories</a>
12
-      <a href="?q={{ .Query }}&type=issues" class="shithub-button {{ if eq .Tab "issues" }}shithub-button-primary{{ end }}">Issues</a>
13
-      <a href="?q={{ .Query }}&type=users" class="shithub-button {{ if eq .Tab "users" }}shithub-button-primary{{ end }}">Users</a>
14
-      <a href="?q={{ .Query }}&type=code" class="shithub-button {{ if eq .Tab "code" }}shithub-button-primary{{ end }}">Code</a>
15
-    </nav>
16
-  </header>
17
-
18
-  {{ if .EmptyQuery }}
19
-  <p class="shithub-empty">Type a query to search.</p>
20
-  {{ else }}
21
-    {{ if eq .Tab "repos" }}
22
-      {{ if .Repos }}
23
-        <p class="shithub-meta">{{ .Total }} repository result{{ if ne .Total 1 }}s{{ end }}</p>
24
-        <ul class="shithub-search-list">
25
-        {{ range .Repos }}
26
-          <li>
27
-            <a href="/{{ .OwnerUsername }}/{{ .Name }}"><strong>{{ .OwnerUsername }}/{{ .Name }}</strong></a>
28
-            {{ if eq .Visibility "private" }}<span class="shithub-pill shithub-pill-private">private</span>{{ end }}
29
-            <small>★ {{ .StarCount }}</small>
30
-            <small>updated {{ relativeTime .UpdatedAt }}</small>
31
-            {{ if .Description }}<p class="shithub-meta">{{ .Description }}</p>{{ end }}
32
-          </li>
2
+<section class="shithub-search-page">
3
+  <div class="shithub-search-shell">
4
+    <aside class="shithub-search-sidebar" aria-label="Search filters">
5
+      <h2>Filter by</h2>
6
+      <nav class="shithub-search-filter-list">
7
+        {{ range .SearchTabs }}
8
+          <a href="{{ .Href }}" class="shithub-search-filter{{ if .Selected }} is-selected{{ end }}"{{ if .Selected }} aria-current="page"{{ end }}>
9
+            <span class="shithub-search-filter-label">{{ octicon .Icon }} {{ .Label }}</span>
10
+            {{ if .Count }}<span class="shithub-search-filter-count">{{ .Count }}</span>{{ end }}
11
+          </a>
3312
         {{ end }}
34
-        </ul>
35
-      {{ else }}<p class="shithub-empty">No repositories matched.</p>{{ end }}
13
+      </nav>
14
+    </aside>
3615
 
37
-    {{ else if eq .Tab "issues" }}
38
-      {{ if .Issues }}
39
-        <p class="shithub-meta">{{ .Total }} issue result{{ if ne .Total 1 }}s{{ end }}</p>
40
-        <ul class="shithub-search-list">
41
-        {{ range .Issues }}
42
-          <li>
43
-            <a href="/{{ .OwnerUsername }}/{{ .RepoName }}/issues/{{ .Number }}">
44
-              <strong>{{ .OwnerUsername }}/{{ .RepoName }}#{{ .Number }}</strong> — {{ .Title }}
45
-            </a>
46
-            <span class="shithub-pill">{{ .State }}</span>
47
-            {{ if .AuthorName }}<small>by @{{ .AuthorName }}</small>{{ end }}
48
-            <small>{{ relativeTime .UpdatedAt }}</small>
49
-          </li>
50
-        {{ end }}
51
-        </ul>
52
-      {{ else }}<p class="shithub-empty">No issues matched.</p>{{ end }}
16
+    <div class="shithub-search-results">
17
+      <form action="/search" method="get" class="shithub-search-query-form">
18
+        <input type="text" name="q" value="{{ .Query }}" placeholder="Search shithub" autofocus>
19
+        <input type="hidden" name="type" value="{{ .Tab }}">
20
+        <button type="submit" class="shithub-button shithub-button-primary">Search</button>
21
+      </form>
5322
 
54
-    {{ else if eq .Tab "users" }}
55
-      {{ if .Users }}
56
-        <p class="shithub-meta">{{ .Total }} user result{{ if ne .Total 1 }}s{{ end }}</p>
57
-        <ul class="shithub-search-list">
58
-        {{ range .Users }}
59
-          <li>
60
-            <a href="/{{ .Username }}"><strong>{{ if .DisplayName }}{{ .DisplayName }}{{ else }}{{ .Username }}{{ end }}</strong></a>
61
-            <small>@{{ .Username }}</small>
62
-            {{ if .Bio }}<p class="shithub-meta">{{ .Bio }}</p>{{ end }}
63
-          </li>
64
-        {{ end }}
65
-        </ul>
66
-      {{ else }}<p class="shithub-empty">No users matched.</p>{{ end }}
23
+      {{ if .EmptyQuery }}
24
+        <div class="shithub-search-blank">
25
+          <h1>Search shithub</h1>
26
+          <p>Enter a query above to find repositories, code, issues, pull requests, and users.</p>
27
+        </div>
28
+      {{ else }}
29
+        <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>
40
+          <details class="shithub-search-sort">
41
+            <summary>Sort: Best match</summary>
42
+            <div>
43
+              <span>Best match</span>
44
+              <span>Most recently updated</span>
45
+            </div>
46
+          </details>
47
+        </div>
48
+
49
+        {{ if eq .Tab "repos" }}
50
+          {{ if .Repos }}
51
+            <ol class="shithub-search-result-list">
52
+            {{ range .Repos }}
53
+              <li class="shithub-search-result shithub-search-repo-result">
54
+                <div class="shithub-search-result-main">
55
+                  <h2>
56
+                    <img src="/avatars/{{ .OwnerUsername }}" alt="" width="20" height="20" class="shithub-search-avatar">
57
+                    <a href="/{{ .OwnerUsername }}/{{ .Name }}">{{ .OwnerUsername }}/{{ .Name }}</a>
58
+                    {{ if eq .Visibility "private" }}<span class="shithub-pill shithub-pill-private">Private</span>{{ end }}
59
+                  </h2>
60
+                  {{ if .Description }}<p>{{ .Description }}</p>{{ end }}
61
+                  <ul class="shithub-search-result-meta">
62
+                    <li>{{ octicon "star" }} {{ .StarCount }}</li>
63
+                    <li>Updated {{ relativeTime .UpdatedAt }}</li>
64
+                  </ul>
65
+                </div>
66
+                <a href="/{{ .OwnerUsername }}/{{ .Name }}/stargazers" class="shithub-button shithub-button-small shithub-search-star">{{ octicon "star" }} Star</a>
67
+              </li>
68
+            {{ end }}
69
+            </ol>
70
+          {{ else }}<p class="shithub-search-empty">No repositories matched your search.</p>{{ end }}
6771
 
68
-    {{ else if eq .Tab "code" }}
69
-      {{ if .Code }}
70
-        <p class="shithub-meta">{{ .Total }} code result{{ if ne .Total 1 }}s{{ end }}</p>
71
-        <ul class="shithub-search-list">
72
-        {{ range .Code }}
73
-          <li>
74
-            <a href="/{{ .OwnerUsername }}/{{ .RepoName }}/blob/{{ .RefName }}/{{ .Path }}">
75
-              <strong>{{ .OwnerUsername }}/{{ .RepoName }}</strong>
76
-              <code>{{ .Path }}</code>
77
-            </a>
78
-          </li>
72
+        {{ else if eq .Tab "issues" }}
73
+          {{ if .Issues }}
74
+            <ol class="shithub-search-result-list">
75
+            {{ range .Issues }}
76
+              <li class="shithub-search-result">
77
+                <div class="shithub-search-result-main">
78
+                  <h2>
79
+                    <span class="shithub-search-state shithub-search-state-{{ .State }}">
80
+                      {{ if eq .State "closed" }}{{ octicon "issue-closed" }}{{ else }}{{ octicon "issue-opened" }}{{ end }}
81
+                    </span>
82
+                    <a href="/{{ .OwnerUsername }}/{{ .RepoName }}/issues/{{ .Number }}">{{ .Title }}</a>
83
+                  </h2>
84
+                  <p class="shithub-search-result-path">{{ .OwnerUsername }}/{{ .RepoName }}#{{ .Number }}</p>
85
+                  <ul class="shithub-search-result-meta">
86
+                    <li>{{ .State }}</li>
87
+                    {{ if .AuthorName }}<li>Opened by @{{ .AuthorName }}</li>{{ end }}
88
+                    <li>Updated {{ relativeTime .UpdatedAt }}</li>
89
+                  </ul>
90
+                </div>
91
+              </li>
92
+            {{ end }}
93
+            </ol>
94
+          {{ else }}<p class="shithub-search-empty">No issues matched your search.</p>{{ end }}
95
+
96
+        {{ else if eq .Tab "pulls" }}
97
+          {{ if .Issues }}
98
+            <ol class="shithub-search-result-list">
99
+            {{ range .Issues }}
100
+              <li class="shithub-search-result">
101
+                <div class="shithub-search-result-main">
102
+                  <h2>
103
+                    <span class="shithub-search-state shithub-search-state-pr">{{ octicon "git-pull-request" }}</span>
104
+                    <a href="/{{ .OwnerUsername }}/{{ .RepoName }}/pulls/{{ .Number }}">{{ .Title }}</a>
105
+                  </h2>
106
+                  <p class="shithub-search-result-path">{{ .OwnerUsername }}/{{ .RepoName }}#{{ .Number }}</p>
107
+                  <ul class="shithub-search-result-meta">
108
+                    <li>{{ .State }}</li>
109
+                    {{ if .AuthorName }}<li>Opened by @{{ .AuthorName }}</li>{{ end }}
110
+                    <li>Updated {{ relativeTime .UpdatedAt }}</li>
111
+                  </ul>
112
+                </div>
113
+              </li>
114
+            {{ end }}
115
+            </ol>
116
+          {{ else }}<p class="shithub-search-empty">No pull requests matched your search.</p>{{ end }}
117
+
118
+        {{ else if eq .Tab "users" }}
119
+          {{ if .Users }}
120
+            <ol class="shithub-search-result-list">
121
+            {{ range .Users }}
122
+              <li class="shithub-search-result shithub-search-user-result">
123
+                <img src="/avatars/{{ .Username }}" alt="" width="40" height="40" class="shithub-search-user-avatar">
124
+                <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 }}
128
+                </div>
129
+              </li>
130
+            {{ end }}
131
+            </ol>
132
+          {{ else }}<p class="shithub-search-empty">No users matched your search.</p>{{ end }}
133
+
134
+        {{ else if eq .Tab "code" }}
135
+          {{ if .Code }}
136
+            <ol class="shithub-search-result-list">
137
+            {{ range .Code }}
138
+              <li class="shithub-search-result">
139
+                <div class="shithub-search-result-main">
140
+                  <h2>
141
+                    {{ octicon "file" }}
142
+                    <a href="/{{ .OwnerUsername }}/{{ .RepoName }}/blob/{{ .RefName }}/{{ .Path }}">{{ .Path }}</a>
143
+                  </h2>
144
+                  <p class="shithub-search-result-path">{{ .OwnerUsername }}/{{ .RepoName }} on {{ .RefName }}</p>
145
+                  {{ if .PreviewLine }}<pre class="shithub-search-code-preview"><code>{{ .PreviewLine }}</code></pre>{{ end }}
146
+                </div>
147
+              </li>
148
+            {{ end }}
149
+            </ol>
150
+          {{ else }}<p class="shithub-search-empty">No code matched your search.</p>{{ end }}
79151
         {{ end }}
80
-        </ul>
81
-      {{ else }}<p class="shithub-empty">No code matched.</p>{{ end }}
82
-    {{ end }}
83152
 
84
-    {{ if or .HasPrev .HasNext }}
85
-    <nav class="shithub-pagination">
86
-      {{ if .HasPrev }}<a href="?q={{ .Query }}&type={{ .Tab }}&page={{ sub .Page 1 }}" class="shithub-button">Previous</a>{{ end }}
87
-      {{ if .HasNext }}<a href="?q={{ .Query }}&type={{ .Tab }}&page={{ add .Page 1 }}" class="shithub-button">Next</a>{{ end }}
88
-    </nav>
89
-    {{ end }}
90
-  {{ end }}
153
+        {{ if or .HasPrev .HasNext }}
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 }}
157
+        </nav>
158
+        {{ end }}
159
+      {{ end }}
160
+    </div>
161
+  </div>
91162
 </section>
92163
 {{- end }}