1// Package theme provides GTK CSS styling for UI components.
2package theme
3
4import (
5 "fmt"
6 "strings"
7)
8
9// GenerateCSS creates GTK4 CSS using the provided palette with default scale.
10func GenerateCSS(p Palette) string {
11 return GenerateCSSWithScale(p, 1.0)
12}
13
14// GenerateCSSWithScale creates GTK4 CSS using the provided palette and UI scale factor.
15// Scale affects widget sizing that uses relative units; text scaling is handled via GtkSettings.
16func GenerateCSSWithScale(p Palette, scale float64) string {
17 return GenerateCSSWithScaleAndFonts(p, scale, DefaultFontConfig())
18}
19
20// FontConfig holds font settings for CSS generation.
21type FontConfig struct {
22 SansFont string
23 MonospaceFont string
24 GtkFont string
25}
26
27// DefaultFontConfig returns safe font fallbacks.
28func DefaultFontConfig() FontConfig {
29 return FontConfig{
30 SansFont: "sans-serif",
31 MonospaceFont: "monospace",
32 GtkFont: DefaultGTKFont(),
33 }
34}
35
36// FontCSSVars generates CSS custom properties for fonts.
37func FontCSSVars(fonts FontConfig) string {
38 var sb strings.Builder
39 // Quote the configured family; include generic fallback.
40 fmt.Fprintf(&sb, " --font-sans: %q, sans-serif;\n", fonts.SansFont)
41 fmt.Fprintf(&sb, " --font-mono: %q, monospace;\n", fonts.MonospaceFont)
42 fmt.Fprintf(&sb, " --font-gtk: %q, sans-serif;\n", fonts.GtkFont)
43 return sb.String()
44}
45
46func generateFontCSS() string {
47 return `/* Font styling */
48window, tooltip, popover {
49 font-family: var(--font-gtk);
50}
51`
52}
53
54// GenerateCSSWithScaleAndFonts creates GTK4 CSS using the provided palette, UI scale factor and fonts.
55// Scale affects widget sizing that uses relative units; text scaling is handled via GtkSettings.
56// Uses default mode colors.
57func GenerateCSSWithScaleAndFonts(p Palette, scale float64, fonts FontConfig) string {
58 return GenerateCSSFull(p, scale, fonts, DefaultModeColors())
59}
60
61// GenerateCSSFull creates GTK4 CSS using all provided configuration.
62// Text scaling is handled via GtkSettings; CSS here stays scale-independent.
63func GenerateCSSFull(p Palette, _ float64, fonts FontConfig, modeColors ModeColors) string {
64 defaults := DefaultFontConfig()
65 fonts = FontConfig{
66 SansFont: Coalesce(fonts.SansFont, defaults.SansFont),
67 MonospaceFont: Coalesce(fonts.MonospaceFont, defaults.MonospaceFont),
68 GtkFont: Coalesce(fonts.GtkFont, defaults.GtkFont),
69 }
70
71 var sb strings.Builder
72
73 // CSS custom properties (variables) - GTK4 uses :root selector
74 sb.WriteString("/* Theme variables */\n")
75 sb.WriteString(":root {\n")
76 sb.WriteString(p.ToCSSVars())
77 sb.WriteString(FontCSSVars(fonts))
78 sb.WriteString(modeColors.ToCSSVars())
79 sb.WriteString("}\n\n")
80
81 // Global font styling
82 sb.WriteString(generateFontCSS())
83 sb.WriteString("\n")
84
85 // Tab bar styling
86 sb.WriteString(generateTabBarCSS(p))
87 sb.WriteString("\n")
88
89 // Omnibox styling
90 sb.WriteString(generateOmniboxCSS(p))
91 sb.WriteString("\n")
92
93 // Find bar styling
94 sb.WriteString(generateFindBarCSS(p))
95 sb.WriteString("\n")
96
97 // Pane styling
98 sb.WriteString(generatePaneCSS(p))
99 sb.WriteString("\n")
100
101 // Stacked pane styling
102 sb.WriteString(generateStackedPaneCSS(p))
103 sb.WriteString("\n")
104
105 // Progress bar styling
106 sb.WriteString(generateProgressBarCSS(p))
107 sb.WriteString("\n")
108
109 // Toaster styling
110 sb.WriteString(generateToasterCSS(p))
111 sb.WriteString("\n")
112
113 // Link status overlay styling
114 sb.WriteString(generateLinkStatusCSS(p))
115 sb.WriteString("\n")
116
117 // Session manager styling
118 sb.WriteString(generateSessionManagerCSS(p))
119 sb.WriteString("\n")
120
121 // Context menu styling
122 sb.WriteString(generateContextMenuCSS())
123 sb.WriteString("\n")
124
125 // Tab picker styling
126 sb.WriteString(generateTabPickerCSS(p))
127 sb.WriteString("\n")
128
129 // Permission popup styling
130 sb.WriteString(generatePermissionPopupCSS(p))
131 sb.WriteString("\n")
132
133 // Floating pane styling
134 sb.WriteString(generateFloatingPaneCSS(p))
135 sb.WriteString("\n")
136
137 // WebRTC permission activity indicator styling
138 sb.WriteString(generateWebRTCPermissionIndicatorCSS(p))
139 sb.WriteString("\n")
140
141 // Accent picker styling
142 sb.WriteString(generateAccentPickerCSS(p))
143
144 return sb.String()
145}
146
147// generateTabBarCSS creates tab bar styles.
148// Uses em units for scalable UI.
149func generateTabBarCSS(p Palette) string {
150 return `/* Tab bar styling */
151/* Content area inset for overlaid tab bars — uses the same 2em token as
152 .tab-bar min-height and remains static to avoid OSR vertical reallocations. */
153.content-area-tabbar-inset-top {
154 padding-top: 2em;
155}
156
157.content-area-tabbar-inset-bottom {
158 padding-bottom: 2em;
159}
160
161.tab-bar {
162 background-color: var(--surface);
163 border-top: 0.125em solid var(--border);
164 padding: 0;
165 min-height: 2em;
166}
167
168/* Tab button styling */
169button.tab-button {
170 background-color: var(--surface-variant);
171 background-image: none;
172 border: none;
173 border-right: 0.0625em solid var(--border);
174 border-radius: 0;
175 padding: 0.25em 0.5em;
176 transition: background-color 200ms ease-in-out;
177}
178
179button.tab-button:hover {
180 background-color: shade(var(--surface-variant), 1.2);
181}
182
183button.tab-button.tab-button-active {
184 background-color: shade(var(--surface-variant), 1.2);
185 border-bottom: 0.125em solid var(--accent);
186 font-weight: 600;
187}
188
189/* Tab title text */
190.tab-title {
191 font-size: 0.75em;
192 color: var(--text);
193 font-weight: 500;
194}
195`
196}
197
198// generateOmniboxCSS creates omnibox styles.
199// Uses em units for scalable UI.
200func generateOmniboxCSS(p Palette) string {
201 return `/* ===== Omnibox Styling ===== */
202
203window.standalone-omnibox-window {
204 background-color: transparent;
205}
206
207/* Omnibox outer container - for positioning in overlay */
208.omnibox-outer {
209 /* Positioning is handled via SetHalign/SetValign/SetMarginTop in Go */
210}
211
212/* Omnibox main container - the visible popup */
213.omnibox-container {
214 background-color: var(--surface);
215 background-image: none;
216 border: 0.0625em solid alpha(var(--border), 0.85);
217 border-radius: 0.1875em;
218 box-shadow: 0 0.875em 2.5em alpha(black, 0.24), 0 0.0625em 0 alpha(white, 0.05) inset;
219 padding: 0;
220}
221
222/* Header with History/Favorites toggle */
223.omnibox-header {
224 background-color: var(--surface);
225 background-image: none;
226 border-bottom: 0.0625em solid alpha(var(--border), 0.75);
227 padding: 0.375em 0.75em;
228}
229
230.omnibox-header-btn {
231 background-color: alpha(var(--surface), 0.12);
232 background-image: none;
233 border: 0.0625em solid transparent;
234 border-radius: 0.125em;
235 padding: 0.25em 0.75em;
236 margin-right: 0.5em;
237 font-size: 0.8125em;
238 font-weight: 500;
239 color: var(--muted);
240 transition: all 100ms ease-in-out;
241}
242
243.omnibox-header-btn:hover {
244 background-color: alpha(var(--accent), 0.14);
245 border-color: alpha(var(--accent), 0.18);
246 color: var(--text);
247}
248
249.omnibox-header-btn.omnibox-header-active {
250 background-color: alpha(var(--accent), 0.18);
251 border-color: alpha(var(--accent), 0.28);
252 color: var(--accent);
253 font-weight: 600;
254}
255
256/* Search entry field */
257entry.omnibox-entry,
258entry.omnibox-entry > text {
259 background-color: alpha(var(--bg), 0.88);
260 background-image: none;
261 color: var(--text);
262 caret-color: var(--accent);
263}
264
265entry.omnibox-entry {
266 border: 0.0625em solid alpha(var(--border), 0.82);
267 border-radius: 0.125em;
268 padding: 0.625em 0.75em;
269 margin: 0.5em 0.75em;
270 font-size: 1em;
271 box-shadow: 0 0.0625em 0 alpha(white, 0.04) inset;
272}
273
274entry.omnibox-entry:focus,
275entry.omnibox-entry:focus-within,
276entry.omnibox-entry:focus-visible,
277entry.omnibox-entry:focus > text,
278entry.omnibox-entry:focus-within > text,
279entry.omnibox-entry:focus-visible > text {
280 border-color: var(--accent);
281 background-color: shade(var(--bg), 1.05);
282 background-image: none;
283 outline-style: none;
284 outline-width: 0px;
285 outline-color: transparent;
286}
287
288/* Override any internal focus styling */
289entry.omnibox-entry > text:focus,
290entry.omnibox-entry > text:focus-visible {
291 outline-style: none;
292 outline-color: transparent;
293}
294
295/* Scrolled window for suggestions */
296.omnibox-scrolled {
297 background-color: var(--surface);
298 border-top: 0.0625em solid alpha(var(--border), 0.65);
299}
300
301/* List box */
302.omnibox-listbox {
303 background-color: transparent;
304}
305
306/* Suggestion rows */
307.omnibox-row {
308 padding: 0.5em 0.75em;
309 margin: 0;
310 border-radius: 0;
311 border-left: 0.1875em solid transparent;
312 border-bottom: 0.0625em solid alpha(var(--border), 0.35);
313 background-color: var(--surface);
314 background-image: none;
315 transition: background-color 100ms ease-in-out, border-left 100ms ease-in-out;
316 min-height: 2.75em;
317}
318
319.omnibox-row:last-child {
320 border-bottom: none;
321}
322
323.omnibox-row:hover {
324 background-color: alpha(var(--accent), 0.12);
325 border-left: 0.1875em solid var(--accent);
326}
327
328.omnibox-row:selected {
329 background-color: alpha(var(--accent), 0.2);
330 border-left: 0.1875em solid var(--accent);
331}
332
333/* Favorite indicator in history mode - keep the row neutral and use the left selection stripe */
334.omnibox-row.omnibox-row-favorite {
335 background-color: transparent;
336 border-left: 0.1875em solid var(--warning);
337}
338
339.omnibox-row.omnibox-row-favorite:hover {
340 background-color: alpha(var(--accent), 0.12);
341 border-left: 0.1875em solid var(--warning);
342}
343
344.omnibox-row.omnibox-row-favorite:selected {
345 background-color: alpha(var(--accent), 0.2);
346 border-left: 0.1875em solid var(--warning);
347}
348
349/* Favicon in omnibox rows */
350.omnibox-favicon {
351 min-width: 1em;
352 min-height: 1em;
353 margin-right: 0.5em;
354}
355
356/* Suggestion title/URL */
357.omnibox-suggestion-title {
358 font-size: 0.875em;
359 color: var(--text);
360 font-weight: 500;
361}
362
363/* Also style labels inside omnibox rows directly */
364.omnibox-row label {
365 color: var(--text);
366}
367
368.omnibox-row:selected .omnibox-suggestion-title {
369 color: var(--text);
370 font-weight: 500;
371}
372
373/* URL text below title - use .omnibox-row prefix for specificity over .omnibox-row label */
374.omnibox-row .omnibox-suggestion-url {
375 font-size: 0.75em;
376 color: alpha(var(--muted), 0.72);
377 font-weight: 400;
378}
379
380.omnibox-favorite-star {
381 color: mix(var(--warning), var(--muted), 0.45);
382 opacity: 0.75;
383 margin-left: 0.625em;
384 margin-right: 0.125em;
385}
386
387.omnibox-row:selected .omnibox-favorite-star {
388 color: mix(var(--warning), var(--text), 0.35);
389 opacity: 0.88;
390}
391
392.omnibox-row:selected .omnibox-suggestion-url {
393 color: alpha(var(--muted), 0.75);
394}
395
396/* Keyboard shortcut badge */
397.omnibox-shortcut-badge {
398 background-color: alpha(var(--muted), 0.3);
399 color: var(--muted);
400 border-radius: 0.25em;
401 padding: 0.125em 0.375em;
402 font-size: 0.625em;
403 font-weight: 500;
404 font-family: var(--font-mono);
405 margin-left: 0.5em;
406}
407
408.omnibox-row:hover .omnibox-shortcut-badge {
409 background-color: alpha(var(--accent), 0.2);
410 color: var(--accent);
411}
412
413.omnibox-row:selected .omnibox-shortcut-badge {
414 background-color: alpha(var(--accent), 0.3);
415 color: var(--accent);
416}
417
418/* Bang indicator badge in omnibox header */
419.omnibox-bang-badge {
420 background-color: alpha(var(--accent), 0.18);
421 color: var(--accent);
422 border-radius: 0.25em;
423 padding: 0.125em 0.5em;
424 font-size: 0.6875em;
425 font-weight: 600;
426}
427
428/* Header badge used for omnibox state */
429.omnibox-header-badge {
430 background-color: alpha(var(--surface-variant), 0.65);
431 color: var(--muted);
432 border: 0.0625em solid alpha(var(--border), 0.85);
433 border-radius: 0.1875em;
434 padding: 0.0625em 0.375em;
435 font-size: 0.6875em;
436 font-weight: 500;
437 transition: background-color 100ms ease-in-out, border-color 100ms ease-in-out, color 100ms ease-in-out;
438}
439
440.omnibox-header-badge:hover {
441 background-color: shade(var(--surface-variant), 1.08);
442 border-color: var(--border);
443 color: var(--text);
444}
445
446/* Zoom indicator in omnibox header */
447.omnibox-zoom-indicator {
448 background-color: alpha(var(--accent), 0.2);
449 color: var(--accent);
450 border-radius: 0.25em;
451 padding: 0.125em 0.5em;
452 font-size: 0.6875em;
453 font-weight: 600;
454 /* Note: margin-left: auto not supported in GTK4 CSS, use SetHalign(End) in code */
455}
456
457entry.omnibox-entry.omnibox-entry-bang-active,
458entry.omnibox-entry.omnibox-entry-bang-active:focus,
459entry.omnibox-entry.omnibox-entry-bang-active:focus-within,
460entry.omnibox-entry.omnibox-entry-bang-active:focus-visible,
461entry.omnibox-entry.omnibox-entry-bang-active > text,
462entry.omnibox-entry.omnibox-entry-bang-active:focus > text,
463entry.omnibox-entry.omnibox-entry-bang-active:focus-within > text,
464entry.omnibox-entry.omnibox-entry-bang-active:focus-visible > text {
465 border-color: var(--accent);
466 background-color: shade(var(--bg), 1.05);
467 background-image: none;
468}
469
470.omnibox-row.omnibox-row-bang .omnibox-suggestion-title {
471 color: var(--accent);
472 font-weight: 600;
473}
474
475/* Ghost text autocomplete overlay - positioned to align with entry text */
476.omnibox-ghost {
477 font-size: 1em;
478 color: var(--muted);
479 margin-top: -0.0625em; /* Nudge up 1px to align with entry text baseline */
480 background-color: transparent;
481}
482`
483}
484
485// generateFindBarCSS creates find bar styles.
486// Uses em units for scalable UI.
487func generateFindBarCSS(p Palette) string {
488 _ = p
489 return `/* ===== Find Bar Styling ===== */
490.find-bar-outer {
491 margin: 0.5em;
492}
493
494.find-bar-container {
495 background-color: var(--surface-variant);
496 border: 0.0625em solid var(--border);
497 border-radius: 0.25em;
498 box-shadow: 0 0.25em 0.75em alpha(black, 0.2);
499 padding: 0.5em;
500 min-width: 20em;
501}
502
503.find-bar-input-row {
504 margin-bottom: 0.25em;
505}
506
507.find-bar-entry {
508 background-color: var(--bg);
509 color: var(--text);
510 border: 0.0625em solid var(--border);
511 border-radius: 0.1875em;
512 padding: 0.4em 0.5em;
513 caret-color: var(--accent);
514}
515
516entry.find-bar-entry:focus,
517entry.find-bar-entry:focus-within,
518entry.find-bar-entry:focus-visible {
519 border-color: var(--accent);
520 outline-style: none;
521 outline-width: 0px;
522 outline-color: transparent;
523}
524
525.find-bar-entry.not-found {
526 background-color: alpha(#ef4444, 0.15);
527 border-color: #ef4444;
528}
529
530.find-bar-count {
531 font-size: 0.8em;
532 color: var(--muted);
533 min-width: 3.5em;
534}
535
536.find-bar-count.find-bar-count-has {
537 color: var(--accent);
538}
539
540.find-bar-nav,
541.find-bar-close,
542.find-bar-toggle {
543 background-image: none;
544 background-color: alpha(var(--bg), 0.6);
545 border: 0.0625em solid var(--border);
546 border-radius: 0.1875em;
547 padding: 0.25em 0.5em;
548 transition: background-color 120ms ease-in-out, border-color 120ms ease-in-out;
549}
550
551.find-bar-nav:hover,
552.find-bar-close:hover,
553.find-bar-toggle:hover {
554 background-color: alpha(var(--accent), 0.15);
555}
556
557.find-bar-toggle:checked {
558 background-color: alpha(var(--accent), 0.2);
559 border-color: var(--accent);
560 color: var(--accent);
561}
562`
563}
564
565// generatePaneCSS creates pane border styles.
566// Uses em units for scalable UI.
567func generatePaneCSS(p Palette) string {
568 return `/* ===== Pane Styling ===== */
569
570/* Pane overlay container - theme background prevents white flash */
571.pane-overlay {
572 background-color: var(--bg);
573}
574
575/* WebView widget - theme background prevents white flash during page load */
576.webview-themed {
577 background-color: var(--bg);
578}
579
580/* Loading skeleton - shown until WebView paints */
581.loading-skeleton {
582 background-color: var(--bg);
583}
584
585.loading-skeleton-spinner {
586 -gtk-icon-size: 32px;
587 min-width: 32px;
588 min-height: 32px;
589 color: var(--muted);
590 opacity: 0.4;
591}
592
593.loading-skeleton-logo {
594 opacity: 0.08;
595 min-width: 256px;
596 min-height: 256px;
597}
598
599.loading-skeleton-version {
600 opacity: 0.25;
601 font-size: 13px;
602 color: var(--muted);
603}
604
605/* Pane border - default transparent */
606.pane-border {
607 border: 0.0625em solid transparent;
608}
609
610/* Active pane - accent border */
611.pane-active {
612 border: 0.0625em solid var(--accent);
613}
614
615/* Hide active border when only one pane exists */
616.single-pane .pane-active {
617 border-color: transparent;
618}
619
620/* Pane mode active - thick inset border (for overlay) */
621.pane-mode-active {
622 background-color: transparent;
623 box-shadow: inset 0 0 0 0.25em var(--pane-mode-color);
624 border-radius: 0;
625}
626
627/* Tab mode active - thick inset border (for overlay) */
628.tab-mode-active {
629 background-color: transparent;
630 box-shadow: inset 0 0 0 0.25em var(--tab-mode-color);
631 border-radius: 0;
632}
633
634/* Resize mode active - thick inset border */
635.resize-mode-active {
636 background-color: transparent;
637 /* Prefer inset shadow, but also set outline for widgets that don't paint shadows */
638 box-shadow: inset 0 0 0 0.25em var(--resize-mode-color);
639 outline: 0.25em solid var(--resize-mode-color);
640 outline-offset: -0.25em;
641 border-radius: 0;
642}
643`
644}
645
646// generateStackedPaneCSS creates stacked pane (Zellij-style tabs within panes) styles.
647// Uses em units for scalable UI.
648func generateStackedPaneCSS(p Palette) string {
649 return `/* ===== Stacked Pane Styling ===== */
650
651/* Title bar for inactive panes in a stack */
652/* The title bar is now a Box with GestureClick, not wrapped in a button */
653.stacked-pane-titlebar {
654 background-color: var(--surface-variant);
655 border-bottom: 0.0625em solid var(--border);
656 padding: 0.25em 0.5em;
657 min-height: 1.5em;
658}
659
660/* Clickable title bar - hover styling */
661.stacked-pane-titlebar.stacked-pane-title-clickable {
662 transition: background-color 150ms ease-in-out;
663}
664
665.stacked-pane-titlebar.stacked-pane-title-clickable:hover {
666 background-color: shade(var(--surface-variant), 1.15);
667}
668
669/* Title bar content box */
670.stacked-pane-titlebar.active {
671 background-color: shade(var(--surface-variant), 1.2);
672 border-left: 0.1875em solid var(--accent);
673}
674
675/* Favicon image in title bar */
676.stacked-pane-titlebar image {
677 min-width: 1em;
678 min-height: 1em;
679 margin-right: 0.375em;
680}
681
682/* Title text in title bar */
683.stacked-pane-titlebar label {
684 color: var(--text);
685 font-size: 0.75em;
686 font-weight: 400;
687}
688
689/* Hover effect on title bar label */
690.stacked-pane-titlebar.stacked-pane-title-clickable:hover label {
691 color: var(--accent);
692}
693
694/* Close button in stacked pane title bar */
695.stacked-pane-close-button {
696 background: transparent;
697 border: none;
698 border-radius: 4px;
699 padding: 4px;
700 margin: 0 2px;
701 min-width: 20px;
702 min-height: 20px;
703 opacity: 0.4;
704 transition: opacity 100ms ease-in-out, background-color 100ms ease-in-out;
705}
706
707.stacked-pane-close-button > image {
708 margin: 0;
709 padding: 0;
710}
711
712.stacked-pane-close-button:hover {
713 opacity: 1.0;
714 background: alpha(var(--destructive), 0.2);
715}
716
717.stacked-pane-close-button:hover > image {
718 color: var(--destructive);
719}
720
721/* Show close button more prominently when hovering title bar */
722.stacked-pane-titlebar.stacked-pane-title-clickable:hover .stacked-pane-close-button {
723 opacity: 0.7;
724}
725
726.stacked-pane-close-button:focus {
727 outline: none;
728 box-shadow: none;
729}
730`
731}
732
733// generateProgressBarCSS creates progress bar styles for page loading indication.
734func generateProgressBarCSS(p Palette) string {
735 return `/* ===== Progress Bar Styling ===== */
736
737/* Native GtkProgressBar with osd class */
738progressbar.osd {
739 min-height: 4px;
740}
741
742progressbar.osd trough {
743 min-height: 4px;
744 min-width: 2px;
745 margin: 0;
746 padding: 0;
747 background-color: alpha(var(--bg), 0.3);
748}
749
750progressbar.osd progress {
751 min-height: 4px;
752 min-width: 2px;
753 margin: 0;
754 padding: 0;
755 background-color: var(--accent);
756}
757`
758}
759
760// generateSessionManagerCSS creates session manager modal styles.
761// Uses em units for scalable UI, matches omnibox styling patterns.
762func generateSessionManagerCSS(p Palette) string {
763 _ = p
764 return `/* ===== Session Manager Styling ===== */
765
766/* Session manager outer container - for positioning in overlay */
767.session-manager-outer {
768 /* Positioning is handled via SetHalign/SetValign in Go */
769}
770
771/* Session manager main container - the visible popup */
772.session-manager-container {
773 background-color: var(--surface-variant);
774 border: 0.0625em solid var(--border);
775 border-radius: 0.1875em;
776 padding: 0;
777 min-width: 28em;
778 /* Note: max-width not supported in GTK4 CSS, rely on container constraints */
779}
780
781/* Header with title */
782.session-manager-header {
783 background-color: shade(var(--surface-variant), 1.1);
784 border-bottom: 0.0625em solid var(--border);
785 padding: 0.5em 0.75em;
786}
787
788.session-manager-title {
789 font-size: 0.9375em;
790 font-weight: 600;
791 color: var(--text);
792}
793
794/* Scrolled window for session list */
795.session-manager-scrolled {
796 background-color: shade(var(--surface-variant), 0.95);
797}
798
799/* List box */
800.session-manager-list {
801 background-color: transparent;
802}
803
804/* Session rows */
805.session-manager-row {
806 padding: 0.5em 0.75em;
807 margin: 0;
808 border-radius: 0;
809 border-left: 0.1875em solid transparent;
810 border-bottom: 0.0625em solid alpha(var(--border), 0.5);
811 transition: background-color 100ms ease-in-out, border-left 100ms ease-in-out;
812 min-height: 2.75em;
813}
814
815.session-manager-row:last-child {
816 border-bottom: none;
817}
818
819.session-manager-row:hover {
820 background-color: alpha(var(--accent), 0.12);
821 border-left: 0.1875em solid var(--accent);
822}
823
824.session-manager-row:selected,
825row:selected .session-manager-row {
826 background-color: alpha(var(--accent), 0.2);
827 border-left: 0.1875em solid var(--accent);
828}
829
830/* Status indicator dot */
831.session-status {
832 font-size: 0.875em;
833 min-width: 1em;
834 margin-right: 0.5em;
835}
836
837/* Current session - accent color dot */
838.session-current .session-status {
839 color: var(--accent);
840}
841
842/* Active session (other instance) - muted dot */
843.session-active .session-status {
844 color: var(--muted);
845}
846
847/* Exited session - dimmed appearance with subtle stop icon */
848.session-exited {
849 opacity: 0.75;
850}
851
852.session-exited .session-status {
853 color: var(--error);
854 opacity: 0.5;
855}
856
857/* Session ID labels */
858.session-id-short {
859 font-size: 0.875em;
860 font-weight: 600;
861 color: var(--accent);
862 font-family: var(--font-mono);
863}
864
865.session-id-prefix {
866 font-size: 0.875em;
867 font-weight: 400;
868 color: var(--muted);
869 font-family: var(--font-mono);
870}
871
872.session-id-badge {
873 font-size: 0.75em;
874 font-weight: 500;
875 color: var(--accent);
876 margin-left: 0.5em;
877}
878
879/* Tab/pane count label */
880.session-count {
881 font-size: 0.75em;
882 color: var(--muted);
883}
884
885/* Relative time label */
886.session-time {
887 font-size: 0.75em;
888 color: var(--muted);
889 /* Note: margin-left: auto not supported in GTK4 CSS, use SetHalign(End) in code */
890}
891
892/* Section divider for EXITED sessions */
893.session-divider {
894 font-size: 0.6875em;
895 font-weight: 600;
896 color: var(--muted);
897 text-transform: uppercase;
898 letter-spacing: 0.05em;
899 padding: 0.5em 0.75em 0.25em 0.75em;
900 background-color: alpha(var(--border), 0.3);
901 border-bottom: 0.0625em solid var(--border);
902}
903
904/* Footer with keyboard shortcuts */
905.session-manager-footer {
906 background-color: shade(var(--surface-variant), 0.9);
907 border-top: 0.0625em solid var(--border);
908 padding: 0.375em 0.75em;
909 font-size: 0.6875em;
910 color: var(--muted);
911 font-family: var(--font-mono);
912}
913
914/* Session mode border */
915.session-mode-active {
916 background-color: transparent;
917 box-shadow: inset 0 0 0 0.25em var(--session-mode-color);
918 border-radius: 0;
919}
920`
921}
922
923// generateContextMenuCSS creates shared context menu styles.
924func generateContextMenuCSS() string {
925 return `/* ===== Context Menu Styling ===== */
926
927popover.context-menu-popover {
928 background-color: transparent;
929 background-image: none;
930}
931
932popover.context-menu-popover > contents {
933 background-color: var(--surface);
934 background-image: none;
935 border: 0.0625em solid alpha(var(--border), 0.85);
936 border-radius: 0.25em;
937 box-shadow: 0 0.75em 1.75em alpha(var(--bg), 0.28), 0 0.0625em 0 alpha(var(--border), 0.18) inset;
938 padding: 0;
939}
940
941.context-menu {
942 padding: 0.125em 0;
943 min-width: 10.5em;
944}
945
946.context-menu button.context-menu-item {
947 background-color: transparent;
948 background-image: none;
949 border: none;
950 border-radius: 0;
951 color: var(--text);
952 padding: 0.25em 0.75em;
953 margin: 0;
954 min-height: 1.75em;
955 transition: background-color 120ms ease-in-out, color 120ms ease-in-out;
956}
957
958.context-menu button.context-menu-item:hover,
959.context-menu button.context-menu-item:focus,
960.context-menu button.context-menu-item:active {
961 background-color: alpha(var(--surface-variant), 0.9);
962 color: var(--text);
963}
964
965.context-menu button.context-menu-item:active {
966 background-color: alpha(var(--accent), 0.16);
967}
968
969.context-menu button.context-menu-item:disabled {
970 color: var(--muted);
971 opacity: 0.7;
972}
973
974.context-menu-separator {
975 background-color: alpha(var(--border), 0.55);
976 margin: 0.25em 0.5em;
977 min-height: 0.0625em;
978}
979`
980}
981
982// generateToasterCSS creates toast notification styles.
983// Uses em units for scalable UI.
984func generateToasterCSS(p Palette) string {
985 return `/* ===== Toaster Styling ===== */
986
987/* Toast notification container */
988.toast {
989 background-color: var(--surface-variant);
990 border-radius: 0.25em;
991 padding: 0.375em 0.625em;
992 margin: 0.5em;
993 font-size: 0.875em;
994 font-weight: 600;
995 box-shadow: 0 2px 8px alpha(black, 0.3);
996 min-width: 2.5em;
997}
998
999/* Toast level: info (default, accent color) */
1000.toast-info {
1001 background-color: alpha(var(--accent), 0.9);
1002 color: var(--bg);
1003}
1004
1005/* Toast level: success */
1006.toast-success {
1007 background-color: alpha(var(--success), 0.9);
1008 color: var(--bg);
1009}
1010
1011/* Toast level: warning */
1012.toast-warning {
1013 background-color: alpha(var(--warning), 0.9);
1014 color: var(--bg);
1015}
1016
1017/* Toast level: error */
1018.toast-error {
1019 background-color: alpha(var(--destructive), 0.9);
1020 color: var(--bg);
1021}
1022
1023/* Toast with custom styling (mode indicator toasters) */
1024.toast-custom {
1025 color: #ffffff;
1026}
1027
1028/* Mode-specific toast colors */
1029.toast-pane-mode {
1030 background-color: var(--pane-mode-color);
1031 color: #ffffff;
1032}
1033
1034.toast-tab-mode {
1035 background-color: var(--tab-mode-color);
1036 color: #ffffff;
1037}
1038
1039.toast-session-mode {
1040 background-color: var(--session-mode-color);
1041 color: #ffffff;
1042}
1043
1044.toast-resize-mode {
1045 background-color: var(--resize-mode-color);
1046 color: #ffffff;
1047}
1048`
1049}
1050
1051// generateLinkStatusCSS creates link status overlay styles.
1052// Uses CSS transitions for fade-in/fade-out effect.
1053func generateLinkStatusCSS(p Palette) string {
1054 return `/* ===== Link Status Overlay Styling ===== */
1055
1056/* Link status container - bottom left positioning */
1057.link-status {
1058 background-color: alpha(var(--surface-variant), 0.95);
1059 border-radius: 0.25em 0.25em 0 0;
1060 padding: 0.25em 0.5em;
1061 margin: 0;
1062 font-size: 0.75em;
1063 /* Note: max-width not supported in GTK4 CSS, use label SetMaxWidthChars instead */
1064 box-shadow: 0 -1px 4px alpha(black, 0.15);
1065
1066 /* Fade transition - hidden by default */
1067 opacity: 0;
1068 transition: opacity 150ms ease-in-out;
1069}
1070
1071/* Visible state - triggered by adding .visible class */
1072.link-status.visible {
1073 opacity: 1;
1074}
1075
1076/* Link status label text */
1077.link-status label {
1078 color: var(--muted);
1079}
1080`
1081}
1082
1083func generateFloatingPaneCSS(p Palette) string {
1084 _ = p
1085 return `/* ===== Floating Pane Styling ===== */
1086.floating-pane-outer {
1087 /* Positioning handled in Go via overlay alignment and margins. */
1088}
1089
1090.floating-pane-container {
1091 background-color: var(--surface-variant);
1092 border: 0.0625em solid var(--border);
1093 border-radius: 0.25em;
1094 box-shadow: 0 0.5em 1.5em alpha(black, 0.25);
1095 opacity: 0;
1096 transition: opacity 110ms ease-out;
1097}
1098
1099.floating-pane-container.floating-pane-visible {
1100 opacity: 1;
1101}
1102
1103.floating-pane-container.pane-active {
1104 border-color: var(--accent);
1105}
1106
1107.floating-pane-omnibox {
1108 background-color: shade(var(--surface-variant), 1.08);
1109 border-bottom: 0.0625em solid var(--border);
1110 padding: 0.375em 0.5em;
1111}
1112
1113.floating-pane-content {
1114 background-color: var(--bg);
1115 min-height: 12em;
1116}
1117`
1118}