vole
Clone
Read only, no credentials:
https://rodent.software/vole.git
Write over SSH with a registered key:
git clone ssh://git@rodent.software:2222/vole.git
A free and open-source forge that lives in your Git repo.
Make the palette the only search surface
1265ea552fb7346628c1e742a63c758c819379c5 · Keenan Nemetz · 10d ago · parent 6c03e6d3
Make the palette the only search surface The full /search page repeated the palette's query and visibility rule, so one of the two had to become the source of truth. Keep the palette: it shows the first 30 matches, says when a query found more, and is reachable from every page and with the keyboard shortcut. The top bar no longer carries a Search destination or a form fallback; its launcher is a plain button, and the palette dialog never navigates to a page behind it.
server/web/templates/chrome.html +33−9
| @@ -29,16 +29,15 @@ | |||
| 29 | 29 | <nav class="topnav" aria-label="Global"> | |
| 30 | 30 | <a class="{{if eq .Section "home"}}active{{end}}" href="/"{{if eq .Section "home"}} aria-current="page"{{end}}>{{icon "house"}}Home</a> | |
| 31 | 31 | <a class="{{if eq .Section "activity"}}active{{end}}" href="/activity"{{if eq .Section "activity"}} aria-current="page"{{end}}>{{icon "activity"}}Activity</a> | |
| 32 | <a class="{{if eq .Section "search"}}active{{end}}" href="/search"{{if eq .Section "search"}} aria-current="page"{{end}}>{{icon "search"}}Search</a> | ||
| 33 | 32 | {{if .InstanceAdmin}}<a class="{{if eq .Section "users"}}active{{end}}" href="/users"{{if eq .Section "users"}} aria-current="page"{{end}}>{{icon "users"}}Accounts</a>{{end}} | |
| 34 | 33 | </nav> | |
| 35 | <form class="topsearch" method="get" action="/search" role="search"> | ||
| 36 | <button type="submit" class="topsearch-box" data-palette-open aria-haspopup="dialog" aria-controls="search-palette" aria-keyshortcuts="Control+K Meta+K"> | ||
| 34 | <div class="topsearch" role="search"> | ||
| 35 | <button type="button" class="topsearch-box" data-palette-open aria-haspopup="dialog" aria-controls="search-palette" aria-keyshortcuts="Control+K Meta+K"> | ||
| 37 | 36 | {{icon "search"}} | |
| 38 | <span class="topsearch-label">Search issues, changes, comments</span> | ||
| 37 | <span class="topsearch-label">Search issues and changes</span> | ||
| 39 | 38 | <kbd class="topsearch-key" data-palette-key aria-hidden="true">Ctrl K</kbd> | |
| 40 | 39 | </button> | |
| 41 | </form> | ||
| 40 | </div> | ||
| 42 | 41 | <div class="topright"> | |
| 43 | 42 | {{if .LoggedIn}} | |
| 44 | 43 | <details class="account-menu"> | |
| @@ -60,11 +59,11 @@ | |||
| 60 | 59 | {{end}} | |
| 61 | 60 | </div> | |
| 62 | 61 | </header> | |
| 63 | {{/* The global search is also a command palette: one field over every issue, | ||
| 64 | change request and comment the reader can see. Without JavaScript the top | ||
| 65 | bar's form still submits to /search; the dialog is the enhancement. */}} | ||
| 62 | {{/* The command palette is the whole search surface: one field over every | ||
| 63 | issue, change request and comment the reader can see. The launcher in the | ||
| 64 | global bar opens it; the dialog itself never navigates to a page. */}} | ||
| 66 | 65 | <dialog class="palette" id="search-palette" aria-label="Search issues, changes and comments"> | |
| 67 | <form class="palette-search" method="get" action="/search" role="search" | ||
| 66 | <form class="palette-search" role="search" | ||
| 68 | 67 | hx-get="/search/palette" hx-trigger="input changed delay:250ms, search" | |
| 69 | 68 | hx-target="#palette-results" hx-swap="innerHTML" hx-sync="this:replace"> | |
| 70 | 69 | <span aria-hidden="true">{{icon "search"}}</span> | |
| @@ -148,3 +147,28 @@ | |||
| 148 | 147 | {{define "notfound"}}{{template "errorpage" .}}{{end}} | |
| 149 | 148 | ||
| 150 | 149 | {{define "servererror"}}{{template "errorpage" .}}{{end}} | |
| 150 | |||
| 151 | {{/* The command palette's initial state and the rows HTMX swaps in. The dialog | ||
| 152 | shell above carries both, so they live beside the chrome that owns them. */}} | ||
| 153 | {{define "palette_hint"}} | ||
| 154 | <p class="palette-empty" role="option" aria-disabled="true">Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.</p> | ||
| 155 | {{end}} | ||
| 156 | |||
| 157 | {{define "palette_results"}} | ||
| 158 | {{if .Message}} | ||
| 159 | <p class="palette-empty" role="option" aria-disabled="true">{{.Message}}</p> | ||
| 160 | {{else if not .Query}} | ||
| 161 | {{template "palette_hint"}} | ||
| 162 | {{else if .Results}} | ||
| 163 | {{range .Results}} | ||
| 164 | <a class="palette-row" data-palette-row role="option" aria-selected="false" href="{{.URL}}"> | ||
| 165 | <span class="palette-row-title">{{.Title}}</span> | ||
| 166 | <span class="palette-row-where">{{.KindLabel}} · {{.Repo}}{{if .Number}} · <span class="sha">{{num .Number}}</span>{{end}}</span> | ||
| 167 | {{if .Snippet}}<span class="search-snippet">{{.Snippet}}</span>{{end}} | ||
| 168 | </a> | ||
| 169 | {{end}} | ||
| 170 | {{if .Limited}}<p class="palette-empty" role="option" aria-disabled="true">Showing the first results. Add a word to narrow the search.</p>{{end}} | ||
| 171 | {{else}} | ||
| 172 | <p class="palette-empty" role="option" aria-disabled="true">Nothing matches “{{.Query}}”.</p> | ||
| 173 | {{end}} | ||
| 174 | {{end}} | ||