detached c0744da

feat(ui): homepage Recent History single-line layout with gradient truncation\n\n- Display as {Title} – {Domain} – {URL}\n- Prevent horizontal overflow and darken URL color\n- Ensure flex children shrink (min-width:0) and container max-widths

commit
c0744da63d93a7ab79ac449d95aa1f363d7eab8c
author
bamen · · 2025-09-10 06:13 UTC
parent
d9a1d3b
signature
unsigned
2 files · +74 -21
M frontend/src/utils/dom.ts +46 -10
@@ -74,10 +74,37 @@ export class DOMRenderer {
7474 historyItem.className = 'history-item';
7575 const parsed = this.safeParseURL(item.url);
7676 const title = item.title && item.title.trim() !== '' ? item.title : (parsed.host || 'Untitled');
77- historyItem.innerHTML = `
78- <div class="history-url">${this.escapeHtml(item.url)}</div>
79- <div class="history-title">${this.escapeHtml(title)}</div>
80- `;
77+
78+ const line = document.createElement('div');
79+ line.className = 'history-line';
80+
81+ const titleEl = document.createElement('span');
82+ titleEl.className = 'history-title';
83+ titleEl.textContent = title;
84+
85+ const sep1 = document.createElement('span');
86+ sep1.className = 'history-sep';
87+ sep1.textContent = ' - ';
88+
89+ const domainEl = document.createElement('span');
90+ domainEl.className = 'history-domain';
91+ domainEl.textContent = parsed.host || '';
92+
93+ const sep2 = document.createElement('span');
94+ sep2.className = 'history-sep';
95+ sep2.textContent = ' - ';
96+
97+ const urlEl = document.createElement('span');
98+ urlEl.className = 'history-url';
99+ urlEl.textContent = item.url;
100+
101+ line.appendChild(titleEl);
102+ line.appendChild(sep1);
103+ line.appendChild(domainEl);
104+ line.appendChild(sep2);
105+ line.appendChild(urlEl);
106+
107+ historyItem.appendChild(line);
81108
82109 historyItem.addEventListener('click', () => {
83110 this.navigateToUrl(item.url);
@@ -110,20 +137,29 @@ export class DOMRenderer {
110137
111138 private extractBaseUrl(templateUrl: string): string {
112139 try {
113- const url = new URL(templateUrl);
114- // Remove query parameters that contain %s template
140+ // Support both %s and {query} placeholders
141+ const candidate = templateUrl.replace(/%s|\{query\}/g, "");
142+ const url = new URL(candidate);
143+ // Remove empty query params left after placeholder removal
115144 const params = new URLSearchParams(url.search);
116- for (const [key, value] of params.entries()) {
117- if (value.includes('%s')) {
145+ for (const key of Array.from(params.keys())) {
146+ const v = params.get(key);
147+ if (v === null || v === "") {
118148 params.delete(key);
119149 }
120150 }
121151 url.search = params.toString();
122152 return url.toString();
123153 } catch (error) {
124- // If URL parsing fails, return the original URL
154+ // Fallbacks: try to return origin or stripped placeholder
125155 console.warn('Failed to parse shortcut URL:', templateUrl);
126- return templateUrl;
156+ try {
157+ const pre = templateUrl.split('%s')[0] || templateUrl.split('{query}')[0] || templateUrl;
158+ const u = new URL(pre);
159+ return `${u.protocol}//${u.host}`;
160+ } catch {
161+ return templateUrl.replace(/%s|\{query\}/g, "");
162+ }
127163 }
128164 }
129165
@@ -74,10 +74,37 @@ export class DOMRenderer {
74 historyItem.className = 'history-item';74 historyItem.className = 'history-item';
75 const parsed = this.safeParseURL(item.url);75 const parsed = this.safeParseURL(item.url);
76 const title = item.title && item.title.trim() !== '' ? item.title : (parsed.host || 'Untitled');76 const title = item.title && item.title.trim() !== '' ? item.title : (parsed.host || 'Untitled');
77- historyItem.innerHTML = `77+
78- <div class="history-url">${this.escapeHtml(item.url)}</div>78+ const line = document.createElement('div');
79- <div class="history-title">${this.escapeHtml(title)}</div>79+ line.className = 'history-line';
80- `;80+
81+ const titleEl = document.createElement('span');
82+ titleEl.className = 'history-title';
83+ titleEl.textContent = title;
84+
85+ const sep1 = document.createElement('span');
86+ sep1.className = 'history-sep';
87+ sep1.textContent = ' - ';
88+
89+ const domainEl = document.createElement('span');
90+ domainEl.className = 'history-domain';
91+ domainEl.textContent = parsed.host || '';
92+
93+ const sep2 = document.createElement('span');
94+ sep2.className = 'history-sep';
95+ sep2.textContent = ' - ';
96+
97+ const urlEl = document.createElement('span');
98+ urlEl.className = 'history-url';
99+ urlEl.textContent = item.url;
100+
101+ line.appendChild(titleEl);
102+ line.appendChild(sep1);
103+ line.appendChild(domainEl);
104+ line.appendChild(sep2);
105+ line.appendChild(urlEl);
106+
107+ historyItem.appendChild(line);
81 108
82 historyItem.addEventListener('click', () => {109 historyItem.addEventListener('click', () => {
83 this.navigateToUrl(item.url);110 this.navigateToUrl(item.url);
@@ -110,20 +137,29 @@ export class DOMRenderer {
110 137
111 private extractBaseUrl(templateUrl: string): string {138 private extractBaseUrl(templateUrl: string): string {
112 try {139 try {
113- const url = new URL(templateUrl);140+ // Support both %s and {query} placeholders
114- // Remove query parameters that contain %s template141+ const candidate = templateUrl.replace(/%s|\{query\}/g, "");
142+ const url = new URL(candidate);
143+ // Remove empty query params left after placeholder removal
115 const params = new URLSearchParams(url.search);144 const params = new URLSearchParams(url.search);
116- for (const [key, value] of params.entries()) {145+ for (const key of Array.from(params.keys())) {
117- if (value.includes('%s')) {146+ const v = params.get(key);
147+ if (v === null || v === "") {
118 params.delete(key);148 params.delete(key);
119 }149 }
120 }150 }
121 url.search = params.toString();151 url.search = params.toString();
122 return url.toString();152 return url.toString();
123 } catch (error) {153 } catch (error) {
124- // If URL parsing fails, return the original URL154+ // Fallbacks: try to return origin or stripped placeholder
125 console.warn('Failed to parse shortcut URL:', templateUrl);155 console.warn('Failed to parse shortcut URL:', templateUrl);
126- return templateUrl;156+ try {
157+ const pre = templateUrl.split('%s')[0] || templateUrl.split('{query}')[0] || templateUrl;
158+ const u = new URL(pre);
159+ return `${u.protocol}//${u.host}`;
160+ } catch {
161+ return templateUrl.replace(/%s|\{query\}/g, "");
162+ }
127 }163 }
128 }164 }
129 165
M frontend/src/utils/html-builder.ts +28 -11
@@ -47,6 +47,7 @@ export class HTMLBuilder {
4747 height: 100vh;
4848 display: flex;
4949 flex-direction: column;
50+ overflow-x: hidden; /* prevent horizontal scroll from long content */
5051 }
5152
5253 .container {
@@ -57,16 +58,19 @@ export class HTMLBuilder {
5758 width: 100%;
5859 padding: 2rem;
5960 gap: 2rem;
61+ overflow-x: hidden;
6062 }
6163
6264 .history-section {
6365 flex: 1;
6466 min-height: 0;
67+ min-width: 0; /* allow flex child to shrink */
6568 }
6669
6770 .shortcuts-section {
6871 flex: 1;
6972 min-height: 0;
73+ min-width: 0; /* allow flex child to shrink */
7074 }
7175
7276 .section-title {
@@ -79,7 +83,9 @@ export class HTMLBuilder {
7983
8084 .history-list {
8185 overflow-y: auto;
86+ overflow-x: hidden;
8287 max-height: calc(100vh - 8rem);
88+ max-width: 100%;
8389 }
8490
8591 .history-item {
@@ -90,6 +96,8 @@ export class HTMLBuilder {
9096 cursor: pointer;
9197 transition: background 0.2s;
9298 border-left: 3px solid #404040;
99+ overflow: hidden;
100+ max-width: 100%;
93101 }
94102
95103 .history-item:hover {
@@ -97,19 +105,28 @@ export class HTMLBuilder {
97105 border-left-color: #0066cc;
98106 }
99107
108+ .history-line {
109+ display: flex;
110+ gap: 0.5rem;
111+ white-space: nowrap;
112+ align-items: baseline;
113+ overflow: hidden;
114+ width: 100%;
115+ min-width: 0; /* allow flex children to shrink */
116+ }
117+
118+ .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; }
120+ .history-sep { color: #666; flex: 0 0 auto; }
100121 .history-url {
101122 font-size: 0.9rem;
102- color: #cccccc;
103- margin-bottom: 0.25rem;
104- word-break: break-all;
105- }
106-
107- .history-title {
108- font-size: 0.8rem;
109- color: #888;
123+ color: #7a7a7a; /* darker than domain */
124+ flex: 1 1 auto;
125+ min-width: 0; /* critical for flex truncation */
110126 overflow: hidden;
111- text-overflow: ellipsis;
112- white-space: nowrap;
127+ text-overflow: ellipsis; /* fallback */
128+ -webkit-mask-image: linear-gradient(to right, rgba(0,0,0,1) 85%, rgba(0,0,0,0) 100%);
129+ mask-image: linear-gradient(to right, rgba(0,0,0,1) 85%, rgba(0,0,0,0) 100%);
113130 }
114131
115132 .shortcuts-grid {
@@ -177,4 +194,4 @@ export class HTMLBuilder {
177194 }
178195 `;
179196 }
180-}⏎̸
197+}
@@ -47,6 +47,7 @@ export class HTMLBuilder {
47 height: 100vh;47 height: 100vh;
48 display: flex;48 display: flex;
49 flex-direction: column;49 flex-direction: column;
50+ overflow-x: hidden; /* prevent horizontal scroll from long content */
50 }51 }
51 52
52 .container {53 .container {
@@ -57,16 +58,19 @@ export class HTMLBuilder {
57 width: 100%;58 width: 100%;
58 padding: 2rem;59 padding: 2rem;
59 gap: 2rem;60 gap: 2rem;
61+ overflow-x: hidden;
60 }62 }
61 63
62 .history-section {64 .history-section {
63 flex: 1;65 flex: 1;
64 min-height: 0;66 min-height: 0;
67+ min-width: 0; /* allow flex child to shrink */
65 }68 }
66 69
67 .shortcuts-section {70 .shortcuts-section {
68 flex: 1;71 flex: 1;
69 min-height: 0;72 min-height: 0;
73+ min-width: 0; /* allow flex child to shrink */
70 }74 }
71 75
72 .section-title {76 .section-title {
@@ -79,7 +83,9 @@ export class HTMLBuilder {
79 83
80 .history-list {84 .history-list {
81 overflow-y: auto;85 overflow-y: auto;
86+ overflow-x: hidden;
82 max-height: calc(100vh - 8rem);87 max-height: calc(100vh - 8rem);
88+ max-width: 100%;
83 }89 }
84 90
85 .history-item {91 .history-item {
@@ -90,6 +96,8 @@ export class HTMLBuilder {
90 cursor: pointer;96 cursor: pointer;
91 transition: background 0.2s;97 transition: background 0.2s;
92 border-left: 3px solid #404040;98 border-left: 3px solid #404040;
99+ overflow: hidden;
100+ max-width: 100%;
93 }101 }
94 102
95 .history-item:hover {103 .history-item:hover {
@@ -97,19 +105,28 @@ export class HTMLBuilder {
97 border-left-color: #0066cc;105 border-left-color: #0066cc;
98 }106 }
99 107
108+ .history-line {
109+ display: flex;
110+ gap: 0.5rem;
111+ white-space: nowrap;
112+ align-items: baseline;
113+ overflow: hidden;
114+ width: 100%;
115+ min-width: 0; /* allow flex children to shrink */
116+ }
117+
118+ .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; }
120+ .history-sep { color: #666; flex: 0 0 auto; }
100 .history-url {121 .history-url {
101 font-size: 0.9rem;122 font-size: 0.9rem;
102- color: #cccccc;123+ color: #7a7a7a; /* darker than domain */
103- margin-bottom: 0.25rem;124+ flex: 1 1 auto;
104- word-break: break-all;125+ min-width: 0; /* critical for flex truncation */
105- }
106-
107- .history-title {
108- font-size: 0.8rem;
109- color: #888;
110 overflow: hidden;126 overflow: hidden;
111- text-overflow: ellipsis;127+ text-overflow: ellipsis; /* fallback */
112- white-space: nowrap;128+ -webkit-mask-image: linear-gradient(to right, rgba(0,0,0,1) 85%, rgba(0,0,0,0) 100%);
129+ mask-image: linear-gradient(to right, rgba(0,0,0,1) 85%, rgba(0,0,0,0) 100%);
113 }130 }
114 131
115 .shortcuts-grid {132 .shortcuts-grid {
@@ -177,4 +194,4 @@ export class HTMLBuilder {
177 }194 }
178 `;195 `;
179 }196 }
180-}⏎̸197+}

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