v0.32.0 0617278

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(--control-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(--control-text);
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	color: var(--control-text);
749	border-bottom: 0.0625em solid var(--border);
750	padding: 0.25em 0.5em;
751	min-height: 1.5em;
752}
753
754/* Keep the background stable so --control-text retains its contrast.
755 * Indicate hover with an underline and the active pane with an accent border. */
756.stacked-pane-titlebar.active {
757	border-left: 0.1875em solid var(--accent);
758}
759
760/* Favicon image in title bar */
761.stacked-pane-titlebar image {
762	color: var(--control-text);
763	min-width: 1em;
764	min-height: 1em;
765	margin-right: 0.375em;
766}
767
768/* Title text in title bar */
769.stacked-pane-titlebar label {
770	color: var(--control-text);
771	font-size: 0.75em;
772	font-weight: 400;
773}
774
775/* Hover effect on title bar label */
776.stacked-pane-titlebar.stacked-pane-title-clickable:hover label {
777	text-decoration-line: underline;
778}
779
780/* Close button in stacked pane title bar */
781.stacked-pane-close-button {
782	background: transparent;
783	border: none;
784	border-radius: 4px;
785	padding: 4px;
786	margin: 0 2px;
787	min-width: 20px;
788	min-height: 20px;
789	opacity: 0.4;
790	transition: opacity 100ms ease-in-out, background-color 100ms ease-in-out;
791}
792
793.stacked-pane-close-button > image {
794	margin: 0;
795	padding: 0;
796}
797
798.stacked-pane-close-button:hover {
799	opacity: 1.0;
800	background: alpha(var(--destructive), 0.2);
801}
802
803.stacked-pane-close-button:hover > image {
804	color: var(--destructive);
805}
806
807/* Show close button more prominently when hovering title bar */
808.stacked-pane-titlebar.stacked-pane-title-clickable:hover .stacked-pane-close-button {
809	opacity: 0.7;
810}
811
812.stacked-pane-close-button:focus {
813	outline: none;
814	box-shadow: none;
815}
816`
817}
818
819// generateProgressBarCSS creates progress bar styles for page loading indication.
820func generateProgressBarCSS(p Palette) string {
821	return `/* ===== Progress Bar Styling ===== */
822
823/* Native GtkProgressBar with osd class */
824progressbar.osd {
825	min-height: 4px;
826}
827
828progressbar.osd trough {
829	min-height: 4px;
830	min-width: 2px;
831	margin: 0;
832	padding: 0;
833	background-color: alpha(var(--bg), 0.3);
834}
835
836progressbar.osd progress {
837	min-height: 4px;
838	min-width: 2px;
839	margin: 0;
840	padding: 0;
841	background-color: var(--accent);
842}
843`
844}
845
846func generateTouchpadNavigationIndicatorCSS(p Palette) string {
847	_ = p
848	return `/* ===== Touchpad Navigation Indicator Styling ===== */
849
850.touchpad-navigation-indicator {
851	background-color: alpha(var(--surface), 0.90);
852	color: var(--text);
853	border: 0.0625em solid alpha(var(--border), 0.72);
854	border-radius: 1.125em;
855	padding: 0.75em 0.875em 0.8125em;
856	opacity: 1;
857	filter: blur(0px);
858	backdrop-filter: blur(0.75em);
859	box-shadow: 0 1em 2.25em alpha(black, 0.30),
860	            0 0.0625em 0 alpha(white, 0.06) inset;
861	transition: opacity 220ms ease-out,
862	            filter 220ms ease-out,
863	            backdrop-filter 220ms ease-out,
864	            background-color 140ms ease-out,
865	            border-color 140ms ease-out,
866	            box-shadow 220ms ease-out;
867}
868
869.touchpad-navigation-indicator.back {
870	border-left: 0.1875em solid alpha(var(--accent), 0.82);
871}
872
873.touchpad-navigation-indicator.forward {
874	border-right: 0.1875em solid alpha(var(--accent), 0.82);
875}
876
877.touchpad-navigation-indicator.threshold-reached {
878	border-color: alpha(var(--accent), 0.95);
879	background-color: alpha(var(--accent), 0.18);
880	box-shadow: 0 1.125em 2.5em alpha(black, 0.34),
881	            0 0 0 0.0625em alpha(var(--accent), 0.24) inset;
882}
883
884.touchpad-navigation-indicator.hiding {
885	opacity: 0;
886	filter: blur(0.0625em);
887	box-shadow: 0 0.375em 0.875em alpha(black, 0.10),
888	            0 0 0 0.0625em alpha(var(--accent), 0.04) inset;
889}
890
891.touchpad-navigation-header {
892	min-width: 9.75em;
893}
894
895.touchpad-navigation-icon {
896	background-color: alpha(var(--accent), 0.16);
897	color: var(--accent);
898	border-radius: 999px;
899	font-size: 0.95em;
900	font-weight: 700;
901	padding: 0.125em 0.375em;
902}
903
904.touchpad-navigation-indicator.threshold-reached .touchpad-navigation-icon {
905	background-color: var(--accent);
906	color: var(--bg);
907}
908
909.touchpad-navigation-label {
910	color: var(--text);
911	font-weight: 700;
912	font-size: 0.92em;
913	letter-spacing: 0.01em;
914}
915
916progressbar.touchpad-navigation-progress {
917	min-height: 0.3125em;
918}
919
920progressbar.touchpad-navigation-progress trough {
921	min-height: 0.3125em;
922	min-width: 9.75em;
923	border-radius: 999px;
924	background-color: alpha(var(--muted), 0.24);
925	box-shadow: 0 0.0625em 0 alpha(white, 0.06) inset;
926}
927
928progressbar.touchpad-navigation-progress progress {
929	min-height: 0.3125em;
930	border-radius: 999px;
931	background-color: var(--accent);
932	box-shadow: 0 0 0.35em alpha(var(--accent), 0.35);
933}
934`
935}
936
937// generateSessionManagerCSS creates session manager modal styles.
938// Uses em units for scalable UI, matches omnibox styling patterns.
939func generateSessionManagerCSS(p Palette) string {
940	_ = p
941	return `/* ===== Session Manager Styling ===== */
942
943/* Session manager outer container - for positioning in overlay */
944.session-manager-outer {
945	/* Positioning is handled via SetHalign/SetValign in Go */
946}
947
948/* Session manager main container - the visible popup */
949.session-manager-container {
950	background-color: var(--surface-variant);
951	border: 0.0625em solid var(--border);
952	border-radius: 0.1875em;
953	padding: 0;
954	/* Width is owned by the Go sizing request so CSS must not force
955	   growth beyond the percentage viewport limit. */
956	/* Note: max-width not supported in GTK4 CSS, rely on container constraints */
957}
958
959/* Header with title */
960.session-manager-header {
961	background-color: shade(var(--surface-variant), 1.1);
962	border-bottom: 0.0625em solid var(--border);
963	padding: 0.5em 0.75em;
964}
965
966.session-manager-title {
967	font-size: 0.9375em;
968	font-weight: 600;
969	color: var(--text);
970}
971
972/* Scrolled window for session list */
973.session-manager-scrolled {
974	background-color: shade(var(--surface-variant), 0.95);
975}
976
977/* List box */
978.session-manager-list {
979	background-color: transparent;
980}
981
982/* Session rows */
983.session-manager-row {
984	padding: 0.5em 0.75em;
985	margin: 0;
986	border-radius: 0;
987	border-left: 0.1875em solid transparent;
988	border-bottom: 0.0625em solid alpha(var(--border), 0.5);
989	transition: background-color 100ms ease-in-out, border-left 100ms ease-in-out;
990	min-height: 2.75em;
991}
992
993.session-manager-row:last-child {
994	border-bottom: none;
995}
996
997.session-manager-row:hover {
998	background-color: alpha(var(--accent), 0.12);
999	border-left: 0.1875em solid var(--accent);
1000}
1001
1002.session-manager-row:selected,
1003row:selected .session-manager-row {
1004	background-color: alpha(var(--accent), 0.2);
1005	border-left: 0.1875em solid var(--accent);
1006}
1007
1008/* Status indicator dot */
1009.session-status {
1010	font-size: 0.875em;
1011	min-width: 1em;
1012	margin-right: 0.5em;
1013}
1014
1015/* Current session - accent color dot */
1016.session-current .session-status {
1017	color: var(--accent);
1018}
1019
1020/* Active session (other instance) - muted dot */
1021.session-active .session-status {
1022	color: var(--muted);
1023}
1024
1025/* Exited session - dimmed appearance with subtle stop icon */
1026.session-exited {
1027	opacity: 0.75;
1028}
1029
1030.session-exited .session-status {
1031	color: var(--error);
1032	opacity: 0.5;
1033}
1034
1035/* Session ID labels */
1036.session-id-short {
1037	font-size: 0.875em;
1038	font-weight: 600;
1039	color: var(--accent);
1040	font-family: var(--font-mono);
1041}
1042
1043.session-id-prefix {
1044	font-size: 0.875em;
1045	font-weight: 400;
1046	color: var(--muted);
1047	font-family: var(--font-mono);
1048}
1049
1050.session-id-badge {
1051	font-size: 0.75em;
1052	font-weight: 500;
1053	color: var(--accent);
1054	margin-left: 0.5em;
1055}
1056
1057/* Tab/pane count label */
1058.session-count {
1059	font-size: 0.75em;
1060	color: var(--muted);
1061}
1062
1063/* Relative time label */
1064.session-time {
1065	font-size: 0.75em;
1066	color: var(--muted);
1067	/* Note: margin-left: auto not supported in GTK4 CSS, use SetHalign(End) in code */
1068}
1069
1070/* Section divider for EXITED sessions */
1071.session-divider {
1072	font-size: 0.6875em;
1073	font-weight: 600;
1074	color: var(--muted);
1075	text-transform: uppercase;
1076	letter-spacing: 0.05em;
1077	padding: 0.5em 0.75em 0.25em 0.75em;
1078	background-color: alpha(var(--border), 0.3);
1079	border-bottom: 0.0625em solid var(--border);
1080}
1081
1082/* Footer with keyboard shortcuts */
1083.session-manager-footer {
1084	background-color: shade(var(--surface-variant), 0.9);
1085	border-top: 0.0625em solid var(--border);
1086	padding: 0.375em 0.75em;
1087	font-size: 0.6875em;
1088	color: var(--muted);
1089	font-family: var(--font-mono);
1090}
1091
1092/* Session mode border */
1093.session-mode-active {
1094	background-color: transparent;
1095	box-shadow: inset 0 0 0 0.25em var(--session-mode-color);
1096	border-radius: 0;
1097}
1098`
1099}
1100
1101// generateContextMenuCSS creates shared context menu styles.
1102func generateContextMenuCSS() string {
1103	return `/* ===== Context Menu Styling ===== */
1104
1105popover.context-menu-popover {
1106	background-color: transparent;
1107	background-image: none;
1108}
1109
1110popover.context-menu-popover > contents {
1111	background-color: var(--surface);
1112	background-image: none;
1113	border: 0.0625em solid alpha(var(--border), 0.85);
1114	border-radius: 0.25em;
1115	box-shadow: 0 0.75em 1.75em alpha(var(--bg), 0.28), 0 0.0625em 0 alpha(var(--border), 0.18) inset;
1116	padding: 0;
1117}
1118
1119.context-menu {
1120	padding: 0.125em 0;
1121	min-width: 10.5em;
1122}
1123
1124.context-menu button.context-menu-item {
1125	background-color: transparent;
1126	background-image: none;
1127	border: none;
1128	border-radius: 0;
1129	color: var(--text);
1130	padding: 0.25em 0.75em;
1131	margin: 0;
1132	min-height: 1.75em;
1133	transition: background-color 120ms ease-in-out, color 120ms ease-in-out;
1134}
1135
1136.context-menu button.context-menu-item:hover,
1137.context-menu button.context-menu-item:focus,
1138.context-menu button.context-menu-item:active {
1139	background-color: alpha(var(--surface-variant), 0.9);
1140	color: var(--text);
1141}
1142
1143.context-menu button.context-menu-item:active {
1144	background-color: alpha(var(--accent), 0.16);
1145}
1146
1147.context-menu button.context-menu-item:disabled {
1148	color: var(--muted);
1149	opacity: 0.7;
1150}
1151
1152.context-menu-separator {
1153	background-color: alpha(var(--border), 0.55);
1154	margin: 0.25em 0.5em;
1155	min-height: 0.0625em;
1156}
1157`
1158}
1159
1160// generateToasterCSS creates toast notification styles.
1161// Uses em units for scalable UI.
1162func generateToasterCSS(p Palette) string {
1163	return `/* ===== Toaster Styling ===== */
1164
1165/* Toast notification container */
1166.toast {
1167	background-color: var(--surface-variant);
1168	border-radius: 0.25em;
1169	padding: 0.375em 0.625em;
1170	margin: 0.5em;
1171	font-size: 0.875em;
1172	font-weight: 600;
1173	box-shadow: 0 2px 8px alpha(black, 0.3);
1174	min-width: 2.5em;
1175}
1176
1177/* Toast level: info (default, accent color) */
1178.toast-info {
1179	background-color: alpha(var(--accent), 0.9);
1180	color: var(--bg);
1181}
1182
1183/* Toast level: success */
1184.toast-success {
1185	background-color: alpha(var(--success), 0.9);
1186	color: var(--bg);
1187}
1188
1189/* Toast level: warning */
1190.toast-warning {
1191	background-color: alpha(var(--warning), 0.9);
1192	color: var(--bg);
1193}
1194
1195/* Toast level: error */
1196.toast-error {
1197	background-color: alpha(var(--destructive), 0.9);
1198	color: var(--bg);
1199}
1200
1201/* Toast with custom styling (mode indicator toasters) */
1202.toast-custom {
1203	color: #ffffff;
1204}
1205
1206/* Mode-specific toast colors */
1207.toast-pane-mode {
1208	background-color: var(--pane-mode-color);
1209	color: #ffffff;
1210}
1211
1212.toast-vim-mode {
1213	background-color: var(--pane-mode-color);
1214	color: #ffffff;
1215}
1216
1217.toast-tab-mode {
1218	background-color: var(--tab-mode-color);
1219	color: #ffffff;
1220}
1221
1222.toast-session-mode {
1223	background-color: var(--session-mode-color);
1224	color: #ffffff;
1225}
1226
1227.toast-resize-mode {
1228	background-color: var(--resize-mode-color);
1229	color: #ffffff;
1230}
1231`
1232}
1233
1234// generateLinkStatusCSS creates link status overlay styles.
1235// Uses CSS transitions for fade-in/fade-out effect.
1236func generateLinkStatusCSS(p Palette) string {
1237	return `/* ===== Link Status Overlay Styling ===== */
1238
1239/* Link status container - bottom left positioning */
1240.link-status {
1241	background-color: alpha(var(--surface-variant), 0.95);
1242	border-radius: 0.25em 0.25em 0 0;
1243	padding: 0.25em 0.5em;
1244	margin: 0;
1245	font-size: 0.75em;
1246	/* Note: max-width not supported in GTK4 CSS, use label SetMaxWidthChars instead */
1247	box-shadow: 0 -1px 4px alpha(black, 0.15);
1248
1249	/* Fade transition - hidden by default */
1250	opacity: 0;
1251	transition: opacity 150ms ease-in-out;
1252}
1253
1254/* Visible state - triggered by adding .visible class */
1255.link-status.visible {
1256	opacity: 1;
1257}
1258
1259/* Link status label text */
1260.link-status label {
1261	color: var(--muted);
1262}
1263`
1264}
1265
1266func generateFloatingPaneCSS(p Palette) string {
1267	_ = p
1268	return `/* ===== Floating Pane Styling ===== */
1269.floating-pane-outer {
1270	/* Positioning handled in Go via overlay alignment and margins. */
1271}
1272
1273.floating-pane-container {
1274	background-color: var(--surface-variant);
1275	border: 0.0625em solid var(--border);
1276	border-radius: 0.25em;
1277	box-shadow: 0 0.5em 1.5em alpha(black, 0.25);
1278	opacity: 0;
1279	transition: opacity 110ms ease-out;
1280}
1281
1282.floating-pane-container.floating-pane-visible {
1283	opacity: 1;
1284}
1285
1286.floating-pane-container.pane-active {
1287	border-color: var(--accent);
1288}
1289
1290.floating-pane-omnibox {
1291	background-color: shade(var(--surface-variant), 1.08);
1292	border-bottom: 0.0625em solid var(--border);
1293	padding: 0.375em 0.5em;
1294}
1295
1296.floating-pane-content {
1297	background-color: var(--bg);
1298	min-height: 12em;
1299}
1300`
1301}

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