v0.27.1 c78ce83

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

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