detached 434719e

feat(homepage): enhance UI with search shortcuts and layout improvements

- Add search shortcuts section fetched from config via messaging bridge
- Display shortcuts in 2-column grid layout for better readability
- Move usage stats to full-width section at bottom
- Improve overflow detection with 1px tolerance and requestAnimationFrame
- Add FOUC prevention with inline critical CSS in svelte:head
- Update shortcut display format (add colon, lowercase descriptions)
commit
434719e2b6fe679deba2a275e82f0f05d2b9c294
author
bamen · · 2025-10-07 18:08 UTC
parent
65e3547
signature
unsigned
1 file · +173 -64
M gui/src/pages/Homepage.svelte +173 -64
@@ -25,6 +25,11 @@
2525 newest_entry?: string;
2626 }
2727
28+ interface SearchShortcut {
29+ url: string;
30+ description: string;
31+ }
32+
2833 let historyItems = $state<HistoryItem[]>([]);
2934 let topVisited = $state<HistoryItem[]>([]);
3035 let shortcuts = $state<Shortcut[]>([]);
@@ -33,6 +38,8 @@
3338 let topVisitedLoading = $state(true);
3439 let shortcutsLoading = $state(true);
3540 let statsLoading = $state(true);
41+ let searchShortcuts = $state<Record<string, SearchShortcut>>({});
42+ let searchShortcutsLoading = $state(true);
3643 let deletingIds = $state<number[]>([]);
3744 let historyOffset = $state(0);
3845 let hasMoreHistory = $state(true);
@@ -622,6 +629,49 @@
622629 }
623630 };
624631
632+ // Fetch search shortcuts via message bridge
633+ const fetchSearchShortcuts = async () => {
634+ try {
635+ // Set up response handler
636+ (window as any).__dumber_search_shortcuts = (data: unknown) => {
637+ try {
638+ if (typeof data !== 'object' || data === null) {
639+ throw new Error('Invalid shortcuts data');
640+ }
641+ const dataObj = data as Record<string, unknown>;
642+ const normalized: Record<string, SearchShortcut> = {};
643+ for (const [key, value] of Object.entries(dataObj)) {
644+ const v = value as Record<string, unknown>;
645+ normalized[key] = {
646+ url: (v.url ?? v.URL ?? '') as string,
647+ description: (v.description ?? v.Description ?? '') as string,
648+ };
649+ }
650+ searchShortcuts = normalized;
651+ searchShortcutsLoading = false;
652+ } catch (error) {
653+ console.error('Failed to process search shortcuts:', error);
654+ searchShortcuts = {};
655+ searchShortcutsLoading = false;
656+ }
657+ };
658+
659+ // Send message to Go backend
660+ const bridge = (window as any).webkit?.messageHandlers?.dumber;
661+ if (bridge && typeof bridge.postMessage === 'function') {
662+ bridge.postMessage(JSON.stringify({
663+ type: 'get_search_shortcuts'
664+ }));
665+ } else {
666+ throw new Error('WebKit message handler not available');
667+ }
668+ } catch (error) {
669+ console.error('Error fetching search shortcuts:', error);
670+ searchShortcuts = {};
671+ searchShortcutsLoading = false;
672+ }
673+ };
674+
625675 // Intersection Observer for infinite scroll
626676 let historyScrollSentinel = $state<HTMLElement | null>(null);
627677
@@ -649,16 +699,26 @@
649699 // Helper to check if text overflows and apply truncation class
650700 const checkOverflow = (node: HTMLElement) => {
651701 const check = () => {
652- if (node.scrollWidth > node.clientWidth) {
702+ // Add 1px tolerance for sub-pixel rendering differences
703+ // This prevents false positives from rounding errors
704+ const isOverflowing = node.scrollWidth > (node.clientWidth + 1);
705+
706+ if (isOverflowing) {
653707 node.classList.add('truncated');
654708 } else {
655709 node.classList.remove('truncated');
656710 }
657711 };
658712
659- check();
713+ // Delay initial check to ensure layout is complete
714+ requestAnimationFrame(() => {
715+ requestAnimationFrame(check);
716+ });
717+
718+ const resizeObserver = new ResizeObserver(() => {
719+ requestAnimationFrame(check);
720+ });
660721
661- const resizeObserver = new ResizeObserver(check);
662722 resizeObserver.observe(node);
663723
664724 return {
@@ -677,6 +737,7 @@
677737 });
678738 fetchStats();
679739 fetchTopVisited();
740+ fetchSearchShortcuts();
680741
681742 // Theme synchronization
682743 const syncThemeState = () => {
@@ -703,6 +764,24 @@
703764 <svelte:head>
704765 <title>Dumber Browser - Homepage</title>
705766 <meta name="description" content="Fast Wayland Browser - Your browsing patterns at a glance" />
767+
768+ {@html `<style>
769+ html { background: #0a0a0a; }
770+ body { background: #0a0a0a; color: #e5e5e5; }
771+ html.light { background: #ffffff; }
772+ html.light body { background: #ffffff; color: #171717; }
773+ </style>`}
774+
775+ {@html `<script>
776+ (function() {
777+ const theme = localStorage.getItem('dumber.theme');
778+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
779+ const shouldBeLight = theme === 'light' || (theme !== 'dark' && !prefersDark);
780+ if (shouldBeLight) {
781+ document.documentElement.classList.add('light');
782+ }
783+ })();
784+ </script>`}
706785 </svelte:head>
707786
708787 <div class="homepage-shell">
@@ -1020,67 +1099,91 @@
10201099 </section>
10211100 </div>
10221101
1023- <div class="terminal-grid supporting-panels">
1024- <section class="stats-panel terminal-pane">
1025- <div class="panel-header">
1026- <h2 class="panel-title">Usage Stats</h2>
1027- <p class="panel-subtitle">Your browsing activity at a glance.</p>
1028- </div>
1029- <div class="panel-body">
1030- {#if statsLoading}
1031- <div class="loading">Loading stats...</div>
1032- {:else if stats}
1033- <div class="stats-grid-horizontal">
1034- <div class="stat-item-horizontal">
1035- <div class="stat-value">{stats.total_entries}</div>
1036- <div class="stat-label">Entries Stored</div>
1102+ <section class="search-shortcuts-panel terminal-pane full-width">
1103+ <div class="panel-header">
1104+ <h2 class="panel-title">Search Shortcuts</h2>
1105+ <p class="panel-subtitle">Quick search prefixes for faster browsing.</p>
1106+ </div>
1107+ <div class="panel-body">
1108+ {#if searchShortcutsLoading}
1109+ <div class="loading">Loading search shortcuts...</div>
1110+ {:else if Object.keys(searchShortcuts).length === 0}
1111+ <div class="empty-state compact">
1112+ <h3>No shortcuts configured</h3>
1113+ <p>Add search shortcuts to your config file.</p>
1114+ </div>
1115+ {:else}
1116+ <div class="shortcuts-list">
1117+ {#each Object.entries(searchShortcuts) as [key, shortcut] (key)}
1118+ <div class="shortcut-item">
1119+ <div class="shortcut-key-badge">{key}:</div>
1120+ <div class="shortcut-desc">{shortcut.description.toLowerCase()}</div>
10371121 </div>
1038- <div class="stat-item-horizontal">
1039- <div class="stat-value">{stats.recent_count}</div>
1040- <div class="stat-label">Recent Window</div>
1122+ {/each}
1123+ </div>
1124+ {/if}
1125+ </div>
1126+ </section>
1127+
1128+ <section class="shortcuts-panel terminal-pane full-width">
1129+ <div class="panel-header">
1130+ <h2 class="panel-title">Keyboard Shortcuts</h2>
1131+ <p class="panel-subtitle">Stay quick with the essentials.</p>
1132+ </div>
1133+ <div class="panel-body">
1134+ {#if shortcutsLoading}
1135+ <div class="loading">Loading shortcuts...</div>
1136+ {:else}
1137+ <div class="shortcuts-list">
1138+ {#each shortcuts as shortcut (shortcut.key)}
1139+ <div class="shortcut-item">
1140+ <div class="shortcut-key-badge">{shortcut.key}</div>
1141+ <div class="shortcut-desc">{shortcut.description}</div>
10411142 </div>
1042- {#if stats.newest_entry}
1043- <div class="stat-item-horizontal">
1044- <div class="stat-value">{formatTime(stats.newest_entry)}</div>
1045- <div class="stat-label">Newest Visit</div>
1046- </div>
1047- {/if}
1048- {#if stats.oldest_entry}
1049- <div class="stat-item-horizontal">
1050- <div class="stat-value">{formatCalendarDate(stats.oldest_entry)}</div>
1051- <div class="stat-label">Oldest Entry</div>
1052- </div>
1053- {/if}
1054- </div>
1055- {:else}
1056- <div class="empty-state compact">
1057- <p>No statistics available</p>
1058- </div>
1059- {/if}
1060- </div>
1061- </section>
1143+ {/each}
1144+ </div>
1145+ {/if}
1146+ </div>
1147+ </section>
10621148
1063- <section class="shortcuts-panel terminal-pane">
1064- <div class="panel-header">
1065- <h2 class="panel-title">Keyboard Shortcuts</h2>
1066- <p class="panel-subtitle">Stay quick with the essentials.</p>
1067- </div>
1068- <div class="panel-body">
1069- {#if shortcutsLoading}
1070- <div class="loading">Loading shortcuts...</div>
1071- {:else}
1072- <div class="shortcuts-list">
1073- {#each shortcuts as shortcut (shortcut.key)}
1074- <div class="shortcut-item">
1075- <div class="shortcut-key-badge">{shortcut.key}</div>
1076- <div class="shortcut-desc">{shortcut.description}</div>
1077- </div>
1078- {/each}
1149+ <section class="stats-panel terminal-pane full-width">
1150+ <div class="panel-header">
1151+ <h2 class="panel-title">Usage Stats</h2>
1152+ <p class="panel-subtitle">Your browsing activity at a glance.</p>
1153+ </div>
1154+ <div class="panel-body">
1155+ {#if statsLoading}
1156+ <div class="loading">Loading stats...</div>
1157+ {:else if stats}
1158+ <div class="stats-grid-horizontal">
1159+ <div class="stat-item-horizontal">
1160+ <div class="stat-value">{stats.total_entries}</div>
1161+ <div class="stat-label">Entries Stored</div>
10791162 </div>
1080- {/if}
1081- </div>
1082- </section>
1083- </div>
1163+ <div class="stat-item-horizontal">
1164+ <div class="stat-value">{stats.recent_count}</div>
1165+ <div class="stat-label">Recent Window</div>
1166+ </div>
1167+ {#if stats.newest_entry}
1168+ <div class="stat-item-horizontal">
1169+ <div class="stat-value">{formatTime(stats.newest_entry)}</div>
1170+ <div class="stat-label">Newest Visit</div>
1171+ </div>
1172+ {/if}
1173+ {#if stats.oldest_entry}
1174+ <div class="stat-item-horizontal">
1175+ <div class="stat-value">{formatCalendarDate(stats.oldest_entry)}</div>
1176+ <div class="stat-label">Oldest Entry</div>
1177+ </div>
1178+ {/if}
1179+ </div>
1180+ {:else}
1181+ <div class="empty-state compact">
1182+ <p>No statistics available</p>
1183+ </div>
1184+ {/if}
1185+ </div>
1186+ </section>
10841187 </div>
10851188 </div>
10861189
@@ -1303,10 +1406,6 @@
13031406 grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
13041407 }
13051408
1306-.supporting-panels {
1307- grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
1308-}
1309-
13101409 .terminal-pane {
13111410 border: 1px solid var(--dynamic-border);
13121411 background: color-mix(in srgb, var(--dynamic-bg) 88%, var(--dynamic-surface) 12%);
@@ -1316,6 +1415,11 @@
13161415 box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--dynamic-border) 18%, transparent);
13171416 }
13181417
1418+.terminal-pane.full-width {
1419+ width: 100%;
1420+ grid-column: 1 / -1;
1421+}
1422+
13191423 .panel-header {
13201424 display: flex;
13211425 flex-direction: column;
@@ -1741,6 +1845,7 @@
17411845
17421846 .shortcuts-list {
17431847 display: grid;
1848+ grid-template-columns: repeat(2, 1fr);
17441849 gap: 0.6rem;
17451850 }
17461851
@@ -1829,5 +1934,9 @@
18291934 .banner-absolute {
18301935 justify-self: flex-start;
18311936 }
1937+
1938+ .shortcuts-list {
1939+ grid-template-columns: 1fr;
1940+ }
18321941 }
18331942 </style>
@@ -25,6 +25,11 @@
25 newest_entry?: string;25 newest_entry?: string;
26 }26 }
27 27
28+ interface SearchShortcut {
29+ url: string;
30+ description: string;
31+ }
32+
28 let historyItems = $state<HistoryItem[]>([]);33 let historyItems = $state<HistoryItem[]>([]);
29 let topVisited = $state<HistoryItem[]>([]);34 let topVisited = $state<HistoryItem[]>([]);
30 let shortcuts = $state<Shortcut[]>([]);35 let shortcuts = $state<Shortcut[]>([]);
@@ -33,6 +38,8 @@
33 let topVisitedLoading = $state(true);38 let topVisitedLoading = $state(true);
34 let shortcutsLoading = $state(true);39 let shortcutsLoading = $state(true);
35 let statsLoading = $state(true);40 let statsLoading = $state(true);
41+ let searchShortcuts = $state<Record<string, SearchShortcut>>({});
42+ let searchShortcutsLoading = $state(true);
36 let deletingIds = $state<number[]>([]);43 let deletingIds = $state<number[]>([]);
37 let historyOffset = $state(0);44 let historyOffset = $state(0);
38 let hasMoreHistory = $state(true);45 let hasMoreHistory = $state(true);
@@ -622,6 +629,49 @@
622 }629 }
623 };630 };
624 631
632+ // Fetch search shortcuts via message bridge
633+ const fetchSearchShortcuts = async () => {
634+ try {
635+ // Set up response handler
636+ (window as any).__dumber_search_shortcuts = (data: unknown) => {
637+ try {
638+ if (typeof data !== 'object' || data === null) {
639+ throw new Error('Invalid shortcuts data');
640+ }
641+ const dataObj = data as Record<string, unknown>;
642+ const normalized: Record<string, SearchShortcut> = {};
643+ for (const [key, value] of Object.entries(dataObj)) {
644+ const v = value as Record<string, unknown>;
645+ normalized[key] = {
646+ url: (v.url ?? v.URL ?? '') as string,
647+ description: (v.description ?? v.Description ?? '') as string,
648+ };
649+ }
650+ searchShortcuts = normalized;
651+ searchShortcutsLoading = false;
652+ } catch (error) {
653+ console.error('Failed to process search shortcuts:', error);
654+ searchShortcuts = {};
655+ searchShortcutsLoading = false;
656+ }
657+ };
658+
659+ // Send message to Go backend
660+ const bridge = (window as any).webkit?.messageHandlers?.dumber;
661+ if (bridge && typeof bridge.postMessage === 'function') {
662+ bridge.postMessage(JSON.stringify({
663+ type: 'get_search_shortcuts'
664+ }));
665+ } else {
666+ throw new Error('WebKit message handler not available');
667+ }
668+ } catch (error) {
669+ console.error('Error fetching search shortcuts:', error);
670+ searchShortcuts = {};
671+ searchShortcutsLoading = false;
672+ }
673+ };
674+
625 // Intersection Observer for infinite scroll675 // Intersection Observer for infinite scroll
626 let historyScrollSentinel = $state<HTMLElement | null>(null);676 let historyScrollSentinel = $state<HTMLElement | null>(null);
627 677
@@ -649,16 +699,26 @@
649 // Helper to check if text overflows and apply truncation class699 // Helper to check if text overflows and apply truncation class
650 const checkOverflow = (node: HTMLElement) => {700 const checkOverflow = (node: HTMLElement) => {
651 const check = () => {701 const check = () => {
652- if (node.scrollWidth > node.clientWidth) {702+ // Add 1px tolerance for sub-pixel rendering differences
703+ // This prevents false positives from rounding errors
704+ const isOverflowing = node.scrollWidth > (node.clientWidth + 1);
705+
706+ if (isOverflowing) {
653 node.classList.add('truncated');707 node.classList.add('truncated');
654 } else {708 } else {
655 node.classList.remove('truncated');709 node.classList.remove('truncated');
656 }710 }
657 };711 };
658 712
659- check();713+ // Delay initial check to ensure layout is complete
714+ requestAnimationFrame(() => {
715+ requestAnimationFrame(check);
716+ });
717+
718+ const resizeObserver = new ResizeObserver(() => {
719+ requestAnimationFrame(check);
720+ });
660 721
661- const resizeObserver = new ResizeObserver(check);
662 resizeObserver.observe(node);722 resizeObserver.observe(node);
663 723
664 return {724 return {
@@ -677,6 +737,7 @@
677 });737 });
678 fetchStats();738 fetchStats();
679 fetchTopVisited();739 fetchTopVisited();
740+ fetchSearchShortcuts();
680 741
681 // Theme synchronization742 // Theme synchronization
682 const syncThemeState = () => {743 const syncThemeState = () => {
@@ -703,6 +764,24 @@
703 <svelte:head>764 <svelte:head>
704 <title>Dumber Browser - Homepage</title>765 <title>Dumber Browser - Homepage</title>
705 <meta name="description" content="Fast Wayland Browser - Your browsing patterns at a glance" />766 <meta name="description" content="Fast Wayland Browser - Your browsing patterns at a glance" />
767+
768+ {@html `<style>
769+ html { background: #0a0a0a; }
770+ body { background: #0a0a0a; color: #e5e5e5; }
771+ html.light { background: #ffffff; }
772+ html.light body { background: #ffffff; color: #171717; }
773+ </style>`}
774+
775+ {@html `<script>
776+ (function() {
777+ const theme = localStorage.getItem('dumber.theme');
778+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
779+ const shouldBeLight = theme === 'light' || (theme !== 'dark' && !prefersDark);
780+ if (shouldBeLight) {
781+ document.documentElement.classList.add('light');
782+ }
783+ })();
784+ </script>`}
706 </svelte:head>785 </svelte:head>
707 786
708 <div class="homepage-shell">787 <div class="homepage-shell">
@@ -1020,67 +1099,91 @@
1020 </section>1099 </section>
1021 </div>1100 </div>
1022 1101
1023- <div class="terminal-grid supporting-panels">1102+ <section class="search-shortcuts-panel terminal-pane full-width">
1024- <section class="stats-panel terminal-pane">1103+ <div class="panel-header">
1025- <div class="panel-header">1104+ <h2 class="panel-title">Search Shortcuts</h2>
1026- <h2 class="panel-title">Usage Stats</h2>1105+ <p class="panel-subtitle">Quick search prefixes for faster browsing.</p>
1027- <p class="panel-subtitle">Your browsing activity at a glance.</p>1106+ </div>
1028- </div>1107+ <div class="panel-body">
1029- <div class="panel-body">1108+ {#if searchShortcutsLoading}
1030- {#if statsLoading}1109+ <div class="loading">Loading search shortcuts...</div>
1031- <div class="loading">Loading stats...</div>1110+ {:else if Object.keys(searchShortcuts).length === 0}
1032- {:else if stats}1111+ <div class="empty-state compact">
1033- <div class="stats-grid-horizontal">1112+ <h3>No shortcuts configured</h3>
1034- <div class="stat-item-horizontal">1113+ <p>Add search shortcuts to your config file.</p>
1035- <div class="stat-value">{stats.total_entries}</div>1114+ </div>
1036- <div class="stat-label">Entries Stored</div>1115+ {:else}
1116+ <div class="shortcuts-list">
1117+ {#each Object.entries(searchShortcuts) as [key, shortcut] (key)}
1118+ <div class="shortcut-item">
1119+ <div class="shortcut-key-badge">{key}:</div>
1120+ <div class="shortcut-desc">{shortcut.description.toLowerCase()}</div>
1037 </div>1121 </div>
1038- <div class="stat-item-horizontal">1122+ {/each}
1039- <div class="stat-value">{stats.recent_count}</div>1123+ </div>
1040- <div class="stat-label">Recent Window</div>1124+ {/if}
1125+ </div>
1126+ </section>
1127+
1128+ <section class="shortcuts-panel terminal-pane full-width">
1129+ <div class="panel-header">
1130+ <h2 class="panel-title">Keyboard Shortcuts</h2>
1131+ <p class="panel-subtitle">Stay quick with the essentials.</p>
1132+ </div>
1133+ <div class="panel-body">
1134+ {#if shortcutsLoading}
1135+ <div class="loading">Loading shortcuts...</div>
1136+ {:else}
1137+ <div class="shortcuts-list">
1138+ {#each shortcuts as shortcut (shortcut.key)}
1139+ <div class="shortcut-item">
1140+ <div class="shortcut-key-badge">{shortcut.key}</div>
1141+ <div class="shortcut-desc">{shortcut.description}</div>
1041 </div>1142 </div>
1042- {#if stats.newest_entry}1143+ {/each}
1043- <div class="stat-item-horizontal">1144+ </div>
1044- <div class="stat-value">{formatTime(stats.newest_entry)}</div>1145+ {/if}
1045- <div class="stat-label">Newest Visit</div>1146+ </div>
1046- </div>1147+ </section>
1047- {/if}
1048- {#if stats.oldest_entry}
1049- <div class="stat-item-horizontal">
1050- <div class="stat-value">{formatCalendarDate(stats.oldest_entry)}</div>
1051- <div class="stat-label">Oldest Entry</div>
1052- </div>
1053- {/if}
1054- </div>
1055- {:else}
1056- <div class="empty-state compact">
1057- <p>No statistics available</p>
1058- </div>
1059- {/if}
1060- </div>
1061- </section>
1062 1148
1063- <section class="shortcuts-panel terminal-pane">1149+ <section class="stats-panel terminal-pane full-width">
1064- <div class="panel-header">1150+ <div class="panel-header">
1065- <h2 class="panel-title">Keyboard Shortcuts</h2>1151+ <h2 class="panel-title">Usage Stats</h2>
1066- <p class="panel-subtitle">Stay quick with the essentials.</p>1152+ <p class="panel-subtitle">Your browsing activity at a glance.</p>
1067- </div>1153+ </div>
1068- <div class="panel-body">1154+ <div class="panel-body">
1069- {#if shortcutsLoading}1155+ {#if statsLoading}
1070- <div class="loading">Loading shortcuts...</div>1156+ <div class="loading">Loading stats...</div>
1071- {:else}1157+ {:else if stats}
1072- <div class="shortcuts-list">1158+ <div class="stats-grid-horizontal">
1073- {#each shortcuts as shortcut (shortcut.key)}1159+ <div class="stat-item-horizontal">
1074- <div class="shortcut-item">1160+ <div class="stat-value">{stats.total_entries}</div>
1075- <div class="shortcut-key-badge">{shortcut.key}</div>1161+ <div class="stat-label">Entries Stored</div>
1076- <div class="shortcut-desc">{shortcut.description}</div>
1077- </div>
1078- {/each}
1079 </div>1162 </div>
1080- {/if}1163+ <div class="stat-item-horizontal">
1081- </div>1164+ <div class="stat-value">{stats.recent_count}</div>
1082- </section>1165+ <div class="stat-label">Recent Window</div>
1083- </div>1166+ </div>
1167+ {#if stats.newest_entry}
1168+ <div class="stat-item-horizontal">
1169+ <div class="stat-value">{formatTime(stats.newest_entry)}</div>
1170+ <div class="stat-label">Newest Visit</div>
1171+ </div>
1172+ {/if}
1173+ {#if stats.oldest_entry}
1174+ <div class="stat-item-horizontal">
1175+ <div class="stat-value">{formatCalendarDate(stats.oldest_entry)}</div>
1176+ <div class="stat-label">Oldest Entry</div>
1177+ </div>
1178+ {/if}
1179+ </div>
1180+ {:else}
1181+ <div class="empty-state compact">
1182+ <p>No statistics available</p>
1183+ </div>
1184+ {/if}
1185+ </div>
1186+ </section>
1084 </div>1187 </div>
1085 </div>1188 </div>
1086 1189
@@ -1303,10 +1406,6 @@
1303 grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);1406 grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
1304 }1407 }
1305 1408
1306-.supporting-panels {
1307- grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
1308-}
1309-
1310 .terminal-pane {1409 .terminal-pane {
1311 border: 1px solid var(--dynamic-border);1410 border: 1px solid var(--dynamic-border);
1312 background: color-mix(in srgb, var(--dynamic-bg) 88%, var(--dynamic-surface) 12%);1411 background: color-mix(in srgb, var(--dynamic-bg) 88%, var(--dynamic-surface) 12%);
@@ -1316,6 +1415,11 @@
1316 box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--dynamic-border) 18%, transparent);1415 box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--dynamic-border) 18%, transparent);
1317 }1416 }
1318 1417
1418+.terminal-pane.full-width {
1419+ width: 100%;
1420+ grid-column: 1 / -1;
1421+}
1422+
1319 .panel-header {1423 .panel-header {
1320 display: flex;1424 display: flex;
1321 flex-direction: column;1425 flex-direction: column;
@@ -1741,6 +1845,7 @@
1741 1845
1742 .shortcuts-list {1846 .shortcuts-list {
1743 display: grid;1847 display: grid;
1848+ grid-template-columns: repeat(2, 1fr);
1744 gap: 0.6rem;1849 gap: 0.6rem;
1745 }1850 }
1746 1851
@@ -1829,5 +1934,9 @@
1829 .banner-absolute {1934 .banner-absolute {
1830 justify-self: flex-start;1935 justify-self: flex-start;
1831 }1936 }
1937+
1938+ .shortcuts-list {
1939+ grid-template-columns: 1fr;
1940+ }
1832 }1941 }
1833 </style>1942 </style>

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