v0.29.0 21b3150

css.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close