v0.31.0 bf0756b

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

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