feat(ui): add toast notification system for user feedback
- commit
- 83a6fc83a2bd82f6bafe9022d9c8c405b9612998
- author
- brice · · 2025-12-17 17:13 UTC
- committer
- brice · · 2025-12-17 17:19 UTC
- parent
- 89a38fc
- signature
- unsigned
5 files · +274 -2
M internal/ui/app.go +1 -0
| @@ -215,6 +215,7 @@ func (a *App) onActivate(ctx context.Context) { | |||
| 215 | 215 | HistoryUC: a.deps.HistoryUC, | |
| 216 | 216 | FavoritesUC: a.deps.FavoritesUC, | |
| 217 | 217 | FaviconCache: a.faviconCache, | |
| 218 | + | Clipboard: a.deps.Clipboard, | |
| 218 | 219 | Shortcuts: a.deps.Config.SearchShortcuts, | |
| 219 | 220 | DefaultSearch: a.deps.Config.DefaultSearchEngine, | |
| 220 | 221 | InitialBehavior: a.deps.Config.Omnibox.InitialBehavior, | |
| @@ -215,6 +215,7 @@ func (a *App) onActivate(ctx context.Context) { | |||||
| 215 | HistoryUC: a.deps.HistoryUC, | 215 | HistoryUC: a.deps.HistoryUC, | ||
| 216 | FavoritesUC: a.deps.FavoritesUC, | 216 | FavoritesUC: a.deps.FavoritesUC, | ||
| 217 | FaviconCache: a.faviconCache, | 217 | FaviconCache: a.faviconCache, | ||
| 218 | + | Clipboard: a.deps.Clipboard, | |||
| 218 | Shortcuts: a.deps.Config.SearchShortcuts, | 219 | Shortcuts: a.deps.Config.SearchShortcuts, | ||
| 219 | DefaultSearch: a.deps.Config.DefaultSearchEngine, | 220 | DefaultSearch: a.deps.Config.DefaultSearchEngine, | ||
| 220 | InitialBehavior: a.deps.Config.Omnibox.InitialBehavior, | 221 | InitialBehavior: a.deps.Config.Omnibox.InitialBehavior, | ||
A internal/ui/component/toaster.go +205 -0
| @@ -0,0 +1,205 @@ | |||
| 1 | + | // Package component provides UI components for the browser. | |
| 2 | + | package component | |
| 3 | + | ||
| 4 | + | import ( | |
| 5 | + | "context" | |
| 6 | + | "fmt" | |
| 7 | + | "sync" | |
| 8 | + | ||
| 9 | + | "github.com/jwijenbergh/puregotk/v4/glib" | |
| 10 | + | "github.com/jwijenbergh/puregotk/v4/gtk" | |
| 11 | + | ||
| 12 | + | "github.com/bnema/dumber/internal/logging" | |
| 13 | + | "github.com/bnema/dumber/internal/ui/layout" | |
| 14 | + | ) | |
| 15 | + | ||
| 16 | + | const ( | |
| 17 | + | // Default auto-dismiss timeout in milliseconds. | |
| 18 | + | toastDismissTimeoutMs = 1500 | |
| 19 | + | ) | |
| 20 | + | ||
| 21 | + | // ToastLevel indicates the visual style of a toast notification. | |
| 22 | + | type ToastLevel int | |
| 23 | + | ||
| 24 | + | const ( | |
| 25 | + | // ToastInfo is for informational messages (accent color). | |
| 26 | + | ToastInfo ToastLevel = iota | |
| 27 | + | // ToastSuccess is for success confirmations (green). | |
| 28 | + | ToastSuccess | |
| 29 | + | // ToastWarning is for warning messages (yellow). | |
| 30 | + | ToastWarning | |
| 31 | + | // ToastError is for error messages (red). | |
| 32 | + | ToastError | |
| 33 | + | ) | |
| 34 | + | ||
| 35 | + | // cssClass returns the CSS class for this toast level. | |
| 36 | + | func (l ToastLevel) cssClass() string { | |
| 37 | + | switch l { | |
| 38 | + | case ToastInfo: | |
| 39 | + | return "toast-info" | |
| 40 | + | case ToastSuccess: | |
| 41 | + | return "toast-success" | |
| 42 | + | case ToastWarning: | |
| 43 | + | return "toast-warning" | |
| 44 | + | case ToastError: | |
| 45 | + | return "toast-error" | |
| 46 | + | default: | |
| 47 | + | return "toast-info" | |
| 48 | + | } | |
| 49 | + | } | |
| 50 | + | ||
| 51 | + | // Toaster displays toast notifications in an overlay. | |
| 52 | + | // It supports different notification levels and auto-dismissal. | |
| 53 | + | // When a new toast is shown while one is already visible, the text | |
| 54 | + | // is updated in-place and the dismiss timer is reset (spam protection). | |
| 55 | + | type Toaster struct { | |
| 56 | + | factory layout.WidgetFactory | |
| 57 | + | container layout.BoxWidget // Outer container for positioning | |
| 58 | + | label layout.LabelWidget // Message text | |
| 59 | + | currentLevel ToastLevel | |
| 60 | + | visible bool | |
| 61 | + | dismissTimer uint // GLib timer source ID | |
| 62 | + | ||
| 63 | + | mu sync.Mutex | |
| 64 | + | } | |
| 65 | + | ||
| 66 | + | // NewToaster creates a new toaster component for overlay display. | |
| 67 | + | // The toaster is positioned in the top-left corner with margin. | |
| 68 | + | func NewToaster(factory layout.WidgetFactory) *Toaster { | |
| 69 | + | // Create container box for styling and positioning | |
| 70 | + | container := factory.NewBox(layout.OrientationHorizontal, 0) | |
| 71 | + | container.AddCssClass("toast") | |
| 72 | + | container.AddCssClass("toast-info") // Default level | |
| 73 | + | ||
| 74 | + | // Position in top-left with margin | |
| 75 | + | container.SetHalign(gtk.AlignStartValue) | |
| 76 | + | container.SetValign(gtk.AlignStartValue) | |
| 77 | + | ||
| 78 | + | // Don't expand to fill space | |
| 79 | + | container.SetHexpand(false) | |
| 80 | + | container.SetVexpand(false) | |
| 81 | + | ||
| 82 | + | // Don't intercept pointer events - let clicks pass through | |
| 83 | + | container.SetCanTarget(false) | |
| 84 | + | container.SetCanFocus(false) | |
| 85 | + | ||
| 86 | + | // Hidden by default | |
| 87 | + | container.SetVisible(false) | |
| 88 | + | ||
| 89 | + | // Create label for message text | |
| 90 | + | label := factory.NewLabel("") | |
| 91 | + | label.SetCanTarget(false) | |
| 92 | + | label.SetCanFocus(false) | |
| 93 | + | container.Append(label) | |
| 94 | + | ||
| 95 | + | return &Toaster{ | |
| 96 | + | factory: factory, | |
| 97 | + | container: container, | |
| 98 | + | label: label, | |
| 99 | + | currentLevel: ToastInfo, | |
| 100 | + | visible: false, | |
| 101 | + | } | |
| 102 | + | } | |
| 103 | + | ||
| 104 | + | // Show displays a toast notification with the given message and level. | |
| 105 | + | // If a toast is already visible, updates the text and resets the dismiss timer. | |
| 106 | + | func (t *Toaster) Show(ctx context.Context, message string, level ToastLevel) { | |
| 107 | + | log := logging.FromContext(ctx) | |
| 108 | + | ||
| 109 | + | t.mu.Lock() | |
| 110 | + | defer t.mu.Unlock() | |
| 111 | + | ||
| 112 | + | // Update level CSS class if changed | |
| 113 | + | if t.currentLevel != level { | |
| 114 | + | t.container.RemoveCssClass(t.currentLevel.cssClass()) | |
| 115 | + | t.container.AddCssClass(level.cssClass()) | |
| 116 | + | t.currentLevel = level | |
| 117 | + | } | |
| 118 | + | ||
| 119 | + | // Update message text | |
| 120 | + | t.label.SetText(message) | |
| 121 | + | ||
| 122 | + | // Cancel existing timer if any | |
| 123 | + | if t.dismissTimer != 0 { | |
| 124 | + | glib.SourceRemove(t.dismissTimer) | |
| 125 | + | t.dismissTimer = 0 | |
| 126 | + | } | |
| 127 | + | ||
| 128 | + | // Show the toast if not already visible | |
| 129 | + | if !t.visible { | |
| 130 | + | t.visible = true | |
| 131 | + | t.container.SetVisible(true) | |
| 132 | + | } | |
| 133 | + | ||
| 134 | + | // Start new dismiss timer | |
| 135 | + | t.startDismissTimer(ctx) | |
| 136 | + | ||
| 137 | + | log.Debug(). | |
| 138 | + | Str("message", message). | |
| 139 | + | Int("level", int(level)). | |
| 140 | + | Msg("toast shown") | |
| 141 | + | } | |
| 142 | + | ||
| 143 | + | // ShowZoom displays a zoom level toast (convenience method). | |
| 144 | + | // Formats the zoom percentage with a % suffix. | |
| 145 | + | func (t *Toaster) ShowZoom(ctx context.Context, zoomPercent int) { | |
| 146 | + | message := fmt.Sprintf("%d%%", zoomPercent) | |
| 147 | + | t.Show(ctx, message, ToastInfo) | |
| 148 | + | } | |
| 149 | + | ||
| 150 | + | // Hide manually dismisses the toast. | |
| 151 | + | func (t *Toaster) Hide() { | |
| 152 | + | t.mu.Lock() | |
| 153 | + | defer t.mu.Unlock() | |
| 154 | + | ||
| 155 | + | t.hide() | |
| 156 | + | } | |
| 157 | + | ||
| 158 | + | // hide performs the actual hide operation (must be called with lock held). | |
| 159 | + | func (t *Toaster) hide() { | |
| 160 | + | if !t.visible { | |
| 161 | + | return | |
| 162 | + | } | |
| 163 | + | ||
| 164 | + | // Cancel timer if running | |
| 165 | + | if t.dismissTimer != 0 { | |
| 166 | + | glib.SourceRemove(t.dismissTimer) | |
| 167 | + | t.dismissTimer = 0 | |
| 168 | + | } | |
| 169 | + | ||
| 170 | + | t.visible = false | |
| 171 | + | t.container.SetVisible(false) | |
| 172 | + | } | |
| 173 | + | ||
| 174 | + | // startDismissTimer starts the auto-dismiss timer. | |
| 175 | + | // Must be called with lock held. | |
| 176 | + | func (t *Toaster) startDismissTimer(ctx context.Context) { | |
| 177 | + | log := logging.FromContext(ctx) | |
| 178 | + | ||
| 179 | + | var cb glib.SourceFunc | |
| 180 | + | cb = func(_ uintptr) bool { | |
| 181 | + | t.mu.Lock() | |
| 182 | + | defer t.mu.Unlock() | |
| 183 | + | ||
| 184 | + | // Only hide if timer hasn't been replaced | |
| 185 | + | t.hide() | |
| 186 | + | t.dismissTimer = 0 | |
| 187 | + | ||
| 188 | + | log.Debug().Msg("toast auto-dismissed") | |
| 189 | + | return false // Don't repeat | |
| 190 | + | } | |
| 191 | + | ||
| 192 | + | t.dismissTimer = glib.TimeoutAdd(toastDismissTimeoutMs, &cb, 0) | |
| 193 | + | } | |
| 194 | + | ||
| 195 | + | // Widget returns the underlying widget for embedding in overlays. | |
| 196 | + | func (t *Toaster) Widget() layout.Widget { | |
| 197 | + | return t.container | |
| 198 | + | } | |
| 199 | + | ||
| 200 | + | // IsVisible returns whether the toast is currently visible. | |
| 201 | + | func (t *Toaster) IsVisible() bool { | |
| 202 | + | t.mu.Lock() | |
| 203 | + | defer t.mu.Unlock() | |
| 204 | + | return t.visible | |
| 205 | + | } | |
| @@ -0,0 +1,205 @@ | |||||
| 1 | + | // Package component provides UI components for the browser. | |||
| 2 | + | package component | |||
| 3 | + | ||||
| 4 | + | import ( | |||
| 5 | + | "context" | |||
| 6 | + | "fmt" | |||
| 7 | + | "sync" | |||
| 8 | + | ||||
| 9 | + | "github.com/jwijenbergh/puregotk/v4/glib" | |||
| 10 | + | "github.com/jwijenbergh/puregotk/v4/gtk" | |||
| 11 | + | ||||
| 12 | + | "github.com/bnema/dumber/internal/logging" | |||
| 13 | + | "github.com/bnema/dumber/internal/ui/layout" | |||
| 14 | + | ) | |||
| 15 | + | ||||
| 16 | + | const ( | |||
| 17 | + | // Default auto-dismiss timeout in milliseconds. | |||
| 18 | + | toastDismissTimeoutMs = 1500 | |||
| 19 | + | ) | |||
| 20 | + | ||||
| 21 | + | // ToastLevel indicates the visual style of a toast notification. | |||
| 22 | + | type ToastLevel int | |||
| 23 | + | ||||
| 24 | + | const ( | |||
| 25 | + | // ToastInfo is for informational messages (accent color). | |||
| 26 | + | ToastInfo ToastLevel = iota | |||
| 27 | + | // ToastSuccess is for success confirmations (green). | |||
| 28 | + | ToastSuccess | |||
| 29 | + | // ToastWarning is for warning messages (yellow). | |||
| 30 | + | ToastWarning | |||
| 31 | + | // ToastError is for error messages (red). | |||
| 32 | + | ToastError | |||
| 33 | + | ) | |||
| 34 | + | ||||
| 35 | + | // cssClass returns the CSS class for this toast level. | |||
| 36 | + | func (l ToastLevel) cssClass() string { | |||
| 37 | + | switch l { | |||
| 38 | + | case ToastInfo: | |||
| 39 | + | return "toast-info" | |||
| 40 | + | case ToastSuccess: | |||
| 41 | + | return "toast-success" | |||
| 42 | + | case ToastWarning: | |||
| 43 | + | return "toast-warning" | |||
| 44 | + | case ToastError: | |||
| 45 | + | return "toast-error" | |||
| 46 | + | default: | |||
| 47 | + | return "toast-info" | |||
| 48 | + | } | |||
| 49 | + | } | |||
| 50 | + | ||||
| 51 | + | // Toaster displays toast notifications in an overlay. | |||
| 52 | + | // It supports different notification levels and auto-dismissal. | |||
| 53 | + | // When a new toast is shown while one is already visible, the text | |||
| 54 | + | // is updated in-place and the dismiss timer is reset (spam protection). | |||
| 55 | + | type Toaster struct { | |||
| 56 | + | factory layout.WidgetFactory | |||
| 57 | + | container layout.BoxWidget // Outer container for positioning | |||
| 58 | + | label layout.LabelWidget // Message text | |||
| 59 | + | currentLevel ToastLevel | |||
| 60 | + | visible bool | |||
| 61 | + | dismissTimer uint // GLib timer source ID | |||
| 62 | + | ||||
| 63 | + | mu sync.Mutex | |||
| 64 | + | } | |||
| 65 | + | ||||
| 66 | + | // NewToaster creates a new toaster component for overlay display. | |||
| 67 | + | // The toaster is positioned in the top-left corner with margin. | |||
| 68 | + | func NewToaster(factory layout.WidgetFactory) *Toaster { | |||
| 69 | + | // Create container box for styling and positioning | |||
| 70 | + | container := factory.NewBox(layout.OrientationHorizontal, 0) | |||
| 71 | + | container.AddCssClass("toast") | |||
| 72 | + | container.AddCssClass("toast-info") // Default level | |||
| 73 | + | ||||
| 74 | + | // Position in top-left with margin | |||
| 75 | + | container.SetHalign(gtk.AlignStartValue) | |||
| 76 | + | container.SetValign(gtk.AlignStartValue) | |||
| 77 | + | ||||
| 78 | + | // Don't expand to fill space | |||
| 79 | + | container.SetHexpand(false) | |||
| 80 | + | container.SetVexpand(false) | |||
| 81 | + | ||||
| 82 | + | // Don't intercept pointer events - let clicks pass through | |||
| 83 | + | container.SetCanTarget(false) | |||
| 84 | + | container.SetCanFocus(false) | |||
| 85 | + | ||||
| 86 | + | // Hidden by default | |||
| 87 | + | container.SetVisible(false) | |||
| 88 | + | ||||
| 89 | + | // Create label for message text | |||
| 90 | + | label := factory.NewLabel("") | |||
| 91 | + | label.SetCanTarget(false) | |||
| 92 | + | label.SetCanFocus(false) | |||
| 93 | + | container.Append(label) | |||
| 94 | + | ||||
| 95 | + | return &Toaster{ | |||
| 96 | + | factory: factory, | |||
| 97 | + | container: container, | |||
| 98 | + | label: label, | |||
| 99 | + | currentLevel: ToastInfo, | |||
| 100 | + | visible: false, | |||
| 101 | + | } | |||
| 102 | + | } | |||
| 103 | + | ||||
| 104 | + | // Show displays a toast notification with the given message and level. | |||
| 105 | + | // If a toast is already visible, updates the text and resets the dismiss timer. | |||
| 106 | + | func (t *Toaster) Show(ctx context.Context, message string, level ToastLevel) { | |||
| 107 | + | log := logging.FromContext(ctx) | |||
| 108 | + | ||||
| 109 | + | t.mu.Lock() | |||
| 110 | + | defer t.mu.Unlock() | |||
| 111 | + | ||||
| 112 | + | // Update level CSS class if changed | |||
| 113 | + | if t.currentLevel != level { | |||
| 114 | + | t.container.RemoveCssClass(t.currentLevel.cssClass()) | |||
| 115 | + | t.container.AddCssClass(level.cssClass()) | |||
| 116 | + | t.currentLevel = level | |||
| 117 | + | } | |||
| 118 | + | ||||
| 119 | + | // Update message text | |||
| 120 | + | t.label.SetText(message) | |||
| 121 | + | ||||
| 122 | + | // Cancel existing timer if any | |||
| 123 | + | if t.dismissTimer != 0 { | |||
| 124 | + | glib.SourceRemove(t.dismissTimer) | |||
| 125 | + | t.dismissTimer = 0 | |||
| 126 | + | } | |||
| 127 | + | ||||
| 128 | + | // Show the toast if not already visible | |||
| 129 | + | if !t.visible { | |||
| 130 | + | t.visible = true | |||
| 131 | + | t.container.SetVisible(true) | |||
| 132 | + | } | |||
| 133 | + | ||||
| 134 | + | // Start new dismiss timer | |||
| 135 | + | t.startDismissTimer(ctx) | |||
| 136 | + | ||||
| 137 | + | log.Debug(). | |||
| 138 | + | Str("message", message). | |||
| 139 | + | Int("level", int(level)). | |||
| 140 | + | Msg("toast shown") | |||
| 141 | + | } | |||
| 142 | + | ||||
| 143 | + | // ShowZoom displays a zoom level toast (convenience method). | |||
| 144 | + | // Formats the zoom percentage with a % suffix. | |||
| 145 | + | func (t *Toaster) ShowZoom(ctx context.Context, zoomPercent int) { | |||
| 146 | + | message := fmt.Sprintf("%d%%", zoomPercent) | |||
| 147 | + | t.Show(ctx, message, ToastInfo) | |||
| 148 | + | } | |||
| 149 | + | ||||
| 150 | + | // Hide manually dismisses the toast. | |||
| 151 | + | func (t *Toaster) Hide() { | |||
| 152 | + | t.mu.Lock() | |||
| 153 | + | defer t.mu.Unlock() | |||
| 154 | + | ||||
| 155 | + | t.hide() | |||
| 156 | + | } | |||
| 157 | + | ||||
| 158 | + | // hide performs the actual hide operation (must be called with lock held). | |||
| 159 | + | func (t *Toaster) hide() { | |||
| 160 | + | if !t.visible { | |||
| 161 | + | return | |||
| 162 | + | } | |||
| 163 | + | ||||
| 164 | + | // Cancel timer if running | |||
| 165 | + | if t.dismissTimer != 0 { | |||
| 166 | + | glib.SourceRemove(t.dismissTimer) | |||
| 167 | + | t.dismissTimer = 0 | |||
| 168 | + | } | |||
| 169 | + | ||||
| 170 | + | t.visible = false | |||
| 171 | + | t.container.SetVisible(false) | |||
| 172 | + | } | |||
| 173 | + | ||||
| 174 | + | // startDismissTimer starts the auto-dismiss timer. | |||
| 175 | + | // Must be called with lock held. | |||
| 176 | + | func (t *Toaster) startDismissTimer(ctx context.Context) { | |||
| 177 | + | log := logging.FromContext(ctx) | |||
| 178 | + | ||||
| 179 | + | var cb glib.SourceFunc | |||
| 180 | + | cb = func(_ uintptr) bool { | |||
| 181 | + | t.mu.Lock() | |||
| 182 | + | defer t.mu.Unlock() | |||
| 183 | + | ||||
| 184 | + | // Only hide if timer hasn't been replaced | |||
| 185 | + | t.hide() | |||
| 186 | + | t.dismissTimer = 0 | |||
| 187 | + | ||||
| 188 | + | log.Debug().Msg("toast auto-dismissed") | |||
| 189 | + | return false // Don't repeat | |||
| 190 | + | } | |||
| 191 | + | ||||
| 192 | + | t.dismissTimer = glib.TimeoutAdd(toastDismissTimeoutMs, &cb, 0) | |||
| 193 | + | } | |||
| 194 | + | ||||
| 195 | + | // Widget returns the underlying widget for embedding in overlays. | |||
| 196 | + | func (t *Toaster) Widget() layout.Widget { | |||
| 197 | + | return t.container | |||
| 198 | + | } | |||
| 199 | + | ||||
| 200 | + | // IsVisible returns whether the toast is currently visible. | |||
| 201 | + | func (t *Toaster) IsVisible() bool { | |||
| 202 | + | t.mu.Lock() | |||
| 203 | + | defer t.mu.Unlock() | |||
| 204 | + | return t.visible | |||
| 205 | + | } | |||
M internal/ui/component/workspace_view.go +6 -2
| @@ -455,8 +455,12 @@ func (wv *WorkspaceView) ShowOmnibox(ctx context.Context, query string) { | |||
| 455 | 455 | return | |
| 456 | 456 | } | |
| 457 | 457 | ||
| 458 | - | // Create omnibox | |
| 459 | - | omnibox := NewOmnibox(ctx, wv.omniboxCfg) | |
| 458 | + | // Create omnibox with pane-specific toast callback | |
| 459 | + | cfg := wv.omniboxCfg | |
| 460 | + | cfg.OnToast = func(message string) { | |
| 461 | + | pv.ShowToast(ctx, message, ToastSuccess) | |
| 462 | + | } | |
| 463 | + | omnibox := NewOmnibox(ctx, cfg) | |
| 460 | 464 | if omnibox == nil { | |
| 461 | 465 | wv.logger.Error().Msg("failed to create omnibox") | |
| 462 | 466 | return | |
| @@ -455,8 +455,12 @@ func (wv *WorkspaceView) ShowOmnibox(ctx context.Context, query string) { | |||||
| 455 | return | 455 | return | ||
| 456 | } | 456 | } | ||
| 457 | 457 | ||||
| 458 | - | // Create omnibox | 458 | + | // Create omnibox with pane-specific toast callback |
| 459 | - | omnibox := NewOmnibox(ctx, wv.omniboxCfg) | 459 | + | cfg := wv.omniboxCfg |
| 460 | + | cfg.OnToast = func(message string) { | |||
| 461 | + | pv.ShowToast(ctx, message, ToastSuccess) | |||
| 462 | + | } | |||
| 463 | + | omnibox := NewOmnibox(ctx, cfg) | |||
| 460 | if omnibox == nil { | 464 | if omnibox == nil { | ||
| 461 | wv.logger.Error().Msg("failed to create omnibox") | 465 | wv.logger.Error().Msg("failed to create omnibox") | ||
| 462 | return | 466 | return | ||
M internal/ui/coordinator/workspace.go +13 -0
| @@ -1567,3 +1567,16 @@ func (c *WorkspaceCoordinator) insertPopupTabbed(ctx context.Context, input Inse | |||
| 1567 | 1567 | ||
| 1568 | 1568 | return nil | |
| 1569 | 1569 | } | |
| 1570 | + | ||
| 1571 | + | // ShowZoomToast displays a zoom level toast on the active pane. | |
| 1572 | + | func (c *WorkspaceCoordinator) ShowZoomToast(ctx context.Context, zoomPercent int) { | |
| 1573 | + | _, wsView := c.getActiveWS() | |
| 1574 | + | if wsView == nil { | |
| 1575 | + | return | |
| 1576 | + | } | |
| 1577 | + | ||
| 1578 | + | paneView := wsView.GetActivePaneView() | |
| 1579 | + | if paneView != nil { | |
| 1580 | + | paneView.ShowZoomToast(ctx, zoomPercent) | |
| 1581 | + | } | |
| 1582 | + | } | |
| @@ -1567,3 +1567,16 @@ func (c *WorkspaceCoordinator) insertPopupTabbed(ctx context.Context, input Inse | |||||
| 1567 | 1567 | ||||
| 1568 | return nil | 1568 | return nil | ||
| 1569 | } | 1569 | } | ||
| 1570 | + | ||||
| 1571 | + | // ShowZoomToast displays a zoom level toast on the active pane. | |||
| 1572 | + | func (c *WorkspaceCoordinator) ShowZoomToast(ctx context.Context, zoomPercent int) { | |||
| 1573 | + | _, wsView := c.getActiveWS() | |||
| 1574 | + | if wsView == nil { | |||
| 1575 | + | return | |||
| 1576 | + | } | |||
| 1577 | + | ||||
| 1578 | + | paneView := wsView.GetActivePaneView() | |||
| 1579 | + | if paneView != nil { | |||
| 1580 | + | paneView.ShowZoomToast(ctx, zoomPercent) | |||
| 1581 | + | } | |||
| 1582 | + | } | |||
M internal/ui/theme/css.go +49 -0
| @@ -50,6 +50,10 @@ func GenerateCSSWithScale(p Palette, scale float64) string { | |||
| 50 | 50 | ||
| 51 | 51 | // Progress bar styling | |
| 52 | 52 | sb.WriteString(generateProgressBarCSS(p)) | |
| 53 | + | sb.WriteString("\n") | |
| 54 | + | ||
| 55 | + | // Toaster styling | |
| 56 | + | sb.WriteString(generateToasterCSS(p)) | |
| 53 | 57 | ||
| 54 | 58 | return sb.String() | |
| 55 | 59 | } | |
| @@ -394,12 +398,57 @@ progressbar.osd { | |||
| 394 | 398 | ||
| 395 | 399 | progressbar.osd trough { | |
| 396 | 400 | min-height: 4px; | |
| 401 | + | min-width: 0px; | |
| 397 | 402 | background-color: alpha(var(--bg), 0.3); | |
| 398 | 403 | } | |
| 399 | 404 | ||
| 400 | 405 | progressbar.osd progress { | |
| 401 | 406 | min-height: 4px; | |
| 407 | + | min-width: 0px; | |
| 402 | 408 | background-color: var(--accent); | |
| 403 | 409 | } | |
| 404 | 410 | ` | |
| 405 | 411 | } | |
| 412 | + | ||
| 413 | + | // generateToasterCSS creates toast notification styles. | |
| 414 | + | // Uses em units for scalable UI. | |
| 415 | + | func generateToasterCSS(p Palette) string { | |
| 416 | + | return `/* ===== Toaster Styling ===== */ | |
| 417 | + | ||
| 418 | + | /* Toast notification container */ | |
| 419 | + | .toast { | |
| 420 | + | background-color: var(--surface-variant); | |
| 421 | + | border-radius: 0.25em; | |
| 422 | + | padding: 0.375em 0.625em; | |
| 423 | + | margin: 0.5em; | |
| 424 | + | font-size: 0.875em; | |
| 425 | + | font-weight: 600; | |
| 426 | + | box-shadow: 0 2px 8px alpha(black, 0.3); | |
| 427 | + | min-width: 2.5em; | |
| 428 | + | } | |
| 429 | + | ||
| 430 | + | /* Toast level: info (default, accent color) */ | |
| 431 | + | .toast-info { | |
| 432 | + | background-color: alpha(var(--accent), 0.9); | |
| 433 | + | color: var(--bg); | |
| 434 | + | } | |
| 435 | + | ||
| 436 | + | /* Toast level: success (green) */ | |
| 437 | + | .toast-success { | |
| 438 | + | background-color: alpha(#4ade80, 0.9); | |
| 439 | + | color: var(--bg); | |
| 440 | + | } | |
| 441 | + | ||
| 442 | + | /* Toast level: warning (yellow/amber) */ | |
| 443 | + | .toast-warning { | |
| 444 | + | background-color: alpha(#fbbf24, 0.9); | |
| 445 | + | color: var(--bg); | |
| 446 | + | } | |
| 447 | + | ||
| 448 | + | /* Toast level: error (red) */ | |
| 449 | + | .toast-error { | |
| 450 | + | background-color: alpha(#ef4444, 0.9); | |
| 451 | + | color: white; | |
| 452 | + | } | |
| 453 | + | ` | |
| 454 | + | } | |
| @@ -50,6 +50,10 @@ func GenerateCSSWithScale(p Palette, scale float64) string { | |||||
| 50 | 50 | ||||
| 51 | // Progress bar styling | 51 | // Progress bar styling | ||
| 52 | sb.WriteString(generateProgressBarCSS(p)) | 52 | sb.WriteString(generateProgressBarCSS(p)) | ||
| 53 | + | sb.WriteString("\n") | |||
| 54 | + | ||||
| 55 | + | // Toaster styling | |||
| 56 | + | sb.WriteString(generateToasterCSS(p)) | |||
| 53 | 57 | ||||
| 54 | return sb.String() | 58 | return sb.String() | ||
| 55 | } | 59 | } | ||
| @@ -394,12 +398,57 @@ progressbar.osd { | |||||
| 394 | 398 | ||||
| 395 | progressbar.osd trough { | 399 | progressbar.osd trough { | ||
| 396 | min-height: 4px; | 400 | min-height: 4px; | ||
| 401 | + | min-width: 0px; | |||
| 397 | background-color: alpha(var(--bg), 0.3); | 402 | background-color: alpha(var(--bg), 0.3); | ||
| 398 | } | 403 | } | ||
| 399 | 404 | ||||
| 400 | progressbar.osd progress { | 405 | progressbar.osd progress { | ||
| 401 | min-height: 4px; | 406 | min-height: 4px; | ||
| 407 | + | min-width: 0px; | |||
| 402 | background-color: var(--accent); | 408 | background-color: var(--accent); | ||
| 403 | } | 409 | } | ||
| 404 | ` | 410 | ` | ||
| 405 | } | 411 | } | ||
| 412 | + | ||||
| 413 | + | // generateToasterCSS creates toast notification styles. | |||
| 414 | + | // Uses em units for scalable UI. | |||
| 415 | + | func generateToasterCSS(p Palette) string { | |||
| 416 | + | return `/* ===== Toaster Styling ===== */ | |||
| 417 | + | ||||
| 418 | + | /* Toast notification container */ | |||
| 419 | + | .toast { | |||
| 420 | + | background-color: var(--surface-variant); | |||
| 421 | + | border-radius: 0.25em; | |||
| 422 | + | padding: 0.375em 0.625em; | |||
| 423 | + | margin: 0.5em; | |||
| 424 | + | font-size: 0.875em; | |||
| 425 | + | font-weight: 600; | |||
| 426 | + | box-shadow: 0 2px 8px alpha(black, 0.3); | |||
| 427 | + | min-width: 2.5em; | |||
| 428 | + | } | |||
| 429 | + | ||||
| 430 | + | /* Toast level: info (default, accent color) */ | |||
| 431 | + | .toast-info { | |||
| 432 | + | background-color: alpha(var(--accent), 0.9); | |||
| 433 | + | color: var(--bg); | |||
| 434 | + | } | |||
| 435 | + | ||||
| 436 | + | /* Toast level: success (green) */ | |||
| 437 | + | .toast-success { | |||
| 438 | + | background-color: alpha(#4ade80, 0.9); | |||
| 439 | + | color: var(--bg); | |||
| 440 | + | } | |||
| 441 | + | ||||
| 442 | + | /* Toast level: warning (yellow/amber) */ | |||
| 443 | + | .toast-warning { | |||
| 444 | + | background-color: alpha(#fbbf24, 0.9); | |||
| 445 | + | color: var(--bg); | |||
| 446 | + | } | |||
| 447 | + | ||||
| 448 | + | /* Toast level: error (red) */ | |||
| 449 | + | .toast-error { | |||
| 450 | + | background-color: alpha(#ef4444, 0.9); | |||
| 451 | + | color: white; | |||
| 452 | + | } | |||
| 453 | + | ` | |||
| 454 | + | } | |||