detached 789ddb7

fix(sidebar): refine search and tag controls

commit
789ddb7d1566f4fb4b4b07f4c57739b8e994c80e
author
brice · · 2026-08-12 06:36 UTC
parent
700b5d6
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
7 files · +33 -12
M internal/ui/component/favorites_sidebar_keyboard.go +4 -4
@@ -17,10 +17,10 @@ func (fs *FavoritesSidebar) setupKeyboardNavigation() {
1717 if keyController == nil {
1818 return
1919 }
20- // Let focused child widgets process their own keys before sidebar commands.
21- // In particular, GtkEntry consumes printable text during the target phase, so
22- // its content cannot reach the global command switch below.
23- keyController.SetPropagationPhase(gtk.PhaseBubbleValue)
20+ // Directional controls must run before GtkSearchEntry and GtkListBox consume
21+ // arrows. Explicit text-input focus tracking below still defers printable
22+ // keys to their focused entry.
23+ keyController.SetPropagationPhase(gtk.PhaseCaptureValue)
2424 keyPressedCb := func(_ gtk.EventControllerKey, keyval uint, _ uint, state gdk.ModifierType) bool {
2525 fs.mu.RLock()
2626 if fs.destroyed {
@@ -17,10 +17,10 @@ func (fs *FavoritesSidebar) setupKeyboardNavigation() {
17 if keyController == nil {17 if keyController == nil {
18 return18 return
19 }19 }
20- // Let focused child widgets process their own keys before sidebar commands.20+ // Directional controls must run before GtkSearchEntry and GtkListBox consume
21- // In particular, GtkEntry consumes printable text during the target phase, so21+ // arrows. Explicit text-input focus tracking below still defers printable
22- // its content cannot reach the global command switch below.22+ // keys to their focused entry.
23- keyController.SetPropagationPhase(gtk.PhaseBubbleValue)23+ keyController.SetPropagationPhase(gtk.PhaseCaptureValue)
24 keyPressedCb := func(_ gtk.EventControllerKey, keyval uint, _ uint, state gdk.ModifierType) bool {24 keyPressedCb := func(_ gtk.EventControllerKey, keyval uint, _ uint, state gdk.ModifierType) bool {
25 fs.mu.RLock()25 fs.mu.RLock()
26 if fs.destroyed {26 if fs.destroyed {
M internal/ui/component/favorites_sidebar_rows.go +1 -1
@@ -126,7 +126,7 @@ func (fs *FavoritesSidebar) appendTagFilter(
126126 }
127127 button.AddCssClass("favorites-sidebar-tag-filter")
128128 if selected {
129- button.AddCssClass("suggested-action")
129+ button.AddCssClass("favorites-sidebar-tag-filter-active")
130130 }
131131 callback := func(_ gtk.Button) { action() }
132132 *callbacks = append(*callbacks, callback)
@@ -126,7 +126,7 @@ func (fs *FavoritesSidebar) appendTagFilter(
126 }126 }
127 button.AddCssClass("favorites-sidebar-tag-filter")127 button.AddCssClass("favorites-sidebar-tag-filter")
128 if selected {128 if selected {
129- button.AddCssClass("suggested-action")129+ button.AddCssClass("favorites-sidebar-tag-filter-active")
130 }130 }
131 callback := func(_ gtk.Button) { action() }131 callback := func(_ gtk.Button) { action() }
132 *callbacks = append(*callbacks, callback)132 *callbacks = append(*callbacks, callback)
M internal/ui/component/favorites_sidebar_test.go +14 -3
@@ -433,9 +433,10 @@ func TestFavoritesSidebarFormTagSelectionAndEditSynchronization(t *testing.T) {
433433 assert.Equal(t, dev.ID, uc.untagged[0].tagID)
434434 }
435435
436-func TestFavoritesSidebarDirectionalFocusSkipsTagControls(t *testing.T) {
437- fs := newFavoritesSidebarHarness(nil, nil)
438- fs.displayRows = []favoriteSidebarDisplayRow{{FavoriteID: 1, Favorite: &entity.Favorite{ID: 1}, Selectable: true}}
436+func TestFavoritesSidebarDirectionalFocusMovesDirectlyBetweenSearchAndFirstFavorite(t *testing.T) {
437+ fav := &entity.Favorite{ID: 1, URL: "https://go.dev"}
438+ fs := newFavoritesSidebarHarness([]*entity.Favorite{fav}, nil)
439+ fs.displayRows = []favoriteSidebarDisplayRow{{FavoriteID: fav.ID, Favorite: fav, URL: fav.URL, Selectable: true}}
439440 fs.focusZone = favoritesSidebarFocusSearch
440441
441442 assert.True(t, fs.routeDirectionalFocus(uint(gdk.KEY_Down)))
@@ -444,6 +445,16 @@ func TestFavoritesSidebarDirectionalFocusSkipsTagControls(t *testing.T) {
444445 assert.Equal(t, favoritesSidebarFocusSearch, fs.focusZone)
445446 }
446447
448+func TestFavoritesSidebarDirectionalFocusConsumesArrowsOnTagControls(t *testing.T) {
449+ fs := newFavoritesSidebarHarness(nil, nil)
450+ fs.focusZone = favoritesSidebarFocusTags
451+
452+ assert.True(t, fs.routeDirectionalFocus(uint(gdk.KEY_Down)))
453+ assert.Equal(t, favoritesSidebarFocusTags, fs.focusZone)
454+ assert.True(t, fs.routeDirectionalFocus(uint(gdk.KEY_Up)))
455+ assert.Equal(t, favoritesSidebarFocusTags, fs.focusZone)
456+}
457+
447458 func TestFavoritesSidebarEnterConfirmsDelete(t *testing.T) {
448459 fav := &entity.Favorite{ID: 5, URL: "https://go.dev"}
449460 fs := newFavoritesSidebarHarness([]*entity.Favorite{fav}, nil)
@@ -433,9 +433,10 @@ func TestFavoritesSidebarFormTagSelectionAndEditSynchronization(t *testing.T) {
433 assert.Equal(t, dev.ID, uc.untagged[0].tagID)433 assert.Equal(t, dev.ID, uc.untagged[0].tagID)
434 }434 }
435 435
436-func TestFavoritesSidebarDirectionalFocusSkipsTagControls(t *testing.T) {436+func TestFavoritesSidebarDirectionalFocusMovesDirectlyBetweenSearchAndFirstFavorite(t *testing.T) {
437- fs := newFavoritesSidebarHarness(nil, nil)437+ fav := &entity.Favorite{ID: 1, URL: "https://go.dev"}
438- fs.displayRows = []favoriteSidebarDisplayRow{{FavoriteID: 1, Favorite: &entity.Favorite{ID: 1}, Selectable: true}}438+ fs := newFavoritesSidebarHarness([]*entity.Favorite{fav}, nil)
439+ fs.displayRows = []favoriteSidebarDisplayRow{{FavoriteID: fav.ID, Favorite: fav, URL: fav.URL, Selectable: true}}
439 fs.focusZone = favoritesSidebarFocusSearch440 fs.focusZone = favoritesSidebarFocusSearch
440 441
441 assert.True(t, fs.routeDirectionalFocus(uint(gdk.KEY_Down)))442 assert.True(t, fs.routeDirectionalFocus(uint(gdk.KEY_Down)))
@@ -444,6 +445,16 @@ func TestFavoritesSidebarDirectionalFocusSkipsTagControls(t *testing.T) {
444 assert.Equal(t, favoritesSidebarFocusSearch, fs.focusZone)445 assert.Equal(t, favoritesSidebarFocusSearch, fs.focusZone)
445 }446 }
446 447
448+func TestFavoritesSidebarDirectionalFocusConsumesArrowsOnTagControls(t *testing.T) {
449+ fs := newFavoritesSidebarHarness(nil, nil)
450+ fs.focusZone = favoritesSidebarFocusTags
451+
452+ assert.True(t, fs.routeDirectionalFocus(uint(gdk.KEY_Down)))
453+ assert.Equal(t, favoritesSidebarFocusTags, fs.focusZone)
454+ assert.True(t, fs.routeDirectionalFocus(uint(gdk.KEY_Up)))
455+ assert.Equal(t, favoritesSidebarFocusTags, fs.focusZone)
456+}
457+
447 func TestFavoritesSidebarEnterConfirmsDelete(t *testing.T) {458 func TestFavoritesSidebarEnterConfirmsDelete(t *testing.T) {
448 fav := &entity.Favorite{ID: 5, URL: "https://go.dev"}459 fav := &entity.Favorite{ID: 5, URL: "https://go.dev"}
449 fs := newFavoritesSidebarHarness([]*entity.Favorite{fav}, nil)460 fs := newFavoritesSidebarHarness([]*entity.Favorite{fav}, nil)
M internal/ui/theme/favorites_sidebar_css.go +1 -1
@@ -28,7 +28,7 @@ func generateFavoritesSidebarCSS(_ Palette) string {
2828 background-color: alpha(var(--accent), 0.10);
2929 }
3030
31-.favorites-sidebar-tag-filter.suggested-action {
31+.favorites-sidebar-tag-filter-active {
3232 color: var(--accent);
3333 background-color: alpha(var(--accent), 0.16);
3434 border-color: alpha(var(--accent), 0.5);
@@ -28,7 +28,7 @@ func generateFavoritesSidebarCSS(_ Palette) string {
28 background-color: alpha(var(--accent), 0.10);28 background-color: alpha(var(--accent), 0.10);
29 }29 }
30 30
31-.favorites-sidebar-tag-filter.suggested-action {31+.favorites-sidebar-tag-filter-active {
32 color: var(--accent);32 color: var(--accent);
33 background-color: alpha(var(--accent), 0.16);33 background-color: alpha(var(--accent), 0.16);
34 border-color: alpha(var(--accent), 0.5);34 border-color: alpha(var(--accent), 0.5);
M internal/ui/theme/favorites_sidebar_css_test.go +3 -1
@@ -13,7 +13,7 @@ func TestGenerateFavoritesSidebarCSS_ContainsCompactControlStyles(t *testing.T)
1313 for _, selector := range []string{
1414 ".favorites-sidebar-tags",
1515 ".favorites-sidebar-tag-filter",
16- ".favorites-sidebar-tag-filter.suggested-action",
16+ ".favorites-sidebar-tag-filter-active",
1717 ".favorites-sidebar-tag-add",
1818 ".favorites-sidebar-tag-prompt",
1919 ".favorites-sidebar-tag-binding",
@@ -25,6 +25,8 @@ func TestGenerateFavoritesSidebarCSS_ContainsCompactControlStyles(t *testing.T)
2525 } {
2626 assert.Contains(t, css, selector+" {")
2727 }
28+ assert.Contains(t, css, ".favorites-sidebar-tag-filter-active {", "active filters use sidebar-owned styling")
29+ assert.NotContains(t, css, ".suggested-action", "native suggested-action geometry must not affect tag filters")
2830 assert.Contains(t, css, "font-size: 0.66em;", "tags and shortcut badges stay compact")
2931 assert.Contains(t, css, "border-bottom: 0.0625em solid var(--border);", "filters are separated from the list")
3032 assert.Contains(t, css, "border-top: 0.0625em solid var(--border);", "the form is separated from the list")
@@ -13,7 +13,7 @@ func TestGenerateFavoritesSidebarCSS_ContainsCompactControlStyles(t *testing.T)
13 for _, selector := range []string{13 for _, selector := range []string{
14 ".favorites-sidebar-tags",14 ".favorites-sidebar-tags",
15 ".favorites-sidebar-tag-filter",15 ".favorites-sidebar-tag-filter",
16- ".favorites-sidebar-tag-filter.suggested-action",16+ ".favorites-sidebar-tag-filter-active",
17 ".favorites-sidebar-tag-add",17 ".favorites-sidebar-tag-add",
18 ".favorites-sidebar-tag-prompt",18 ".favorites-sidebar-tag-prompt",
19 ".favorites-sidebar-tag-binding",19 ".favorites-sidebar-tag-binding",
@@ -25,6 +25,8 @@ func TestGenerateFavoritesSidebarCSS_ContainsCompactControlStyles(t *testing.T)
25 } {25 } {
26 assert.Contains(t, css, selector+" {")26 assert.Contains(t, css, selector+" {")
27 }27 }
28+ assert.Contains(t, css, ".favorites-sidebar-tag-filter-active {", "active filters use sidebar-owned styling")
29+ assert.NotContains(t, css, ".suggested-action", "native suggested-action geometry must not affect tag filters")
28 assert.Contains(t, css, "font-size: 0.66em;", "tags and shortcut badges stay compact")30 assert.Contains(t, css, "font-size: 0.66em;", "tags and shortcut badges stay compact")
29 assert.Contains(t, css, "border-bottom: 0.0625em solid var(--border);", "filters are separated from the list")31 assert.Contains(t, css, "border-bottom: 0.0625em solid var(--border);", "filters are separated from the list")
30 assert.Contains(t, css, "border-top: 0.0625em solid var(--border);", "the form is separated from the list")32 assert.Contains(t, css, "border-top: 0.0625em solid var(--border);", "the form is separated from the list")
M internal/ui/theme/history_sidebar_css_test.go +7 -0
@@ -116,6 +116,13 @@ func TestGenerateSidebarCSS_CustomPaletteValuesInterpolated(t *testing.T) {
116116 assert.Contains(t, css, ".sidebar-empty {")
117117 }
118118
119+func TestGenerateSidebarCSS_UsesCompactBalancedSearchChrome(t *testing.T) {
120+ css := generateSidebarCSS(DefaultDarkPalette())
121+
122+ assert.Contains(t, css, ".sidebar-search-box {\n\tpadding: 0.5em 0.5em 0.375em;")
123+ assert.Contains(t, css, ".sidebar-search {\n\tmin-height: 0;\n\tpadding: 0.0625em 0.375em;")
124+}
125+
119126 func TestGenerateSidebarCSS_ContainsTransition(t *testing.T) {
120127 css := generateSidebarCSS(DefaultDarkPalette())
121128
@@ -116,6 +116,13 @@ func TestGenerateSidebarCSS_CustomPaletteValuesInterpolated(t *testing.T) {
116 assert.Contains(t, css, ".sidebar-empty {")116 assert.Contains(t, css, ".sidebar-empty {")
117 }117 }
118 118
119+func TestGenerateSidebarCSS_UsesCompactBalancedSearchChrome(t *testing.T) {
120+ css := generateSidebarCSS(DefaultDarkPalette())
121+
122+ assert.Contains(t, css, ".sidebar-search-box {\n\tpadding: 0.5em 0.5em 0.375em;")
123+ assert.Contains(t, css, ".sidebar-search {\n\tmin-height: 0;\n\tpadding: 0.0625em 0.375em;")
124+}
125+
119 func TestGenerateSidebarCSS_ContainsTransition(t *testing.T) {126 func TestGenerateSidebarCSS_ContainsTransition(t *testing.T) {
120 css := generateSidebarCSS(DefaultDarkPalette())127 css := generateSidebarCSS(DefaultDarkPalette())
121 128
M internal/ui/theme/sidebar_css.go +3 -2
@@ -12,13 +12,14 @@ func generateSidebarCSS(_ Palette) string {
1212 }
1313
1414 .sidebar-search-box {
15- padding: 0.375em 0.5em;
15+ padding: 0.5em 0.5em 0.375em;
1616 border-bottom: 0.0625em solid var(--border);
1717 background-color: var(--surface);
1818 }
1919
2020 .sidebar-search {
21- padding: 0.125em 0.375em;
21+ min-height: 0;
22+ padding: 0.0625em 0.375em;
2223 font-size: 0.85em;
2324 }
2425
@@ -12,13 +12,14 @@ func generateSidebarCSS(_ Palette) string {
12 }12 }
13 13
14 .sidebar-search-box {14 .sidebar-search-box {
15- padding: 0.375em 0.5em;15+ padding: 0.5em 0.5em 0.375em;
16 border-bottom: 0.0625em solid var(--border);16 border-bottom: 0.0625em solid var(--border);
17 background-color: var(--surface);17 background-color: var(--surface);
18 }18 }
19 19
20 .sidebar-search {20 .sidebar-search {
21- padding: 0.125em 0.375em;21+ min-height: 0;
22+ padding: 0.0625em 0.375em;
22 font-size: 0.85em;23 font-size: 0.85em;
23 }24 }
24 25

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