fix(systemviews): simplify history header layout
- commit
- 4e23726143761692a1941f4a8ca047adb6b12bda
- author
- brice · · 2026-04-26 07:07 UTC
- parent
- d685499
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
7 files · +287 -478
M assets/systemviews/systemviews.css +41 -43
| @@ -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 | 243 | .sv-stat-grid { | |
| 267 | 244 | display: grid; | |
| 268 | 245 | grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr)); | |
| @@ -270,12 +247,6 @@ body { | |||
| 270 | 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 | 250 | .sv-stat-card { | |
| 280 | 251 | display: grid; | |
| 281 | 252 | gap: 0.15rem; | |
| @@ -285,10 +256,6 @@ body { | |||
| 285 | 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 | 259 | .sv-stat-value { | |
| 293 | 260 | color: var(--sv-accent, #8ab4ff); | |
| 294 | 261 | font-size: 1.35rem; | |
| @@ -307,28 +274,36 @@ body { | |||
| 307 | 274 | .sv-domain-list { | |
| 308 | 275 | display: flex; | |
| 309 | 276 | 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; | |
| 312 | 281 | } | |
| 313 | 282 | ||
| 314 | 283 | .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; | |
| 321 | 290 | border: 1px solid var(--sv-border, #2a313d); | |
| 322 | 291 | border-radius: var(--sv-radius-chip, 0.45rem); | |
| 323 | 292 | background: var(--sv-surface-variant, #1d2129); | |
| 324 | 293 | } | |
| 325 | 294 | ||
| 326 | 295 | .sv-domain-chip-main { | |
| 327 | - | display: grid; | |
| 328 | - | gap: 0.15rem; | |
| 296 | + | display: inline-flex; | |
| 297 | + | gap: 0.3rem; | |
| 298 | + | align-items: baseline; | |
| 329 | 299 | min-width: 0; | |
| 330 | 300 | } | |
| 331 | 301 | ||
| 302 | + | .sv-domain-meta { | |
| 303 | + | flex: 0 0 auto; | |
| 304 | + | white-space: nowrap; | |
| 305 | + | } | |
| 306 | + | ||
| 332 | 307 | .sv-domain-meta, | |
| 333 | 308 | .sv-history-meta, | |
| 334 | 309 | .sv-history-url { | |
| @@ -619,6 +594,7 @@ body { | |||
| 619 | 594 | ||
| 620 | 595 | .sv-filter-chip button, | |
| 621 | 596 | .sv-domain-filter, | |
| 597 | + | .sv-domain-delete, | |
| 622 | 598 | .sv-icon-button { | |
| 623 | 599 | border: 0; | |
| 624 | 600 | background: transparent; | |
| @@ -628,10 +604,32 @@ body { | |||
| 628 | 604 | } | |
| 629 | 605 | ||
| 630 | 606 | .sv-domain-filter { | |
| 607 | + | min-width: 0; | |
| 608 | + | overflow: hidden; | |
| 631 | 609 | color: var(--sv-text, #e8ebf0); | |
| 632 | 610 | font-weight: 700; | |
| 633 | 611 | padding: 0; | |
| 634 | 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 | 635 | .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() { | |||
| 513 | 513 | Error: a.historyError, | |
| 514 | 514 | Loading: true, | |
| 515 | 515 | } | |
| 516 | + | title := historyDocumentTitle(data) | |
| 516 | 517 | a.renderedHTML = renderAppFrame(renderedPage{ | |
| 517 | 518 | route: RouteHistory, | |
| 518 | - | title: "History — Loading", | |
| 519 | - | subtitle: "Recent visits", | |
| 519 | + | title: title, | |
| 520 | + | subtitle: title, | |
| 520 | 521 | body: historyHTML(data), | |
| 521 | 522 | }, a.shellTheme) | |
| 522 | 523 | } | |
| @@ -527,7 +528,7 @@ func (a *App) loadHistoryRoute(ctx context.Context) error { | |||
| 527 | 528 | a.renderedHTML = renderAppFrame(renderedPage{ | |
| 528 | 529 | route: a.currentRoute, | |
| 529 | 530 | title: routeDocumentTitle(RouteHistory), | |
| 530 | - | subtitle: "Recent visits", | |
| 531 | + | subtitle: routeDocumentTitle(RouteHistory), | |
| 531 | 532 | body: placeholderHTML(a.currentRoute), | |
| 532 | 533 | }, a.shellTheme) | |
| 533 | 534 | return nil | |
| @@ -585,7 +586,7 @@ func (a *App) renderHistoryRouteSnapshot(ctx context.Context, snapshot historyRo | |||
| 585 | 586 | html: renderAppFrame(renderedPage{ | |
| 586 | 587 | route: snapshot.route, | |
| 587 | 588 | title: routeDocumentTitle(RouteHistory), | |
| 588 | - | subtitle: "Recent visits", | |
| 589 | + | subtitle: routeDocumentTitle(RouteHistory), | |
| 589 | 590 | body: placeholderHTML(snapshot.route), | |
| 590 | 591 | }, snapshot.shellTheme), | |
| 591 | 592 | }, nil | |
| @@ -630,10 +631,11 @@ func (a *App) renderHistoryRouteSnapshot(ctx context.Context, snapshot historyRo | |||
| 630 | 631 | Notice: snapshot.notice, | |
| 631 | 632 | Error: snapshot.error, | |
| 632 | 633 | } | |
| 634 | + | title := historyDocumentTitle(data) | |
| 633 | 635 | result.html = renderAppFrame(renderedPage{ | |
| 634 | 636 | route: RouteHistory, | |
| 635 | - | title: historyDocumentTitle(data), | |
| 636 | - | subtitle: "Recent visits", | |
| 637 | + | title: title, | |
| 638 | + | subtitle: title, | |
| 637 | 639 | body: historyHTML(data), | |
| 638 | 640 | }, snapshot.shellTheme) | |
| 639 | 641 | 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 nil | 534 | 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 | }, nil | 592 | }, 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, nil | 641 | return result, nil | ||
M internal/ui/systemviews/app_test.go +5 -5
| @@ -90,7 +90,7 @@ func TestAppLoadInitialHistoryRouteUsesStyledSections(t *testing.T) { | |||
| 90 | 90 | assert.NotContains(t, app.renderedHTML, "<body") | |
| 91 | 91 | assert.Contains(t, app.renderedHTML, `class="sv-shell"`) | |
| 92 | 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 | 94 | assert.Contains(t, app.renderedHTML, `sv-section`) | |
| 95 | 95 | assert.Contains(t, app.renderedHTML, `class="sv-list"`) | |
| 96 | 96 | } | |
| @@ -110,10 +110,10 @@ func TestAppLoadInitialHistoryRouteShowsTotalStatsWithWindowedTimeline(t *testin | |||
| 110 | 110 | require.NoError(t, app.LoadInitial(context.Background())) | |
| 111 | 111 | assert.True(t, history.statsCalled) | |
| 112 | 112 | 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`) | |
| 117 | 117 | } | |
| 118 | 118 | ||
| 119 | 119 | 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) { | |||
| 12 | 12 | @Section("sv-history-controls", "Manage history") { | |
| 13 | 13 | @HistoryControls(data) | |
| 14 | 14 | } | |
| 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 | + | } | |
| 23 | 18 | @Section("sv-history-timeline", "Recent visits") { | |
| 24 | 19 | @HistoryTimeline(data) | |
| 25 | 20 | } | |
| @@ -57,28 +52,6 @@ templ HistoryControls(data historyRenderData) { | |||
| 57 | 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 | 55 | templ HistoryDomains(data historyRenderData) { | |
| 83 | 56 | if data.Loading { | |
| 84 | 57 | @LoadingPanel("Loading top domains…") | |
| @@ -93,9 +66,9 @@ templ HistoryDomains(data historyRenderData) { | |||
| 93 | 66 | <span class="sv-domain-chip"> | |
| 94 | 67 | <span class="sv-domain-chip-main"> | |
| 95 | 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 | 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 | 72 | </span> | |
| 100 | 73 | } | |
| 101 | 74 | } | |
| @@ -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 | |||
| 235 | 235 | if _, ok := dom.(DOMHistoryTimelineAppender); !ok { | |
| 236 | 236 | fullData := data | |
| 237 | 237 | fullData.Entries = a.historyEntries | |
| 238 | + | title := historyDocumentTitle(fullData) | |
| 238 | 239 | a.renderedHTML = renderAppFrame(renderedPage{ | |
| 239 | 240 | route: RouteHistory, | |
| 240 | - | title: historyDocumentTitle(fullData), | |
| 241 | - | subtitle: "Recent visits", | |
| 241 | + | title: title, | |
| 242 | + | subtitle: title, | |
| 242 | 243 | body: historyHTML(fullData), | |
| 243 | 244 | }, a.shellTheme) | |
| 244 | 245 | 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 := data | 236 | fullData := data | ||
| 237 | fullData.Entries = a.historyEntries | 237 | 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.renderedHTML | 245 | 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 ( | |||
| 12 | 12 | ||
| 13 | 13 | const historyURLDisplayMaxRunes = 180 | |
| 14 | 14 | ||
| 15 | - | const historySearchTitleMaxRunes = 48 | |
| 16 | - | ||
| 17 | 15 | type historyRenderData struct { | |
| 18 | 16 | Entries []*entity.HistoryEntry | |
| 19 | 17 | Stats *entity.HistoryStats | |
| @@ -55,37 +53,23 @@ func historyTimelineAppendHTML(data historyRenderData) string { | |||
| 55 | 53 | ||
| 56 | 54 | func historyDocumentTitle(data historyRenderData) string { | |
| 57 | 55 | 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" | |
| 62 | 57 | } | |
| 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 | + | ) | |
| 81 | 65 | } | |
| 82 | 66 | ||
| 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 | |
| 87 | 71 | } | |
| 88 | - | return string(runes[:maxRunes]) + "…" | |
| 72 | + | return fmt.Sprintf("%d %s", count, label) | |
| 89 | 73 | } | |
| 90 | 74 | ||
| 91 | 75 | var historyCleanupRanges = []historyCleanupItem{ | |
| @@ -12,8 +12,6 @@ import ( | |||||
| 12 | 12 | ||||
| 13 | const historyURLDisplayMaxRunes = 180 | 13 | const historyURLDisplayMaxRunes = 180 | ||
| 14 | 14 | ||||
| 15 | - | const historySearchTitleMaxRunes = 48 | |||
| 16 | - | ||||
| 17 | type historyRenderData struct { | 15 | type historyRenderData struct { | ||
| 18 | Entries []*entity.HistoryEntry | 16 | Entries []*entity.HistoryEntry | ||
| 19 | Stats *entity.HistoryStats | 17 | 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 = domain | 61 | + | historyCountLabel(entries, "entry", "entries"), |
| 67 | - | } | 62 | + | historyCountLabel(visits, "visit", "visits"), |
| 68 | - | return "History — " + label | 63 | + | 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{ | ||