// Package theme provides GTK CSS styling for UI components. package theme import ( "fmt" "strings" ) // GenerateCSS creates GTK4 CSS using the provided palette with default scale. func GenerateCSS(p Palette) string { return GenerateCSSWithScale(p, 1.0) } // GenerateCSSWithScale creates GTK4 CSS using the provided palette and UI scale factor. // Scale affects widget sizing that uses relative units; text scaling is handled via GtkSettings. func GenerateCSSWithScale(p Palette, scale float64) string { return GenerateCSSWithScaleAndFonts(p, scale, DefaultFontConfig()) } // FontConfig holds font settings for CSS generation. type FontConfig struct { SansFont string MonospaceFont string GtkFont string } // DefaultFontConfig returns safe font fallbacks. func DefaultFontConfig() FontConfig { return FontConfig{ SansFont: "sans-serif", MonospaceFont: "monospace", GtkFont: DefaultGTKFont(), } } // FontCSSVars generates CSS custom properties for fonts. func FontCSSVars(fonts FontConfig) string { var sb strings.Builder // Quote the configured family; include generic fallback. fmt.Fprintf(&sb, " --font-sans: %q, sans-serif;\n", fonts.SansFont) fmt.Fprintf(&sb, " --font-mono: %q, monospace;\n", fonts.MonospaceFont) fmt.Fprintf(&sb, " --font-gtk: %q, sans-serif;\n", fonts.GtkFont) return sb.String() } func generateFontCSS() string { return `/* Font styling */ window, tooltip, popover { font-family: var(--font-gtk); } ` } const defaultTransitionDurationMs = 120 // GenerateCSSWithScaleAndFonts creates GTK4 CSS using the provided palette, UI scale factor and fonts. // Scale affects widget sizing that uses relative units; text scaling is handled via GtkSettings. // Uses default mode colors. func GenerateCSSWithScaleAndFonts(p Palette, scale float64, fonts FontConfig) string { return GenerateCSSFull(p, scale, fonts, DefaultModeColors()) } // GenerateCSSFull creates GTK4 CSS using all provided configuration with the // default transition duration. // Text scaling is handled via GtkSettings; CSS here stays scale-independent. func GenerateCSSFull(p Palette, _ float64, fonts FontConfig, modeColors ModeColors) string { return GenerateCSSFullWithTiming(p, 1.0, fonts, modeColors, defaultTransitionDurationMs) } // GenerateCSSFullWithTiming creates GTK4 CSS using all provided configuration // and an explicit transition duration from workspace styling config. func GenerateCSSFullWithTiming(p Palette, _ float64, fonts FontConfig, modeColors ModeColors, transitionDurationMs int) string { defaults := DefaultFontConfig() fonts = FontConfig{ SansFont: Coalesce(fonts.SansFont, defaults.SansFont), MonospaceFont: Coalesce(fonts.MonospaceFont, defaults.MonospaceFont), GtkFont: Coalesce(fonts.GtkFont, defaults.GtkFont), } var sb strings.Builder // CSS custom properties (variables) - GTK4 uses :root selector sb.WriteString("/* Theme variables */\n") sb.WriteString(":root {\n") sb.WriteString(p.ToCSSVars()) sb.WriteString(FontCSSVars(fonts)) sb.WriteString(modeColors.ToCSSVars()) sb.WriteString("}\n\n") // Global font styling sb.WriteString(generateFontCSS()) sb.WriteString("\n") // Tab bar styling sb.WriteString(generateTabBarCSS(p)) sb.WriteString("\n") // Omnibox and modal legend styling sb.WriteString(generateOmniboxCSS(p)) sb.WriteString(generateModeLegendCSS()) sb.WriteString("\n") // Find bar styling sb.WriteString(generateFindBarCSS(p)) sb.WriteString("\n") // Pane styling sb.WriteString(generatePaneCSS(p)) sb.WriteString("\n") // Stacked pane styling sb.WriteString(generateStackedPaneCSS(p)) sb.WriteString("\n") // Vim mode styling sb.WriteString(generateVimModeCSS(transitionDurationMs)) sb.WriteString("\n") // Progress bar styling sb.WriteString(generateProgressBarCSS(p)) sb.WriteString("\n") // Touchpad history navigation indicator styling sb.WriteString(generateTouchpadNavigationIndicatorCSS(p)) sb.WriteString("\n") // Toaster styling sb.WriteString(generateToasterCSS(p)) sb.WriteString("\n") // Link status overlay styling sb.WriteString(generateLinkStatusCSS(p)) sb.WriteString("\n") // Session manager styling sb.WriteString(generateSessionManagerCSS(p)) sb.WriteString("\n") // Context menu styling sb.WriteString(generateContextMenuCSS()) sb.WriteString("\n") // Tab picker styling sb.WriteString(generateTabPickerCSS(p)) sb.WriteString("\n") // Permission popup styling sb.WriteString(generatePermissionPopupCSS(p)) sb.WriteString("\n") // Floating pane styling sb.WriteString(generateFloatingPaneCSS(p)) sb.WriteString("\n") // WebRTC permission activity indicator styling sb.WriteString(generateWebRTCPermissionIndicatorCSS(p)) sb.WriteString("\n") // Accent picker styling sb.WriteString(generateAccentPickerCSS(p)) sb.WriteString("\n") appendNativeSidebarCSS(&sb, p) return sb.String() } func appendNativeSidebarCSS(sb *strings.Builder, p Palette) { // Native sidebar styling sb.WriteString(generateSidebarCSS(p)) sb.WriteString("\n") sb.WriteString(generateHistorySidebarCSS(p)) sb.WriteString("\n") sb.WriteString(generateFavoritesSidebarCSS(p)) } // generateTabBarCSS creates tab bar styles. // Uses em units for scalable UI. func generateTabBarCSS(p Palette) string { return `/* Tab bar styling */ /* Content area inset for overlaid tab bars — uses the same 2em token as .tab-bar min-height and remains static to avoid OSR vertical reallocations. */ .content-area-tabbar-inset-top { padding-top: 2em; } .content-area-tabbar-inset-bottom { padding-bottom: 2em; } .tab-bar { background-color: var(--surface); border-top: 0.125em solid var(--border); padding: 0; min-height: 2em; } /* Tab button styling */ button.tab-button { background-color: var(--surface-variant); background-image: none; border: none; border-right: 0.0625em solid var(--border); border-radius: 0; padding: 0.25em 0.5em; transition: background-color 200ms ease-in-out; } button.tab-button:hover { background-color: shade(var(--surface-variant), 1.2); } button.tab-button.tab-button-active { background-color: shade(var(--surface-variant), 1.2); border-bottom: 0.125em solid var(--accent); font-weight: 600; } /* Tab title text */ .tab-title { font-size: 0.75em; color: var(--text); font-weight: 500; } ` } // generateOmniboxCSS creates omnibox styles. // Uses em units for scalable UI. func generateOmniboxCSS(p Palette) string { return `/* ===== Omnibox Styling ===== */ window.standalone-omnibox-window { background-color: transparent; } /* Omnibox outer container - for positioning in overlay */ .omnibox-outer { /* Positioning is handled via SetHalign/SetValign/SetMarginTop in Go */ } /* Omnibox main container - the visible popup */ .omnibox-container { background-color: var(--surface); background-image: none; border: 0.09375em solid alpha(var(--border), 0.9); border-radius: 0.1875em; box-shadow: 0 0.875em 2.5em alpha(black, 0.24), 0 0.0625em 0 alpha(white, 0.05) inset; padding: 0; } /* Header with History/Favorites toggle */ .omnibox-header { background-color: var(--surface); background-image: none; border-bottom: 0.0625em solid alpha(var(--border), 0.75); padding: 0.375em 0.75em; } .omnibox-header-btn { background-color: alpha(var(--surface), 0.12); background-image: none; border: 0.0625em solid transparent; border-radius: 0.125em; padding: 0.25em 0.75em; margin-right: 0.5em; font-size: 0.8125em; font-weight: 500; color: var(--muted); transition: all 100ms ease-in-out; } .omnibox-header-btn:hover { background-color: alpha(var(--accent), 0.14); border-color: alpha(var(--accent), 0.18); color: var(--text); } .omnibox-header-btn.omnibox-header-active { background-color: alpha(var(--accent), 0.18); border-color: alpha(var(--accent), 0.28); color: var(--accent); font-weight: 600; } /* Search entry field */ entry.omnibox-entry, entry.omnibox-entry > text { background-color: var(--surface-variant); background-image: none; color: var(--control-text); caret-color: var(--accent); } entry.omnibox-entry { border: 0.0625em solid alpha(var(--border), 0.82); border-radius: 0.125em; padding: 0.625em 0.75em; margin: 0.5em 0.75em; font-size: 1em; box-shadow: 0 0.0625em 0 alpha(white, 0.04) inset; } entry.omnibox-entry:focus, entry.omnibox-entry:focus-within, entry.omnibox-entry:focus-visible, entry.omnibox-entry:focus > text, entry.omnibox-entry:focus-within > text, entry.omnibox-entry:focus-visible > text { border-color: var(--accent); background-color: shade(var(--surface-variant), 1.08); background-image: none; outline-style: none; outline-width: 0px; outline-color: transparent; } /* Override any internal focus styling */ entry.omnibox-entry > text:focus, entry.omnibox-entry > text:focus-visible { outline-style: none; outline-color: transparent; } /* Scrolled window for suggestions */ .omnibox-scrolled { background-color: var(--surface); border-top: 0.0625em solid alpha(var(--border), 0.65); } /* List box */ .omnibox-listbox { background-color: transparent; } /* Suggestion rows */ .omnibox-row { padding: 0.5em 0.75em; margin: 0; border-radius: 0; border-left: 0.1875em solid transparent; border-bottom: 0.0625em solid alpha(var(--border), 0.35); background-color: var(--surface); background-image: none; transition: background-color 100ms ease-in-out, border-left 100ms ease-in-out; min-height: 2.75em; } .omnibox-row:last-child { border-bottom: none; } .omnibox-row:hover { background-color: alpha(var(--accent), 0.12); border-left: 0.1875em solid var(--accent); } .omnibox-row:selected { background-color: alpha(var(--accent), 0.2); border-left: 0.1875em solid var(--accent); } /* Favorite indicator in history mode - keep the row neutral and use the left selection stripe */ .omnibox-row.omnibox-row-favorite { background-color: transparent; border-left: 0.1875em solid var(--warning); } .omnibox-row.omnibox-row-favorite:hover { background-color: alpha(var(--accent), 0.12); border-left: 0.1875em solid var(--warning); } .omnibox-row.omnibox-row-favorite:selected { background-color: alpha(var(--accent), 0.2); border-left: 0.1875em solid var(--warning); } /* Favicon in omnibox rows */ .omnibox-favicon { min-width: 1em; min-height: 1em; margin-right: 0.5em; } /* Suggestion title/URL */ .omnibox-suggestion-title { font-size: 0.875em; color: var(--text); font-weight: 500; } /* Also style labels inside omnibox rows directly */ .omnibox-row label { color: var(--text); } .omnibox-row:selected .omnibox-suggestion-title { color: var(--text); font-weight: 500; } /* URL text below title - use .omnibox-row prefix for specificity over .omnibox-row label */ .omnibox-row .omnibox-suggestion-url { font-size: 0.75em; color: alpha(var(--muted), 0.72); font-weight: 400; } .omnibox-row .omnibox-favorite-star { color: mix(var(--warning), var(--muted), 0.45); opacity: 0.75; margin-left: 0.625em; margin-right: 0.125em; font-family: var(--font-mono); font-size: 1em; font-weight: 700; } .omnibox-row:selected .omnibox-favorite-star { color: mix(var(--warning), var(--text), 0.35); opacity: 0.88; } .omnibox-row:selected .omnibox-suggestion-url { color: alpha(var(--muted), 0.75); } /* Keyboard shortcut badge */ .omnibox-shortcut-badge { background-color: alpha(var(--muted), 0.3); color: var(--muted); border-radius: 0.25em; padding: 0.125em 0.375em; font-size: 0.625em; font-weight: 500; font-family: var(--font-mono); margin-left: 0.5em; } .omnibox-row:hover .omnibox-shortcut-badge { background-color: alpha(var(--accent), 0.2); color: var(--accent); } .omnibox-row:selected .omnibox-shortcut-badge { background-color: alpha(var(--accent), 0.3); color: var(--accent); } /* Bang indicator badge in omnibox header */ .omnibox-bang-badge { background-color: alpha(var(--accent), 0.18); color: var(--accent); border-radius: 0.25em; padding: 0.125em 0.5em; font-size: 0.6875em; font-weight: 600; } /* Header badge used for omnibox state */ .omnibox-header-badge { background-color: alpha(var(--surface-variant), 0.65); color: var(--muted); border: 0.0625em solid alpha(var(--border), 0.85); border-radius: 0.1875em; padding: 0.0625em 0.375em; font-size: 0.6875em; font-weight: 500; } /* Zoom indicator in omnibox header */ .omnibox-zoom-indicator { background-color: alpha(var(--accent), 0.2); color: var(--accent); border-radius: 0.25em; padding: 0.125em 0.5em; font-size: 0.6875em; font-weight: 600; /* Note: margin-left: auto not supported in GTK4 CSS, use SetHalign(End) in code */ } entry.omnibox-entry.omnibox-entry-bang-active, entry.omnibox-entry.omnibox-entry-bang-active:focus, entry.omnibox-entry.omnibox-entry-bang-active:focus-within, entry.omnibox-entry.omnibox-entry-bang-active:focus-visible, entry.omnibox-entry.omnibox-entry-bang-active > text, entry.omnibox-entry.omnibox-entry-bang-active:focus > text, entry.omnibox-entry.omnibox-entry-bang-active:focus-within > text, entry.omnibox-entry.omnibox-entry-bang-active:focus-visible > text { border-color: var(--accent); background-color: shade(var(--surface-variant), 1.08); background-image: none; } .omnibox-row.omnibox-row-bang .omnibox-suggestion-title { color: var(--accent); font-weight: 600; } /* Ghost text autocomplete overlay - positioned to align with entry text */ .omnibox-ghost { font-size: 1em; color: var(--control-text); margin-top: -0.0625em; /* Nudge up 1px to align with entry text baseline */ background-color: transparent; } /* Multiplexer: segmented status line, square prompt, and strong active rail. */ .omnibox-container.omnibox-style-multiplexer { border-radius: 0; border-color: var(--border); box-shadow: 0 1.125em 3em alpha(black, 0.34); } .omnibox-style-multiplexer .omnibox-header { padding: 0; min-height: 2em; background-color: shade(var(--surface), 0.94); } .omnibox-style-multiplexer .omnibox-header-btn { margin: 0; padding: 0.375em 1em; border: none; border-right: 0.0625em solid var(--border); border-radius: 0; font-family: var(--font-mono); font-size: 0.75em; font-weight: 600; } .omnibox-style-multiplexer .omnibox-header-btn.omnibox-header-active { background-color: var(--accent); color: var(--accent-text); } .omnibox-style-multiplexer .omnibox-header-badge { padding: 0.1875em 0.5em; background-color: transparent; border: none; color: var(--muted); font-family: var(--font-mono); font-size: 0.6875em; font-weight: 600; } .omnibox-style-multiplexer .omnibox-bang-badge, .omnibox-style-multiplexer .omnibox-zoom-indicator, .omnibox-style-multiplexer .omnibox-shortcut-badge { border-radius: 0; font-family: var(--font-mono); } .omnibox-style-multiplexer entry.omnibox-entry { margin: 0.75em; border-radius: 0; border-left: 0.25em solid var(--accent); font-family: var(--font-mono); } .omnibox-style-multiplexer .omnibox-row { border-left-width: 0.25em; } .omnibox-style-multiplexer .omnibox-row:selected { background-color: alpha(var(--accent), 0.14); box-shadow: 0.1875em 0 0 var(--accent) inset; } .omnibox-style-multiplexer .omnibox-row.omnibox-row-favorite:selected { border-left-color: var(--warning); box-shadow: 0.1875em 0 0 var(--warning) inset; } .omnibox-style-multiplexer .omnibox-favorite-star { min-width: 1.25em; margin: 0 0.375em 0 0.125em; padding: 0; color: var(--warning); opacity: 0.85; } /* Command: a calmer command palette with a floating input and compact tabs. */ .omnibox-container.omnibox-style-command { border: 0.125em solid alpha(var(--border), 0.95); border-radius: 0.625em; box-shadow: 0 1.25em 3.5em alpha(black, 0.38), 0 0 0 0.0625em alpha(white, 0.04) inset; padding: 0.375em; } .omnibox-style-command .omnibox-header { border-bottom: none; padding: 0.25em 0.375em 0.125em; } .omnibox-style-command .omnibox-header-btn { border-radius: 1em; padding: 0.25em 0.75em; font-size: 0.75em; } .omnibox-style-command .omnibox-header-btn.omnibox-header-active { background-color: alpha(var(--accent), 0.13); border-color: transparent; } .omnibox-style-command .omnibox-header-badge { padding: 0.1875em 0.5em; background-color: transparent; border: none; color: var(--muted); font-family: var(--font-mono); font-size: 0.6875em; font-weight: 600; } .omnibox-style-command entry.omnibox-entry { margin: 0.5em 0.375em 0.75em; padding: 0.75em 0.875em; border-radius: 0.375em; font-family: var(--font-mono); box-shadow: 0 0 0 0.0625em alpha(var(--accent), 0.12), 0 0.5em 1.5em alpha(black, 0.12); } .omnibox-style-command .omnibox-scrolled { border-top-color: alpha(var(--border), 0.35); } .omnibox-style-command .omnibox-row { margin: 0.125em 0.25em; border: none; border-radius: 0.25em; } .omnibox-style-command .omnibox-row:selected { background-color: alpha(var(--accent), 0.16); border-left-color: transparent; } .omnibox-style-command .omnibox-row.omnibox-row-favorite:selected { border-left: 0.1875em solid var(--warning); } .omnibox-style-command .omnibox-favorite-star { min-width: 1.25em; margin: 0 0.375em 0 0.125em; padding: 0; color: var(--warning); opacity: 0.85; } /* Minimal: terminal typography and hairline structure with almost no chrome. */ .omnibox-container.omnibox-style-minimal { border-radius: 0; box-shadow: 0 0.75em 2em alpha(black, 0.2); } .omnibox-style-minimal .omnibox-header { padding: 0.25em 0.75em; background-color: var(--surface); } .omnibox-style-minimal .omnibox-header-btn { margin-right: 1em; padding: 0.25em 0; border: none; border-bottom: 0.125em solid transparent; border-radius: 0; background-color: transparent; font-family: var(--font-mono); font-size: 0.75em; } .omnibox-style-minimal .omnibox-header-btn.omnibox-header-active { background-color: transparent; border-bottom-color: var(--accent); } .omnibox-style-minimal entry.omnibox-entry { margin: 0; padding: 0.75em; border: none; border-bottom: 0.0625em solid var(--border); border-radius: 0; background-color: var(--surface); font-family: var(--font-mono); box-shadow: none; } .omnibox-style-minimal .omnibox-scrolled { border-top: none; } .omnibox-style-minimal .omnibox-row { padding-top: 0.4375em; padding-bottom: 0.4375em; border-left-width: 0.125em; font-family: var(--font-mono); } .omnibox-style-minimal .omnibox-row.omnibox-row-favorite:selected { border-left: 0.125em solid var(--warning); } .omnibox-style-minimal .omnibox-favorite-star { color: var(--warning); opacity: 0.85; } .omnibox-style-minimal .omnibox-header-badge { background-color: transparent; border: none; color: var(--muted); font-family: var(--font-mono); } .omnibox-style-minimal .omnibox-shortcut-badge { background-color: transparent; border: 0.0625em solid alpha(var(--border), 0.7); border-radius: 0; } ` } // generateFindBarCSS creates find bar styles. // Uses em units for scalable UI. func generateFindBarCSS(p Palette) string { _ = p return `/* ===== Find Bar Styling ===== */ .find-bar-outer { margin: 0.5em; } .find-bar-container { background-color: var(--surface-variant); border: 0.0625em solid var(--border); border-radius: 0.25em; box-shadow: 0 0.25em 0.75em alpha(black, 0.2); padding: 0.5em; min-width: 20em; } .find-bar-input-row { margin-bottom: 0.25em; } .find-bar-entry { background-color: var(--bg); color: var(--text); border: 0.0625em solid var(--border); border-radius: 0.1875em; padding: 0.4em 0.5em; caret-color: var(--accent); } entry.find-bar-entry:focus, entry.find-bar-entry:focus-within, entry.find-bar-entry:focus-visible { border-color: var(--accent); outline-style: none; outline-width: 0px; outline-color: transparent; } .find-bar-entry.not-found { background-color: alpha(#ef4444, 0.15); border-color: #ef4444; } .find-bar-count { font-size: 0.8em; color: var(--muted); min-width: 3.5em; } .find-bar-count.find-bar-count-has { color: var(--accent); } .find-bar-nav, .find-bar-close, .find-bar-toggle { background-image: none; background-color: alpha(var(--bg), 0.6); border: 0.0625em solid var(--border); border-radius: 0.1875em; padding: 0.25em 0.5em; transition: background-color 120ms ease-in-out, border-color 120ms ease-in-out; } .find-bar-nav:hover, .find-bar-close:hover, .find-bar-toggle:hover { background-color: alpha(var(--accent), 0.15); } .find-bar-toggle:checked { background-color: alpha(var(--accent), 0.2); border-color: var(--accent); color: var(--accent); } ` } // generatePaneCSS creates pane border styles. // Uses em units for scalable UI. // generateVimModeCSS creates the pane-local Vim Mode accent and pulse styling. // It uses --vim-mode-color (from workspace.styling.vim_mode_color). func generateVimModeCSS(transitionDurationMs int) string { if transitionDurationMs <= 0 { transitionDurationMs = defaultTransitionDurationMs } normalPulseMs := transitionDurationMs * 3 fastPulseMs := transitionDurationMs * 6 return fmt.Sprintf(`/* ===== Vim Mode Styling ===== */ /* Vim mode frame on the active pane; pulses use this same widget. */ .vim-mode-active { box-shadow: inset 0 0 0 0.125em alpha(var(--vim-mode-color), 0.35); border-radius: 0; } /* Frame pulse keyframes — start/end at the active accent state so the frame keeps its Vim mode border while briefly flaring brighter. The separate -anim-a and -anim-b pairs are required to restart repeated pulse animations. */ @keyframes vim-mode-overlay-pulse-anim-a { 0%% { box-shadow: inset 0 0 0 0.125em alpha(var(--vim-mode-color), 0.35); } 35%% { box-shadow: inset 0 0 0 0.2em alpha(var(--vim-mode-color), 0.78); } 100%% { box-shadow: inset 0 0 0 0.125em alpha(var(--vim-mode-color), 0.35); } } @keyframes vim-mode-overlay-pulse-anim-b { 0%% { box-shadow: inset 0 0 0 0.125em alpha(var(--vim-mode-color), 0.35); } 35%% { box-shadow: inset 0 0 0 0.2em alpha(var(--vim-mode-color), 0.78); } 100%% { box-shadow: inset 0 0 0 0.125em alpha(var(--vim-mode-color), 0.35); } } @keyframes vim-mode-overlay-pulse-fast-anim-a { 0%% { box-shadow: inset 0 0 0 0.125em alpha(var(--vim-mode-color), 0.35); } 25%% { box-shadow: inset 0 0 0 0.26em alpha(var(--vim-mode-color), 0.95); } 100%% { box-shadow: inset 0 0 0 0.125em alpha(var(--vim-mode-color), 0.35); } } @keyframes vim-mode-overlay-pulse-fast-anim-b { 0%% { box-shadow: inset 0 0 0 0.125em alpha(var(--vim-mode-color), 0.35); } 25%% { box-shadow: inset 0 0 0 0.26em alpha(var(--vim-mode-color), 0.95); } 100%% { box-shadow: inset 0 0 0 0.125em alpha(var(--vim-mode-color), 0.35); } } /* Pane overlay scroll pulse — derived from transition_duration. */ .vim-mode-pulse.vim-mode-pulse-cycle-a { animation: vim-mode-overlay-pulse-anim-a %dms ease-in-out; } .vim-mode-pulse.vim-mode-pulse-cycle-b { animation: vim-mode-overlay-pulse-anim-b %dms ease-in-out; } .vim-mode-pulse-fast.vim-mode-pulse-cycle-a { animation: vim-mode-overlay-pulse-fast-anim-a %dms ease-in-out; } .vim-mode-pulse-fast.vim-mode-pulse-cycle-b { animation: vim-mode-overlay-pulse-fast-anim-b %dms ease-in-out; } `, normalPulseMs, normalPulseMs, fastPulseMs, fastPulseMs) } func generatePaneCSS(p Palette) string { return `/* ===== Pane Styling ===== */ /* Pane overlay container - theme background prevents white flash */ .pane-overlay { background-color: var(--bg); } /* WebView widget - theme background prevents white flash during page load */ .webview-themed { background-color: var(--bg); } /* Loading skeleton - shown until WebView paints */ .loading-skeleton { background-color: var(--bg); } .loading-skeleton-spinner { -gtk-icon-size: 2.1818em; min-width: 2.1818em; min-height: 2.1818em; color: var(--muted); opacity: 0.4; } .loading-skeleton-logo { opacity: 0.08; min-width: 17.4545em; min-height: 17.4545em; } .loading-skeleton-version { opacity: 0.25; font-size: 0.8864em; color: var(--muted); } /* Pane border - default transparent */ .pane-border { border: 0.0625em solid transparent; } /* Active pane - accent border */ .pane-active { border: 0.0625em solid var(--accent); } /* Hide active border when only one pane exists */ .single-pane .pane-active { border-color: transparent; } /* Mode frame: a click-through overlay drawn above web content. */ .mode-frame-border { background-color: transparent; border-radius: 0; } .mode-frame-border.pane-mode-active { box-shadow: inset 0 0 0 0.25em var(--pane-mode-color); } .mode-frame-border.tab-mode-active { box-shadow: inset 0 0 0 0.25em var(--tab-mode-color); } .mode-frame-border.resize-mode-active { box-shadow: inset 0 0 0 0.25em var(--resize-mode-color); } .mode-frame-border.prefix-mode-active { box-shadow: inset 0 0 0 0.25em var(--pane-mode-color); } ` } // generateStackedPaneCSS creates stacked pane (Zellij-style tabs within panes) styles. // Text uses scalable units while vertical chrome stays compact in device pixels. func generateStackedPaneCSS(p Palette) string { return `/* ===== Stacked Pane Styling ===== */ /* Title bar for inactive panes in a stack */ /* The title bar is now a Box with GestureClick, not wrapped in a button */ .stacked-pane-titlebar { background-color: var(--surface-variant); color: var(--control-text); border-bottom: 0.0625em solid var(--border); padding: 4px 0.5em; min-height: 24px; } /* Keep the background stable so --control-text retains its contrast. * Indicate hover with an underline and the active pane with an accent border. */ .stacked-pane-titlebar.active { border-left: 0.1875em solid var(--accent); } /* Favicon image in title bar */ .stacked-pane-titlebar image { color: var(--control-text); min-width: 1em; min-height: 1em; margin-right: 0.375em; } /* Title text in title bar */ .stacked-pane-titlebar label { color: var(--control-text); font-size: 0.75em; font-weight: 400; } /* Hover effect on title bar label */ .stacked-pane-titlebar.stacked-pane-title-clickable:hover label { text-decoration-line: underline; } /* Close button in stacked pane title bar. * Its vertical footprint is intentionally smaller than the original scale-1 baseline. */ .stacked-pane-close-button { background: transparent; border: none; border-radius: 0.2727em; padding: 3px; margin: 0 2px; min-width: 1.3636em; min-height: 15px; opacity: 0.4; transition: opacity 100ms ease-in-out, background-color 100ms ease-in-out; } .stacked-pane-close-button > image { margin: 0; padding: 0; } .stacked-pane-close-button:hover { opacity: 1.0; background: alpha(var(--destructive), 0.2); } .stacked-pane-close-button:hover > image { color: var(--destructive); } /* Show close button more prominently when hovering title bar */ .stacked-pane-titlebar.stacked-pane-title-clickable:hover .stacked-pane-close-button { opacity: 0.7; } .stacked-pane-close-button:focus { outline: none; box-shadow: none; } ` } // generateProgressBarCSS creates progress bar styles for page loading indication. func generateProgressBarCSS(p Palette) string { return `/* ===== Progress Bar Styling ===== */ /* Native GtkProgressBar with osd class */ progressbar.osd { min-height: 0.2727em; } progressbar.osd trough { min-height: 0.2727em; min-width: 0.1364em; margin: 0; padding: 0; background-color: alpha(var(--bg), 0.3); } progressbar.osd progress { min-height: 0.2727em; min-width: 0.1364em; margin: 0; padding: 0; background-color: var(--accent); } ` } func generateTouchpadNavigationIndicatorCSS(p Palette) string { _ = p return `/* ===== Touchpad Navigation Indicator Styling ===== */ .touchpad-navigation-indicator { background-color: alpha(var(--surface), 0.90); color: var(--text); border: 0.0625em solid alpha(var(--border), 0.72); border-radius: 1.125em; padding: 0.75em 0.875em 0.8125em; opacity: 1; filter: blur(0px); backdrop-filter: blur(0.75em); box-shadow: 0 1em 2.25em alpha(black, 0.30), 0 0.0625em 0 alpha(white, 0.06) inset; transition: opacity 220ms ease-out, filter 220ms ease-out, backdrop-filter 220ms ease-out, background-color 140ms ease-out, border-color 140ms ease-out, box-shadow 220ms ease-out; } .touchpad-navigation-indicator.back { border-left: 0.1875em solid alpha(var(--accent), 0.82); } .touchpad-navigation-indicator.forward { border-right: 0.1875em solid alpha(var(--accent), 0.82); } .touchpad-navigation-indicator.threshold-reached { border-color: alpha(var(--accent), 0.95); background-color: alpha(var(--accent), 0.18); box-shadow: 0 1.125em 2.5em alpha(black, 0.34), 0 0 0 0.0625em alpha(var(--accent), 0.24) inset; } .touchpad-navigation-indicator.hiding { opacity: 0; filter: blur(0.0625em); box-shadow: 0 0.375em 0.875em alpha(black, 0.10), 0 0 0 0.0625em alpha(var(--accent), 0.04) inset; } .touchpad-navigation-header { min-width: 9.75em; } .touchpad-navigation-icon { background-color: alpha(var(--accent), 0.16); color: var(--accent); border-radius: 999px; font-size: 0.95em; font-weight: 700; padding: 0.125em 0.375em; } .touchpad-navigation-indicator.threshold-reached .touchpad-navigation-icon { background-color: var(--accent); color: var(--bg); } .touchpad-navigation-label { color: var(--text); font-weight: 700; font-size: 0.92em; letter-spacing: 0.01em; } progressbar.touchpad-navigation-progress { min-height: 0.3125em; } progressbar.touchpad-navigation-progress trough { min-height: 0.3125em; min-width: 9.75em; border-radius: 999px; background-color: alpha(var(--muted), 0.24); box-shadow: 0 0.0625em 0 alpha(white, 0.06) inset; } progressbar.touchpad-navigation-progress progress { min-height: 0.3125em; border-radius: 999px; background-color: var(--accent); box-shadow: 0 0 0.35em alpha(var(--accent), 0.35); } ` } // generateSessionManagerCSS creates session manager modal styles. // Uses em units for scalable UI, matches omnibox styling patterns. func generateSessionManagerCSS(p Palette) string { _ = p return `/* ===== Session Manager Styling ===== */ /* Session manager outer container - for positioning in overlay */ .session-manager-outer { /* Positioning is handled via SetHalign/SetValign in Go */ } /* Session manager main container - the visible popup */ .session-manager-container { background-color: var(--surface-variant); border: 0.0625em solid var(--border); border-radius: 0.1875em; padding: 0; /* Width is owned by the Go sizing request so CSS must not force growth beyond the percentage viewport limit. */ /* Note: max-width not supported in GTK4 CSS, rely on container constraints */ } /* Header with title */ .session-manager-header { background-color: shade(var(--surface-variant), 1.1); border-bottom: 0.0625em solid var(--border); padding: 0.5em 0.75em; } .session-manager-title { font-size: 0.9375em; font-weight: 600; color: var(--text); } /* Scrolled window for session list */ .session-manager-scrolled { background-color: shade(var(--surface-variant), 0.95); } /* List box */ .session-manager-list { background-color: transparent; } /* Session rows */ .session-manager-row { padding: 0.5em 0.75em; margin: 0; border-radius: 0; border-left: 0.1875em solid transparent; border-bottom: 0.0625em solid alpha(var(--border), 0.5); transition: background-color 100ms ease-in-out, border-left 100ms ease-in-out; min-height: 2.75em; } .session-manager-row:last-child { border-bottom: none; } .session-manager-row:hover { background-color: alpha(var(--accent), 0.12); border-left: 0.1875em solid var(--accent); } .session-manager-row:selected, row:selected .session-manager-row { background-color: alpha(var(--accent), 0.2); border-left: 0.1875em solid var(--accent); } /* Status indicator dot */ .session-status { font-size: 0.875em; min-width: 1em; margin-right: 0.5em; } /* Current session - accent color dot */ .session-current .session-status { color: var(--accent); } /* Active session (other instance) - muted dot */ .session-active .session-status { color: var(--muted); } /* Exited session - dimmed appearance with subtle stop icon */ .session-exited { opacity: 0.75; } .session-exited .session-status { color: var(--error); opacity: 0.5; } /* Session ID labels */ .session-id-short { font-size: 0.875em; font-weight: 600; color: var(--accent); font-family: var(--font-mono); } .session-id-prefix { font-size: 0.875em; font-weight: 400; color: var(--muted); font-family: var(--font-mono); } .session-id-badge { font-size: 0.75em; font-weight: 500; color: var(--accent); margin-left: 0.5em; } /* Tab/pane count label */ .session-count { font-size: 0.75em; color: var(--muted); } /* Relative time label */ .session-time { font-size: 0.75em; color: var(--muted); /* Note: margin-left: auto not supported in GTK4 CSS, use SetHalign(End) in code */ } /* Section divider for EXITED sessions */ .session-divider { font-size: 0.6875em; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em; padding: 0.5em 0.75em 0.25em 0.75em; background-color: alpha(var(--border), 0.3); border-bottom: 0.0625em solid var(--border); } /* Footer with keyboard shortcuts */ .session-manager-footer { background-color: shade(var(--surface-variant), 0.9); border-top: 0.0625em solid var(--border); padding: 0.375em 0.75em; font-size: 0.6875em; color: var(--muted); font-family: var(--font-mono); } /* Session mode border */ .mode-frame-border.session-mode-active { box-shadow: inset 0 0 0 0.25em var(--session-mode-color); } ` } // generateContextMenuCSS creates shared context menu styles. func generateContextMenuCSS() string { return `/* ===== Context Menu Styling ===== */ popover.context-menu-popover { background-color: transparent; background-image: none; } popover.context-menu-popover > contents { background-color: var(--surface); background-image: none; border: 0.0625em solid alpha(var(--border), 0.85); border-radius: 0.25em; box-shadow: 0 0.75em 1.75em alpha(var(--bg), 0.28), 0 0.0625em 0 alpha(var(--border), 0.18) inset; padding: 0; } .context-menu { padding: 0.125em 0; min-width: 10.5em; } .context-menu button.context-menu-item { background-color: transparent; background-image: none; border: none; border-radius: 0; color: var(--text); padding: 0.25em 0.75em; margin: 0; min-height: 1.75em; transition: background-color 120ms ease-in-out, color 120ms ease-in-out; } .context-menu button.context-menu-item:hover, .context-menu button.context-menu-item:focus, .context-menu button.context-menu-item:active { background-color: alpha(var(--surface-variant), 0.9); color: var(--text); } .context-menu button.context-menu-item:active { background-color: alpha(var(--accent), 0.16); } .context-menu button.context-menu-item:disabled { color: var(--muted); opacity: 0.7; } .context-menu-separator { background-color: alpha(var(--border), 0.55); margin: 0.25em 0.5em; min-height: 0.0625em; } ` } // generateToasterCSS creates toast notification styles. // Uses em units for scalable UI. func generateToasterCSS(p Palette) string { return `/* ===== Toaster Styling ===== */ /* Toast notification container */ .toast { background-color: var(--surface-variant); border-radius: 0.25em; padding: 0.375em 0.625em; margin: 0.5em; font-size: 0.875em; font-weight: 600; box-shadow: 0 2px 8px alpha(black, 0.3); min-width: 2.5em; } /* Toast level: info (default, accent color) */ .toast-info { background-color: alpha(var(--accent), 0.9); color: var(--accent-text); } /* Toast level: success */ .toast-success { background-color: alpha(var(--success), 0.9); color: var(--success-text); } /* Toast level: warning */ .toast-warning { background-color: alpha(var(--warning), 0.9); color: var(--warning-text); } /* Toast level: error */ .toast-error { background-color: alpha(var(--destructive), 0.9); color: var(--destructive-text); } ` } // generateLinkStatusCSS creates link status overlay styles. // Uses CSS transitions for fade-in/fade-out effect. func generateLinkStatusCSS(p Palette) string { return `/* ===== Link Status Overlay Styling ===== */ /* Link status container - bottom left positioning */ .link-status { background-color: alpha(var(--surface-variant), 0.95); border-radius: 0.25em 0.25em 0 0; padding: 0.25em 0.5em; margin: 0; font-size: 0.75em; /* Note: max-width not supported in GTK4 CSS, use label SetMaxWidthChars instead */ box-shadow: 0 -1px 4px alpha(black, 0.15); /* Fade transition - hidden by default */ opacity: 0; transition: opacity 150ms ease-in-out; } /* Visible state - triggered by adding .visible class */ .link-status.visible { opacity: 1; } /* Link status label text */ .link-status label { color: var(--control-muted); } ` } func generateFloatingPaneCSS(p Palette) string { _ = p return `/* ===== Floating Pane Styling ===== */ .floating-pane-outer { /* Positioning handled in Go via overlay alignment and margins. */ } .floating-pane-container { background-color: var(--surface-variant); border: 0.0625em solid var(--border); border-radius: 0.25em; box-shadow: 0 0.5em 1.5em alpha(black, 0.25); opacity: 0; transition: opacity 110ms ease-out; } .floating-pane-container.floating-pane-visible { opacity: 1; } .floating-pane-container.pane-active { border-color: var(--accent); } .floating-pane-omnibox { background-color: shade(var(--surface-variant), 1.08); border-bottom: 0.0625em solid var(--border); padding: 0.375em 0.5em; } .floating-pane-content { background-color: var(--bg); min-height: 12em; } ` }