Monorepo for Tangled tangled.org
855
fork

Configure Feed

Select the types of activity you want to include in your feed.

appview: add topbar quick-search #331

open opened by eti.tf targeting master from eti.tf/core-2: eti/search-in-nav

Adds an inline search widget to the topbar.

On desktop, a search input appears directly in the nav with a dropdown of quick results (keyboard-navigable with ↑↓/Enter/Escape, ⌘K to focus). On mobile, a search button opens a fullscreen overlay with the same results.

Signed-off-by: eti eti@eti.tf

Labels

None yet.

assignee

None yet.

Participants 1
AT URI
at://did:plc:xu5apv6kmu5jp7g5hwdnej42/sh.tangled.repo.pull/3ml6n7w7fmq22
+289 -222
Interdiff #0 #1
.gitignore

This file has not been changed.

appview/pages/pages.go

This file has not been changed.

+244 -170
appview/pages/static/topbar-search.js
··· 1 - (function () { 2 - if (window._navSearchReady) return; 3 - window._navSearchReady = true; 4 - 5 - var scrollY = 0; 6 - var vvListener = null; 7 - var mobileResultsTouchLock = null; 8 - 9 - function updateOverlayHeight() { 10 - var overlay = document.getElementById('mobile-search-overlay'); 11 - if (!overlay) return; 12 - var vv = window.visualViewport; 13 - var layoutH = window.innerHeight; 14 - var visH = vv ? vv.height : layoutH; 15 - overlay.style.height = layoutH + 'px'; 16 - overlay.style.top = '0px'; 17 - var spacer = document.getElementById('mobile-search-spacer'); 18 - if (spacer) spacer.style.height = Math.max(0, layoutH - visH) + 'px'; 19 - } 20 - 21 - function openMobile() { 22 - var overlay = document.getElementById('mobile-search-overlay'); 23 - if (!overlay) return; 24 - if (overlay.classList.contains('opacity-100')) return; 25 - overlay.classList.remove('opacity-0', 'pointer-events-none'); 26 - overlay.classList.add('opacity-100', 'pointer-events-auto'); 27 - overlay.setAttribute('aria-hidden', 'false'); 28 - scrollY = window.scrollY; 29 - document.body.style.position = 'fixed'; 30 - document.body.style.top = '-' + scrollY + 'px'; 31 - document.body.style.width = '100%'; 32 - updateOverlayHeight(); 33 - if (window.visualViewport) { 34 - vvListener = updateOverlayHeight; 35 - window.visualViewport.addEventListener('resize', vvListener); 36 - } 37 - var input = document.getElementById('mobile-search-input'); 38 - if (input) input.focus({ preventScroll: true }); 39 - var results = document.getElementById('mobile-search-results'); 40 - if (results && !mobileResultsTouchLock) { 41 - mobileResultsTouchLock = function (e) { e.preventDefault(); }; 42 - results.addEventListener('touchmove', mobileResultsTouchLock, { passive: false }); 43 - } 44 - } 45 - 46 - function closeMobile() { 47 - var overlay = document.getElementById('mobile-search-overlay'); 48 - if (!overlay) return; 49 - overlay.classList.remove('opacity-100', 'pointer-events-auto'); 50 - overlay.classList.add('opacity-0', 'pointer-events-none'); 51 - overlay.setAttribute('aria-hidden', 'true'); 52 - overlay.style.height = ''; 53 - overlay.style.top = ''; 54 - var spacer = document.getElementById('mobile-search-spacer'); 55 - if (spacer) { spacer.style.height = ''; spacer.classList.add('hidden'); } 56 - if (window.visualViewport && vvListener) { 57 - window.visualViewport.removeEventListener('resize', vvListener); 58 - vvListener = null; 59 - } 60 - document.body.style.position = ''; 61 - document.body.style.top = ''; 62 - document.body.style.width = ''; 63 - window.scrollTo(0, scrollY); 64 - var input = document.getElementById('mobile-search-input'); 65 - if (input) { input.value = ''; input.blur(); } 66 - var results = document.getElementById('mobile-search-results'); 67 - if (results) results.innerHTML = ''; 68 - } 69 - 70 - function clearDesktop() { 71 - var r = document.getElementById('topbar-search-results'); 72 - if (r) r.innerHTML = ''; 73 - var b = document.getElementById('topbar-search-box'); 74 - if (b) { b.classList.add('rounded'); b.classList.remove('rounded-t'); } 75 - } 76 - 77 - function submitFromInput(input) { 78 - var v = input.value.trim(); 79 - if (v) window.location.href = '/search?q=' + encodeURIComponent(v); 80 - } 81 - 82 - document.addEventListener('click', function (e) { 83 - var t = e.target.closest('[data-action]'); 84 - if (t) { 85 - var a = t.getAttribute('data-action'); 86 - if (a === 'open-mobile-search') { e.preventDefault(); openMobile(); return; } 87 - if (a === 'close-mobile-search') { e.preventDefault(); closeMobile(); return; } 88 - } 89 - var c = document.getElementById('topbar-search-container'); 90 - if (c && !c.contains(e.target)) clearDesktop(); 91 - }); 92 - 93 - // Defer so a click on a result fires before results are cleared. 94 - document.addEventListener('focusout', function (e) { 95 - var c = document.getElementById('topbar-search-container'); 96 - if (c && c.contains(e.target) && !c.contains(e.relatedTarget)) { 97 - setTimeout(clearDesktop, 0); 98 - } 99 - }); 100 - 101 - document.addEventListener('htmx:afterSwap', function (e) { 102 - var t = e.detail.target; 103 - if (!t) return; 104 - if (t.id === 'topbar-search-results') { 105 - var b = document.getElementById('topbar-search-box'); 106 - var open = t.children.length > 0; 107 - if (b) { b.classList.toggle('rounded', !open); b.classList.toggle('rounded-t', open); } 108 - return; 109 - } 110 - if (t.id === 'mobile-search-results') { 111 - if (mobileResultsTouchLock) { 112 - t.removeEventListener('touchmove', mobileResultsTouchLock); 113 - mobileResultsTouchLock = null; 114 - } 115 - var spacer = document.getElementById('mobile-search-spacer'); 116 - if (!spacer) return; 117 - var hasResults = !!t.querySelector('[data-results-footer]'); 118 - spacer.classList.toggle('hidden', !hasResults); 119 - } 120 - }); 121 - 122 - document.addEventListener('keydown', function (e) { 123 - var input = document.getElementById('topbar-search-input'); 124 - var results = document.getElementById('topbar-search-results'); 125 - var mobileOverlay = document.getElementById('mobile-search-overlay'); 126 - var mobileInput = document.getElementById('mobile-search-input'); 127 - var active = document.activeElement; 128 - 129 - if ((e.metaKey || e.ctrlKey) && e.key === 'k') { 130 - e.preventDefault(); 131 - if (input) { input.focus(); input.select(); } 132 - return; 133 - } 134 - 135 - if (e.key === 'Enter') { 136 - if (active === input) { e.preventDefault(); submitFromInput(input); return; } 137 - if (active === mobileInput) { e.preventDefault(); submitFromInput(mobileInput); return; } 138 - } 139 - 140 - if (e.key === 'Escape') { 141 - if (mobileOverlay && !mobileOverlay.classList.contains('opacity-0')) { 142 - e.preventDefault(); 143 - closeMobile(); 144 - return; 145 - } 146 - if (input) { 147 - var ls = results ? Array.from(results.querySelectorAll('[data-nav-result]')) : []; 148 - if (active === input || ls.indexOf(active) >= 0) { 149 - e.preventDefault(); 150 - clearDesktop(); 151 - input.blur(); 152 - } 153 - } 154 - } 155 - 156 - if (!input || !results) return; 157 - var links = Array.from(results.querySelectorAll('[data-nav-result]')); 158 - var inInput = active === input; 159 - var idx = links.indexOf(active); 160 - 161 - if (e.key === 'ArrowDown') { 162 - if (inInput && links.length) { e.preventDefault(); links[0].focus(); } 163 - else if (idx >= 0 && idx < links.length - 1) { e.preventDefault(); links[idx + 1].focus(); } 164 - } 165 - 166 - if (e.key === 'ArrowUp') { 167 - if (idx === 0) { e.preventDefault(); input.focus(); } 168 - else if (idx > 0) { e.preventDefault(); links[idx - 1].focus(); } 169 - } 170 - }); 1 + (() => { 2 + if (window._navSearchReady) return; 3 + window._navSearchReady = true; 4 + 5 + const $ = (id) => document.getElementById(id); 6 + 7 + const submitFromInput = (input) => { 8 + const query = input.value.trim(); 9 + if (query) 10 + window.location.href = `/search?q=${encodeURIComponent(query)}`; 11 + }; 12 + 13 + // mobile-related code 14 + let savedScrollY = 0; 15 + let touchMoveHandler = null; 16 + 17 + const updateOverlayHeight = () => { 18 + const overlay = $("mobile-search-overlay"); 19 + if (!overlay) return; 20 + 21 + const layoutHeight = window.innerHeight; 22 + const visibleHeight = window.visualViewport?.height ?? layoutHeight; 23 + 24 + overlay.style.height = `${layoutHeight}px`; 25 + overlay.style.top = "0px"; 26 + 27 + const spacer = $("mobile-search-spacer"); 28 + if (spacer) 29 + spacer.style.height = `${Math.max(0, layoutHeight - visibleHeight)}px`; 30 + }; 31 + 32 + const openMobile = () => { 33 + const overlay = $("mobile-search-overlay"); 34 + if (!overlay || overlay.classList.contains("opacity-100")) return; 35 + 36 + overlay.classList.remove("opacity-0", "pointer-events-none"); 37 + overlay.classList.add("opacity-100", "pointer-events-auto"); 38 + overlay.setAttribute("aria-hidden", "false"); 39 + 40 + savedScrollY = window.scrollY; 41 + Object.assign(document.body.style, { 42 + position: "fixed", 43 + top: `-${savedScrollY}px`, 44 + width: "100%", 45 + }); 46 + updateOverlayHeight(); 47 + 48 + if (window.visualViewport) { 49 + window.visualViewport.addEventListener( 50 + "resize", 51 + updateOverlayHeight, 52 + ); 53 + } 54 + 55 + $("mobile-search-input")?.focus({ preventScroll: true }); 56 + 57 + const results = $("mobile-search-results"); 58 + if (results && !touchMoveHandler) { 59 + touchMoveHandler = (e) => e.preventDefault(); 60 + results.addEventListener("touchmove", touchMoveHandler, { 61 + passive: false, 62 + }); 63 + } 64 + }; 65 + 66 + const closeMobile = () => { 67 + const overlay = $("mobile-search-overlay"); 68 + if (!overlay) return; 69 + 70 + overlay.classList.remove("opacity-100", "pointer-events-auto"); 71 + overlay.classList.add("opacity-0", "pointer-events-none"); 72 + overlay.setAttribute("aria-hidden", "true"); 73 + overlay.style.height = ""; 74 + overlay.style.top = ""; 75 + 76 + const spacer = $("mobile-search-spacer"); 77 + if (spacer) { 78 + spacer.style.height = ""; 79 + spacer.classList.add("hidden"); 80 + } 81 + 82 + if (window.visualViewport) { 83 + window.visualViewport.removeEventListener( 84 + "resize", 85 + updateOverlayHeight, 86 + ); 87 + } 88 + 89 + Object.assign(document.body.style, { 90 + position: "", 91 + top: "", 92 + width: "", 93 + }); 94 + window.scrollTo(0, savedScrollY); 95 + 96 + const input = $("mobile-search-input"); 97 + if (input) { 98 + input.value = ""; 99 + input.blur(); 100 + } 101 + 102 + $("mobile-search-results")?.replaceChildren(); 103 + }; 104 + 105 + // desktop-related things 106 + const clearDesktop = () => { 107 + $("topbar-search-results")?.replaceChildren(); 108 + 109 + const box = $("topbar-search-box"); 110 + box?.classList.add("rounded"); 111 + box?.classList.remove("rounded-t"); 112 + }; 113 + 114 + // events 115 + document.addEventListener("click", ({ target }) => { 116 + // mobile: open/close overlay via data-action buttons 117 + const action = target 118 + .closest("[data-action]") 119 + ?.getAttribute("data-action"); 120 + if (action === "open-mobile-search") { 121 + openMobile(); 122 + return; 123 + } 124 + if (action === "close-mobile-search") { 125 + closeMobile(); 126 + return; 127 + } 128 + 129 + // desktop: clicking outside the search container clears results 130 + const container = $("topbar-search-container"); 131 + if (container && !container.contains(target)) clearDesktop(); 132 + }); 133 + 134 + // desktop: defer so a click on a result fires before results are cleared 135 + document.addEventListener("focusout", ({ target, relatedTarget }) => { 136 + const container = $("topbar-search-container"); 137 + if (container?.contains(target) && !container.contains(relatedTarget)) { 138 + setTimeout(clearDesktop, 0); 139 + } 140 + }); 141 + 142 + document.addEventListener("htmx:afterSwap", ({ detail: { target } }) => { 143 + if (!target) return; 144 + 145 + // desktop: toggle rounded corners based on whether results are open 146 + if (target.id === "topbar-search-results") { 147 + const box = $("topbar-search-box"); 148 + const open = target.children.length > 0; 149 + box?.classList.toggle("rounded", !open); 150 + box?.classList.toggle("rounded-t", open); 151 + return; 152 + } 153 + 154 + // mobile: restore touch listener and show spacer when results arrive 155 + if (target.id === "mobile-search-results") { 156 + if (touchMoveHandler) { 157 + target.removeEventListener("touchmove", touchMoveHandler); 158 + touchMoveHandler = null; 159 + } 160 + 161 + const hasResults = !!target.querySelector("[data-results-footer]"); 162 + $("mobile-search-spacer")?.classList.toggle("hidden", !hasResults); 163 + } 164 + }); 165 + 166 + document.addEventListener("keydown", (e) => { 167 + const { key, metaKey, ctrlKey } = e; 168 + const input = $("topbar-search-input"); 169 + const results = $("topbar-search-results"); 170 + const mobileOverlay = $("mobile-search-overlay"); 171 + const mobileInput = $("mobile-search-input"); 172 + const active = document.activeElement; 173 + 174 + // desktop: ⌘K / Ctrl+K focuses the search input 175 + if ((metaKey || ctrlKey) && key === "k") { 176 + e.preventDefault(); 177 + input?.focus(); 178 + input?.select(); 179 + return; 180 + } 181 + 182 + if (key === "Enter") { 183 + if (active === input) { 184 + e.preventDefault(); 185 + submitFromInput(input); 186 + return; 187 + } // desktop 188 + if (active === mobileInput) { 189 + e.preventDefault(); 190 + submitFromInput(mobileInput); 191 + return; 192 + } // mobile 193 + } 194 + 195 + if (key === "Escape") { 196 + // mobile: close the overlay 197 + if ( 198 + mobileOverlay && 199 + !mobileOverlay.classList.contains("opacity-0") 200 + ) { 201 + e.preventDefault(); 202 + closeMobile(); 203 + return; 204 + } 205 + // desktop: clear results and blur 206 + if (input) { 207 + const links = results 208 + ? [...results.querySelectorAll("[data-nav-result]")] 209 + : []; 210 + if (active === input || links.includes(active)) { 211 + e.preventDefault(); 212 + clearDesktop(); 213 + input.blur(); 214 + } 215 + } 216 + } 217 + 218 + // desktop: arrow key navigation through results 219 + if (!input || !results) return; 220 + 221 + const links = [...results.querySelectorAll("[data-nav-result]")]; 222 + const inputFocused = active === input; 223 + const focusedIndex = links.indexOf(active); 224 + 225 + if (key === "ArrowDown") { 226 + if (inputFocused && links.length) { 227 + e.preventDefault(); 228 + links[0].focus(); 229 + } else if (focusedIndex >= 0 && focusedIndex < links.length - 1) { 230 + e.preventDefault(); 231 + links[focusedIndex + 1].focus(); 232 + } 233 + } 234 + 235 + if (key === "ArrowUp") { 236 + if (focusedIndex === 0) { 237 + e.preventDefault(); 238 + input.focus(); 239 + } else if (focusedIndex > 0) { 240 + e.preventDefault(); 241 + links[focusedIndex - 1].focus(); 242 + } 243 + } 244 + }); 171 245 })();
appview/pages/templates/layouts/base.html

This file has not been changed.

+35 -13
appview/pages/templates/layouts/fragments/topbar.html
··· 11 11 {{ with .LoggedInUser }} 12 12 <div class="flex items-center order-1 md:order-3">{{ block "newButton" . }} {{ end }}</div> 13 13 <div class="flex items-center order-2 md:order-4">{{ template "notifications/fragments/bell" }}</div> 14 - <div class="flex items-center order-3 md:order-1">{{ block "searchButton" . }} {{ end }}</div> 14 + <div class="flex items-center order-3 md:order-1">{{ block "search" . }} {{ end }}</div> 15 15 <div class="hidden md:block md:order-2 w-px h-6 bg-gray-200 dark:bg-gray-700 self-center"></div> 16 16 <div class="flex items-center order-4 md:order-5">{{ block "profileDropdown" . }} {{ end }}</div> 17 17 {{ else }} ··· 41 41 42 42 43 43 44 - 44 + </details> 45 45 {{ end }} 46 46 47 - {{ define "searchButton" }} 47 + {{ define "search" }} 48 48 <div class="relative hidden md:block" id="topbar-search-container"> 49 - <div id="topbar-search-box" class="relative flex items-center gap-2 px-2 pr-1.5 py-4 max-h-[30px] w-80 border border-gray-200 dark:border-gray-700 rounded bg-white dark:bg-gray-900 focus-within:z-[51] before:content-[''] before:absolute before:inset-0 before:rounded-md before:invisible focus-within:before:visible before:ring-2 before:ring-transparent before:ring-offset-transparent before:ring-offset-1 before:pointer-events-none focus-within:before:ring-gray-300 dark:focus-within:before:ring-gray-600"> 49 + <div id="topbar-search-box" class="relative flex items-center gap-2 px-2 pr-1.5 py-4 max-h-[30px] md:w-80 lg:w-96 border border-gray-200 dark:border-gray-700 rounded bg-white dark:bg-gray-900 focus-within:z-[51] before:content-[''] before:absolute before:inset-0 before:rounded-md before:invisible focus-within:before:visible before:ring-2 before:ring-transparent before:ring-offset-transparent before:ring-offset-1 before:pointer-events-none focus-within:before:ring-gray-300 dark:focus-within:before:ring-gray-600"> 50 50 {{ i "search" "size-4 text-gray-400" }} 51 51 <input 52 52 type="text" ··· 56 56 autocomplete="off" 57 57 class="flex-1 border-none bg-transparent p-0 focus:outline-none focus:[box-shadow:none] text-sm text-gray-800 dark:text-gray-200 placeholder-gray-400 dark:placeholder-gray-600 peer" 58 58 hx-get="/search/quick" 59 - hx-trigger="input changed delay:250ms" 59 + hx-trigger="input changed delay:10ms" 60 60 hx-target="#topbar-search-results" 61 61 hx-swap="innerHTML" 62 62 hx-indicator="#topbar-search-indicator" 63 63 /> 64 - <kbd class="pointer-events-none flex items-center shrink-0 text-xs border border-gray-200 dark:border-gray-600 rounded border-b-2 px-1 font-sans leading-5 text-gray-400 dark:text-gray-500 peer-focus:hidden peer-[:not(:placeholder-shown)]:hidden">⌘K</kbd> 65 - <span id="topbar-search-indicator" class="hidden [&.htmx-request]:block shrink-0"> 64 + <kbd class="pointer-events-none flex items-center text-xs border border-gray-200 dark:border-gray-600 rounded border-b-2 px-1 font-sans leading-5 text-gray-400 dark:text-gray-500 peer-focus:hidden peer-[:not(:placeholder-shown)]:hidden">⌘K</kbd> 65 + <span id="topbar-search-indicator" class="shrink-0"> 66 66 {{ i "loader-circle" "size-4 text-gray-400 animate-spin" }} 67 67 </span> 68 + <button 69 + type="button" 70 + class="hidden peer-[:not(:placeholder-shown)]:block text-gray-400 hover:text-gray-600 dark:hover:text-gray-300" 71 + onclick="const i=document.getElementById('topbar-search-input');i.value='';htmx.trigger(i,'input');i.focus()" 72 + >{{ i "x" "size-4" }}</button> 68 73 </div> 69 - <div id="topbar-search-results" class="absolute right-0 top-full z-50 w-80"></div> 74 + <div id="topbar-search-results" class="absolute w-full z-50"></div> 70 75 </div> 71 76 72 77 <button id="mobile-search-btn" class="md:hidden" type="button" data-action="open-mobile-search"> ··· 90 95 placeholder="search..." 91 96 autocomplete="off" 92 97 inputmode="search" 93 - class="flex-1 min-w-0 border-none bg-transparent p-0 focus:outline-none focus:[box-shadow:none] text-[16px] text-gray-800 dark:text-gray-200 placeholder-gray-400 dark:placeholder-gray-600" 98 + class="flex-1 min-w-0 border-none bg-transparent p-0 focus:outline-none focus:[box-shadow:none] text-[16px] text-gray-800 dark:text-gray-200 placeholder-gray-400 dark:placeholder-gray-600 peer" 94 99 hx-get="/search/quick/mobile" 95 - hx-trigger="input changed delay:250ms" 100 + hx-trigger="input changed delay:10ms" 96 101 hx-target="#mobile-search-results" 97 102 hx-swap="innerHTML" 98 103 hx-indicator="#mobile-search-indicator" 99 104 /> 100 - <span id="mobile-search-indicator" class="hidden [&.htmx-request]:block shrink-0"> 105 + <span id="mobile-search-indicator" class="shrink-0"> 101 106 {{ i "loader-circle" "size-4 text-gray-400 animate-spin" }} 102 107 </span> 103 - <button type="button" class="shrink-0 p-1 -mr-1 hit-area hit-area-4" data-action="close-mobile-search"> 108 + <button type="button" class="p-1 -mr-1 hit-area hit-area-4" data-action="close-mobile-search"> 104 109 {{ i "x" "size-5 text-gray-400" }} 105 110 </button> 106 111 </div> 107 112 <div id="mobile-search-results" class="flex-1 flex flex-col overflow-hidden pb-[env(safe-area-inset-bottom)]"></div> 108 - <div id="mobile-search-spacer" class="shrink-0 bg-gray-50 dark:bg-gray-800 hidden"></div> 113 + <div id="mobile-search-spacer" class="bg-gray-50 dark:bg-gray-800 hidden"></div> 109 114 </div> 115 + <style> 116 + #topbar-search-indicator, 117 + #mobile-search-indicator { 118 + display: none; 119 + opacity: 0; 120 + transition-property: opacity, display; 121 + transition-duration: 150ms, 150ms; 122 + transition-timing-function: ease-out; 123 + transition-behavior: allow-discrete; 124 + } 125 + #topbar-search-indicator.htmx-request, 126 + #mobile-search-indicator.htmx-request { 127 + display: block; 128 + opacity: 1; 129 + transition-duration: 0s, 0s; 130 + } 131 + </style> 110 132 {{ end }} 111 133 112 134 {{ define "profileDropdown" }}
+3 -36
appview/pages/templates/search/fragments/quick.html
··· 30 30 31 31 {{define "search/fragments/quickPage"}} 32 32 {{- range .Repos -}} 33 - {{ template "search/fragments/quickItem" . }} 33 + {{ template "user/fragments/repoCard" (list $ . true false (dict) true) }} 34 34 {{- end -}} 35 35 {{end}} 36 36 37 - {{define "search/fragments/quickItem"}} 38 - {{ $owner := resolve .Did }} 39 - <a href="/{{ $owner }}/{{ .Name }}" 40 - data-nav-result 41 - class="flex flex-col gap-1.5 px-4 py-3 hover:bg-gray-100 dark:hover:bg-gray-700/50 focus:bg-gray-100 dark:focus:bg-gray-700/50 focus:outline-none no-underline hover:no-underline last:border-b last:border-gray-200 dark:last:border-gray-700"> 42 - <div class="flex items-center gap-2 font-medium text-gray-800 dark:text-gray-200"> 43 - {{ i "book-marked" "size-4 pt-0.5 text-gray-400" }} 44 - <span class="truncate">{{ $owner }}/{{ .Name }}</span> 45 - </div> 46 - {{- with .Description -}} 47 - <div class="text-sm text-gray-500 dark:text-gray-400 line-clamp-2 pl-6">{{ . }}</div> 48 - {{- end -}} 49 - {{- with .RepoStats -}} 50 - {{- if or .Language .StarCount .IssueCount.Open .PullCount.Open -}} 51 - <div class="flex gap-3 items-center text-xs text-gray-400 pl-6 font-mono"> 52 - {{- with .Language -}} 53 - <span class="flex items-center gap-1">{{ template "repo/fragments/colorBall" (dict "color" (langColor .)) }}{{ . }}</span> 54 - {{- end -}} 55 - {{- with .StarCount -}} 56 - <span class="flex items-center gap-1">{{ i "star" "w-3 h-3" }} {{ . }}</span> 57 - {{- end -}} 58 - {{- with .IssueCount.Open -}} 59 - <span class="flex items-center gap-1">{{ i "circle-dot" "w-3 h-3" }} {{ . }}</span> 60 - {{- end -}} 61 - {{- with .PullCount.Open -}} 62 - <span class="flex items-center gap-1">{{ i "git-pull-request" "w-3 h-3" }} {{ . }}</span> 63 - {{- end -}} 64 - </div> 65 - {{- end -}} 66 - {{- end -}} 67 - </a> 68 - {{end}} 69 - 70 37 {{define "search/fragments/quickFooter"}} 71 - <div data-results-footer class="bg-gray-50 dark:bg-gray-800 px-4 py-2 flex justify-between items-center text-gray-600 dark:text-gray-400 border-t border-gray-100 dark:border-gray-700"> 72 - <span>{{ .Total }} results</span> 38 + <div data-results-footer class="bg-gray-50 dark:bg-gray-800 px-4 py-2 flex justify-between items-center text-sm text-gray-600 dark:text-gray-400 border-t border-gray-100 dark:border-gray-700"> 39 + <span>showing {{ len .Repos }} of {{ .Total }}</span> 73 40 <a href="/search?q={{ urlquery .Query }}" 74 41 class="flex items-center gap-1 hover:text-gray-600 dark:hover:text-gray-300 no-underline hover:no-underline"> 75 42 all results {{ i "arrow-right" "size-4" }}
appview/state/router.go

This patch was likely rebased, as context lines do not match.

appview/state/search.go

This file has not been changed.

+7 -3
appview/pages/templates/user/fragments/repoCard.html
··· 5 5 {{ $fullName := index . 2 }} 6 6 {{ $starButton := false }} 7 7 {{ $starData := dict }} 8 + {{ $compact := false }} 8 9 {{ if gt (len .) 3 }} 9 10 {{ $starButton = index . 3 }} 10 11 {{ if gt (len .) 4 }} 11 12 {{ $starData = index . 4 }} 13 + {{ if gt (len .) 5 }} 14 + {{ $compact = index . 5 }} 15 + {{ end }} 12 16 {{ end }} 13 17 {{ end }} 14 18 15 19 {{ with $repo }} 16 - <div class="py-4 px-6 gap-1 flex flex-col drop-shadow-sm bg-white dark:bg-gray-800 min-h-32"> 20 + <div class="{{ if not $compact }} min-h-32 {{ end }} {{ if $compact }} focus-within:bg-gray-100 dark:focus-within:bg-gray-800/80 {{ end }} py-4 px-6 gap-1 flex flex-col drop-shadow-sm bg-white dark:bg-gray-800"> 17 21 <div class="font-medium dark:text-white flex items-center justify-between"> 18 22 <div class="flex items-center min-w-0 flex-1 mr-2"> 19 23 {{ if .Source }} ··· 23 27 {{ end }} 24 28 {{ $repoOwner := resolve .Did }} 25 29 {{- if $fullName -}} 26 - <a href="/{{ $repoOwner }}/{{ .Name }}" class="truncate min-w-0">{{ $repoOwner }}/{{ .Name }}</a> 30 + <a href="/{{ $repoOwner }}/{{ .Name }}" data-nav-result class="truncate min-w-0 {{ if $compact }} focus:outline-none {{ end }}">{{ $repoOwner }}/{{ .Name }}</a> 27 31 {{- else -}} 28 - <a href="/{{ $repoOwner }}/{{ .Name }}" class="truncate min-w-0">{{ .Name }}</a> 32 + <a href="/{{ $repoOwner }}/{{ .Name }}" data-nav-result class="truncate min-w-0 {{ if $compact }} focus:outline-none {{ end }}">{{ .Name }}</a> 29 33 {{- end -}} 30 34 </div> 31 35 {{ if and $starButton $root.LoggedInUser }}

History

3 rounds 0 comments
sign up or login to add to the discussion
1 commit
expand
appview: add topbar quick-search
merge conflicts detected
expand
  • .gitignore:6
  • appview/pages/pages.go:1674
  • appview/pages/templates/layouts/base.html:3
  • appview/pages/templates/layouts/fragments/topbar.html:9
  • appview/pages/templates/user/fragments/repoCard.html:5
  • appview/state/router.go:165
  • appview/state/search.go:1
expand 0 comments
eti.tf submitted #1
5 commits
expand
appview: add topbar quick-search
appview: improve topbar quick-search ux
appview: modernize topbar-search.js
appview: use shared repoCard in quick-search results
appview: make quick-search more responsive
expand 0 comments
eti.tf submitted #0
1 commit
expand
appview: add topbar quick-search
expand 0 comments