feat(browser): add configurable active pane borders with smooth transitions
- Make border color configurable via workspace.styling.border_color config - Support GTK theme variables (@theme_selected_bg_color) and custom colors - Replace border-jump CSS with smooth outline-color transitions - Add base transparent outline to prevent visual jumps - Fallback to theme-appropriate colors (dark: #4A90E2, light: #87CEEB)
- commit
- 365d0f44da28ff5d3b8d3d144b0fc6d2dfb8118b
- author
- Brice · · 2025-09-27 21:47 UTC
- parent
- 7c100fb
- signature
- unsigned
1 file · +28 -14
M internal/app/browser/workspace_css.go +28 -14
| @@ -3,6 +3,7 @@ package browser | |||
| 3 | 3 | ||
| 4 | 4 | import ( | |
| 5 | 5 | "fmt" | |
| 6 | + | "strings" | |
| 6 | 7 | ||
| 7 | 8 | "github.com/bnema/dumber/internal/config" | |
| 8 | 9 | "github.com/bnema/dumber/pkg/webkit" | |
| @@ -34,8 +35,19 @@ func getStackTitleTextColor(isDark bool) string { | |||
| 34 | 35 | return "#333333" | |
| 35 | 36 | } | |
| 36 | 37 | ||
| 37 | - | // getActivePaneBorderColor returns the border color for active panes based on theme | |
| 38 | - | func getActivePaneBorderColor(isDark bool) string { | |
| 38 | + | // getActivePaneBorderColor returns the border color for active panes based on config and theme | |
| 39 | + | func getActivePaneBorderColor(styling config.WorkspaceStylingConfig, isDark bool) string { | |
| 40 | + | // Use configured border color if set | |
| 41 | + | if styling.BorderColor != "" { | |
| 42 | + | // Check if it's a GTK theme variable (starts with @) | |
| 43 | + | if strings.HasPrefix(styling.BorderColor, "@") { | |
| 44 | + | return styling.BorderColor | |
| 45 | + | } | |
| 46 | + | // Return the configured color as-is (could be hex, rgb, etc.) | |
| 47 | + | return styling.BorderColor | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | // Fallback to hardcoded colors based on theme | |
| 39 | 51 | if isDark { | |
| 40 | 52 | return "#4A90E2" // Deeper blue pastel for dark theme | |
| 41 | 53 | } | |
| @@ -58,30 +70,33 @@ func (wm *WorkspaceManager) generateWorkspaceCSS() string { | |||
| 58 | 70 | windowBackgroundColor = "#ffffff" // Light window background | |
| 59 | 71 | } | |
| 60 | 72 | ||
| 61 | - | activeBorderColor := getActivePaneBorderColor(isDark) | |
| 73 | + | activeBorderColor := getActivePaneBorderColor(styling, isDark) | |
| 62 | 74 | ||
| 63 | 75 | css := fmt.Sprintf(`window { | |
| 64 | 76 | background-color: %s; | |
| 65 | 77 | } | |
| 66 | 78 | ||
| 67 | - | /* Active pane border styling using outline (no layout impact) */ | |
| 68 | - | .workspace-pane-active { | |
| 69 | - | outline: 2px solid %s; | |
| 79 | + | /* Base pane styling - transparent outline for smooth transitions */ | |
| 80 | + | .workspace-pane, .stacked-pane-container { | |
| 81 | + | outline: 2px solid transparent; | |
| 70 | 82 | outline-offset: -2px; | |
| 71 | 83 | transition: outline-color %dms ease-in-out; | |
| 72 | 84 | } | |
| 73 | 85 | ||
| 86 | + | /* Active pane border styling using outline (no layout impact) */ | |
| 87 | + | .workspace-pane-active { | |
| 88 | + | outline-color: %s; | |
| 89 | + | } | |
| 90 | + | ||
| 74 | 91 | /* Stacked panes styling */ | |
| 75 | 92 | .stacked-pane-container { | |
| 76 | 93 | background-color: %s; | |
| 77 | 94 | border-radius: %dpx; | |
| 78 | 95 | } | |
| 79 | 96 | ||
| 80 | - | /* Active stacked pane container gets the outline */ | |
| 97 | + | /* Active stacked pane container gets the outline color */ | |
| 81 | 98 | .stacked-pane-container.workspace-pane-active { | |
| 82 | - | outline: 2px solid %s; | |
| 83 | - | outline-offset: -2px; | |
| 84 | - | transition: outline-color %dms ease-in-out; | |
| 99 | + | outline-color: %s; | |
| 85 | 100 | } | |
| 86 | 101 | ||
| 87 | 102 | .stacked-pane-title { | |
| @@ -110,12 +125,11 @@ func (wm *WorkspaceManager) generateWorkspaceCSS() string { | |||
| 110 | 125 | /* Collapsed panes are hidden - handled in code via widget visibility */ | |
| 111 | 126 | }`, | |
| 112 | 127 | windowBackgroundColor, // window background | |
| 113 | - | activeBorderColor, // workspace-pane-active border | |
| 114 | - | styling.TransitionDuration, // workspace-pane-active transition | |
| 128 | + | styling.TransitionDuration, // base pane outline transition | |
| 129 | + | activeBorderColor, // workspace-pane-active outline-color | |
| 115 | 130 | windowBackgroundColor, // stacked-pane-container background | |
| 116 | 131 | styling.BorderRadius, // stacked-pane-container border-radius | |
| 117 | - | activeBorderColor, // stacked-pane-container.workspace-pane-active border | |
| 118 | - | styling.TransitionDuration, // stacked-pane-container.workspace-pane-active transition | |
| 132 | + | activeBorderColor, // stacked-pane-container.workspace-pane-active outline-color | |
| 119 | 133 | getStackTitleBg(isDark), // stacked-pane-title background | |
| 120 | 134 | inactiveBorderColor, // stacked-pane-title border-bottom | |
| 121 | 135 | styling.TransitionDuration, // stacked-pane-title transition | |
| @@ -3,6 +3,7 @@ package browser | |||||
| 3 | 3 | ||||
| 4 | import ( | 4 | import ( | ||
| 5 | "fmt" | 5 | "fmt" | ||
| 6 | + | "strings" | |||
| 6 | 7 | ||||
| 7 | "github.com/bnema/dumber/internal/config" | 8 | "github.com/bnema/dumber/internal/config" | ||
| 8 | "github.com/bnema/dumber/pkg/webkit" | 9 | "github.com/bnema/dumber/pkg/webkit" | ||
| @@ -34,8 +35,19 @@ func getStackTitleTextColor(isDark bool) string { | |||||
| 34 | return "#333333" | 35 | return "#333333" | ||
| 35 | } | 36 | } | ||
| 36 | 37 | ||||
| 37 | - | // getActivePaneBorderColor returns the border color for active panes based on theme | 38 | + | // getActivePaneBorderColor returns the border color for active panes based on config and theme |
| 38 | - | func getActivePaneBorderColor(isDark bool) string { | 39 | + | func getActivePaneBorderColor(styling config.WorkspaceStylingConfig, isDark bool) string { |
| 40 | + | // Use configured border color if set | |||
| 41 | + | if styling.BorderColor != "" { | |||
| 42 | + | // Check if it's a GTK theme variable (starts with @) | |||
| 43 | + | if strings.HasPrefix(styling.BorderColor, "@") { | |||
| 44 | + | return styling.BorderColor | |||
| 45 | + | } | |||
| 46 | + | // Return the configured color as-is (could be hex, rgb, etc.) | |||
| 47 | + | return styling.BorderColor | |||
| 48 | + | } | |||
| 49 | + | ||||
| 50 | + | // Fallback to hardcoded colors based on theme | |||
| 39 | if isDark { | 51 | if isDark { | ||
| 40 | return "#4A90E2" // Deeper blue pastel for dark theme | 52 | return "#4A90E2" // Deeper blue pastel for dark theme | ||
| 41 | } | 53 | } | ||
| @@ -58,30 +70,33 @@ func (wm *WorkspaceManager) generateWorkspaceCSS() string { | |||||
| 58 | windowBackgroundColor = "#ffffff" // Light window background | 70 | windowBackgroundColor = "#ffffff" // Light window background | ||
| 59 | } | 71 | } | ||
| 60 | 72 | ||||
| 61 | - | activeBorderColor := getActivePaneBorderColor(isDark) | 73 | + | activeBorderColor := getActivePaneBorderColor(styling, isDark) |
| 62 | 74 | ||||
| 63 | css := fmt.Sprintf(`window { | 75 | css := fmt.Sprintf(`window { | ||
| 64 | background-color: %s; | 76 | background-color: %s; | ||
| 65 | } | 77 | } | ||
| 66 | 78 | ||||
| 67 | - | /* Active pane border styling using outline (no layout impact) */ | 79 | + | /* Base pane styling - transparent outline for smooth transitions */ |
| 68 | - | .workspace-pane-active { | 80 | + | .workspace-pane, .stacked-pane-container { |
| 69 | - | outline: 2px solid %s; | 81 | + | outline: 2px solid transparent; |
| 70 | outline-offset: -2px; | 82 | outline-offset: -2px; | ||
| 71 | transition: outline-color %dms ease-in-out; | 83 | transition: outline-color %dms ease-in-out; | ||
| 72 | } | 84 | } | ||
| 73 | 85 | ||||
| 86 | + | /* Active pane border styling using outline (no layout impact) */ | |||
| 87 | + | .workspace-pane-active { | |||
| 88 | + | outline-color: %s; | |||
| 89 | + | } | |||
| 90 | + | ||||
| 74 | /* Stacked panes styling */ | 91 | /* Stacked panes styling */ | ||
| 75 | .stacked-pane-container { | 92 | .stacked-pane-container { | ||
| 76 | background-color: %s; | 93 | background-color: %s; | ||
| 77 | border-radius: %dpx; | 94 | border-radius: %dpx; | ||
| 78 | } | 95 | } | ||
| 79 | 96 | ||||
| 80 | - | /* Active stacked pane container gets the outline */ | 97 | + | /* Active stacked pane container gets the outline color */ |
| 81 | .stacked-pane-container.workspace-pane-active { | 98 | .stacked-pane-container.workspace-pane-active { | ||
| 82 | - | outline: 2px solid %s; | 99 | + | outline-color: %s; |
| 83 | - | outline-offset: -2px; | |||
| 84 | - | transition: outline-color %dms ease-in-out; | |||
| 85 | } | 100 | } | ||
| 86 | 101 | ||||
| 87 | .stacked-pane-title { | 102 | .stacked-pane-title { | ||
| @@ -110,12 +125,11 @@ func (wm *WorkspaceManager) generateWorkspaceCSS() string { | |||||
| 110 | /* Collapsed panes are hidden - handled in code via widget visibility */ | 125 | /* Collapsed panes are hidden - handled in code via widget visibility */ | ||
| 111 | }`, | 126 | }`, | ||
| 112 | windowBackgroundColor, // window background | 127 | windowBackgroundColor, // window background | ||
| 113 | - | activeBorderColor, // workspace-pane-active border | 128 | + | styling.TransitionDuration, // base pane outline transition |
| 114 | - | styling.TransitionDuration, // workspace-pane-active transition | 129 | + | activeBorderColor, // workspace-pane-active outline-color |
| 115 | windowBackgroundColor, // stacked-pane-container background | 130 | windowBackgroundColor, // stacked-pane-container background | ||
| 116 | styling.BorderRadius, // stacked-pane-container border-radius | 131 | styling.BorderRadius, // stacked-pane-container border-radius | ||
| 117 | - | activeBorderColor, // stacked-pane-container.workspace-pane-active border | 132 | + | activeBorderColor, // stacked-pane-container.workspace-pane-active outline-color |
| 118 | - | styling.TransitionDuration, // stacked-pane-container.workspace-pane-active transition | |||
| 119 | getStackTitleBg(isDark), // stacked-pane-title background | 133 | getStackTitleBg(isDark), // stacked-pane-title background | ||
| 120 | inactiveBorderColor, // stacked-pane-title border-bottom | 134 | inactiveBorderColor, // stacked-pane-title border-bottom | ||
| 121 | styling.TransitionDuration, // stacked-pane-title transition | 135 | styling.TransitionDuration, // stacked-pane-title transition | ||