detached 4e23726

fix(systemviews): simplify history header layout

commit
4e23726143761692a1941f4a8ca047adb6b12bda
author
brice · · 2026-04-26 07:07 UTC
parent
d685499
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
7 files · +287 -478
M assets/systemviews/systemviews.css +41 -43
@@ -240,29 +240,6 @@ body {
240240 }
241241 }
242242
243-.sv-history-overview {
244- display: grid;
245- gap: 1rem;
246- align-items: start;
247-}
248-
249-.sv-history-overview > .sv-section {
250- min-width: 0;
251- height: 100%;
252-}
253-
254-@media (min-width: 720px) {
255- .sv-history-overview {
256- grid-template-columns: minmax(12rem, 3.8fr) minmax(0, 6.2fr);
257- }
258-}
259-
260-@media (min-width: 900px) {
261- .sv-history-overview {
262- grid-template-columns: minmax(13rem, 3fr) minmax(0, 7fr);
263- }
264-}
265-
266243 .sv-stat-grid {
267244 display: grid;
268245 grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
@@ -270,12 +247,6 @@ body {
270247 margin-top: 0.75rem;
271248 }
272249
273-.sv-history-overview .sv-stat-grid {
274- grid-template-columns: 1fr;
275- gap: 0.55rem;
276- margin-top: 0.5rem;
277-}
278-
279250 .sv-stat-card {
280251 display: grid;
281252 gap: 0.15rem;
@@ -285,10 +256,6 @@ body {
285256 background: var(--sv-surface-variant, #1d2129);
286257 }
287258
288-.sv-history-overview .sv-stat-card {
289- padding: 0.65rem 0.75rem;
290-}
291-
292259 .sv-stat-value {
293260 color: var(--sv-accent, #8ab4ff);
294261 font-size: 1.35rem;
@@ -307,28 +274,36 @@ body {
307274 .sv-domain-list {
308275 display: flex;
309276 flex-wrap: wrap;
310- gap: 0.5rem;
311- margin-top: 0.75rem;
277+ gap: 0.35rem;
278+ max-height: 4rem;
279+ margin-top: 0.5rem;
280+ overflow: hidden;
312281 }
313282
314283 .sv-domain-chip {
315- display: grid;
316- grid-template-columns: minmax(0, 1fr) auto;
317- gap: 0.5rem;
318- align-items: start;
319- min-width: 10.5rem;
320- padding: 0.55rem 0.65rem;
284+ display: inline-flex;
285+ gap: 0.35rem;
286+ align-items: center;
287+ max-width: 13rem;
288+ min-width: 0;
289+ padding: 0.25rem 0.35rem 0.25rem 0.5rem;
321290 border: 1px solid var(--sv-border, #2a313d);
322291 border-radius: var(--sv-radius-chip, 0.45rem);
323292 background: var(--sv-surface-variant, #1d2129);
324293 }
325294
326295 .sv-domain-chip-main {
327- display: grid;
328- gap: 0.15rem;
296+ display: inline-flex;
297+ gap: 0.3rem;
298+ align-items: baseline;
329299 min-width: 0;
330300 }
331301
302+.sv-domain-meta {
303+ flex: 0 0 auto;
304+ white-space: nowrap;
305+}
306+
332307 .sv-domain-meta,
333308 .sv-history-meta,
334309 .sv-history-url {
@@ -619,6 +594,7 @@ body {
619594
620595 .sv-filter-chip button,
621596 .sv-domain-filter,
597+.sv-domain-delete,
622598 .sv-icon-button {
623599 border: 0;
624600 background: transparent;
@@ -628,10 +604,32 @@ body {
628604 }
629605
630606 .sv-domain-filter {
607+ min-width: 0;
608+ overflow: hidden;
631609 color: var(--sv-text, #e8ebf0);
632610 font-weight: 700;
633611 padding: 0;
634612 text-align: left;
613+ text-overflow: ellipsis;
614+ white-space: nowrap;
615+}
616+
617+.sv-domain-delete {
618+ display: inline-grid;
619+ place-items: center;
620+ flex: 0 0 auto;
621+ width: 1rem;
622+ height: 1rem;
623+ padding: 0;
624+ border-radius: 999px;
625+ color: var(--sv-danger-text, #fecaca);
626+ font-size: 0.85rem;
627+ font-weight: 700;
628+ line-height: 1;
629+}
630+
631+.sv-domain-delete:hover {
632+ background: color-mix(in srgb, var(--sv-danger, #ef4444) 18%, transparent);
635633 }
636634
637635 .sv-icon-button {
@@ -240,29 +240,6 @@ body {
240 }240 }
241 }241 }
242 242
243-.sv-history-overview {
244- display: grid;
245- gap: 1rem;
246- align-items: start;
247-}
248-
249-.sv-history-overview > .sv-section {
250- min-width: 0;
251- height: 100%;
252-}
253-
254-@media (min-width: 720px) {
255- .sv-history-overview {
256- grid-template-columns: minmax(12rem, 3.8fr) minmax(0, 6.2fr);
257- }
258-}
259-
260-@media (min-width: 900px) {
261- .sv-history-overview {
262- grid-template-columns: minmax(13rem, 3fr) minmax(0, 7fr);
263- }
264-}
265-
266 .sv-stat-grid {243 .sv-stat-grid {
267 display: grid;244 display: grid;
268 grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));245 grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
@@ -270,12 +247,6 @@ body {
270 margin-top: 0.75rem;247 margin-top: 0.75rem;
271 }248 }
272 249
273-.sv-history-overview .sv-stat-grid {
274- grid-template-columns: 1fr;
275- gap: 0.55rem;
276- margin-top: 0.5rem;
277-}
278-
279 .sv-stat-card {250 .sv-stat-card {
280 display: grid;251 display: grid;
281 gap: 0.15rem;252 gap: 0.15rem;
@@ -285,10 +256,6 @@ body {
285 background: var(--sv-surface-variant, #1d2129);256 background: var(--sv-surface-variant, #1d2129);
286 }257 }
287 258
288-.sv-history-overview .sv-stat-card {
289- padding: 0.65rem 0.75rem;
290-}
291-
292 .sv-stat-value {259 .sv-stat-value {
293 color: var(--sv-accent, #8ab4ff);260 color: var(--sv-accent, #8ab4ff);
294 font-size: 1.35rem;261 font-size: 1.35rem;
@@ -307,28 +274,36 @@ body {
307 .sv-domain-list {274 .sv-domain-list {
308 display: flex;275 display: flex;
309 flex-wrap: wrap;276 flex-wrap: wrap;
310- gap: 0.5rem;277+ gap: 0.35rem;
311- margin-top: 0.75rem;278+ max-height: 4rem;
279+ margin-top: 0.5rem;
280+ overflow: hidden;
312 }281 }
313 282
314 .sv-domain-chip {283 .sv-domain-chip {
315- display: grid;284+ display: inline-flex;
316- grid-template-columns: minmax(0, 1fr) auto;285+ gap: 0.35rem;
317- gap: 0.5rem;286+ align-items: center;
318- align-items: start;287+ max-width: 13rem;
319- min-width: 10.5rem;288+ min-width: 0;
320- padding: 0.55rem 0.65rem;289+ padding: 0.25rem 0.35rem 0.25rem 0.5rem;
321 border: 1px solid var(--sv-border, #2a313d);290 border: 1px solid var(--sv-border, #2a313d);
322 border-radius: var(--sv-radius-chip, 0.45rem);291 border-radius: var(--sv-radius-chip, 0.45rem);
323 background: var(--sv-surface-variant, #1d2129);292 background: var(--sv-surface-variant, #1d2129);
324 }293 }
325 294
326 .sv-domain-chip-main {295 .sv-domain-chip-main {
327- display: grid;296+ display: inline-flex;
328- gap: 0.15rem;297+ gap: 0.3rem;
298+ align-items: baseline;
329 min-width: 0;299 min-width: 0;
330 }300 }
331 301
302+.sv-domain-meta {
303+ flex: 0 0 auto;
304+ white-space: nowrap;
305+}
306+
332 .sv-domain-meta,307 .sv-domain-meta,
333 .sv-history-meta,308 .sv-history-meta,
334 .sv-history-url {309 .sv-history-url {
@@ -619,6 +594,7 @@ body {
619 594
620 .sv-filter-chip button,595 .sv-filter-chip button,
621 .sv-domain-filter,596 .sv-domain-filter,
597+.sv-domain-delete,
622 .sv-icon-button {598 .sv-icon-button {
623 border: 0;599 border: 0;
624 background: transparent;600 background: transparent;
@@ -628,10 +604,32 @@ body {
628 }604 }
629 605
630 .sv-domain-filter {606 .sv-domain-filter {
607+ min-width: 0;
608+ overflow: hidden;
631 color: var(--sv-text, #e8ebf0);609 color: var(--sv-text, #e8ebf0);
632 font-weight: 700;610 font-weight: 700;
633 padding: 0;611 padding: 0;
634 text-align: left;612 text-align: left;
613+ text-overflow: ellipsis;
614+ white-space: nowrap;
615+}
616+
617+.sv-domain-delete {
618+ display: inline-grid;
619+ place-items: center;
620+ flex: 0 0 auto;
621+ width: 1rem;
622+ height: 1rem;
623+ padding: 0;
624+ border-radius: 999px;
625+ color: var(--sv-danger-text, #fecaca);
626+ font-size: 0.85rem;
627+ font-weight: 700;
628+ line-height: 1;
629+}
630+
631+.sv-domain-delete:hover {
632+ background: color-mix(in srgb, var(--sv-danger, #ef4444) 18%, transparent);
635 }633 }
636 634
637 .sv-icon-button {635 .sv-icon-button {
M internal/ui/systemviews/app.go +8 -6
@@ -513,10 +513,11 @@ func (a *App) loadHistoryLoadingRoute() {
513513 Error: a.historyError,
514514 Loading: true,
515515 }
516+ title := historyDocumentTitle(data)
516517 a.renderedHTML = renderAppFrame(renderedPage{
517518 route: RouteHistory,
518- title: "History — Loading",
519- subtitle: "Recent visits",
519+ title: title,
520+ subtitle: title,
520521 body: historyHTML(data),
521522 }, a.shellTheme)
522523 }
@@ -527,7 +528,7 @@ func (a *App) loadHistoryRoute(ctx context.Context) error {
527528 a.renderedHTML = renderAppFrame(renderedPage{
528529 route: a.currentRoute,
529530 title: routeDocumentTitle(RouteHistory),
530- subtitle: "Recent visits",
531+ subtitle: routeDocumentTitle(RouteHistory),
531532 body: placeholderHTML(a.currentRoute),
532533 }, a.shellTheme)
533534 return nil
@@ -585,7 +586,7 @@ func (a *App) renderHistoryRouteSnapshot(ctx context.Context, snapshot historyRo
585586 html: renderAppFrame(renderedPage{
586587 route: snapshot.route,
587588 title: routeDocumentTitle(RouteHistory),
588- subtitle: "Recent visits",
589+ subtitle: routeDocumentTitle(RouteHistory),
589590 body: placeholderHTML(snapshot.route),
590591 }, snapshot.shellTheme),
591592 }, nil
@@ -630,10 +631,11 @@ func (a *App) renderHistoryRouteSnapshot(ctx context.Context, snapshot historyRo
630631 Notice: snapshot.notice,
631632 Error: snapshot.error,
632633 }
634+ title := historyDocumentTitle(data)
633635 result.html = renderAppFrame(renderedPage{
634636 route: RouteHistory,
635- title: historyDocumentTitle(data),
636- subtitle: "Recent visits",
637+ title: title,
638+ subtitle: title,
637639 body: historyHTML(data),
638640 }, snapshot.shellTheme)
639641 return result, nil
@@ -513,10 +513,11 @@ func (a *App) loadHistoryLoadingRoute() {
513 Error: a.historyError,513 Error: a.historyError,
514 Loading: true,514 Loading: true,
515 }515 }
516+ title := historyDocumentTitle(data)
516 a.renderedHTML = renderAppFrame(renderedPage{517 a.renderedHTML = renderAppFrame(renderedPage{
517 route: RouteHistory,518 route: RouteHistory,
518- title: "History — Loading",519+ title: title,
519- subtitle: "Recent visits",520+ subtitle: title,
520 body: historyHTML(data),521 body: historyHTML(data),
521 }, a.shellTheme)522 }, a.shellTheme)
522 }523 }
@@ -527,7 +528,7 @@ func (a *App) loadHistoryRoute(ctx context.Context) error {
527 a.renderedHTML = renderAppFrame(renderedPage{528 a.renderedHTML = renderAppFrame(renderedPage{
528 route: a.currentRoute,529 route: a.currentRoute,
529 title: routeDocumentTitle(RouteHistory),530 title: routeDocumentTitle(RouteHistory),
530- subtitle: "Recent visits",531+ subtitle: routeDocumentTitle(RouteHistory),
531 body: placeholderHTML(a.currentRoute),532 body: placeholderHTML(a.currentRoute),
532 }, a.shellTheme)533 }, a.shellTheme)
533 return nil534 return nil
@@ -585,7 +586,7 @@ func (a *App) renderHistoryRouteSnapshot(ctx context.Context, snapshot historyRo
585 html: renderAppFrame(renderedPage{586 html: renderAppFrame(renderedPage{
586 route: snapshot.route,587 route: snapshot.route,
587 title: routeDocumentTitle(RouteHistory),588 title: routeDocumentTitle(RouteHistory),
588- subtitle: "Recent visits",589+ subtitle: routeDocumentTitle(RouteHistory),
589 body: placeholderHTML(snapshot.route),590 body: placeholderHTML(snapshot.route),
590 }, snapshot.shellTheme),591 }, snapshot.shellTheme),
591 }, nil592 }, nil
@@ -630,10 +631,11 @@ func (a *App) renderHistoryRouteSnapshot(ctx context.Context, snapshot historyRo
630 Notice: snapshot.notice,631 Notice: snapshot.notice,
631 Error: snapshot.error,632 Error: snapshot.error,
632 }633 }
634+ title := historyDocumentTitle(data)
633 result.html = renderAppFrame(renderedPage{635 result.html = renderAppFrame(renderedPage{
634 route: RouteHistory,636 route: RouteHistory,
635- title: historyDocumentTitle(data),637+ title: title,
636- subtitle: "Recent visits",638+ subtitle: title,
637 body: historyHTML(data),639 body: historyHTML(data),
638 }, snapshot.shellTheme)640 }, snapshot.shellTheme)
639 return result, nil641 return result, nil
M internal/ui/systemviews/app_test.go +5 -5
@@ -90,7 +90,7 @@ func TestAppLoadInitialHistoryRouteUsesStyledSections(t *testing.T) {
9090 assert.NotContains(t, app.renderedHTML, "<body")
9191 assert.Contains(t, app.renderedHTML, `class="sv-shell"`)
9292 assert.Contains(t, app.renderedHTML, `data-route="history"`)
93- assert.Contains(t, app.renderedHTML, `data-page-title="History — 1 entry"`)
93+ assert.Contains(t, app.renderedHTML, `data-page-title="History - 1 entry, 1 visit, 0 days"`)
9494 assert.Contains(t, app.renderedHTML, `sv-section`)
9595 assert.Contains(t, app.renderedHTML, `class="sv-list"`)
9696 }
@@ -110,10 +110,10 @@ func TestAppLoadInitialHistoryRouteShowsTotalStatsWithWindowedTimeline(t *testin
110110 require.NoError(t, app.LoadInitial(context.Background()))
111111 assert.True(t, history.statsCalled)
112112 assert.Contains(t, app.renderedHTML, "Loaded 2 items")
113- assert.Contains(t, app.renderedHTML, `data-page-title="History — 8904 entries"`)
114- assert.Contains(t, app.renderedHTML, `<span class="sv-stat-value">8904</span><span class="sv-stat-label">Entries</span>`)
115- assert.Contains(t, app.renderedHTML, `<span class="sv-stat-value">40947</span><span class="sv-stat-label">Visits</span>`)
116- assert.Contains(t, app.renderedHTML, `<span class="sv-stat-value">121</span><span class="sv-stat-label">Days</span>`)
113+ assert.Contains(t, app.renderedHTML, `data-page-title="History - 8904 entries, 40947 visits, 121 days"`)
114+ assert.Contains(t, app.renderedHTML, `<h1 class="sv-title">History - 8904 entries, 40947 visits, 121 days</h1>`)
115+ assert.NotContains(t, app.renderedHTML, `sv-history-summary`)
116+ assert.NotContains(t, app.renderedHTML, `sv-stat-value`)
117117 }
118118
119119 func TestAppRunMountsHistoryLoadingBeforeAsyncHydration(t *testing.T) {
@@ -90,7 +90,7 @@ func TestAppLoadInitialHistoryRouteUsesStyledSections(t *testing.T) {
90 assert.NotContains(t, app.renderedHTML, "<body")90 assert.NotContains(t, app.renderedHTML, "<body")
91 assert.Contains(t, app.renderedHTML, `class="sv-shell"`)91 assert.Contains(t, app.renderedHTML, `class="sv-shell"`)
92 assert.Contains(t, app.renderedHTML, `data-route="history"`)92 assert.Contains(t, app.renderedHTML, `data-route="history"`)
93- assert.Contains(t, app.renderedHTML, `data-page-title="History — 1 entry"`)93+ assert.Contains(t, app.renderedHTML, `data-page-title="History - 1 entry, 1 visit, 0 days"`)
94 assert.Contains(t, app.renderedHTML, `sv-section`)94 assert.Contains(t, app.renderedHTML, `sv-section`)
95 assert.Contains(t, app.renderedHTML, `class="sv-list"`)95 assert.Contains(t, app.renderedHTML, `class="sv-list"`)
96 }96 }
@@ -110,10 +110,10 @@ func TestAppLoadInitialHistoryRouteShowsTotalStatsWithWindowedTimeline(t *testin
110 require.NoError(t, app.LoadInitial(context.Background()))110 require.NoError(t, app.LoadInitial(context.Background()))
111 assert.True(t, history.statsCalled)111 assert.True(t, history.statsCalled)
112 assert.Contains(t, app.renderedHTML, "Loaded 2 items")112 assert.Contains(t, app.renderedHTML, "Loaded 2 items")
113- assert.Contains(t, app.renderedHTML, `data-page-title="History — 8904 entries"`)113+ assert.Contains(t, app.renderedHTML, `data-page-title="History - 8904 entries, 40947 visits, 121 days"`)
114- assert.Contains(t, app.renderedHTML, `<span class="sv-stat-value">8904</span><span class="sv-stat-label">Entries</span>`)114+ assert.Contains(t, app.renderedHTML, `<h1 class="sv-title">History - 8904 entries, 40947 visits, 121 days</h1>`)
115- assert.Contains(t, app.renderedHTML, `<span class="sv-stat-value">40947</span><span class="sv-stat-label">Visits</span>`)115+ assert.NotContains(t, app.renderedHTML, `sv-history-summary`)
116- assert.Contains(t, app.renderedHTML, `<span class="sv-stat-value">121</span><span class="sv-stat-label">Days</span>`)116+ assert.NotContains(t, app.renderedHTML, `sv-stat-value`)
117 }117 }
118 118
119 func TestAppRunMountsHistoryLoadingBeforeAsyncHydration(t *testing.T) {119 func TestAppRunMountsHistoryLoadingBeforeAsyncHydration(t *testing.T) {
M internal/ui/systemviews/history.templ +5 -32
@@ -12,14 +12,9 @@ templ HistoryView(data historyRenderData) {
1212 @Section("sv-history-controls", "Manage history") {
1313 @HistoryControls(data)
1414 }
15- <div class="sv-history-overview">
16- @Section("sv-history-summary", "History stats") {
17- @HistorySummary(data)
18- }
19- @Section("sv-history-domains", "Top domains") {
20- @HistoryDomains(data)
21- }
22- </div>
15+ @Section("sv-history-domains", "Top domains") {
16+ @HistoryDomains(data)
17+ }
2318 @Section("sv-history-timeline", "Recent visits") {
2419 @HistoryTimeline(data)
2520 }
@@ -57,28 +52,6 @@ templ HistoryControls(data historyRenderData) {
5752 </div>
5853 }
5954
60-templ HistorySummary(data historyRenderData) {
61- if historySummaryLoading(data) {
62- <div class="sv-stat-grid">
63- @LoadingCard("Entries")
64- @LoadingCard("Visits")
65- @LoadingCard("Days")
66- </div>
67- } else {
68- {{ entries, visits, days := historySummaryValues(data) }}
69- <div class="sv-stat-grid">
70- @HistoryStatCard("Entries", entries)
71- @HistoryStatCard("Visits", visits)
72- @HistoryStatCard("Days", days)
73- </div>
74- }
75-}
76-
77-
78-templ HistoryStatCard(label string, value int64) {
79- <div class="sv-stat-card"><span class="sv-stat-value">{ fmt.Sprintf("%d", value) }</span><span class="sv-stat-label">{ label }</span></div>
80-}
81-
8255 templ HistoryDomains(data historyRenderData) {
8356 if data.Loading {
8457 @LoadingPanel("Loading top domains…")
@@ -93,9 +66,9 @@ templ HistoryDomains(data historyRenderData) {
9366 <span class="sv-domain-chip">
9467 <span class="sv-domain-chip-main">
9568 <button type="button" class="sv-domain-filter" data-sv-action="history.filterDomain" data-domain={ domainKey }>{ displayDomain }</button>
96- <span class="sv-domain-meta">{ fmt.Sprintf("%d pages · %d visits", domain.PageCount, domain.TotalVisits) }</span>
69+ <span class="sv-domain-meta">{ fmt.Sprintf("%dp · %dv", domain.PageCount, domain.TotalVisits) }</span>
9770 </span>
98- <button type="button" class="sv-icon-button sv-danger" data-sv-action="history.deleteDomain" data-domain={ domainKey } data-sv-confirm={ "Delete all history for " + displayDomain + "?" } aria-label={ "Delete history for " + displayDomain }>@IconOnly(iconTrash, "Delete history for " + displayDomain)</button>
71+ <button type="button" class="sv-domain-delete" data-sv-action="history.deleteDomain" data-domain={ domainKey } data-sv-confirm={ "Delete all history for " + displayDomain + "?" } aria-label={ "Delete history for " + displayDomain }><span aria-hidden="true">×</span><span class="sv-sr-only">Delete history for { displayDomain }</span></button>
9972 </span>
10073 }
10174 }
@@ -12,14 +12,9 @@ templ HistoryView(data historyRenderData) {
12 @Section("sv-history-controls", "Manage history") {12 @Section("sv-history-controls", "Manage history") {
13 @HistoryControls(data)13 @HistoryControls(data)
14 }14 }
15- <div class="sv-history-overview">15+ @Section("sv-history-domains", "Top domains") {
16- @Section("sv-history-summary", "History stats") {16+ @HistoryDomains(data)
17- @HistorySummary(data)17+ }
18- }
19- @Section("sv-history-domains", "Top domains") {
20- @HistoryDomains(data)
21- }
22- </div>
23 @Section("sv-history-timeline", "Recent visits") {18 @Section("sv-history-timeline", "Recent visits") {
24 @HistoryTimeline(data)19 @HistoryTimeline(data)
25 }20 }
@@ -57,28 +52,6 @@ templ HistoryControls(data historyRenderData) {
57 </div>52 </div>
58 }53 }
59 54
60-templ HistorySummary(data historyRenderData) {
61- if historySummaryLoading(data) {
62- <div class="sv-stat-grid">
63- @LoadingCard("Entries")
64- @LoadingCard("Visits")
65- @LoadingCard("Days")
66- </div>
67- } else {
68- {{ entries, visits, days := historySummaryValues(data) }}
69- <div class="sv-stat-grid">
70- @HistoryStatCard("Entries", entries)
71- @HistoryStatCard("Visits", visits)
72- @HistoryStatCard("Days", days)
73- </div>
74- }
75-}
76-
77-
78-templ HistoryStatCard(label string, value int64) {
79- <div class="sv-stat-card"><span class="sv-stat-value">{ fmt.Sprintf("%d", value) }</span><span class="sv-stat-label">{ label }</span></div>
80-}
81-
82 templ HistoryDomains(data historyRenderData) {55 templ HistoryDomains(data historyRenderData) {
83 if data.Loading {56 if data.Loading {
84 @LoadingPanel("Loading top domains…")57 @LoadingPanel("Loading top domains…")
@@ -93,9 +66,9 @@ templ HistoryDomains(data historyRenderData) {
93 <span class="sv-domain-chip">66 <span class="sv-domain-chip">
94 <span class="sv-domain-chip-main">67 <span class="sv-domain-chip-main">
95 <button type="button" class="sv-domain-filter" data-sv-action="history.filterDomain" data-domain={ domainKey }>{ displayDomain }</button>68 <button type="button" class="sv-domain-filter" data-sv-action="history.filterDomain" data-domain={ domainKey }>{ displayDomain }</button>
96- <span class="sv-domain-meta">{ fmt.Sprintf("%d pages · %d visits", domain.PageCount, domain.TotalVisits) }</span>69+ <span class="sv-domain-meta">{ fmt.Sprintf("%dp · %dv", domain.PageCount, domain.TotalVisits) }</span>
97 </span>70 </span>
98- <button type="button" class="sv-icon-button sv-danger" data-sv-action="history.deleteDomain" data-domain={ domainKey } data-sv-confirm={ "Delete all history for " + displayDomain + "?" } aria-label={ "Delete history for " + displayDomain }>@IconOnly(iconTrash, "Delete history for " + displayDomain)</button>71+ <button type="button" class="sv-domain-delete" data-sv-action="history.deleteDomain" data-domain={ domainKey } data-sv-confirm={ "Delete all history for " + displayDomain + "?" } aria-label={ "Delete history for " + displayDomain }><span aria-hidden="true">×</span><span class="sv-sr-only">Delete history for { displayDomain }</span></button>
99 </span>72 </span>
100 }73 }
101 }74 }
M internal/ui/systemviews/history_actions.go +3 -2
@@ -235,10 +235,11 @@ func (a *App) handleHistoryLoadMore(ctx context.Context, event DOMAction) error
235235 if _, ok := dom.(DOMHistoryTimelineAppender); !ok {
236236 fullData := data
237237 fullData.Entries = a.historyEntries
238+ title := historyDocumentTitle(fullData)
238239 a.renderedHTML = renderAppFrame(renderedPage{
239240 route: RouteHistory,
240- title: historyDocumentTitle(fullData),
241- subtitle: "Recent visits",
241+ title: title,
242+ subtitle: title,
242243 body: historyHTML(fullData),
243244 }, a.shellTheme)
244245 fallbackHTML = a.renderedHTML
@@ -235,10 +235,11 @@ func (a *App) handleHistoryLoadMore(ctx context.Context, event DOMAction) error
235 if _, ok := dom.(DOMHistoryTimelineAppender); !ok {235 if _, ok := dom.(DOMHistoryTimelineAppender); !ok {
236 fullData := data236 fullData := data
237 fullData.Entries = a.historyEntries237 fullData.Entries = a.historyEntries
238+ title := historyDocumentTitle(fullData)
238 a.renderedHTML = renderAppFrame(renderedPage{239 a.renderedHTML = renderAppFrame(renderedPage{
239 route: RouteHistory,240 route: RouteHistory,
240- title: historyDocumentTitle(fullData),241+ title: title,
241- subtitle: "Recent visits",242+ subtitle: title,
242 body: historyHTML(fullData),243 body: historyHTML(fullData),
243 }, a.shellTheme)244 }, a.shellTheme)
244 fallbackHTML = a.renderedHTML245 fallbackHTML = a.renderedHTML
M internal/ui/systemviews/history_templ.go +212 -361 generated file: collapsed
M internal/ui/systemviews/history_view.go +13 -29
@@ -12,8 +12,6 @@ import (
1212
1313 const historyURLDisplayMaxRunes = 180
1414
15-const historySearchTitleMaxRunes = 48
16-
1715 type historyRenderData struct {
1816 Entries []*entity.HistoryEntry
1917 Stats *entity.HistoryStats
@@ -55,37 +53,23 @@ func historyTimelineAppendHTML(data historyRenderData) string {
5553
5654 func historyDocumentTitle(data historyRenderData) string {
5755 if data.Loading {
58- return "History — Loading"
59- }
60- if query := strings.TrimSpace(data.Query); query != "" {
61- return "History — search: " + truncateTitle(query, historySearchTitleMaxRunes)
56+ return "History - Loading"
6257 }
63- if domain := strings.TrimSpace(data.DomainFilter); domain != "" {
64- label := browserurl.DisplayDomain(domain)
65- if label == "" {
66- label = domain
67- }
68- return "History — " + label
69- }
70-
71- entries := int64(countHistoryEntries(data.Entries))
72- if data.Stats != nil {
73- entries = data.Stats.TotalEntries
74- } else if data.Analytics != nil {
75- entries = data.Analytics.TotalEntries
76- }
77- if entries == 1 {
78- return "History — 1 entry"
79- }
80- return fmt.Sprintf("History — %d entries", entries)
58+ entries, visits, days := historySummaryValues(data)
59+ return fmt.Sprintf(
60+ "History - %s, %s, %s",
61+ historyCountLabel(entries, "entry", "entries"),
62+ historyCountLabel(visits, "visit", "visits"),
63+ historyCountLabel(days, "day", "days"),
64+ )
8165 }
8266
83-func truncateTitle(value string, maxRunes int) string {
84- runes := []rune(strings.TrimSpace(value))
85- if maxRunes <= 0 || len(runes) <= maxRunes {
86- return string(runes)
67+func historyCountLabel(count int64, singular, plural string) string {
68+ label := plural
69+ if count == 1 {
70+ label = singular
8771 }
88- return string(runes[:maxRunes]) + "…"
72+ return fmt.Sprintf("%d %s", count, label)
8973 }
9074
9175 var historyCleanupRanges = []historyCleanupItem{
@@ -12,8 +12,6 @@ import (
12 12
13 const historyURLDisplayMaxRunes = 18013 const historyURLDisplayMaxRunes = 180
14 14
15-const historySearchTitleMaxRunes = 48
16-
17 type historyRenderData struct {15 type historyRenderData struct {
18 Entries []*entity.HistoryEntry16 Entries []*entity.HistoryEntry
19 Stats *entity.HistoryStats17 Stats *entity.HistoryStats
@@ -55,37 +53,23 @@ func historyTimelineAppendHTML(data historyRenderData) string {
55 53
56 func historyDocumentTitle(data historyRenderData) string {54 func historyDocumentTitle(data historyRenderData) string {
57 if data.Loading {55 if data.Loading {
58- return "History — Loading"56+ return "History - Loading"
59- }
60- if query := strings.TrimSpace(data.Query); query != "" {
61- return "History — search: " + truncateTitle(query, historySearchTitleMaxRunes)
62 }57 }
63- if domain := strings.TrimSpace(data.DomainFilter); domain != "" {58+ entries, visits, days := historySummaryValues(data)
64- label := browserurl.DisplayDomain(domain)59+ return fmt.Sprintf(
65- if label == "" {60+ "History - %s, %s, %s",
66- label = domain61+ historyCountLabel(entries, "entry", "entries"),
67- }62+ historyCountLabel(visits, "visit", "visits"),
68- return "History — " + label63+ historyCountLabel(days, "day", "days"),
69- }64+ )
70-
71- entries := int64(countHistoryEntries(data.Entries))
72- if data.Stats != nil {
73- entries = data.Stats.TotalEntries
74- } else if data.Analytics != nil {
75- entries = data.Analytics.TotalEntries
76- }
77- if entries == 1 {
78- return "History — 1 entry"
79- }
80- return fmt.Sprintf("History — %d entries", entries)
81 }65 }
82 66
83-func truncateTitle(value string, maxRunes int) string {67+func historyCountLabel(count int64, singular, plural string) string {
84- runes := []rune(strings.TrimSpace(value))68+ label := plural
85- if maxRunes <= 0 || len(runes) <= maxRunes {69+ if count == 1 {
86- return string(runes)70+ label = singular
87 }71 }
88- return string(runes[:maxRunes]) + "…"72+ return fmt.Sprintf("%d %s", count, label)
89 }73 }
90 74
91 var historyCleanupRanges = []historyCleanupItem{75 var historyCleanupRanges = []historyCleanupItem{

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