fix(systemviews): refine history overview layout
- commit
- d68549983bcfd892c1ba1ac86a28d1fb4a71b42b
- author
- brice · · 2026-04-26 06:50 UTC
- parent
- 95983dd
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
5 files · +166 -110
M assets/systemviews/systemviews.css +35 -0
| @@ -240,6 +240,29 @@ 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 | + | ||
| 243 | 266 | .sv-stat-grid { | |
| 244 | 267 | display: grid; | |
| 245 | 268 | grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr)); | |
| @@ -247,6 +270,12 @@ body { | |||
| 247 | 270 | margin-top: 0.75rem; | |
| 248 | 271 | } | |
| 249 | 272 | ||
| 273 | + | .sv-history-overview .sv-stat-grid { | |
| 274 | + | grid-template-columns: 1fr; | |
| 275 | + | gap: 0.55rem; | |
| 276 | + | margin-top: 0.5rem; | |
| 277 | + | } | |
| 278 | + | ||
| 250 | 279 | .sv-stat-card { | |
| 251 | 280 | display: grid; | |
| 252 | 281 | gap: 0.15rem; | |
| @@ -256,10 +285,16 @@ body { | |||
| 256 | 285 | background: var(--sv-surface-variant, #1d2129); | |
| 257 | 286 | } | |
| 258 | 287 | ||
| 288 | + | .sv-history-overview .sv-stat-card { | |
| 289 | + | padding: 0.65rem 0.75rem; | |
| 290 | + | } | |
| 291 | + | ||
| 259 | 292 | .sv-stat-value { | |
| 260 | 293 | color: var(--sv-accent, #8ab4ff); | |
| 261 | 294 | font-size: 1.35rem; | |
| 262 | 295 | font-weight: 700; | |
| 296 | + | line-height: 1.05; | |
| 297 | + | font-variant-numeric: tabular-nums; | |
| 263 | 298 | } | |
| 264 | 299 | ||
| 265 | 300 | .sv-stat-label { | |
| @@ -240,6 +240,29 @@ 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 | + | ||||
| 243 | .sv-stat-grid { | 266 | .sv-stat-grid { | ||
| 244 | display: grid; | 267 | display: grid; | ||
| 245 | grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr)); | 268 | grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr)); | ||
| @@ -247,6 +270,12 @@ body { | |||||
| 247 | margin-top: 0.75rem; | 270 | margin-top: 0.75rem; | ||
| 248 | } | 271 | } | ||
| 249 | 272 | ||||
| 273 | + | .sv-history-overview .sv-stat-grid { | |||
| 274 | + | grid-template-columns: 1fr; | |||
| 275 | + | gap: 0.55rem; | |||
| 276 | + | margin-top: 0.5rem; | |||
| 277 | + | } | |||
| 278 | + | ||||
| 250 | .sv-stat-card { | 279 | .sv-stat-card { | ||
| 251 | display: grid; | 280 | display: grid; | ||
| 252 | gap: 0.15rem; | 281 | gap: 0.15rem; | ||
| @@ -256,10 +285,16 @@ body { | |||||
| 256 | background: var(--sv-surface-variant, #1d2129); | 285 | background: var(--sv-surface-variant, #1d2129); | ||
| 257 | } | 286 | } | ||
| 258 | 287 | ||||
| 288 | + | .sv-history-overview .sv-stat-card { | |||
| 289 | + | padding: 0.65rem 0.75rem; | |||
| 290 | + | } | |||
| 291 | + | ||||
| 259 | .sv-stat-value { | 292 | .sv-stat-value { | ||
| 260 | color: var(--sv-accent, #8ab4ff); | 293 | color: var(--sv-accent, #8ab4ff); | ||
| 261 | font-size: 1.35rem; | 294 | font-size: 1.35rem; | ||
| 262 | font-weight: 700; | 295 | font-weight: 700; | ||
| 296 | + | line-height: 1.05; | |||
| 297 | + | font-variant-numeric: tabular-nums; | |||
| 263 | } | 298 | } | ||
| 264 | 299 | ||||
| 265 | .sv-stat-label { | 300 | .sv-stat-label { | ||
M internal/ui/systemviews/app_test.go +2 -2
| @@ -309,8 +309,8 @@ func TestAppLoadInitialHistoryRouteRendersManagementActions(t *testing.T) { | |||
| 309 | 309 | assert.Contains(t, app.renderedHTML, `data-sv-action="history.deleteDomain"`) | |
| 310 | 310 | assert.Contains(t, app.renderedHTML, `data-domain="example.com:8080"`) | |
| 311 | 311 | assert.Contains(t, app.renderedHTML, `>example.com</button>`) | |
| 312 | - | assert.Contains(t, app.renderedHTML, "Keys:") | |
| 313 | - | assert.Contains(t, app.renderedHTML, "Enter") | |
| 312 | + | assert.Contains(t, app.renderedHTML, `data-sv-history-search data-sv-autofocus`) | |
| 313 | + | assert.NotContains(t, app.renderedHTML, "Keys:") | |
| 314 | 314 | } | |
| 315 | 315 | ||
| 316 | 316 | func TestAppHandleHistoryActionsRefreshesDOM(t *testing.T) { | |
| @@ -309,8 +309,8 @@ func TestAppLoadInitialHistoryRouteRendersManagementActions(t *testing.T) { | |||||
| 309 | assert.Contains(t, app.renderedHTML, `data-sv-action="history.deleteDomain"`) | 309 | assert.Contains(t, app.renderedHTML, `data-sv-action="history.deleteDomain"`) | ||
| 310 | assert.Contains(t, app.renderedHTML, `data-domain="example.com:8080"`) | 310 | assert.Contains(t, app.renderedHTML, `data-domain="example.com:8080"`) | ||
| 311 | assert.Contains(t, app.renderedHTML, `>example.com</button>`) | 311 | assert.Contains(t, app.renderedHTML, `>example.com</button>`) | ||
| 312 | - | assert.Contains(t, app.renderedHTML, "Keys:") | 312 | + | assert.Contains(t, app.renderedHTML, `data-sv-history-search data-sv-autofocus`) |
| 313 | - | assert.Contains(t, app.renderedHTML, "Enter") | 313 | + | assert.NotContains(t, app.renderedHTML, "Keys:") |
| 314 | } | 314 | } | ||
| 315 | 315 | ||||
| 316 | func TestAppHandleHistoryActionsRefreshesDOM(t *testing.T) { | 316 | func TestAppHandleHistoryActionsRefreshesDOM(t *testing.T) { | ||
M internal/ui/systemviews/dom_js.go +12 -0
| @@ -43,6 +43,7 @@ func (d *browserDOM) Mount(markup string) error { | |||
| 43 | 43 | d.updateDocumentTitle() | |
| 44 | 44 | d.scheduleAlertDismissal() | |
| 45 | 45 | d.syncPerformanceCustomInputStates() | |
| 46 | + | d.focusAutofocusTarget() | |
| 46 | 47 | d.setupHistoryInfiniteScroll() | |
| 47 | 48 | return nil | |
| 48 | 49 | } | |
| @@ -116,6 +117,17 @@ func (d *browserDOM) updateDocumentTitle() { | |||
| 116 | 117 | d.document.Set("title", title) | |
| 117 | 118 | } | |
| 118 | 119 | ||
| 120 | + | func (d *browserDOM) focusAutofocusTarget() { | |
| 121 | + | if d == nil || !d.target.Truthy() { | |
| 122 | + | return | |
| 123 | + | } | |
| 124 | + | target := d.target.Call("querySelector", "[data-sv-autofocus]") | |
| 125 | + | if !target.Truthy() || !target.Get("focus").Truthy() { | |
| 126 | + | return | |
| 127 | + | } | |
| 128 | + | target.Call("focus") | |
| 129 | + | } | |
| 130 | + | ||
| 119 | 131 | func (d *browserDOM) scheduleAlertDismissal() { | |
| 120 | 132 | if d == nil || !d.target.Truthy() { | |
| 121 | 133 | return | |
| @@ -43,6 +43,7 @@ func (d *browserDOM) Mount(markup string) error { | |||||
| 43 | d.updateDocumentTitle() | 43 | d.updateDocumentTitle() | ||
| 44 | d.scheduleAlertDismissal() | 44 | d.scheduleAlertDismissal() | ||
| 45 | d.syncPerformanceCustomInputStates() | 45 | d.syncPerformanceCustomInputStates() | ||
| 46 | + | d.focusAutofocusTarget() | |||
| 46 | d.setupHistoryInfiniteScroll() | 47 | d.setupHistoryInfiniteScroll() | ||
| 47 | return nil | 48 | return nil | ||
| 48 | } | 49 | } | ||
| @@ -116,6 +117,17 @@ func (d *browserDOM) updateDocumentTitle() { | |||||
| 116 | d.document.Set("title", title) | 117 | d.document.Set("title", title) | ||
| 117 | } | 118 | } | ||
| 118 | 119 | ||||
| 120 | + | func (d *browserDOM) focusAutofocusTarget() { | |||
| 121 | + | if d == nil || !d.target.Truthy() { | |||
| 122 | + | return | |||
| 123 | + | } | |||
| 124 | + | target := d.target.Call("querySelector", "[data-sv-autofocus]") | |||
| 125 | + | if !target.Truthy() || !target.Get("focus").Truthy() { | |||
| 126 | + | return | |||
| 127 | + | } | |||
| 128 | + | target.Call("focus") | |||
| 129 | + | } | |||
| 130 | + | ||||
| 119 | func (d *browserDOM) scheduleAlertDismissal() { | 131 | func (d *browserDOM) scheduleAlertDismissal() { | ||
| 120 | if d == nil || !d.target.Truthy() { | 132 | if d == nil || !d.target.Truthy() { | ||
| 121 | return | 133 | return | ||
M internal/ui/systemviews/history.templ +9 -8
| @@ -12,12 +12,14 @@ templ HistoryView(data historyRenderData) { | |||
| 12 | 12 | @Section("sv-history-controls", "Manage history") { | |
| 13 | 13 | @HistoryControls(data) | |
| 14 | 14 | } | |
| 15 | - | @Section("sv-history-summary", "History") { | |
| 16 | - | @HistorySummary(data) | |
| 17 | - | } | |
| 18 | - | @Section("sv-history-domains", "Top domains") { | |
| 19 | - | @HistoryDomains(data) | |
| 20 | - | } | |
| 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> | |
| 21 | 23 | @Section("sv-history-timeline", "Recent visits") { | |
| 22 | 24 | @HistoryTimeline(data) | |
| 23 | 25 | } | |
| @@ -27,7 +29,7 @@ templ HistoryControls(data historyRenderData) { | |||
| 27 | 29 | <form class="sv-toolbar sv-history-search" data-sv-action="history.search"> | |
| 28 | 30 | <label class="sv-search-label"> | |
| 29 | 31 | <span>Search</span> | |
| 30 | - | <input data-sv-history-search name="query" type="search" value={ data.Query } placeholder="Search title or URL" autocomplete="off"/> | |
| 32 | + | <input data-sv-history-search data-sv-autofocus name="query" type="search" value={ data.Query } placeholder="Search title or URL" autocomplete="off" autofocus/> | |
| 31 | 33 | </label> | |
| 32 | 34 | <button class="sv-button" type="submit">@ButtonIconLabel(iconSearch, "Search")</button> | |
| 33 | 35 | <button class="sv-button sv-button-secondary" type="button" data-sv-action="history.clear">@ButtonIconLabel(iconX, "Clear")</button> | |
| @@ -53,7 +55,6 @@ templ HistoryControls(data historyRenderData) { | |||
| 53 | 55 | } | |
| 54 | 56 | </div> | |
| 55 | 57 | </div> | |
| 56 | - | <p class="sv-meta sv-key-hints">Keys: <kbd>/</kbd> search, <kbd>j</kbd>/<kbd>k</kbd> move, <kbd>Enter</kbd> open, <kbd>d</kbd> delete focused entry.</p> | |
| 57 | 58 | } | |
| 58 | 59 | ||
| 59 | 60 | templ HistorySummary(data historyRenderData) { | |
| @@ -12,12 +12,14 @@ 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 | - | @Section("sv-history-summary", "History") { | 15 | + | <div class="sv-history-overview"> |
| 16 | - | @HistorySummary(data) | 16 | + | @Section("sv-history-summary", "History stats") { |
| 17 | - | } | 17 | + | @HistorySummary(data) |
| 18 | - | @Section("sv-history-domains", "Top domains") { | 18 | + | } |
| 19 | - | @HistoryDomains(data) | 19 | + | @Section("sv-history-domains", "Top domains") { |
| 20 | - | } | 20 | + | @HistoryDomains(data) |
| 21 | + | } | |||
| 22 | + | </div> | |||
| 21 | @Section("sv-history-timeline", "Recent visits") { | 23 | @Section("sv-history-timeline", "Recent visits") { | ||
| 22 | @HistoryTimeline(data) | 24 | @HistoryTimeline(data) | ||
| 23 | } | 25 | } | ||
| @@ -27,7 +29,7 @@ templ HistoryControls(data historyRenderData) { | |||||
| 27 | <form class="sv-toolbar sv-history-search" data-sv-action="history.search"> | 29 | <form class="sv-toolbar sv-history-search" data-sv-action="history.search"> | ||
| 28 | <label class="sv-search-label"> | 30 | <label class="sv-search-label"> | ||
| 29 | <span>Search</span> | 31 | <span>Search</span> | ||
| 30 | - | <input data-sv-history-search name="query" type="search" value={ data.Query } placeholder="Search title or URL" autocomplete="off"/> | 32 | + | <input data-sv-history-search data-sv-autofocus name="query" type="search" value={ data.Query } placeholder="Search title or URL" autocomplete="off" autofocus/> |
| 31 | </label> | 33 | </label> | ||
| 32 | <button class="sv-button" type="submit">@ButtonIconLabel(iconSearch, "Search")</button> | 34 | <button class="sv-button" type="submit">@ButtonIconLabel(iconSearch, "Search")</button> | ||
| 33 | <button class="sv-button sv-button-secondary" type="button" data-sv-action="history.clear">@ButtonIconLabel(iconX, "Clear")</button> | 35 | <button class="sv-button sv-button-secondary" type="button" data-sv-action="history.clear">@ButtonIconLabel(iconX, "Clear")</button> | ||
| @@ -53,7 +55,6 @@ templ HistoryControls(data historyRenderData) { | |||||
| 53 | } | 55 | } | ||
| 54 | </div> | 56 | </div> | ||
| 55 | </div> | 57 | </div> | ||
| 56 | - | <p class="sv-meta sv-key-hints">Keys: <kbd>/</kbd> search, <kbd>j</kbd>/<kbd>k</kbd> move, <kbd>Enter</kbd> open, <kbd>d</kbd> delete focused entry.</p> | |||
| 57 | } | 58 | } | ||
| 58 | 59 | ||||
| 59 | templ HistorySummary(data historyRenderData) { | 60 | templ HistorySummary(data historyRenderData) { | ||