v0.30.0 eb3068f

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

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