feat(omnibox): add terminal-inspired visual styles (#437)
* feat(omnibox): add terminal-inspired visual styles * test(config): cover omnibox style migration * fix(omnibox): refine passive status styling * fix(config): reject empty omnibox styles * fix(omnibox): align terminal style indicators
- commit
- acc7e1ee079bb4665eaefa5b35c07fb41081e822
- author
- bnema · · 2026-09-14 19:57 UTC
- committer
- GitHub · · 2026-09-14 19:57 UTC
- parent
- 5e1311a
- signature
- GPG Unverified
B5690EEEBB952194The key is unknown
20 files · +315 -50
M CHANGELOG.md +2 -0
| @@ -6,12 +6,14 @@ All notable changes to this project will be documented in this file. | |||
| 6 | 6 | ||
| 7 | 7 | ### Added | |
| 8 | 8 | ||
| 9 | + | - **Omnibox visual styles**: Added configurable `command`, `multiplexer`, and `minimal` GTK treatments. The terminal-inspired `command` palette is the default. | |
| 9 | 10 | - **Vim mode for keyboard-driven page navigation**: Added configurable multi-key sequences for page scrolling, fast scrolling, page-input focus, and live heading navigation. Pressing `Enter` on a selected CEF heading opens its associated link before leaving Vim Mode. CEF accessibility data also provides the foundation for navigating structured page content such as headings, lists, paragraphs, and sections. | |
| 10 | 11 | - **Native favorites sidebar**: Added a keyboard-driven GTK sidebar toggled with `Ctrl+B`, including search, tag filtering, tag creation, favorite editing, shortcut assignment, deletion, and opening favorites in the current pane or a new split. | |
| 11 | 12 | - **WebP favicon decoding**: Added safe WebP decoding for sites that publish WebP favicons. | |
| 12 | 13 | ||
| 13 | 14 | ### Changed | |
| 14 | 15 | ||
| 16 | + | - **Omnibox status indicators**: The history-order control is now a passive status badge switched with `Ctrl+R`, and favorites use a terminal-style `*` marker. | |
| 15 | 17 | - **Favorites organization**: Favorites now use a tags-first model. Existing folder assignments are migrated into deterministic tags, with collision handling and folded-name uniqueness preserved during migration. | |
| 16 | 18 | - **Page mode configuration**: Page Mode is now called Vim Mode and uses the `workspace.vim_mode` configuration namespace for activation, timeouts, and action sequences. Existing page-mode bindings are migrated by the configuration loader. | |
| 17 | 19 | - **Browsing-context routing**: External links, floating panes, popups, and standalone omnibox launches now use explicit browsing-context ownership so they open in the appropriate pane or browser window. | |
| @@ -6,12 +6,14 @@ All notable changes to this project will be documented in this file. | |||||
| 6 | 6 | ||||
| 7 | ### Added | 7 | ### Added | ||
| 8 | 8 | ||||
| 9 | + | - **Omnibox visual styles**: Added configurable `command`, `multiplexer`, and `minimal` GTK treatments. The terminal-inspired `command` palette is the default. | |||
| 9 | - **Vim mode for keyboard-driven page navigation**: Added configurable multi-key sequences for page scrolling, fast scrolling, page-input focus, and live heading navigation. Pressing `Enter` on a selected CEF heading opens its associated link before leaving Vim Mode. CEF accessibility data also provides the foundation for navigating structured page content such as headings, lists, paragraphs, and sections. | 10 | - **Vim mode for keyboard-driven page navigation**: Added configurable multi-key sequences for page scrolling, fast scrolling, page-input focus, and live heading navigation. Pressing `Enter` on a selected CEF heading opens its associated link before leaving Vim Mode. CEF accessibility data also provides the foundation for navigating structured page content such as headings, lists, paragraphs, and sections. | ||
| 10 | - **Native favorites sidebar**: Added a keyboard-driven GTK sidebar toggled with `Ctrl+B`, including search, tag filtering, tag creation, favorite editing, shortcut assignment, deletion, and opening favorites in the current pane or a new split. | 11 | - **Native favorites sidebar**: Added a keyboard-driven GTK sidebar toggled with `Ctrl+B`, including search, tag filtering, tag creation, favorite editing, shortcut assignment, deletion, and opening favorites in the current pane or a new split. | ||
| 11 | - **WebP favicon decoding**: Added safe WebP decoding for sites that publish WebP favicons. | 12 | - **WebP favicon decoding**: Added safe WebP decoding for sites that publish WebP favicons. | ||
| 12 | 13 | ||||
| 13 | ### Changed | 14 | ### Changed | ||
| 14 | 15 | ||||
| 16 | + | - **Omnibox status indicators**: The history-order control is now a passive status badge switched with `Ctrl+R`, and favorites use a terminal-style `*` marker. | |||
| 15 | - **Favorites organization**: Favorites now use a tags-first model. Existing folder assignments are migrated into deterministic tags, with collision handling and folded-name uniqueness preserved during migration. | 17 | - **Favorites organization**: Favorites now use a tags-first model. Existing folder assignments are migrated into deterministic tags, with collision handling and folded-name uniqueness preserved during migration. | ||
| 16 | - **Page mode configuration**: Page Mode is now called Vim Mode and uses the `workspace.vim_mode` configuration namespace for activation, timeouts, and action sequences. Existing page-mode bindings are migrated by the configuration loader. | 18 | - **Page mode configuration**: Page Mode is now called Vim Mode and uses the `workspace.vim_mode` configuration namespace for activation, timeouts, and action sequences. Existing page-mode bindings are migrated by the configuration loader. | ||
| 17 | - **Browsing-context routing**: External links, floating panes, popups, and standalone omnibox launches now use explicit browsing-context ownership so they open in the appropriate pane or browser window. | 19 | - **Browsing-context routing**: External links, floating panes, popups, and standalone omnibox launches now use explicit browsing-context ownership so they open in the appropriate pane or browser window. | ||
M docs/config/index.md +9 -1
| @@ -103,17 +103,25 @@ dumber dmenu --days 14 --most-visited | |||
| 103 | 103 | ||
| 104 | 104 | | Key | Type | Default | Valid Values | Description | | |
| 105 | 105 | |-----|------|---------|--------------|-------------| | |
| 106 | - | | `omnibox.initial_behavior` | string | `"recent"` | `recent`, `most_visited`, `none` | Initial history display behavior | | |
| 106 | + | | `omnibox.style` | string | `"command"` | `multiplexer`, `command`, `minimal` | Omnibox visual treatment | | |
| 107 | + | | `omnibox.initial_behavior` | string | `"recent"` | `recent`, `most_visited`, `none` | Initial history display behavior; press `Ctrl+R` in the omnibox to switch | | |
| 107 | 108 | | `omnibox.most_visited_days` | int | `30` | `>= 0` | Days of history to consider when `initial_behavior = "most_visited"` (`0` = all history) | | |
| 108 | 109 | | `omnibox.auto_open_on_new_pane` | bool | `false` | - | Automatically open the omnibox after creating a new pane | | |
| 109 | 110 | ||
| 111 | + | Changing `omnibox.style` takes effect after restarting Dumber. | |
| 112 | + | ||
| 110 | 113 | **Example:** | |
| 111 | 114 | ```toml | |
| 112 | 115 | [omnibox] | |
| 116 | + | style = "command" # Soft terminal-inspired command palette | |
| 113 | 117 | initial_behavior = "recent" # Show recent history when omnibox opens | |
| 114 | 118 | most_visited_days = 30 # Days of history used for most_visited | |
| 115 | 119 | auto_open_on_new_pane = false | |
| 116 | 120 | ||
| 121 | + | # Alternative visual treatments: | |
| 122 | + | # style = "multiplexer" # Segmented terminal multiplexer treatment | |
| 123 | + | # style = "minimal" # Bare terminal typography and hairlines | |
| 124 | + | ||
| 117 | 125 | # Alternative options: | |
| 118 | 126 | # initial_behavior = "most_visited" # Show most visited sites | |
| 119 | 127 | # initial_behavior = "none" # Show no initial suggestions | |
| @@ -103,17 +103,25 @@ dumber dmenu --days 14 --most-visited | |||||
| 103 | 103 | ||||
| 104 | | Key | Type | Default | Valid Values | Description | | 104 | | Key | Type | Default | Valid Values | Description | | ||
| 105 | |-----|------|---------|--------------|-------------| | 105 | |-----|------|---------|--------------|-------------| | ||
| 106 | - | | `omnibox.initial_behavior` | string | `"recent"` | `recent`, `most_visited`, `none` | Initial history display behavior | | 106 | + | | `omnibox.style` | string | `"command"` | `multiplexer`, `command`, `minimal` | Omnibox visual treatment | |
| 107 | + | | `omnibox.initial_behavior` | string | `"recent"` | `recent`, `most_visited`, `none` | Initial history display behavior; press `Ctrl+R` in the omnibox to switch | | |||
| 107 | | `omnibox.most_visited_days` | int | `30` | `>= 0` | Days of history to consider when `initial_behavior = "most_visited"` (`0` = all history) | | 108 | | `omnibox.most_visited_days` | int | `30` | `>= 0` | Days of history to consider when `initial_behavior = "most_visited"` (`0` = all history) | | ||
| 108 | | `omnibox.auto_open_on_new_pane` | bool | `false` | - | Automatically open the omnibox after creating a new pane | | 109 | | `omnibox.auto_open_on_new_pane` | bool | `false` | - | Automatically open the omnibox after creating a new pane | | ||
| 109 | 110 | ||||
| 111 | + | Changing `omnibox.style` takes effect after restarting Dumber. | |||
| 112 | + | ||||
| 110 | **Example:** | 113 | **Example:** | ||
| 111 | ```toml | 114 | ```toml | ||
| 112 | [omnibox] | 115 | [omnibox] | ||
| 116 | + | style = "command" # Soft terminal-inspired command palette | |||
| 113 | initial_behavior = "recent" # Show recent history when omnibox opens | 117 | initial_behavior = "recent" # Show recent history when omnibox opens | ||
| 114 | most_visited_days = 30 # Days of history used for most_visited | 118 | most_visited_days = 30 # Days of history used for most_visited | ||
| 115 | auto_open_on_new_pane = false | 119 | auto_open_on_new_pane = false | ||
| 116 | 120 | ||||
| 121 | + | # Alternative visual treatments: | |||
| 122 | + | # style = "multiplexer" # Segmented terminal multiplexer treatment | |||
| 123 | + | # style = "minimal" # Bare terminal typography and hairlines | |||
| 124 | + | ||||
| 117 | # Alternative options: | 125 | # Alternative options: | ||
| 118 | # initial_behavior = "most_visited" # Show most visited sites | 126 | # initial_behavior = "most_visited" # Show most visited sites | ||
| 119 | # initial_behavior = "none" # Show no initial suggestions | 127 | # initial_behavior = "none" # Show no initial suggestions | ||
M docs/reference/configuration.md +1 -0
| @@ -19,6 +19,7 @@ | |||
| 19 | 19 | | `dmenu.url_prefix` | string | `🌐` | | | |
| 20 | 20 | | `dmenu.date_format` | string | `2006-01-02 15:04` | Go time format | | |
| 21 | 21 | | `dmenu.sort_by_visit_count` | bool | `true` | | | |
| 22 | + | | `omnibox.style` | string | `command` | `multiplexer`, `command`, `minimal` | | |
| 22 | 23 | | `omnibox.initial_behavior` | string | `recent` | `recent`, `most_visited`, `none` | | |
| 23 | 24 | | `omnibox.most_visited_days` | int | `30` | `>= 0` | | |
| 24 | 25 | | `omnibox.auto_open_on_new_pane` | bool | `false` | | | |
| @@ -19,6 +19,7 @@ | |||||
| 19 | | `dmenu.url_prefix` | string | `🌐` | | | 19 | | `dmenu.url_prefix` | string | `🌐` | | | ||
| 20 | | `dmenu.date_format` | string | `2006-01-02 15:04` | Go time format | | 20 | | `dmenu.date_format` | string | `2006-01-02 15:04` | Go time format | | ||
| 21 | | `dmenu.sort_by_visit_count` | bool | `true` | | | 21 | | `dmenu.sort_by_visit_count` | bool | `true` | | | ||
| 22 | + | | `omnibox.style` | string | `command` | `multiplexer`, `command`, `minimal` | | |||
| 22 | | `omnibox.initial_behavior` | string | `recent` | `recent`, `most_visited`, `none` | | 23 | | `omnibox.initial_behavior` | string | `recent` | `recent`, `most_visited`, `none` | | ||
| 23 | | `omnibox.most_visited_days` | int | `30` | `>= 0` | | 24 | | `omnibox.most_visited_days` | int | `30` | `>= 0` | | ||
| 24 | | `omnibox.auto_open_on_new_pane` | bool | `false` | | | 25 | | `omnibox.auto_open_on_new_pane` | bool | `false` | | | ||
M internal/bootstrap/runtime_config.go +1 -0
| @@ -96,6 +96,7 @@ func RuntimeConfigSnapshotFromConfig(cfg *config.Config) entity.RuntimeConfigSna | |||
| 96 | 96 | SearchShortcuts: runtimeSearchShortcutsFromConfig(cfg.SearchShortcuts), | |
| 97 | 97 | DefaultSearchEngine: cfg.DefaultSearchEngine, | |
| 98 | 98 | Omnibox: entity.RuntimeOmniboxConfig{ | |
| 99 | + | Style: cfg.Omnibox.Style, | |
| 99 | 100 | InitialBehavior: cfg.Omnibox.InitialBehavior, | |
| 100 | 101 | MostVisitedDays: cfg.Omnibox.MostVisitedDays, | |
| 101 | 102 | AutoOpenOnNewPane: cfg.Omnibox.AutoOpenOnNewPane, | |
| @@ -96,6 +96,7 @@ func RuntimeConfigSnapshotFromConfig(cfg *config.Config) entity.RuntimeConfigSna | |||||
| 96 | SearchShortcuts: runtimeSearchShortcutsFromConfig(cfg.SearchShortcuts), | 96 | SearchShortcuts: runtimeSearchShortcutsFromConfig(cfg.SearchShortcuts), | ||
| 97 | DefaultSearchEngine: cfg.DefaultSearchEngine, | 97 | DefaultSearchEngine: cfg.DefaultSearchEngine, | ||
| 98 | Omnibox: entity.RuntimeOmniboxConfig{ | 98 | Omnibox: entity.RuntimeOmniboxConfig{ | ||
| 99 | + | Style: cfg.Omnibox.Style, | |||
| 99 | InitialBehavior: cfg.Omnibox.InitialBehavior, | 100 | InitialBehavior: cfg.Omnibox.InitialBehavior, | ||
| 100 | MostVisitedDays: cfg.Omnibox.MostVisitedDays, | 101 | MostVisitedDays: cfg.Omnibox.MostVisitedDays, | ||
| 101 | AutoOpenOnNewPane: cfg.Omnibox.AutoOpenOnNewPane, | 102 | AutoOpenOnNewPane: cfg.Omnibox.AutoOpenOnNewPane, | ||
M internal/domain/entity/runtime_config.go +1 -0
| @@ -64,6 +64,7 @@ type RuntimeSearchShortcut struct { | |||
| 64 | 64 | } | |
| 65 | 65 | ||
| 66 | 66 | type RuntimeOmniboxConfig struct { | |
| 67 | + | Style string | |
| 67 | 68 | InitialBehavior OmniboxInitialBehavior | |
| 68 | 69 | MostVisitedDays int | |
| 69 | 70 | AutoOpenOnNewPane bool | |
| @@ -64,6 +64,7 @@ type RuntimeSearchShortcut struct { | |||||
| 64 | } | 64 | } | ||
| 65 | 65 | ||||
| 66 | type RuntimeOmniboxConfig struct { | 66 | type RuntimeOmniboxConfig struct { | ||
| 67 | + | Style string | |||
| 67 | InitialBehavior OmniboxInitialBehavior | 68 | InitialBehavior OmniboxInitialBehavior | ||
| 68 | MostVisitedDays int | 69 | MostVisitedDays int | ||
| 69 | AutoOpenOnNewPane bool | 70 | AutoOpenOnNewPane bool | ||
M internal/infrastructure/config/defaults.go +2 -0
| @@ -33,6 +33,7 @@ const ( | |||
| 33 | 33 | defaultNewPaneURL = "about:blank" | |
| 34 | 34 | ||
| 35 | 35 | // Omnibox defaults | |
| 36 | + | defaultOmniboxStyle = "command" | |
| 36 | 37 | defaultOmniboxInitialBehavior = OmniboxInitialBehaviorRecent | |
| 37 | 38 | defaultOmniboxMostVisitedDays = 30 | |
| 38 | 39 | defaultOmniboxAutoOpenOnNewPane = false | |
| @@ -405,6 +406,7 @@ func DefaultConfig() *Config { | |||
| 405 | 406 | AutoCopyOnSelection: true, // Enabled by default (zellij-style) | |
| 406 | 407 | }, | |
| 407 | 408 | Omnibox: OmniboxConfig{ | |
| 409 | + | Style: defaultOmniboxStyle, | |
| 408 | 410 | InitialBehavior: defaultOmniboxInitialBehavior, | |
| 409 | 411 | MostVisitedDays: defaultOmniboxMostVisitedDays, | |
| 410 | 412 | AutoOpenOnNewPane: defaultOmniboxAutoOpenOnNewPane, | |
| @@ -33,6 +33,7 @@ const ( | |||||
| 33 | defaultNewPaneURL = "about:blank" | 33 | defaultNewPaneURL = "about:blank" | ||
| 34 | 34 | ||||
| 35 | // Omnibox defaults | 35 | // Omnibox defaults | ||
| 36 | + | defaultOmniboxStyle = "command" | |||
| 36 | defaultOmniboxInitialBehavior = OmniboxInitialBehaviorRecent | 37 | defaultOmniboxInitialBehavior = OmniboxInitialBehaviorRecent | ||
| 37 | defaultOmniboxMostVisitedDays = 30 | 38 | defaultOmniboxMostVisitedDays = 30 | ||
| 38 | defaultOmniboxAutoOpenOnNewPane = false | 39 | defaultOmniboxAutoOpenOnNewPane = false | ||
| @@ -405,6 +406,7 @@ func DefaultConfig() *Config { | |||||
| 405 | AutoCopyOnSelection: true, // Enabled by default (zellij-style) | 406 | AutoCopyOnSelection: true, // Enabled by default (zellij-style) | ||
| 406 | }, | 407 | }, | ||
| 407 | Omnibox: OmniboxConfig{ | 408 | Omnibox: OmniboxConfig{ | ||
| 409 | + | Style: defaultOmniboxStyle, | |||
| 408 | InitialBehavior: defaultOmniboxInitialBehavior, | 410 | InitialBehavior: defaultOmniboxInitialBehavior, | ||
| 409 | MostVisitedDays: defaultOmniboxMostVisitedDays, | 411 | MostVisitedDays: defaultOmniboxMostVisitedDays, | ||
| 410 | AutoOpenOnNewPane: defaultOmniboxAutoOpenOnNewPane, | 412 | AutoOpenOnNewPane: defaultOmniboxAutoOpenOnNewPane, | ||
M internal/infrastructure/config/loader.go +1 -0
| @@ -753,6 +753,7 @@ func (m *Manager) setClipboardDefaults(defaults *Config) { | |||
| 753 | 753 | } | |
| 754 | 754 | ||
| 755 | 755 | func (m *Manager) setOmniboxDefaults(defaults *Config) { | |
| 756 | + | m.viper.SetDefault("omnibox.style", defaults.Omnibox.Style) | |
| 756 | 757 | m.viper.SetDefault("omnibox.initial_behavior", defaults.Omnibox.InitialBehavior) | |
| 757 | 758 | m.viper.SetDefault("omnibox.most_visited_days", defaults.Omnibox.MostVisitedDays) | |
| 758 | 759 | m.viper.SetDefault("omnibox.auto_open_on_new_pane", defaults.Omnibox.AutoOpenOnNewPane) | |
| @@ -753,6 +753,7 @@ func (m *Manager) setClipboardDefaults(defaults *Config) { | |||||
| 753 | } | 753 | } | ||
| 754 | 754 | ||||
| 755 | func (m *Manager) setOmniboxDefaults(defaults *Config) { | 755 | func (m *Manager) setOmniboxDefaults(defaults *Config) { | ||
| 756 | + | m.viper.SetDefault("omnibox.style", defaults.Omnibox.Style) | |||
| 756 | m.viper.SetDefault("omnibox.initial_behavior", defaults.Omnibox.InitialBehavior) | 757 | m.viper.SetDefault("omnibox.initial_behavior", defaults.Omnibox.InitialBehavior) | ||
| 757 | m.viper.SetDefault("omnibox.most_visited_days", defaults.Omnibox.MostVisitedDays) | 758 | m.viper.SetDefault("omnibox.most_visited_days", defaults.Omnibox.MostVisitedDays) | ||
| 758 | m.viper.SetDefault("omnibox.auto_open_on_new_pane", defaults.Omnibox.AutoOpenOnNewPane) | 759 | m.viper.SetDefault("omnibox.auto_open_on_new_pane", defaults.Omnibox.AutoOpenOnNewPane) | ||
M internal/infrastructure/config/migrate_test.go +1 -0
| @@ -39,6 +39,7 @@ func TestMigrator_GetAllDefaultKeys(t *testing.T) { | |||
| 39 | 39 | assert.True(t, keySet["history.max_entries"], "should include history.max_entries") | |
| 40 | 40 | assert.True(t, keySet["logging.level"], "should include logging.level") | |
| 41 | 41 | assert.True(t, keySet["appearance.sans_font"], "should include appearance.sans_font") | |
| 42 | + | assert.True(t, keySet["omnibox.style"], "should include omnibox.style for config migrate") | |
| 42 | 43 | } | |
| 43 | 44 | ||
| 44 | 45 | func TestMigrator_GetUserConfigKeys(t *testing.T) { | |
| @@ -39,6 +39,7 @@ func TestMigrator_GetAllDefaultKeys(t *testing.T) { | |||||
| 39 | assert.True(t, keySet["history.max_entries"], "should include history.max_entries") | 39 | assert.True(t, keySet["history.max_entries"], "should include history.max_entries") | ||
| 40 | assert.True(t, keySet["logging.level"], "should include logging.level") | 40 | assert.True(t, keySet["logging.level"], "should include logging.level") | ||
| 41 | assert.True(t, keySet["appearance.sans_font"], "should include appearance.sans_font") | 41 | assert.True(t, keySet["appearance.sans_font"], "should include appearance.sans_font") | ||
| 42 | + | assert.True(t, keySet["omnibox.style"], "should include omnibox.style for config migrate") | |||
| 42 | } | 43 | } | ||
| 43 | 44 | ||||
| 44 | func TestMigrator_GetUserConfigKeys(t *testing.T) { | 45 | func TestMigrator_GetUserConfigKeys(t *testing.T) { | ||
M internal/infrastructure/config/schema.go +2 -0
| @@ -389,6 +389,8 @@ type ClipboardConfig struct { | |||
| 389 | 389 | ||
| 390 | 390 | // OmniboxConfig holds omnibox behavior preferences | |
| 391 | 391 | type OmniboxConfig struct { | |
| 392 | + | // Style selects the omnibox visual treatment: "multiplexer", "command", or "minimal". | |
| 393 | + | Style string `mapstructure:"style" yaml:"style" toml:"style"` | |
| 392 | 394 | // InitialBehavior controls what to show when omnibox opens with empty input | |
| 393 | 395 | // Values: "recent" (recent visits), "most_visited" (most visited sites), "none" (nothing) | |
| 394 | 396 | InitialBehavior OmniboxInitialBehavior `mapstructure:"initial_behavior" yaml:"initial_behavior" toml:"initial_behavior"` | |
| @@ -389,6 +389,8 @@ type ClipboardConfig struct { | |||||
| 389 | 389 | ||||
| 390 | // OmniboxConfig holds omnibox behavior preferences | 390 | // OmniboxConfig holds omnibox behavior preferences | ||
| 391 | type OmniboxConfig struct { | 391 | type OmniboxConfig struct { | ||
| 392 | + | // Style selects the omnibox visual treatment: "multiplexer", "command", or "minimal". | |||
| 393 | + | Style string `mapstructure:"style" yaml:"style" toml:"style"` | |||
| 392 | // InitialBehavior controls what to show when omnibox opens with empty input | 394 | // InitialBehavior controls what to show when omnibox opens with empty input | ||
| 393 | // Values: "recent" (recent visits), "most_visited" (most visited sites), "none" (nothing) | 395 | // Values: "recent" (recent visits), "most_visited" (most visited sites), "none" (nothing) | ||
| 394 | InitialBehavior OmniboxInitialBehavior `mapstructure:"initial_behavior" yaml:"initial_behavior" toml:"initial_behavior"` | 396 | InitialBehavior OmniboxInitialBehavior `mapstructure:"initial_behavior" yaml:"initial_behavior" toml:"initial_behavior"` | ||
M internal/infrastructure/config/schema_provider.go +8 -0
| @@ -779,6 +779,14 @@ func (*SchemaProvider) getSessionKeys(defaults *Config) []entity.ConfigKeyInfo { | |||
| 779 | 779 | ||
| 780 | 780 | func (*SchemaProvider) getOmniboxKeys(defaults *Config) []entity.ConfigKeyInfo { | |
| 781 | 781 | return []entity.ConfigKeyInfo{ | |
| 782 | + | { | |
| 783 | + | Key: "omnibox.style", | |
| 784 | + | Type: "string", | |
| 785 | + | Default: defaults.Omnibox.Style, | |
| 786 | + | Description: "Visual treatment for the omnibox", | |
| 787 | + | Values: []string{"multiplexer", "command", "minimal"}, | |
| 788 | + | Section: SectionOmnibox, | |
| 789 | + | }, | |
| 782 | 790 | { | |
| 783 | 791 | Key: "omnibox.initial_behavior", | |
| 784 | 792 | Type: "string", | |
| @@ -779,6 +779,14 @@ func (*SchemaProvider) getSessionKeys(defaults *Config) []entity.ConfigKeyInfo { | |||||
| 779 | 779 | ||||
| 780 | func (*SchemaProvider) getOmniboxKeys(defaults *Config) []entity.ConfigKeyInfo { | 780 | func (*SchemaProvider) getOmniboxKeys(defaults *Config) []entity.ConfigKeyInfo { | ||
| 781 | return []entity.ConfigKeyInfo{ | 781 | return []entity.ConfigKeyInfo{ | ||
| 782 | + | { | |||
| 783 | + | Key: "omnibox.style", | |||
| 784 | + | Type: "string", | |||
| 785 | + | Default: defaults.Omnibox.Style, | |||
| 786 | + | Description: "Visual treatment for the omnibox", | |||
| 787 | + | Values: []string{"multiplexer", "command", "minimal"}, | |||
| 788 | + | Section: SectionOmnibox, | |||
| 789 | + | }, | |||
| 782 | { | 790 | { | ||
| 783 | Key: "omnibox.initial_behavior", | 791 | Key: "omnibox.initial_behavior", | ||
| 784 | Type: "string", | 792 | Type: "string", | ||
M internal/infrastructure/config/validation.go +8 -0
| @@ -646,6 +646,14 @@ func looksLikeBareRelativeWorkspacePath(value string) bool { | |||
| 646 | 646 | ||
| 647 | 647 | func validateOmnibox(config *Config) []string { | |
| 648 | 648 | var validationErrors []string | |
| 649 | + | switch config.Omnibox.Style { | |
| 650 | + | case "multiplexer", "command", "minimal": | |
| 651 | + | default: | |
| 652 | + | validationErrors = append(validationErrors, fmt.Sprintf( | |
| 653 | + | "omnibox.style must be one of: multiplexer, command, minimal (got: %s)", | |
| 654 | + | config.Omnibox.Style, | |
| 655 | + | )) | |
| 656 | + | } | |
| 649 | 657 | if config.Omnibox.MostVisitedDays < 0 { | |
| 650 | 658 | validationErrors = append(validationErrors, "omnibox.most_visited_days must be non-negative") | |
| 651 | 659 | } | |
| @@ -646,6 +646,14 @@ func looksLikeBareRelativeWorkspacePath(value string) bool { | |||||
| 646 | 646 | ||||
| 647 | func validateOmnibox(config *Config) []string { | 647 | func validateOmnibox(config *Config) []string { | ||
| 648 | var validationErrors []string | 648 | var validationErrors []string | ||
| 649 | + | switch config.Omnibox.Style { | |||
| 650 | + | case "multiplexer", "command", "minimal": | |||
| 651 | + | default: | |||
| 652 | + | validationErrors = append(validationErrors, fmt.Sprintf( | |||
| 653 | + | "omnibox.style must be one of: multiplexer, command, minimal (got: %s)", | |||
| 654 | + | config.Omnibox.Style, | |||
| 655 | + | )) | |||
| 656 | + | } | |||
| 649 | if config.Omnibox.MostVisitedDays < 0 { | 657 | if config.Omnibox.MostVisitedDays < 0 { | ||
| 650 | validationErrors = append(validationErrors, "omnibox.most_visited_days must be non-negative") | 658 | validationErrors = append(validationErrors, "omnibox.most_visited_days must be non-negative") | ||
| 651 | } | 659 | } | ||
M internal/infrastructure/config/validation_test.go +27 -0
| @@ -10,6 +10,33 @@ import ( | |||
| 10 | 10 | "github.com/stretchr/testify/require" | |
| 11 | 11 | ) | |
| 12 | 12 | ||
| 13 | + | func TestValidateConfig_OmniboxStyle(t *testing.T) { | |
| 14 | + | tests := []struct { | |
| 15 | + | style string | |
| 16 | + | wantErr bool | |
| 17 | + | }{ | |
| 18 | + | {style: "command"}, | |
| 19 | + | {style: "multiplexer"}, | |
| 20 | + | {style: "minimal"}, | |
| 21 | + | {style: "", wantErr: true}, | |
| 22 | + | {style: "classic", wantErr: true}, | |
| 23 | + | } | |
| 24 | + | ||
| 25 | + | for _, tt := range tests { | |
| 26 | + | t.Run(tt.style, func(t *testing.T) { | |
| 27 | + | cfg := DefaultConfig() | |
| 28 | + | cfg.Omnibox.Style = tt.style | |
| 29 | + | err := validateConfig(cfg) | |
| 30 | + | if tt.wantErr { | |
| 31 | + | require.Error(t, err) | |
| 32 | + | assert.Contains(t, err.Error(), "omnibox.style") | |
| 33 | + | return | |
| 34 | + | } | |
| 35 | + | require.NoError(t, err) | |
| 36 | + | }) | |
| 37 | + | } | |
| 38 | + | } | |
| 39 | + | ||
| 13 | 40 | func TestValidateConfig_EngineType(t *testing.T) { | |
| 14 | 41 | t.Setenv("DUMBER_ENGINE", "") | |
| 15 | 42 | ||
| @@ -10,6 +10,33 @@ import ( | |||||
| 10 | "github.com/stretchr/testify/require" | 10 | "github.com/stretchr/testify/require" | ||
| 11 | ) | 11 | ) | ||
| 12 | 12 | ||||
| 13 | + | func TestValidateConfig_OmniboxStyle(t *testing.T) { | |||
| 14 | + | tests := []struct { | |||
| 15 | + | style string | |||
| 16 | + | wantErr bool | |||
| 17 | + | }{ | |||
| 18 | + | {style: "command"}, | |||
| 19 | + | {style: "multiplexer"}, | |||
| 20 | + | {style: "minimal"}, | |||
| 21 | + | {style: "", wantErr: true}, | |||
| 22 | + | {style: "classic", wantErr: true}, | |||
| 23 | + | } | |||
| 24 | + | ||||
| 25 | + | for _, tt := range tests { | |||
| 26 | + | t.Run(tt.style, func(t *testing.T) { | |||
| 27 | + | cfg := DefaultConfig() | |||
| 28 | + | cfg.Omnibox.Style = tt.style | |||
| 29 | + | err := validateConfig(cfg) | |||
| 30 | + | if tt.wantErr { | |||
| 31 | + | require.Error(t, err) | |||
| 32 | + | assert.Contains(t, err.Error(), "omnibox.style") | |||
| 33 | + | return | |||
| 34 | + | } | |||
| 35 | + | require.NoError(t, err) | |||
| 36 | + | }) | |||
| 37 | + | } | |||
| 38 | + | } | |||
| 39 | + | ||||
| 13 | func TestValidateConfig_EngineType(t *testing.T) { | 40 | func TestValidateConfig_EngineType(t *testing.T) { | ||
| 14 | t.Setenv("DUMBER_ENGINE", "") | 41 | t.Setenv("DUMBER_ENGINE", "") | ||
| 15 | 42 | ||||
M internal/ui/app.go +1 -0
| @@ -1309,6 +1309,7 @@ func buildOmniboxConfig( | |||
| 1309 | 1309 | } | |
| 1310 | 1310 | ||
| 1311 | 1311 | return component.OmniboxConfig{ | |
| 1312 | + | Style: runtimeCfg.Omnibox.Style, | |
| 1312 | 1313 | HistoryUC: deps.HistoryUC, | |
| 1313 | 1314 | FavoritesUC: deps.FavoritesUC, | |
| 1314 | 1315 | FaviconAdapter: faviconAdapter, | |
| @@ -1309,6 +1309,7 @@ func buildOmniboxConfig( | |||||
| 1309 | } | 1309 | } | ||
| 1310 | 1310 | ||||
| 1311 | return component.OmniboxConfig{ | 1311 | return component.OmniboxConfig{ | ||
| 1312 | + | Style: runtimeCfg.Omnibox.Style, | |||
| 1312 | HistoryUC: deps.HistoryUC, | 1313 | HistoryUC: deps.HistoryUC, | ||
| 1313 | FavoritesUC: deps.FavoritesUC, | 1314 | FavoritesUC: deps.FavoritesUC, | ||
| 1314 | FaviconAdapter: faviconAdapter, | 1315 | FaviconAdapter: faviconAdapter, | ||
M internal/ui/app_omnibox_config_test.go +4 -0
| @@ -19,6 +19,7 @@ func TestBuildOmniboxConfigUsesRuntimeUIConfig(t *testing.T) { | |||
| 19 | 19 | }, | |
| 20 | 20 | }, | |
| 21 | 21 | Omnibox: entity.RuntimeOmniboxConfig{ | |
| 22 | + | Style: "command", | |
| 22 | 23 | InitialBehavior: entity.OmniboxInitialBehaviorMostVisited, | |
| 23 | 24 | MostVisitedDays: 7, | |
| 24 | 25 | }, | |
| @@ -29,6 +30,9 @@ func TestBuildOmniboxConfigUsesRuntimeUIConfig(t *testing.T) { | |||
| 29 | 30 | return "normalized:" + input | |
| 30 | 31 | }, | |
| 31 | 32 | }) | |
| 33 | + | if got.Style != "command" { | |
| 34 | + | t.Fatalf("Style = %q, want command", got.Style) | |
| 35 | + | } | |
| 32 | 36 | if got.MostVisitedDays != 7 { | |
| 33 | 37 | t.Fatalf("MostVisitedDays = %d, want 7", got.MostVisitedDays) | |
| 34 | 38 | } | |
| @@ -19,6 +19,7 @@ func TestBuildOmniboxConfigUsesRuntimeUIConfig(t *testing.T) { | |||||
| 19 | }, | 19 | }, | ||
| 20 | }, | 20 | }, | ||
| 21 | Omnibox: entity.RuntimeOmniboxConfig{ | 21 | Omnibox: entity.RuntimeOmniboxConfig{ | ||
| 22 | + | Style: "command", | |||
| 22 | InitialBehavior: entity.OmniboxInitialBehaviorMostVisited, | 23 | InitialBehavior: entity.OmniboxInitialBehaviorMostVisited, | ||
| 23 | MostVisitedDays: 7, | 24 | MostVisitedDays: 7, | ||
| 24 | }, | 25 | }, | ||
| @@ -29,6 +30,9 @@ func TestBuildOmniboxConfigUsesRuntimeUIConfig(t *testing.T) { | |||||
| 29 | return "normalized:" + input | 30 | return "normalized:" + input | ||
| 30 | }, | 31 | }, | ||
| 31 | }) | 32 | }) | ||
| 33 | + | if got.Style != "command" { | |||
| 34 | + | t.Fatalf("Style = %q, want command", got.Style) | |||
| 35 | + | } | |||
| 32 | if got.MostVisitedDays != 7 { | 36 | if got.MostVisitedDays != 7 { | ||
| 33 | t.Fatalf("MostVisitedDays = %d, want 7", got.MostVisitedDays) | 37 | t.Fatalf("MostVisitedDays = %d, want 7", got.MostVisitedDays) | ||
| 34 | } | 38 | } | ||
M internal/ui/component/omnibox.go +21 -24
| @@ -31,7 +31,7 @@ const ( | |||
| 31 | 31 | endBoxSpacing = 6 | |
| 32 | 32 | defaultOmniboxPlaceholder = "Search history or enter URL… (! lists bangs)" | |
| 33 | 33 | minGhostInputLength = 1 | |
| 34 | - | initialBehaviorBadgeTooltip = "Toggle default history order (Ctrl+R)" | |
| 34 | + | initialBehaviorBadgeTooltip = "Default history order · Ctrl+R to switch" | |
| 35 | 35 | ) | |
| 36 | 36 | ||
| 37 | 37 | type favoriteRowIndicatorUpdate struct { | |
| @@ -140,7 +140,7 @@ type Omnibox struct { | |||
| 140 | 140 | outerBox *gtk.Box // Outer container for positioning | |
| 141 | 141 | mainBox *gtk.Box // Main content box | |
| 142 | 142 | headerBox *gtk.Box | |
| 143 | - | initialBehaviorBadge *gtk.Button | |
| 143 | + | initialBehaviorBadge *gtk.Label | |
| 144 | 144 | historyBtn *gtk.Button | |
| 145 | 145 | favoritesBtn *gtk.Button | |
| 146 | 146 | zoomLabel *gtk.Label | |
| @@ -151,6 +151,7 @@ type Omnibox struct { | |||
| 151 | 151 | ||
| 152 | 152 | // Parent overlay reference for sizing (set via SetParentOverlay) | |
| 153 | 153 | parentOverlay layout.OverlayWidget | |
| 154 | + | style string | |
| 154 | 155 | ||
| 155 | 156 | // State | |
| 156 | 157 | mu sync.RWMutex | |
| @@ -224,6 +225,7 @@ type Omnibox struct { | |||
| 224 | 225 | ||
| 225 | 226 | // OmniboxConfig holds configuration for creating an Omnibox. | |
| 226 | 227 | type OmniboxConfig struct { | |
| 228 | + | Style string | |
| 227 | 229 | HistoryUC *usecase.SearchHistoryUseCase | |
| 228 | 230 | FavoritesUC *usecase.ManageFavoritesUseCase | |
| 229 | 231 | FaviconAdapter *adapter.FaviconAdapter | |
| @@ -257,6 +259,7 @@ func NewOmnibox(ctx context.Context, cfg OmniboxConfig) *Omnibox { | |||
| 257 | 259 | sizeCfg := ResolveModalSizeConfig(cfg.SizeConfig, OmniboxSizeDefaults) | |
| 258 | 260 | ||
| 259 | 261 | o := &Omnibox{ | |
| 262 | + | style: cfg.Style, | |
| 260 | 263 | viewMode: ViewModeHistory, | |
| 261 | 264 | selectedIndex: -1, | |
| 262 | 265 | historyUC: cfg.HistoryUC, | |
| @@ -509,7 +512,7 @@ func (o *Omnibox) refreshInitialBehaviorBadge() { | |||
| 509 | 512 | if !state.visible { | |
| 510 | 513 | return | |
| 511 | 514 | } | |
| 512 | - | o.initialBehaviorBadge.SetLabel(state.label) | |
| 515 | + | o.initialBehaviorBadge.SetText(state.label) | |
| 513 | 516 | tooltip := state.tooltip | |
| 514 | 517 | o.initialBehaviorBadge.SetTooltipText(&tooltip) | |
| 515 | 518 | } | |
| @@ -754,6 +757,11 @@ func (o *Omnibox) initMainBox() error { | |||
| 754 | 757 | return errNilWidget("mainBox") | |
| 755 | 758 | } | |
| 756 | 759 | o.mainBox.AddCssClass("omnibox-container") | |
| 760 | + | style := strings.TrimSpace(o.style) | |
| 761 | + | if style == "" { | |
| 762 | + | style = "command" | |
| 763 | + | } | |
| 764 | + | o.mainBox.AddCssClass("omnibox-style-" + style) | |
| 757 | 765 | return nil | |
| 758 | 766 | } | |
| 759 | 767 | ||
| @@ -780,19 +788,11 @@ func (o *Omnibox) initHeader() error { | |||
| 780 | 788 | o.favoritesBtn.AddCssClass("omnibox-header-btn") | |
| 781 | 789 | o.favoritesBtn.SetCanFocus(false) | |
| 782 | 790 | ||
| 783 | - | o.initialBehaviorBadge = gtk.NewButtonWithLabel("") | |
| 791 | + | o.initialBehaviorBadge = gtk.NewLabel(nil) | |
| 784 | 792 | if o.initialBehaviorBadge == nil { | |
| 785 | 793 | return errNilWidget("initialBehaviorBadge") | |
| 786 | 794 | } | |
| 787 | 795 | o.initialBehaviorBadge.AddCssClass("omnibox-header-badge") | |
| 788 | - | o.initialBehaviorBadge.SetCanFocus(false) | |
| 789 | - | o.initialBehaviorBadge.SetFocusOnClick(false) | |
| 790 | - | ||
| 791 | - | initialBehaviorClickCb := func(_ gtk.Button) { | |
| 792 | - | o.toggleInitialBehaviorPreference() | |
| 793 | - | } | |
| 794 | - | o.retainedCallbacks = append(o.retainedCallbacks, initialBehaviorClickCb) | |
| 795 | - | o.initialBehaviorBadge.ConnectClicked(&initialBehaviorClickCb) | |
| 796 | 796 | o.refreshInitialBehaviorBadge() | |
| 797 | 797 | ||
| 798 | 798 | historyClickCb := func(_ gtk.Button) { | |
| @@ -2177,11 +2177,8 @@ func (o *Omnibox) createFaviconImage(rawURL, fallbackIcon string) *gtk.Image { | |||
| 2177 | 2177 | return favicon | |
| 2178 | 2178 | } | |
| 2179 | 2179 | ||
| 2180 | - | const favoriteStarBaseSize = 18 | |
| 2181 | 2180 | const favoriteStarSlotClass = "omnibox-favorite-star-slot" | |
| 2182 | 2181 | ||
| 2183 | - | func favoriteStarSize(scale float64) int { return ScaleValue(favoriteStarBaseSize, scale) } | |
| 2184 | - | ||
| 2185 | 2182 | func shouldShowFavoriteStar(s Suggestion) bool { return s.IsFavorite } | |
| 2186 | 2183 | ||
| 2187 | 2184 | func clearBoxChildren(box *gtk.Box) { | |
| @@ -2224,17 +2221,17 @@ func favoriteStarSlotForRow(row *gtk.ListBoxRow) *gtk.Box { | |||
| 2224 | 2221 | return nil | |
| 2225 | 2222 | } | |
| 2226 | 2223 | ||
| 2227 | - | func (o *Omnibox) createFavoriteStarIcon() *gtk.Image { | |
| 2228 | - | star := gtk.NewImage() | |
| 2229 | - | if star == nil { | |
| 2224 | + | func (*Omnibox) createFavoriteStarIcon() *gtk.Label { | |
| 2225 | + | marker := gtk.NewLabel(nil) | |
| 2226 | + | if marker == nil { | |
| 2230 | 2227 | return nil | |
| 2231 | 2228 | } | |
| 2232 | - | iconName := "starred-symbolic" | |
| 2233 | - | star.SetFromIconName(&iconName) | |
| 2234 | - | star.SetPixelSize(favoriteStarSize(o.uiScale)) | |
| 2235 | - | star.AddCssClass("omnibox-favorite-star") | |
| 2236 | - | star.SetValign(gtk.AlignCenterValue) | |
| 2237 | - | return star | |
| 2229 | + | marker.SetText("*") | |
| 2230 | + | marker.AddCssClass("omnibox-favorite-star") | |
| 2231 | + | marker.SetValign(gtk.AlignCenterValue) | |
| 2232 | + | tooltip := "Favorite" | |
| 2233 | + | marker.SetTooltipText(&tooltip) | |
| 2234 | + | return marker | |
| 2238 | 2235 | } | |
| 2239 | 2236 | ||
| 2240 | 2237 | func (o *Omnibox) appendSuggestionTitleAndURL(textBox *gtk.Box, title, displayURL string) { | |
| @@ -31,7 +31,7 @@ const ( | |||||
| 31 | endBoxSpacing = 6 | 31 | endBoxSpacing = 6 | ||
| 32 | defaultOmniboxPlaceholder = "Search history or enter URL… (! lists bangs)" | 32 | defaultOmniboxPlaceholder = "Search history or enter URL… (! lists bangs)" | ||
| 33 | minGhostInputLength = 1 | 33 | minGhostInputLength = 1 | ||
| 34 | - | initialBehaviorBadgeTooltip = "Toggle default history order (Ctrl+R)" | 34 | + | initialBehaviorBadgeTooltip = "Default history order · Ctrl+R to switch" |
| 35 | ) | 35 | ) | ||
| 36 | 36 | ||||
| 37 | type favoriteRowIndicatorUpdate struct { | 37 | type favoriteRowIndicatorUpdate struct { | ||
| @@ -140,7 +140,7 @@ type Omnibox struct { | |||||
| 140 | outerBox *gtk.Box // Outer container for positioning | 140 | outerBox *gtk.Box // Outer container for positioning | ||
| 141 | mainBox *gtk.Box // Main content box | 141 | mainBox *gtk.Box // Main content box | ||
| 142 | headerBox *gtk.Box | 142 | headerBox *gtk.Box | ||
| 143 | - | initialBehaviorBadge *gtk.Button | 143 | + | initialBehaviorBadge *gtk.Label |
| 144 | historyBtn *gtk.Button | 144 | historyBtn *gtk.Button | ||
| 145 | favoritesBtn *gtk.Button | 145 | favoritesBtn *gtk.Button | ||
| 146 | zoomLabel *gtk.Label | 146 | zoomLabel *gtk.Label | ||
| @@ -151,6 +151,7 @@ type Omnibox struct { | |||||
| 151 | 151 | ||||
| 152 | // Parent overlay reference for sizing (set via SetParentOverlay) | 152 | // Parent overlay reference for sizing (set via SetParentOverlay) | ||
| 153 | parentOverlay layout.OverlayWidget | 153 | parentOverlay layout.OverlayWidget | ||
| 154 | + | style string | |||
| 154 | 155 | ||||
| 155 | // State | 156 | // State | ||
| 156 | mu sync.RWMutex | 157 | mu sync.RWMutex | ||
| @@ -224,6 +225,7 @@ type Omnibox struct { | |||||
| 224 | 225 | ||||
| 225 | // OmniboxConfig holds configuration for creating an Omnibox. | 226 | // OmniboxConfig holds configuration for creating an Omnibox. | ||
| 226 | type OmniboxConfig struct { | 227 | type OmniboxConfig struct { | ||
| 228 | + | Style string | |||
| 227 | HistoryUC *usecase.SearchHistoryUseCase | 229 | HistoryUC *usecase.SearchHistoryUseCase | ||
| 228 | FavoritesUC *usecase.ManageFavoritesUseCase | 230 | FavoritesUC *usecase.ManageFavoritesUseCase | ||
| 229 | FaviconAdapter *adapter.FaviconAdapter | 231 | FaviconAdapter *adapter.FaviconAdapter | ||
| @@ -257,6 +259,7 @@ func NewOmnibox(ctx context.Context, cfg OmniboxConfig) *Omnibox { | |||||
| 257 | sizeCfg := ResolveModalSizeConfig(cfg.SizeConfig, OmniboxSizeDefaults) | 259 | sizeCfg := ResolveModalSizeConfig(cfg.SizeConfig, OmniboxSizeDefaults) | ||
| 258 | 260 | ||||
| 259 | o := &Omnibox{ | 261 | o := &Omnibox{ | ||
| 262 | + | style: cfg.Style, | |||
| 260 | viewMode: ViewModeHistory, | 263 | viewMode: ViewModeHistory, | ||
| 261 | selectedIndex: -1, | 264 | selectedIndex: -1, | ||
| 262 | historyUC: cfg.HistoryUC, | 265 | historyUC: cfg.HistoryUC, | ||
| @@ -509,7 +512,7 @@ func (o *Omnibox) refreshInitialBehaviorBadge() { | |||||
| 509 | if !state.visible { | 512 | if !state.visible { | ||
| 510 | return | 513 | return | ||
| 511 | } | 514 | } | ||
| 512 | - | o.initialBehaviorBadge.SetLabel(state.label) | 515 | + | o.initialBehaviorBadge.SetText(state.label) |
| 513 | tooltip := state.tooltip | 516 | tooltip := state.tooltip | ||
| 514 | o.initialBehaviorBadge.SetTooltipText(&tooltip) | 517 | o.initialBehaviorBadge.SetTooltipText(&tooltip) | ||
| 515 | } | 518 | } | ||
| @@ -754,6 +757,11 @@ func (o *Omnibox) initMainBox() error { | |||||
| 754 | return errNilWidget("mainBox") | 757 | return errNilWidget("mainBox") | ||
| 755 | } | 758 | } | ||
| 756 | o.mainBox.AddCssClass("omnibox-container") | 759 | o.mainBox.AddCssClass("omnibox-container") | ||
| 760 | + | style := strings.TrimSpace(o.style) | |||
| 761 | + | if style == "" { | |||
| 762 | + | style = "command" | |||
| 763 | + | } | |||
| 764 | + | o.mainBox.AddCssClass("omnibox-style-" + style) | |||
| 757 | return nil | 765 | return nil | ||
| 758 | } | 766 | } | ||
| 759 | 767 | ||||
| @@ -780,19 +788,11 @@ func (o *Omnibox) initHeader() error { | |||||
| 780 | o.favoritesBtn.AddCssClass("omnibox-header-btn") | 788 | o.favoritesBtn.AddCssClass("omnibox-header-btn") | ||
| 781 | o.favoritesBtn.SetCanFocus(false) | 789 | o.favoritesBtn.SetCanFocus(false) | ||
| 782 | 790 | ||||
| 783 | - | o.initialBehaviorBadge = gtk.NewButtonWithLabel("") | 791 | + | o.initialBehaviorBadge = gtk.NewLabel(nil) |
| 784 | if o.initialBehaviorBadge == nil { | 792 | if o.initialBehaviorBadge == nil { | ||
| 785 | return errNilWidget("initialBehaviorBadge") | 793 | return errNilWidget("initialBehaviorBadge") | ||
| 786 | } | 794 | } | ||
| 787 | o.initialBehaviorBadge.AddCssClass("omnibox-header-badge") | 795 | o.initialBehaviorBadge.AddCssClass("omnibox-header-badge") | ||
| 788 | - | o.initialBehaviorBadge.SetCanFocus(false) | |||
| 789 | - | o.initialBehaviorBadge.SetFocusOnClick(false) | |||
| 790 | - | ||||
| 791 | - | initialBehaviorClickCb := func(_ gtk.Button) { | |||
| 792 | - | o.toggleInitialBehaviorPreference() | |||
| 793 | - | } | |||
| 794 | - | o.retainedCallbacks = append(o.retainedCallbacks, initialBehaviorClickCb) | |||
| 795 | - | o.initialBehaviorBadge.ConnectClicked(&initialBehaviorClickCb) | |||
| 796 | o.refreshInitialBehaviorBadge() | 796 | o.refreshInitialBehaviorBadge() | ||
| 797 | 797 | ||||
| 798 | historyClickCb := func(_ gtk.Button) { | 798 | historyClickCb := func(_ gtk.Button) { | ||
| @@ -2177,11 +2177,8 @@ func (o *Omnibox) createFaviconImage(rawURL, fallbackIcon string) *gtk.Image { | |||||
| 2177 | return favicon | 2177 | return favicon | ||
| 2178 | } | 2178 | } | ||
| 2179 | 2179 | ||||
| 2180 | - | const favoriteStarBaseSize = 18 | |||
| 2181 | const favoriteStarSlotClass = "omnibox-favorite-star-slot" | 2180 | const favoriteStarSlotClass = "omnibox-favorite-star-slot" | ||
| 2182 | 2181 | ||||
| 2183 | - | func favoriteStarSize(scale float64) int { return ScaleValue(favoriteStarBaseSize, scale) } | |||
| 2184 | - | ||||
| 2185 | func shouldShowFavoriteStar(s Suggestion) bool { return s.IsFavorite } | 2182 | func shouldShowFavoriteStar(s Suggestion) bool { return s.IsFavorite } | ||
| 2186 | 2183 | ||||
| 2187 | func clearBoxChildren(box *gtk.Box) { | 2184 | func clearBoxChildren(box *gtk.Box) { | ||
| @@ -2224,17 +2221,17 @@ func favoriteStarSlotForRow(row *gtk.ListBoxRow) *gtk.Box { | |||||
| 2224 | return nil | 2221 | return nil | ||
| 2225 | } | 2222 | } | ||
| 2226 | 2223 | ||||
| 2227 | - | func (o *Omnibox) createFavoriteStarIcon() *gtk.Image { | 2224 | + | func (*Omnibox) createFavoriteStarIcon() *gtk.Label { |
| 2228 | - | star := gtk.NewImage() | 2225 | + | marker := gtk.NewLabel(nil) |
| 2229 | - | if star == nil { | 2226 | + | if marker == nil { |
| 2230 | return nil | 2227 | return nil | ||
| 2231 | } | 2228 | } | ||
| 2232 | - | iconName := "starred-symbolic" | 2229 | + | marker.SetText("*") |
| 2233 | - | star.SetFromIconName(&iconName) | 2230 | + | marker.AddCssClass("omnibox-favorite-star") |
| 2234 | - | star.SetPixelSize(favoriteStarSize(o.uiScale)) | 2231 | + | marker.SetValign(gtk.AlignCenterValue) |
| 2235 | - | star.AddCssClass("omnibox-favorite-star") | 2232 | + | tooltip := "Favorite" |
| 2236 | - | star.SetValign(gtk.AlignCenterValue) | 2233 | + | marker.SetTooltipText(&tooltip) |
| 2237 | - | return star | 2234 | + | return marker |
| 2238 | } | 2235 | } | ||
| 2239 | 2236 | ||||
| 2240 | func (o *Omnibox) appendSuggestionTitleAndURL(textBox *gtk.Box, title, displayURL string) { | 2237 | func (o *Omnibox) appendSuggestionTitleAndURL(textBox *gtk.Box, title, displayURL string) { | ||
M internal/ui/component/omnibox_favorite_icon_test.go +0 -5
| @@ -6,11 +6,6 @@ import ( | |||
| 6 | 6 | "github.com/stretchr/testify/assert" | |
| 7 | 7 | ) | |
| 8 | 8 | ||
| 9 | - | func TestOmniboxFavoriteStarSize(t *testing.T) { | |
| 10 | - | assert.Equal(t, 18, favoriteStarSize(1.0)) | |
| 11 | - | assert.Equal(t, 27, favoriteStarSize(1.5)) | |
| 12 | - | } | |
| 13 | - | ||
| 14 | 9 | func TestOmniboxFavoriteStarVisibility(t *testing.T) { | |
| 15 | 10 | assert.True(t, shouldShowFavoriteStar(Suggestion{IsFavorite: true})) | |
| 16 | 11 | assert.False(t, shouldShowFavoriteStar(Suggestion{IsFavorite: false})) | |
| @@ -6,11 +6,6 @@ import ( | |||||
| 6 | "github.com/stretchr/testify/assert" | 6 | "github.com/stretchr/testify/assert" | ||
| 7 | ) | 7 | ) | ||
| 8 | 8 | ||||
| 9 | - | func TestOmniboxFavoriteStarSize(t *testing.T) { | |||
| 10 | - | assert.Equal(t, 18, favoriteStarSize(1.0)) | |||
| 11 | - | assert.Equal(t, 27, favoriteStarSize(1.5)) | |||
| 12 | - | } | |||
| 13 | - | ||||
| 14 | func TestOmniboxFavoriteStarVisibility(t *testing.T) { | 9 | func TestOmniboxFavoriteStarVisibility(t *testing.T) { | ||
| 15 | assert.True(t, shouldShowFavoriteStar(Suggestion{IsFavorite: true})) | 10 | assert.True(t, shouldShowFavoriteStar(Suggestion{IsFavorite: true})) | ||
| 16 | assert.False(t, shouldShowFavoriteStar(Suggestion{IsFavorite: false})) | 11 | assert.False(t, shouldShowFavoriteStar(Suggestion{IsFavorite: false})) | ||
M internal/ui/component/omnibox_initial_behavior_test.go +2 -2
| @@ -27,7 +27,7 @@ func TestOmniboxInitialBehaviorBadgeState(t *testing.T) { | |||
| 27 | 27 | behavior: entity.OmniboxInitialBehaviorRecent, | |
| 28 | 28 | wantVisible: true, | |
| 29 | 29 | wantLabel: "Recent", | |
| 30 | - | wantTooltip: "Toggle default history order (Ctrl+R)", | |
| 30 | + | wantTooltip: "Default history order · Ctrl+R to switch", | |
| 31 | 31 | wantNext: entity.OmniboxInitialBehaviorMostVisited, | |
| 32 | 32 | }, | |
| 33 | 33 | { | |
| @@ -35,7 +35,7 @@ func TestOmniboxInitialBehaviorBadgeState(t *testing.T) { | |||
| 35 | 35 | behavior: entity.OmniboxInitialBehaviorMostVisited, | |
| 36 | 36 | wantVisible: true, | |
| 37 | 37 | wantLabel: "Most used", | |
| 38 | - | wantTooltip: "Toggle default history order (Ctrl+R)", | |
| 38 | + | wantTooltip: "Default history order · Ctrl+R to switch", | |
| 39 | 39 | wantNext: entity.OmniboxInitialBehaviorRecent, | |
| 40 | 40 | }, | |
| 41 | 41 | { | |
| @@ -27,7 +27,7 @@ func TestOmniboxInitialBehaviorBadgeState(t *testing.T) { | |||||
| 27 | behavior: entity.OmniboxInitialBehaviorRecent, | 27 | behavior: entity.OmniboxInitialBehaviorRecent, | ||
| 28 | wantVisible: true, | 28 | wantVisible: true, | ||
| 29 | wantLabel: "Recent", | 29 | wantLabel: "Recent", | ||
| 30 | - | wantTooltip: "Toggle default history order (Ctrl+R)", | 30 | + | wantTooltip: "Default history order · Ctrl+R to switch", |
| 31 | wantNext: entity.OmniboxInitialBehaviorMostVisited, | 31 | wantNext: entity.OmniboxInitialBehaviorMostVisited, | ||
| 32 | }, | 32 | }, | ||
| 33 | { | 33 | { | ||
| @@ -35,7 +35,7 @@ func TestOmniboxInitialBehaviorBadgeState(t *testing.T) { | |||||
| 35 | behavior: entity.OmniboxInitialBehaviorMostVisited, | 35 | behavior: entity.OmniboxInitialBehaviorMostVisited, | ||
| 36 | wantVisible: true, | 36 | wantVisible: true, | ||
| 37 | wantLabel: "Most used", | 37 | wantLabel: "Most used", | ||
| 38 | - | wantTooltip: "Toggle default history order (Ctrl+R)", | 38 | + | wantTooltip: "Default history order · Ctrl+R to switch", |
| 39 | wantNext: entity.OmniboxInitialBehaviorRecent, | 39 | wantNext: entity.OmniboxInitialBehaviorRecent, | ||
| 40 | }, | 40 | }, | ||
| 41 | { | 41 | { | ||
M internal/ui/theme/css.go +205 -8
| @@ -406,11 +406,14 @@ entry.omnibox-entry > text:focus-visible { | |||
| 406 | 406 | font-weight: 400; | |
| 407 | 407 | } | |
| 408 | 408 | ||
| 409 | - | .omnibox-favorite-star { | |
| 409 | + | .omnibox-row .omnibox-favorite-star { | |
| 410 | 410 | color: mix(var(--warning), var(--muted), 0.45); | |
| 411 | 411 | opacity: 0.75; | |
| 412 | 412 | margin-left: 0.625em; | |
| 413 | 413 | margin-right: 0.125em; | |
| 414 | + | font-family: var(--font-mono); | |
| 415 | + | font-size: 1em; | |
| 416 | + | font-weight: 700; | |
| 414 | 417 | } | |
| 415 | 418 | ||
| 416 | 419 | .omnibox-row:selected .omnibox-favorite-star { | |
| @@ -463,13 +466,6 @@ entry.omnibox-entry > text:focus-visible { | |||
| 463 | 466 | padding: 0.0625em 0.375em; | |
| 464 | 467 | font-size: 0.6875em; | |
| 465 | 468 | font-weight: 500; | |
| 466 | - | transition: background-color 100ms ease-in-out, border-color 100ms ease-in-out, color 100ms ease-in-out; | |
| 467 | - | } | |
| 468 | - | ||
| 469 | - | .omnibox-header-badge:hover { | |
| 470 | - | background-color: shade(var(--surface-variant), 1.08); | |
| 471 | - | border-color: var(--border); | |
| 472 | - | color: var(--text); | |
| 473 | 469 | } | |
| 474 | 470 | ||
| 475 | 471 | /* Zoom indicator in omnibox header */ | |
| @@ -508,6 +504,207 @@ entry.omnibox-entry.omnibox-entry-bang-active:focus-visible > text { | |||
| 508 | 504 | margin-top: -0.0625em; /* Nudge up 1px to align with entry text baseline */ | |
| 509 | 505 | background-color: transparent; | |
| 510 | 506 | } | |
| 507 | + | ||
| 508 | + | /* Multiplexer: segmented status line, square prompt, and strong active rail. */ | |
| 509 | + | .omnibox-container.omnibox-style-multiplexer { | |
| 510 | + | border-radius: 0; | |
| 511 | + | border-color: var(--border); | |
| 512 | + | box-shadow: 0 1.125em 3em alpha(black, 0.34); | |
| 513 | + | } | |
| 514 | + | ||
| 515 | + | .omnibox-style-multiplexer .omnibox-header { | |
| 516 | + | padding: 0; | |
| 517 | + | min-height: 2em; | |
| 518 | + | background-color: shade(var(--surface), 0.94); | |
| 519 | + | } | |
| 520 | + | ||
| 521 | + | .omnibox-style-multiplexer .omnibox-header-btn { | |
| 522 | + | margin: 0; | |
| 523 | + | padding: 0.375em 1em; | |
| 524 | + | border: none; | |
| 525 | + | border-right: 0.0625em solid var(--border); | |
| 526 | + | border-radius: 0; | |
| 527 | + | font-family: var(--font-mono); | |
| 528 | + | font-size: 0.75em; | |
| 529 | + | font-weight: 600; | |
| 530 | + | } | |
| 531 | + | ||
| 532 | + | .omnibox-style-multiplexer .omnibox-header-btn.omnibox-header-active { | |
| 533 | + | background-color: var(--accent); | |
| 534 | + | color: var(--accent-text); | |
| 535 | + | } | |
| 536 | + | ||
| 537 | + | .omnibox-style-multiplexer .omnibox-header-badge { | |
| 538 | + | padding: 0.1875em 0.5em; | |
| 539 | + | background-color: transparent; | |
| 540 | + | border: none; | |
| 541 | + | color: var(--muted); | |
| 542 | + | font-family: var(--font-mono); | |
| 543 | + | font-size: 0.6875em; | |
| 544 | + | font-weight: 600; | |
| 545 | + | } | |
| 546 | + | ||
| 547 | + | .omnibox-style-multiplexer .omnibox-bang-badge, | |
| 548 | + | .omnibox-style-multiplexer .omnibox-zoom-indicator, | |
| 549 | + | .omnibox-style-multiplexer .omnibox-shortcut-badge { | |
| 550 | + | border-radius: 0; | |
| 551 | + | font-family: var(--font-mono); | |
| 552 | + | } | |
| 553 | + | ||
| 554 | + | .omnibox-style-multiplexer entry.omnibox-entry { | |
| 555 | + | margin: 0.75em; | |
| 556 | + | border-radius: 0; | |
| 557 | + | border-left: 0.25em solid var(--accent); | |
| 558 | + | font-family: var(--font-mono); | |
| 559 | + | } | |
| 560 | + | ||
| 561 | + | .omnibox-style-multiplexer .omnibox-row { | |
| 562 | + | border-left-width: 0.25em; | |
| 563 | + | } | |
| 564 | + | ||
| 565 | + | .omnibox-style-multiplexer .omnibox-row:selected { | |
| 566 | + | background-color: alpha(var(--accent), 0.14); | |
| 567 | + | box-shadow: 0.1875em 0 0 var(--accent) inset; | |
| 568 | + | } | |
| 569 | + | ||
| 570 | + | .omnibox-style-multiplexer .omnibox-favorite-star { | |
| 571 | + | min-width: 1.25em; | |
| 572 | + | margin: 0 0.375em 0 0.125em; | |
| 573 | + | padding: 0; | |
| 574 | + | color: var(--warning); | |
| 575 | + | opacity: 0.85; | |
| 576 | + | } | |
| 577 | + | ||
| 578 | + | /* Command: a calmer command palette with a floating input and compact tabs. */ | |
| 579 | + | .omnibox-container.omnibox-style-command { | |
| 580 | + | border-radius: 0.625em; | |
| 581 | + | box-shadow: 0 1.25em 3.5em alpha(black, 0.38), 0 0 0 0.0625em alpha(white, 0.04) inset; | |
| 582 | + | padding: 0.375em; | |
| 583 | + | } | |
| 584 | + | ||
| 585 | + | .omnibox-style-command .omnibox-header { | |
| 586 | + | border-bottom: none; | |
| 587 | + | padding: 0.25em 0.375em 0.125em; | |
| 588 | + | } | |
| 589 | + | ||
| 590 | + | .omnibox-style-command .omnibox-header-btn { | |
| 591 | + | border-radius: 1em; | |
| 592 | + | padding: 0.25em 0.75em; | |
| 593 | + | font-size: 0.75em; | |
| 594 | + | } | |
| 595 | + | ||
| 596 | + | .omnibox-style-command .omnibox-header-btn.omnibox-header-active { | |
| 597 | + | background-color: alpha(var(--accent), 0.13); | |
| 598 | + | border-color: transparent; | |
| 599 | + | } | |
| 600 | + | ||
| 601 | + | .omnibox-style-command .omnibox-header-badge { | |
| 602 | + | padding: 0.1875em 0.5em; | |
| 603 | + | background-color: transparent; | |
| 604 | + | border: none; | |
| 605 | + | color: var(--muted); | |
| 606 | + | font-family: var(--font-mono); | |
| 607 | + | font-size: 0.6875em; | |
| 608 | + | font-weight: 600; | |
| 609 | + | } | |
| 610 | + | ||
| 611 | + | .omnibox-style-command entry.omnibox-entry { | |
| 612 | + | margin: 0.5em 0.375em 0.75em; | |
| 613 | + | padding: 0.75em 0.875em; | |
| 614 | + | border-radius: 0.375em; | |
| 615 | + | font-family: var(--font-mono); | |
| 616 | + | box-shadow: 0 0 0 0.0625em alpha(var(--accent), 0.12), 0 0.5em 1.5em alpha(black, 0.12); | |
| 617 | + | } | |
| 618 | + | ||
| 619 | + | .omnibox-style-command .omnibox-scrolled { | |
| 620 | + | border-top-color: alpha(var(--border), 0.35); | |
| 621 | + | } | |
| 622 | + | ||
| 623 | + | .omnibox-style-command .omnibox-row { | |
| 624 | + | margin: 0.125em 0.25em; | |
| 625 | + | border: none; | |
| 626 | + | border-radius: 0.25em; | |
| 627 | + | } | |
| 628 | + | ||
| 629 | + | .omnibox-style-command .omnibox-row:selected { | |
| 630 | + | background-color: alpha(var(--accent), 0.16); | |
| 631 | + | border-left-color: transparent; | |
| 632 | + | } | |
| 633 | + | ||
| 634 | + | .omnibox-style-command .omnibox-favorite-star { | |
| 635 | + | min-width: 1.25em; | |
| 636 | + | margin: 0 0.375em 0 0.125em; | |
| 637 | + | padding: 0; | |
| 638 | + | color: var(--warning); | |
| 639 | + | opacity: 0.85; | |
| 640 | + | } | |
| 641 | + | ||
| 642 | + | /* Minimal: terminal typography and hairline structure with almost no chrome. */ | |
| 643 | + | .omnibox-container.omnibox-style-minimal { | |
| 644 | + | border-radius: 0; | |
| 645 | + | box-shadow: 0 0.75em 2em alpha(black, 0.2); | |
| 646 | + | } | |
| 647 | + | ||
| 648 | + | .omnibox-style-minimal .omnibox-header { | |
| 649 | + | padding: 0.25em 0.75em; | |
| 650 | + | background-color: var(--surface); | |
| 651 | + | } | |
| 652 | + | ||
| 653 | + | .omnibox-style-minimal .omnibox-header-btn { | |
| 654 | + | margin-right: 1em; | |
| 655 | + | padding: 0.25em 0; | |
| 656 | + | border: none; | |
| 657 | + | border-bottom: 0.125em solid transparent; | |
| 658 | + | border-radius: 0; | |
| 659 | + | background-color: transparent; | |
| 660 | + | font-family: var(--font-mono); | |
| 661 | + | font-size: 0.75em; | |
| 662 | + | } | |
| 663 | + | ||
| 664 | + | .omnibox-style-minimal .omnibox-header-btn.omnibox-header-active { | |
| 665 | + | background-color: transparent; | |
| 666 | + | border-bottom-color: var(--accent); | |
| 667 | + | } | |
| 668 | + | ||
| 669 | + | .omnibox-style-minimal entry.omnibox-entry { | |
| 670 | + | margin: 0; | |
| 671 | + | padding: 0.75em; | |
| 672 | + | border: none; | |
| 673 | + | border-bottom: 0.0625em solid var(--border); | |
| 674 | + | border-radius: 0; | |
| 675 | + | background-color: var(--surface); | |
| 676 | + | font-family: var(--font-mono); | |
| 677 | + | box-shadow: none; | |
| 678 | + | } | |
| 679 | + | ||
| 680 | + | .omnibox-style-minimal .omnibox-scrolled { | |
| 681 | + | border-top: none; | |
| 682 | + | } | |
| 683 | + | ||
| 684 | + | .omnibox-style-minimal .omnibox-row { | |
| 685 | + | padding-top: 0.4375em; | |
| 686 | + | padding-bottom: 0.4375em; | |
| 687 | + | border-left-width: 0.125em; | |
| 688 | + | font-family: var(--font-mono); | |
| 689 | + | } | |
| 690 | + | ||
| 691 | + | .omnibox-style-minimal .omnibox-favorite-star { | |
| 692 | + | color: var(--warning); | |
| 693 | + | opacity: 0.85; | |
| 694 | + | } | |
| 695 | + | ||
| 696 | + | .omnibox-style-minimal .omnibox-header-badge { | |
| 697 | + | background-color: transparent; | |
| 698 | + | border: none; | |
| 699 | + | color: var(--muted); | |
| 700 | + | font-family: var(--font-mono); | |
| 701 | + | } | |
| 702 | + | ||
| 703 | + | .omnibox-style-minimal .omnibox-shortcut-badge { | |
| 704 | + | background-color: transparent; | |
| 705 | + | border: 0.0625em solid alpha(var(--border), 0.7); | |
| 706 | + | border-radius: 0; | |
| 707 | + | } | |
| 511 | 708 | ` | |
| 512 | 709 | } | |
| 513 | 710 | ||
| @@ -406,11 +406,14 @@ entry.omnibox-entry > text:focus-visible { | |||||
| 406 | font-weight: 400; | 406 | font-weight: 400; | ||
| 407 | } | 407 | } | ||
| 408 | 408 | ||||
| 409 | - | .omnibox-favorite-star { | 409 | + | .omnibox-row .omnibox-favorite-star { |
| 410 | color: mix(var(--warning), var(--muted), 0.45); | 410 | color: mix(var(--warning), var(--muted), 0.45); | ||
| 411 | opacity: 0.75; | 411 | opacity: 0.75; | ||
| 412 | margin-left: 0.625em; | 412 | margin-left: 0.625em; | ||
| 413 | margin-right: 0.125em; | 413 | margin-right: 0.125em; | ||
| 414 | + | font-family: var(--font-mono); | |||
| 415 | + | font-size: 1em; | |||
| 416 | + | font-weight: 700; | |||
| 414 | } | 417 | } | ||
| 415 | 418 | ||||
| 416 | .omnibox-row:selected .omnibox-favorite-star { | 419 | .omnibox-row:selected .omnibox-favorite-star { | ||
| @@ -463,13 +466,6 @@ entry.omnibox-entry > text:focus-visible { | |||||
| 463 | padding: 0.0625em 0.375em; | 466 | padding: 0.0625em 0.375em; | ||
| 464 | font-size: 0.6875em; | 467 | font-size: 0.6875em; | ||
| 465 | font-weight: 500; | 468 | font-weight: 500; | ||
| 466 | - | transition: background-color 100ms ease-in-out, border-color 100ms ease-in-out, color 100ms ease-in-out; | |||
| 467 | - | } | |||
| 468 | - | ||||
| 469 | - | .omnibox-header-badge:hover { | |||
| 470 | - | background-color: shade(var(--surface-variant), 1.08); | |||
| 471 | - | border-color: var(--border); | |||
| 472 | - | color: var(--text); | |||
| 473 | } | 469 | } | ||
| 474 | 470 | ||||
| 475 | /* Zoom indicator in omnibox header */ | 471 | /* Zoom indicator in omnibox header */ | ||
| @@ -508,6 +504,207 @@ entry.omnibox-entry.omnibox-entry-bang-active:focus-visible > text { | |||||
| 508 | margin-top: -0.0625em; /* Nudge up 1px to align with entry text baseline */ | 504 | margin-top: -0.0625em; /* Nudge up 1px to align with entry text baseline */ | ||
| 509 | background-color: transparent; | 505 | background-color: transparent; | ||
| 510 | } | 506 | } | ||
| 507 | + | ||||
| 508 | + | /* Multiplexer: segmented status line, square prompt, and strong active rail. */ | |||
| 509 | + | .omnibox-container.omnibox-style-multiplexer { | |||
| 510 | + | border-radius: 0; | |||
| 511 | + | border-color: var(--border); | |||
| 512 | + | box-shadow: 0 1.125em 3em alpha(black, 0.34); | |||
| 513 | + | } | |||
| 514 | + | ||||
| 515 | + | .omnibox-style-multiplexer .omnibox-header { | |||
| 516 | + | padding: 0; | |||
| 517 | + | min-height: 2em; | |||
| 518 | + | background-color: shade(var(--surface), 0.94); | |||
| 519 | + | } | |||
| 520 | + | ||||
| 521 | + | .omnibox-style-multiplexer .omnibox-header-btn { | |||
| 522 | + | margin: 0; | |||
| 523 | + | padding: 0.375em 1em; | |||
| 524 | + | border: none; | |||
| 525 | + | border-right: 0.0625em solid var(--border); | |||
| 526 | + | border-radius: 0; | |||
| 527 | + | font-family: var(--font-mono); | |||
| 528 | + | font-size: 0.75em; | |||
| 529 | + | font-weight: 600; | |||
| 530 | + | } | |||
| 531 | + | ||||
| 532 | + | .omnibox-style-multiplexer .omnibox-header-btn.omnibox-header-active { | |||
| 533 | + | background-color: var(--accent); | |||
| 534 | + | color: var(--accent-text); | |||
| 535 | + | } | |||
| 536 | + | ||||
| 537 | + | .omnibox-style-multiplexer .omnibox-header-badge { | |||
| 538 | + | padding: 0.1875em 0.5em; | |||
| 539 | + | background-color: transparent; | |||
| 540 | + | border: none; | |||
| 541 | + | color: var(--muted); | |||
| 542 | + | font-family: var(--font-mono); | |||
| 543 | + | font-size: 0.6875em; | |||
| 544 | + | font-weight: 600; | |||
| 545 | + | } | |||
| 546 | + | ||||
| 547 | + | .omnibox-style-multiplexer .omnibox-bang-badge, | |||
| 548 | + | .omnibox-style-multiplexer .omnibox-zoom-indicator, | |||
| 549 | + | .omnibox-style-multiplexer .omnibox-shortcut-badge { | |||
| 550 | + | border-radius: 0; | |||
| 551 | + | font-family: var(--font-mono); | |||
| 552 | + | } | |||
| 553 | + | ||||
| 554 | + | .omnibox-style-multiplexer entry.omnibox-entry { | |||
| 555 | + | margin: 0.75em; | |||
| 556 | + | border-radius: 0; | |||
| 557 | + | border-left: 0.25em solid var(--accent); | |||
| 558 | + | font-family: var(--font-mono); | |||
| 559 | + | } | |||
| 560 | + | ||||
| 561 | + | .omnibox-style-multiplexer .omnibox-row { | |||
| 562 | + | border-left-width: 0.25em; | |||
| 563 | + | } | |||
| 564 | + | ||||
| 565 | + | .omnibox-style-multiplexer .omnibox-row:selected { | |||
| 566 | + | background-color: alpha(var(--accent), 0.14); | |||
| 567 | + | box-shadow: 0.1875em 0 0 var(--accent) inset; | |||
| 568 | + | } | |||
| 569 | + | ||||
| 570 | + | .omnibox-style-multiplexer .omnibox-favorite-star { | |||
| 571 | + | min-width: 1.25em; | |||
| 572 | + | margin: 0 0.375em 0 0.125em; | |||
| 573 | + | padding: 0; | |||
| 574 | + | color: var(--warning); | |||
| 575 | + | opacity: 0.85; | |||
| 576 | + | } | |||
| 577 | + | ||||
| 578 | + | /* Command: a calmer command palette with a floating input and compact tabs. */ | |||
| 579 | + | .omnibox-container.omnibox-style-command { | |||
| 580 | + | border-radius: 0.625em; | |||
| 581 | + | box-shadow: 0 1.25em 3.5em alpha(black, 0.38), 0 0 0 0.0625em alpha(white, 0.04) inset; | |||
| 582 | + | padding: 0.375em; | |||
| 583 | + | } | |||
| 584 | + | ||||
| 585 | + | .omnibox-style-command .omnibox-header { | |||
| 586 | + | border-bottom: none; | |||
| 587 | + | padding: 0.25em 0.375em 0.125em; | |||
| 588 | + | } | |||
| 589 | + | ||||
| 590 | + | .omnibox-style-command .omnibox-header-btn { | |||
| 591 | + | border-radius: 1em; | |||
| 592 | + | padding: 0.25em 0.75em; | |||
| 593 | + | font-size: 0.75em; | |||
| 594 | + | } | |||
| 595 | + | ||||
| 596 | + | .omnibox-style-command .omnibox-header-btn.omnibox-header-active { | |||
| 597 | + | background-color: alpha(var(--accent), 0.13); | |||
| 598 | + | border-color: transparent; | |||
| 599 | + | } | |||
| 600 | + | ||||
| 601 | + | .omnibox-style-command .omnibox-header-badge { | |||
| 602 | + | padding: 0.1875em 0.5em; | |||
| 603 | + | background-color: transparent; | |||
| 604 | + | border: none; | |||
| 605 | + | color: var(--muted); | |||
| 606 | + | font-family: var(--font-mono); | |||
| 607 | + | font-size: 0.6875em; | |||
| 608 | + | font-weight: 600; | |||
| 609 | + | } | |||
| 610 | + | ||||
| 611 | + | .omnibox-style-command entry.omnibox-entry { | |||
| 612 | + | margin: 0.5em 0.375em 0.75em; | |||
| 613 | + | padding: 0.75em 0.875em; | |||
| 614 | + | border-radius: 0.375em; | |||
| 615 | + | font-family: var(--font-mono); | |||
| 616 | + | box-shadow: 0 0 0 0.0625em alpha(var(--accent), 0.12), 0 0.5em 1.5em alpha(black, 0.12); | |||
| 617 | + | } | |||
| 618 | + | ||||
| 619 | + | .omnibox-style-command .omnibox-scrolled { | |||
| 620 | + | border-top-color: alpha(var(--border), 0.35); | |||
| 621 | + | } | |||
| 622 | + | ||||
| 623 | + | .omnibox-style-command .omnibox-row { | |||
| 624 | + | margin: 0.125em 0.25em; | |||
| 625 | + | border: none; | |||
| 626 | + | border-radius: 0.25em; | |||
| 627 | + | } | |||
| 628 | + | ||||
| 629 | + | .omnibox-style-command .omnibox-row:selected { | |||
| 630 | + | background-color: alpha(var(--accent), 0.16); | |||
| 631 | + | border-left-color: transparent; | |||
| 632 | + | } | |||
| 633 | + | ||||
| 634 | + | .omnibox-style-command .omnibox-favorite-star { | |||
| 635 | + | min-width: 1.25em; | |||
| 636 | + | margin: 0 0.375em 0 0.125em; | |||
| 637 | + | padding: 0; | |||
| 638 | + | color: var(--warning); | |||
| 639 | + | opacity: 0.85; | |||
| 640 | + | } | |||
| 641 | + | ||||
| 642 | + | /* Minimal: terminal typography and hairline structure with almost no chrome. */ | |||
| 643 | + | .omnibox-container.omnibox-style-minimal { | |||
| 644 | + | border-radius: 0; | |||
| 645 | + | box-shadow: 0 0.75em 2em alpha(black, 0.2); | |||
| 646 | + | } | |||
| 647 | + | ||||
| 648 | + | .omnibox-style-minimal .omnibox-header { | |||
| 649 | + | padding: 0.25em 0.75em; | |||
| 650 | + | background-color: var(--surface); | |||
| 651 | + | } | |||
| 652 | + | ||||
| 653 | + | .omnibox-style-minimal .omnibox-header-btn { | |||
| 654 | + | margin-right: 1em; | |||
| 655 | + | padding: 0.25em 0; | |||
| 656 | + | border: none; | |||
| 657 | + | border-bottom: 0.125em solid transparent; | |||
| 658 | + | border-radius: 0; | |||
| 659 | + | background-color: transparent; | |||
| 660 | + | font-family: var(--font-mono); | |||
| 661 | + | font-size: 0.75em; | |||
| 662 | + | } | |||
| 663 | + | ||||
| 664 | + | .omnibox-style-minimal .omnibox-header-btn.omnibox-header-active { | |||
| 665 | + | background-color: transparent; | |||
| 666 | + | border-bottom-color: var(--accent); | |||
| 667 | + | } | |||
| 668 | + | ||||
| 669 | + | .omnibox-style-minimal entry.omnibox-entry { | |||
| 670 | + | margin: 0; | |||
| 671 | + | padding: 0.75em; | |||
| 672 | + | border: none; | |||
| 673 | + | border-bottom: 0.0625em solid var(--border); | |||
| 674 | + | border-radius: 0; | |||
| 675 | + | background-color: var(--surface); | |||
| 676 | + | font-family: var(--font-mono); | |||
| 677 | + | box-shadow: none; | |||
| 678 | + | } | |||
| 679 | + | ||||
| 680 | + | .omnibox-style-minimal .omnibox-scrolled { | |||
| 681 | + | border-top: none; | |||
| 682 | + | } | |||
| 683 | + | ||||
| 684 | + | .omnibox-style-minimal .omnibox-row { | |||
| 685 | + | padding-top: 0.4375em; | |||
| 686 | + | padding-bottom: 0.4375em; | |||
| 687 | + | border-left-width: 0.125em; | |||
| 688 | + | font-family: var(--font-mono); | |||
| 689 | + | } | |||
| 690 | + | ||||
| 691 | + | .omnibox-style-minimal .omnibox-favorite-star { | |||
| 692 | + | color: var(--warning); | |||
| 693 | + | opacity: 0.85; | |||
| 694 | + | } | |||
| 695 | + | ||||
| 696 | + | .omnibox-style-minimal .omnibox-header-badge { | |||
| 697 | + | background-color: transparent; | |||
| 698 | + | border: none; | |||
| 699 | + | color: var(--muted); | |||
| 700 | + | font-family: var(--font-mono); | |||
| 701 | + | } | |||
| 702 | + | ||||
| 703 | + | .omnibox-style-minimal .omnibox-shortcut-badge { | |||
| 704 | + | background-color: transparent; | |||
| 705 | + | border: 0.0625em solid alpha(var(--border), 0.7); | |||
| 706 | + | border-radius: 0; | |||
| 707 | + | } | |||
| 511 | ` | 708 | ` | ||
| 512 | } | 709 | } | ||
| 513 | 710 | ||||
M internal/ui/theme/css_test.go +18 -10
| @@ -103,9 +103,6 @@ func TestGenerateCSS_OmniboxHeaderBadgeSelectorsExist(t *testing.T) { | |||
| 103 | 103 | if !strings.Contains(css, ".omnibox-header-badge {") { | |
| 104 | 104 | t.Fatalf("expected omnibox header badge selector in CSS") | |
| 105 | 105 | } | |
| 106 | - | if !strings.Contains(css, ".omnibox-header-badge:hover {") { | |
| 107 | - | t.Fatalf("expected omnibox header badge hover selector in CSS") | |
| 108 | - | } | |
| 109 | 106 | } | |
| 110 | 107 | ||
| 111 | 108 | func TestGenerateCSS_OmniboxHeaderBadgeUsesPassiveNeutralChipStyling(t *testing.T) { | |
| @@ -116,11 +113,6 @@ func TestGenerateCSS_OmniboxHeaderBadgeUsesPassiveNeutralChipStyling(t *testing. | |||
| 116 | 113 | t.Fatalf("expected omnibox header badge to use passive neutral chip styling") | |
| 117 | 114 | } | |
| 118 | 115 | ||
| 119 | - | hoverRe := regexp.MustCompile(`(?s)\.omnibox-header-badge:hover\s*\{[^}]*background-color:\s*shade\(var\(--surface-variant\),\s*1\.08\);[^}]*border-color:\s*var\(--border\);[^}]*color:\s*var\(--text\);`) | |
| 120 | - | if !hoverRe.MatchString(css) { | |
| 121 | - | t.Fatalf("expected omnibox header badge hover styling to stay neutral while hinting interactivity") | |
| 122 | - | } | |
| 123 | - | ||
| 124 | 116 | badgeBlock := regexp.MustCompile(`(?s)\.omnibox-header-badge\s*\{[^}]*\}`).FindString(css) | |
| 125 | 117 | if strings.Contains(badgeBlock, "background-color: alpha(var(--accent)") { | |
| 126 | 118 | t.Fatalf("expected omnibox header badge resting state to avoid accent-based styling") | |
| @@ -142,8 +134,9 @@ func TestGenerateCSS_FavoriteRowsOnlyTintTrailingAffordance(t *testing.T) { | |||
| 142 | 134 | if strings.Contains(css, "border-right: 0.125em solid alpha(var(--warning), 0.55);") { | |
| 143 | 135 | t.Fatalf("expected favorite indicator to move off the right edge") | |
| 144 | 136 | } | |
| 145 | - | if !strings.Contains(css, "color: mix(var(--warning), var(--muted), 0.45);") { | |
| 146 | - | t.Fatalf("expected favorite star color to be softened") | |
| 137 | + | favoriteMarker := cssRuleBlock(t, css, ".omnibox-row .omnibox-favorite-star") | |
| 138 | + | if !strings.Contains(favoriteMarker, "color: mix(var(--warning), var(--muted), 0.45);") { | |
| 139 | + | t.Fatalf("expected favorite marker color to be softened") | |
| 147 | 140 | } | |
| 148 | 141 | if strings.Contains(css, ".omnibox-row.omnibox-row-favorite .omnibox-favorite-star-slot") { | |
| 149 | 142 | t.Fatalf("expected favorite highlight to move off the star slot") | |
| @@ -258,6 +251,21 @@ func TestGenerateCSSWithScale_UsesMediumWeightForOmniboxSuggestionTitle(t *testi | |||
| 258 | 251 | assert.NotContains(t, css, ".omnibox-suggestion-title {\n\tfont-size: 0.875em;\n\tcolor: var(--text);\n\tfont-weight: 400;") | |
| 259 | 252 | } | |
| 260 | 253 | ||
| 254 | + | func TestGenerateCSS_OmniboxIncludesVisualConcepts(t *testing.T) { | |
| 255 | + | css := GenerateCSS(DefaultDarkPalette()) | |
| 256 | + | ||
| 257 | + | for _, style := range []string{"multiplexer", "command", "minimal"} { | |
| 258 | + | assert.Contains(t, css, ".omnibox-container.omnibox-style-"+style) | |
| 259 | + | } | |
| 260 | + | assert.Contains(t, css, ".omnibox-style-multiplexer .omnibox-header-btn.omnibox-header-active") | |
| 261 | + | assert.Contains(t, css, ".omnibox-style-command .omnibox-header-badge") | |
| 262 | + | assert.Contains(t, css, ".omnibox-style-command .omnibox-favorite-star") | |
| 263 | + | assert.Contains(t, css, ".omnibox-style-command entry.omnibox-entry") | |
| 264 | + | minimalFavorite := cssRuleBlock(t, css, ".omnibox-style-minimal .omnibox-favorite-star") | |
| 265 | + | assert.Contains(t, minimalFavorite, "color: var(--warning);") | |
| 266 | + | assert.Contains(t, css, ".omnibox-style-minimal .omnibox-shortcut-badge") | |
| 267 | + | } | |
| 268 | + | ||
| 261 | 269 | func TestGenerateCSS_ContentAreaTabBarInset(t *testing.T) { | |
| 262 | 270 | css := GenerateCSS(DefaultDarkPalette()) | |
| 263 | 271 | ||
| @@ -103,9 +103,6 @@ func TestGenerateCSS_OmniboxHeaderBadgeSelectorsExist(t *testing.T) { | |||||
| 103 | if !strings.Contains(css, ".omnibox-header-badge {") { | 103 | if !strings.Contains(css, ".omnibox-header-badge {") { | ||
| 104 | t.Fatalf("expected omnibox header badge selector in CSS") | 104 | t.Fatalf("expected omnibox header badge selector in CSS") | ||
| 105 | } | 105 | } | ||
| 106 | - | if !strings.Contains(css, ".omnibox-header-badge:hover {") { | |||
| 107 | - | t.Fatalf("expected omnibox header badge hover selector in CSS") | |||
| 108 | - | } | |||
| 109 | } | 106 | } | ||
| 110 | 107 | ||||
| 111 | func TestGenerateCSS_OmniboxHeaderBadgeUsesPassiveNeutralChipStyling(t *testing.T) { | 108 | func TestGenerateCSS_OmniboxHeaderBadgeUsesPassiveNeutralChipStyling(t *testing.T) { | ||
| @@ -116,11 +113,6 @@ func TestGenerateCSS_OmniboxHeaderBadgeUsesPassiveNeutralChipStyling(t *testing. | |||||
| 116 | t.Fatalf("expected omnibox header badge to use passive neutral chip styling") | 113 | t.Fatalf("expected omnibox header badge to use passive neutral chip styling") | ||
| 117 | } | 114 | } | ||
| 118 | 115 | ||||
| 119 | - | hoverRe := regexp.MustCompile(`(?s)\.omnibox-header-badge:hover\s*\{[^}]*background-color:\s*shade\(var\(--surface-variant\),\s*1\.08\);[^}]*border-color:\s*var\(--border\);[^}]*color:\s*var\(--text\);`) | |||
| 120 | - | if !hoverRe.MatchString(css) { | |||
| 121 | - | t.Fatalf("expected omnibox header badge hover styling to stay neutral while hinting interactivity") | |||
| 122 | - | } | |||
| 123 | - | ||||
| 124 | badgeBlock := regexp.MustCompile(`(?s)\.omnibox-header-badge\s*\{[^}]*\}`).FindString(css) | 116 | badgeBlock := regexp.MustCompile(`(?s)\.omnibox-header-badge\s*\{[^}]*\}`).FindString(css) | ||
| 125 | if strings.Contains(badgeBlock, "background-color: alpha(var(--accent)") { | 117 | if strings.Contains(badgeBlock, "background-color: alpha(var(--accent)") { | ||
| 126 | t.Fatalf("expected omnibox header badge resting state to avoid accent-based styling") | 118 | t.Fatalf("expected omnibox header badge resting state to avoid accent-based styling") | ||
| @@ -142,8 +134,9 @@ func TestGenerateCSS_FavoriteRowsOnlyTintTrailingAffordance(t *testing.T) { | |||||
| 142 | if strings.Contains(css, "border-right: 0.125em solid alpha(var(--warning), 0.55);") { | 134 | if strings.Contains(css, "border-right: 0.125em solid alpha(var(--warning), 0.55);") { | ||
| 143 | t.Fatalf("expected favorite indicator to move off the right edge") | 135 | t.Fatalf("expected favorite indicator to move off the right edge") | ||
| 144 | } | 136 | } | ||
| 145 | - | if !strings.Contains(css, "color: mix(var(--warning), var(--muted), 0.45);") { | 137 | + | favoriteMarker := cssRuleBlock(t, css, ".omnibox-row .omnibox-favorite-star") |
| 146 | - | t.Fatalf("expected favorite star color to be softened") | 138 | + | if !strings.Contains(favoriteMarker, "color: mix(var(--warning), var(--muted), 0.45);") { |
| 139 | + | t.Fatalf("expected favorite marker color to be softened") | |||
| 147 | } | 140 | } | ||
| 148 | if strings.Contains(css, ".omnibox-row.omnibox-row-favorite .omnibox-favorite-star-slot") { | 141 | if strings.Contains(css, ".omnibox-row.omnibox-row-favorite .omnibox-favorite-star-slot") { | ||
| 149 | t.Fatalf("expected favorite highlight to move off the star slot") | 142 | t.Fatalf("expected favorite highlight to move off the star slot") | ||
| @@ -258,6 +251,21 @@ func TestGenerateCSSWithScale_UsesMediumWeightForOmniboxSuggestionTitle(t *testi | |||||
| 258 | assert.NotContains(t, css, ".omnibox-suggestion-title {\n\tfont-size: 0.875em;\n\tcolor: var(--text);\n\tfont-weight: 400;") | 251 | assert.NotContains(t, css, ".omnibox-suggestion-title {\n\tfont-size: 0.875em;\n\tcolor: var(--text);\n\tfont-weight: 400;") | ||
| 259 | } | 252 | } | ||
| 260 | 253 | ||||
| 254 | + | func TestGenerateCSS_OmniboxIncludesVisualConcepts(t *testing.T) { | |||
| 255 | + | css := GenerateCSS(DefaultDarkPalette()) | |||
| 256 | + | ||||
| 257 | + | for _, style := range []string{"multiplexer", "command", "minimal"} { | |||
| 258 | + | assert.Contains(t, css, ".omnibox-container.omnibox-style-"+style) | |||
| 259 | + | } | |||
| 260 | + | assert.Contains(t, css, ".omnibox-style-multiplexer .omnibox-header-btn.omnibox-header-active") | |||
| 261 | + | assert.Contains(t, css, ".omnibox-style-command .omnibox-header-badge") | |||
| 262 | + | assert.Contains(t, css, ".omnibox-style-command .omnibox-favorite-star") | |||
| 263 | + | assert.Contains(t, css, ".omnibox-style-command entry.omnibox-entry") | |||
| 264 | + | minimalFavorite := cssRuleBlock(t, css, ".omnibox-style-minimal .omnibox-favorite-star") | |||
| 265 | + | assert.Contains(t, minimalFavorite, "color: var(--warning);") | |||
| 266 | + | assert.Contains(t, css, ".omnibox-style-minimal .omnibox-shortcut-badge") | |||
| 267 | + | } | |||
| 268 | + | ||||
| 261 | func TestGenerateCSS_ContentAreaTabBarInset(t *testing.T) { | 269 | func TestGenerateCSS_ContentAreaTabBarInset(t *testing.T) { | ||
| 262 | css := GenerateCSS(DefaultDarkPalette()) | 270 | css := GenerateCSS(DefaultDarkPalette()) | ||
| 263 | 271 | ||||
M internal/ui/theme/palette.go +1 -0
| @@ -272,6 +272,7 @@ func (p Palette) ToCSSVars() string { | |||
| 272 | 272 | sb.WriteString(" --text: " + p.Text + ";\n") | |
| 273 | 273 | sb.WriteString(" --muted: " + p.Muted + ";\n") | |
| 274 | 274 | sb.WriteString(" --accent: " + p.Accent + ";\n") | |
| 275 | + | sb.WriteString(" --accent-text: " + readableTextColor(p.Accent, p.Background) + ";\n") | |
| 275 | 276 | sb.WriteString(" --border: " + p.Border + ";\n") | |
| 276 | 277 | sb.WriteString(" --success: " + p.Success + ";\n") | |
| 277 | 278 | sb.WriteString(" --warning: " + p.Warning + ";\n") | |
| @@ -272,6 +272,7 @@ func (p Palette) ToCSSVars() string { | |||||
| 272 | sb.WriteString(" --text: " + p.Text + ";\n") | 272 | sb.WriteString(" --text: " + p.Text + ";\n") | ||
| 273 | sb.WriteString(" --muted: " + p.Muted + ";\n") | 273 | sb.WriteString(" --muted: " + p.Muted + ";\n") | ||
| 274 | sb.WriteString(" --accent: " + p.Accent + ";\n") | 274 | sb.WriteString(" --accent: " + p.Accent + ";\n") | ||
| 275 | + | sb.WriteString(" --accent-text: " + readableTextColor(p.Accent, p.Background) + ";\n") | |||
| 275 | sb.WriteString(" --border: " + p.Border + ";\n") | 276 | sb.WriteString(" --border: " + p.Border + ";\n") | ||
| 276 | sb.WriteString(" --success: " + p.Success + ";\n") | 277 | sb.WriteString(" --success: " + p.Success + ";\n") | ||
| 277 | sb.WriteString(" --warning: " + p.Warning + ";\n") | 278 | sb.WriteString(" --warning: " + p.Warning + ";\n") | ||