Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Search every issue, change and comment from the top bar

6c03e6d3243d31a82210cc7bc689ab3e28226b6c · Keenan Nemetz · 9d ago · parent 389317ee

Search every issue, change and comment from the top bar

The top bar's search now opens a command palette instead of a text field.
The dialog asks the same index and visibility rule as /search, so private
repositories stay out, and it answers with a short list plus a link to the
full results. A button that looks like the old input launches it, labelled
with the platform's shortcut; Ctrl K or Cmd K works anywhere.

The bar's contents also line up with the page column now, and Home is marked
current only on the landing page rather than on every repository page.

server/web/static/vole.css +176−11

@@ -8,7 +8,8 @@
8 8 what it was quietly overriding:
9 9
10 10 tokens custom properties, dark and light (:root)
11 chrome document shell, global bar, account menu
11 chrome document shell, global bar, account menu,
12 command palette
12 13 repository header name, visibility, tab row
13 14 panels and tables the bordered surfaces lists and pages sit on
14 15 controls buttons, inputs, selects, form layout
@@ -86,6 +87,11 @@
86 87 /* One width for every search input: the global bar and a page's filter row
87 88 should not resize the field when the reader moves between them. */
88 89 --search-w: 18rem;
90 /* One measure for the page column and the global bar. The bar pads itself out
91 to the page's content edges, so the brand and the account menu sit on the
92 same vertical line as the content below them. */
93 --page-max: 1440px;
94 --page-gutter: 24px;
89 95 --font: "Lexend", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Noto Sans", "Liberation Sans", Arial, sans-serif;
90 96 --mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
91 97 --fs: 15.4px;
@@ -198,7 +204,9 @@ img { max-width: 100%; }
198 204 align-items: center;
199 205 gap: 20px;
200 206 height: 46px;
201 padding: 0 20px;
207 /* The bar spans the window; its items stay inside the page column. */
208 padding-block: 0;
209 padding-inline: max(var(--page-gutter), calc((100% - var(--page-max)) / 2 + var(--page-gutter)));
202 210 background: color-mix(in srgb, var(--bg-panel) 92%, transparent);
203 211 border-bottom: 1px solid var(--border);
204 212 position: relative;
@@ -253,9 +261,53 @@ img { max-width: 100%; }
253 261 color: var(--accent);
254 262 }
255 263
256 .topsearch { display: flex; align-items: center; gap: 6px; min-width: 0; margin-left: auto; }
257 .topsearch input[type="search"] { flex: 1 1 auto; width: var(--search-w); min-width: 8rem; max-width: 28vw; }
258 .topsearch button { flex: 0 0 auto; }
264 .topsearch {
265 flex: 1 1 auto;
266 min-width: 8rem;
267 max-width: var(--search-w);
268 margin-left: auto;
269 }
270 /* The reader is opening the palette, not editing a field, so this is a button
271 shaped like the search input it replaces. The label stands in for the
272 placeholder, and the last item names the shortcut the browser will hear. */
273 .topsearch-box {
274 display: flex;
275 align-items: center;
276 gap: 8px;
277 width: 100%;
278 min-width: 0;
279 min-height: 30px;
280 padding: 5px 8px;
281 border: 1px solid var(--border-strong);
282 border-radius: var(--radius);
283 background: var(--bg);
284 color: var(--fg-muted);
285 font: inherit;
286 font-size: calc(13.75px * var(--font-scale));
287 text-align: left;
288 cursor: pointer;
289 }
290 .topsearch-box:hover {
291 border-color: color-mix(in srgb, var(--accent) 45%, var(--border));
292 color: var(--fg);
293 }
294 .topsearch-box .icon { width: 13px; height: 13px; color: var(--fg-faint); }
295 .topsearch-label {
296 flex: 1 1 auto;
297 min-width: 0;
298 overflow: hidden;
299 text-overflow: ellipsis;
300 white-space: nowrap;
301 }
302 .topsearch-key {
303 flex: 0 0 auto;
304 padding: 0 4px;
305 border: 1px solid var(--border);
306 border-radius: 4px;
307 background: var(--bg-panel);
308 color: var(--fg-faint);
309 font-size: calc(11.55px * var(--font-scale));
310 }
259 311
260 312 .topright {
261 313 display: flex;
@@ -320,28 +372,141 @@ img { max-width: 100%; }
320 372 .account-pop form { margin: 0; }
321 373 .account-sep { border-top: 1px solid var(--border); margin: 4px 0; }
322 374
375 /* ---------- command palette ---------- */
376
377 /* The global bar's search opens this: one field over every issue, change and
378 comment the reader can see. A row is a link, so Enter and a click take the
379 same route; the stylesheet only has to make the selected row visible. */
380 .palette {
381 width: min(40rem, calc(100vw - 2 * var(--page-gutter)));
382 max-height: min(80vh, 44rem);
383 margin: 10vh auto auto;
384 padding: 0;
385 border: 1px solid var(--border-strong);
386 border-radius: var(--radius-lg);
387 background: var(--bg-raised);
388 color: var(--fg);
389 box-shadow: 0 24px 64px rgba(0, 0, 0, 0.45);
390 overflow: hidden;
391 }
392 .palette::backdrop { background: rgba(0, 0, 0, 0.5); }
393 .palette-search {
394 display: flex;
395 align-items: center;
396 gap: 10px;
397 padding: 10px 12px;
398 border-bottom: 1px solid var(--border);
399 }
400 .palette-search .icon { width: 16px; height: 16px; color: var(--fg-muted); }
401 .palette-search input[type="search"] {
402 flex: 1 1 auto;
403 width: auto;
404 max-width: none;
405 min-height: 0;
406 padding: 2px 0;
407 border: 0;
408 background: transparent;
409 font-size: calc(15.4px * var(--font-scale));
410 }
411 /* The field is the dialog's heading, not a control with a box of its own. */
412 .palette-search input[type="search"]:focus-visible { outline: 0; }
413 .palette-close {
414 display: inline-flex;
415 align-items: center;
416 justify-content: center;
417 padding: 4px;
418 border: 0;
419 border-radius: var(--radius);
420 background: none;
421 color: var(--fg-faint);
422 cursor: pointer;
423 }
424 .palette-close:hover { background: var(--bg-hover); color: var(--fg); }
425 .palette-results { max-height: min(60vh, 30rem); overflow-y: auto; padding: 6px; }
426 .palette-row {
427 display: block;
428 padding: 7px 9px;
429 border-radius: var(--radius);
430 color: var(--fg);
431 }
432 .palette-row:hover { background: var(--bg-hover); text-decoration: none; }
433 .palette-row.is-selected { background: var(--accent-soft); }
434 .palette-row-title {
435 display: block;
436 font-weight: 500;
437 overflow: hidden;
438 text-overflow: ellipsis;
439 white-space: nowrap;
440 }
441 .palette-row-where {
442 display: block;
443 color: var(--fg-faint);
444 font-size: calc(12.65px * var(--font-scale));
445 }
446 /* A palette row is one line high per part; the editable page can afford the
447 wrapped excerpt, the dialog cannot. */
448 .palette-row .search-snippet {
449 margin: 0;
450 overflow: hidden;
451 text-overflow: ellipsis;
452 white-space: nowrap;
453 }
454 .palette-all {
455 margin-top: 4px;
456 border-top: 1px solid var(--border);
457 border-radius: 0 0 var(--radius) var(--radius);
458 color: var(--accent);
459 font-size: calc(13.75px * var(--font-scale));
460 }
461 .palette-empty {
462 margin: 0;
463 padding: 16px 12px;
464 color: var(--fg-muted);
465 font-size: calc(13.75px * var(--font-scale));
466 }
467 .palette-foot {
468 display: flex;
469 gap: 14px;
470 padding: 7px 12px;
471 border-top: 1px solid var(--border);
472 color: var(--fg-faint);
473 font-size: calc(12.65px * var(--font-scale));
474 }
475 .palette-foot kbd {
476 padding: 0 4px;
477 border: 1px solid var(--border);
478 border-radius: 4px;
479 background: var(--bg-panel);
480 color: var(--fg-muted);
481 font-size: calc(11.55px * var(--font-scale));
482 }
483
323 484 .page {
324 width: min(100%, 1440px);
485 width: min(100%, var(--page-max));
325 486 margin: 0 auto;
326 padding: 20px 24px 72px;
487 padding: 20px var(--page-gutter) 72px;
327 488 }
328 489
329 490 @media (max-width: 720px) {
330 491 .tabs { mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent); }
492 :root { --page-gutter: 12px; }
331 493 .topbar {
332 494 height: auto;
333 495 flex-wrap: wrap;
334 padding: 8px 12px;
496 padding-block: 8px;
335 497 gap: 8px 12px;
336 498 }
337 .topsearch { order: 3; flex: 1 1 100%; margin-left: 0; }
338 .topsearch input[type="search"] { width: 100%; min-width: 0; max-width: none; }
499 .topsearch { order: 3; flex: 1 1 100%; max-width: none; margin-left: 0; }
500 .topsearch-box { width: 100%; }
501 /* A keyboard shortcut is not part of a phone's vocabulary. */
502 .topsearch-key { display: none; }
339 503 .topright { margin-left: auto; }
340 504 .topnav { gap: 4px; }
341 505 .topnav a { padding: 4px 8px; }
342 506 /* The icon is enough on a phone; the menu keeps the full name. */
343 507 .account-name { display: none; }
344 .page { padding: 16px 12px 56px; }
508 .page { padding: 16px var(--page-gutter) 56px; }
509 .palette { margin-top: 6vh; }
345 510 }
346 511
347 512 /* ---------- repository header ---------- */