detached c914750

homepage: show favicons in Recent History list (circular #ccc chip, 16px icon); compute /favicon.ico from entry URL; hide on error

commit
c91475000ce134a2c79321c42ac506a48872cd28
author
bamen · · 2025-09-09 18:14 UTC
committer
bamen · · 2025-09-09 18:28 UTC
parent
a863db8
signature
unsigned
2 files · +33 -0
M frontend/src/utils/dom.ts +20 -0
@@ -78,6 +78,15 @@ export class DOMRenderer {
7878 const line = document.createElement('div');
7979 line.className = 'history-line';
8080
81+ // Favicon chip
82+ const icon = document.createElement('img');
83+ icon.className = 'history-favicon';
84+ icon.width = 16; icon.height = 16;
85+ icon.loading = 'lazy';
86+ icon.referrerPolicy = 'no-referrer';
87+ icon.src = this.buildFaviconURL(item.url);
88+ icon.addEventListener('error', () => { icon.style.display = 'none'; });
89+
8190 const titleEl = document.createElement('span');
8291 titleEl.className = 'history-title';
8392 titleEl.textContent = title;
@@ -98,6 +107,7 @@ export class DOMRenderer {
98107 urlEl.className = 'history-url';
99108 urlEl.textContent = item.url;
100109
110+ line.appendChild(icon);
101111 line.appendChild(titleEl);
102112 line.appendChild(sep1);
103113 line.appendChild(domainEl);
@@ -186,4 +196,14 @@ export class DOMRenderer {
186196 return { host: '' };
187197 }
188198 }
199+
200+ private buildFaviconURL(raw: string): string {
201+ try {
202+ const u = new URL(raw);
203+ const scheme = u.protocol && u.protocol !== ':' ? u.protocol : 'https:';
204+ return `${scheme}//${u.host}/favicon.ico`;
205+ } catch {
206+ return '';
207+ }
208+ }
189209 }
@@ -78,6 +78,15 @@ export class DOMRenderer {
78 const line = document.createElement('div');78 const line = document.createElement('div');
79 line.className = 'history-line';79 line.className = 'history-line';
80 80
81+ // Favicon chip
82+ const icon = document.createElement('img');
83+ icon.className = 'history-favicon';
84+ icon.width = 16; icon.height = 16;
85+ icon.loading = 'lazy';
86+ icon.referrerPolicy = 'no-referrer';
87+ icon.src = this.buildFaviconURL(item.url);
88+ icon.addEventListener('error', () => { icon.style.display = 'none'; });
89+
81 const titleEl = document.createElement('span');90 const titleEl = document.createElement('span');
82 titleEl.className = 'history-title';91 titleEl.className = 'history-title';
83 titleEl.textContent = title;92 titleEl.textContent = title;
@@ -98,6 +107,7 @@ export class DOMRenderer {
98 urlEl.className = 'history-url';107 urlEl.className = 'history-url';
99 urlEl.textContent = item.url;108 urlEl.textContent = item.url;
100 109
110+ line.appendChild(icon);
101 line.appendChild(titleEl);111 line.appendChild(titleEl);
102 line.appendChild(sep1);112 line.appendChild(sep1);
103 line.appendChild(domainEl);113 line.appendChild(domainEl);
@@ -186,4 +196,14 @@ export class DOMRenderer {
186 return { host: '' };196 return { host: '' };
187 }197 }
188 }198 }
199+
200+ private buildFaviconURL(raw: string): string {
201+ try {
202+ const u = new URL(raw);
203+ const scheme = u.protocol && u.protocol !== ':' ? u.protocol : 'https:';
204+ return `${scheme}//${u.host}/favicon.ico`;
205+ } catch {
206+ return '';
207+ }
208+ }
189 }209 }
M frontend/src/utils/html-builder.ts +13 -0
@@ -115,6 +115,19 @@ export class HTMLBuilder {
115115 min-width: 0; /* allow flex children to shrink */
116116 }
117117
118+ .history-favicon {
119+ flex: 0 0 20px;
120+ width: 16px; height: 16px;
121+ padding: 2px;
122+ box-sizing: content-box;
123+ border-radius: 50%;
124+ background: #ccc;
125+ border: 1px solid rgba(0,0,0,.12);
126+ box-shadow: 0 1px 2px rgba(0,0,0,.12);
127+ filter: brightness(1.06) contrast(1.03);
128+ image-rendering: -webkit-optimize-contrast;
129+ }
130+
118131 .history-title { font-size: 0.95rem; color: #e6e6e6; flex: 0 0 auto; }
119132 .history-domain { font-size: 0.9rem; color: #a5a5a5; flex: 0 0 auto; }
120133 .history-sep { color: #666; flex: 0 0 auto; }
@@ -115,6 +115,19 @@ export class HTMLBuilder {
115 min-width: 0; /* allow flex children to shrink */115 min-width: 0; /* allow flex children to shrink */
116 }116 }
117 117
118+ .history-favicon {
119+ flex: 0 0 20px;
120+ width: 16px; height: 16px;
121+ padding: 2px;
122+ box-sizing: content-box;
123+ border-radius: 50%;
124+ background: #ccc;
125+ border: 1px solid rgba(0,0,0,.12);
126+ box-shadow: 0 1px 2px rgba(0,0,0,.12);
127+ filter: brightness(1.06) contrast(1.03);
128+ image-rendering: -webkit-optimize-contrast;
129+ }
130+
118 .history-title { font-size: 0.95rem; color: #e6e6e6; flex: 0 0 auto; }131 .history-title { font-size: 0.95rem; color: #e6e6e6; flex: 0 0 auto; }
119 .history-domain { font-size: 0.9rem; color: #a5a5a5; flex: 0 0 auto; }132 .history-domain { font-size: 0.9rem; color: #a5a5a5; flex: 0 0 auto; }
120 .history-sep { color: #666; flex: 0 0 auto; }133 .history-sep { color: #666; flex: 0 0 auto; }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close