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 @@ | |||
| 25 | 25 | newest_entry?: string; | |
| 26 | 26 | } | |
| 27 | 27 | ||
| 28 | + | interface SearchShortcut { | |
| 29 | + | url: string; | |
| 30 | + | description: string; | |
| 31 | + | } | |
| 32 | + | ||
| 28 | 33 | let historyItems = $state<HistoryItem[]>([]); | |
| 29 | 34 | let topVisited = $state<HistoryItem[]>([]); | |
| 30 | 35 | let shortcuts = $state<Shortcut[]>([]); | |
| @@ -33,6 +38,8 @@ | |||
| 33 | 38 | let topVisitedLoading = $state(true); | |
| 34 | 39 | let shortcutsLoading = $state(true); | |
| 35 | 40 | let statsLoading = $state(true); | |
| 41 | + | let searchShortcuts = $state<Record<string, SearchShortcut>>({}); | |
| 42 | + | let searchShortcutsLoading = $state(true); | |
| 36 | 43 | let deletingIds = $state<number[]>([]); | |
| 37 | 44 | let historyOffset = $state(0); | |
| 38 | 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 | 675 | // Intersection Observer for infinite scroll | |
| 626 | 676 | let historyScrollSentinel = $state<HTMLElement | null>(null); | |
| 627 | 677 | ||
| @@ -649,16 +699,26 @@ | |||
| 649 | 699 | // Helper to check if text overflows and apply truncation class | |
| 650 | 700 | const checkOverflow = (node: HTMLElement) => { | |
| 651 | 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 | 707 | node.classList.add('truncated'); | |
| 654 | 708 | } else { | |
| 655 | 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 | 722 | resizeObserver.observe(node); | |
| 663 | 723 | ||
| 664 | 724 | return { | |
| @@ -677,6 +737,7 @@ | |||
| 677 | 737 | }); | |
| 678 | 738 | fetchStats(); | |
| 679 | 739 | fetchTopVisited(); | |
| 740 | + | fetchSearchShortcuts(); | |
| 680 | 741 | ||
| 681 | 742 | // Theme synchronization | |
| 682 | 743 | const syncThemeState = () => { | |
| @@ -703,6 +764,24 @@ | |||
| 703 | 764 | <svelte:head> | |
| 704 | 765 | <title>Dumber Browser - Homepage</title> | |
| 705 | 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 | 785 | </svelte:head> | |
| 707 | 786 | ||
| 708 | 787 | <div class="homepage-shell"> | |
| @@ -1020,67 +1099,91 @@ | |||
| 1020 | 1099 | </section> | |
| 1021 | 1100 | </div> | |
| 1022 | 1101 | ||
| 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> | |
| 1037 | 1121 | </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> | |
| 1041 | 1142 | </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> | |
| 1062 | 1148 | ||
| 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> | |
| 1079 | 1162 | </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> | |
| 1084 | 1187 | </div> | |
| 1085 | 1188 | </div> | |
| 1086 | 1189 | ||
| @@ -1303,10 +1406,6 @@ | |||
| 1303 | 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 | 1409 | .terminal-pane { | |
| 1311 | 1410 | border: 1px solid var(--dynamic-border); | |
| 1312 | 1411 | background: color-mix(in srgb, var(--dynamic-bg) 88%, var(--dynamic-surface) 12%); | |
| @@ -1316,6 +1415,11 @@ | |||
| 1316 | 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 | 1423 | .panel-header { | |
| 1320 | 1424 | display: flex; | |
| 1321 | 1425 | flex-direction: column; | |
| @@ -1741,6 +1845,7 @@ | |||
| 1741 | 1845 | ||
| 1742 | 1846 | .shortcuts-list { | |
| 1743 | 1847 | display: grid; | |
| 1848 | + | grid-template-columns: repeat(2, 1fr); | |
| 1744 | 1849 | gap: 0.6rem; | |
| 1745 | 1850 | } | |
| 1746 | 1851 | ||
| @@ -1829,5 +1934,9 @@ | |||
| 1829 | 1934 | .banner-absolute { | |
| 1830 | 1935 | justify-self: flex-start; | |
| 1831 | 1936 | } | |
| 1937 | + | ||
| 1938 | + | .shortcuts-list { | |
| 1939 | + | grid-template-columns: 1fr; | |
| 1940 | + | } | |
| 1832 | 1941 | } | |
| 1833 | 1942 | </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 scroll | 675 | // 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 class | 699 | // 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 synchronization | 742 | // 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> | ||