1<script lang="ts">
2 import { onMount } from 'svelte';
3 import Footer from '$components/Footer.svelte';
4 // Icons disabled for investigation: removed @lucide/svelte imports
5
6 interface HistoryItem {
7 id: number;
8 url: string;
9 title: string;
10 favicon_url?: string;
11 visit_count: number;
12 last_visited: string;
13 created_at: string;
14 }
15
16 interface Shortcut {
17 key: string;
18 description: string;
19 action: () => void;
20 }
21
22 interface HistoryStats {
23 total_entries: number;
24 recent_count: number;
25 oldest_entry?: string;
26 newest_entry?: string;
27 }
28
29 let historyItems = $state<HistoryItem[]>([]);
30 let topVisited = $state<HistoryItem[]>([]);
31 let shortcuts = $state<Shortcut[]>([]);
32 let stats = $state<HistoryStats | null>(null);
33 let historyLoading = $state(true);
34 let topVisitedLoading = $state(true);
35 let shortcutsLoading = $state(true);
36 let statsLoading = $state(true);
37 let deletingIds = $state<number[]>([]);
38 let historyOffset = $state(0);
39 let hasMoreHistory = $state(true);
40 let loadingMoreHistory = $state(false);
41 const MAX_QUICK_ACCESS_ITEMS = 9;
42 type ThemeMode = "light" | "dark";
43 const THEME_STORAGE_KEY = "dumber.theme";
44 let themeMode = $state<ThemeMode>("dark");
45 let themeObserver: MutationObserver | null = null;
46 let historyHeaderRef = $state<HTMLElement | null>(null);
47 let historyBodyRef = $state<HTMLElement | null>(null);
48 let insightsPanelRef = $state<HTMLElement | null>(null);
49 let resizeSyncRaf = 0;
50
51 const syncHistoryPanelSize = () => {
52 if (!historyBodyRef || !historyHeaderRef || !insightsPanelRef) return;
53
54 if (window.innerWidth <= 960) {
55 historyBodyRef.style.maxHeight = "";
56 return;
57 }
58
59 const insightsHeight = insightsPanelRef.getBoundingClientRect().height;
60 if (!insightsHeight || Number.isNaN(insightsHeight)) {
61 historyBodyRef.style.maxHeight = "";
62 return;
63 }
64
65 const headerHeight = historyHeaderRef.getBoundingClientRect().height;
66 const bodyStyles = getComputedStyle(historyBodyRef);
67 const paddingTop = parseFloat(bodyStyles.paddingTop) || 0;
68 const paddingBottom = parseFloat(bodyStyles.paddingBottom) || 0;
69 const maxHeight = insightsHeight - headerHeight - paddingTop - paddingBottom;
70
71 if (maxHeight > 0) {
72 historyBodyRef.style.maxHeight = `${maxHeight}px`;
73 } else {
74 historyBodyRef.style.maxHeight = "";
75 }
76 };
77
78 const scheduleHistoryPanelSync = () => {
79 if (resizeSyncRaf) cancelAnimationFrame(resizeSyncRaf);
80 resizeSyncRaf = requestAnimationFrame(() => {
81 resizeSyncRaf = 0;
82 syncHistoryPanelSize();
83 });
84 };
85
86 // Function for checking if an item is being deleted
87 const isDeleting = (id: number) => deletingIds.includes(id);
88
89 // Initialize shortcuts (these are browser shortcuts, not dynamic from API)
90 const initializeShortcuts = () => {
91 shortcuts = [
92 // Browser shortcuts
93 {
94 key: 'Ctrl+L',
95 description: 'Focus address bar',
96 action: () => console.log('Ctrl+L triggered')
97 },
98 {
99 key: 'Ctrl+F',
100 description: 'Find in page',
101 action: () => console.log('Ctrl+F triggered')
102 },
103 {
104 key: 'Ctrl+Shift+C',
105 description: 'Copy URL',
106 action: () => console.log('Ctrl+Shift+C triggered')
107 },
108 {
109 key: 'Ctrl+R / F5',
110 description: 'Reload page',
111 action: () => window.location.reload()
112 },
113 {
114 key: 'Ctrl+Shift+R',
115 description: 'Hard reload',
116 action: () => console.log('Ctrl+Shift+R triggered')
117 },
118 {
119 key: 'F12',
120 description: 'DevTools',
121 action: () => console.log('F12 triggered')
122 },
123 {
124 key: 'Ctrl+← / Ctrl+→',
125 description: 'Navigate back/forward',
126 action: () => console.log('Ctrl+Arrow triggered')
127 },
128 {
129 key: 'Ctrl+0',
130 description: 'Reset zoom',
131 action: () => console.log('Ctrl+0 triggered')
132 },
133 {
134 key: 'Ctrl++ / Ctrl+-',
135 description: 'Zoom in/out',
136 action: () => console.log('Ctrl+Zoom triggered')
137 },
138 // Workspace pane mode
139 {
140 key: 'Ctrl+P',
141 description: 'Enter pane mode',
142 action: () => console.log('Ctrl+P triggered')
143 },
144 {
145 key: '→ / R (pane mode)',
146 description: 'Split pane right',
147 action: () => console.log('Split right triggered')
148 },
149 {
150 key: '← / L (pane mode)',
151 description: 'Split pane left',
152 action: () => console.log('Split left triggered')
153 },
154 {
155 key: '↑ / U (pane mode)',
156 description: 'Split pane up',
157 action: () => console.log('Split up triggered')
158 },
159 {
160 key: '↓ / D (pane mode)',
161 description: 'Split pane down',
162 action: () => console.log('Split down triggered')
163 },
164 {
165 key: 'X (pane mode)',
166 description: 'Close current pane',
167 action: () => console.log('Close pane triggered')
168 },
169 {
170 key: 'Enter (pane mode)',
171 description: 'Confirm action',
172 action: () => console.log('Confirm triggered')
173 },
174 {
175 key: 'Escape (pane mode)',
176 description: 'Exit pane mode',
177 action: () => console.log('Exit pane mode triggered')
178 },
179 // Workspace navigation
180 {
181 key: 'Alt+Arrow Keys',
182 description: 'Navigate between panes',
183 action: () => console.log('Navigate panes triggered')
184 }
185 ];
186 shortcutsLoading = false;
187 };
188
189 // Setup message bridge callbacks (once)
190 let callbacksInitialized = false;
191 const pendingRequests = new Map();
192
193 const initializeCallbacks = () => {
194 if (callbacksInitialized) return;
195
196 (window as any).__dumber_history_recent = (data: any[], requestId?: string) => {
197 const request = pendingRequests.get(requestId || 'default');
198 if (request) {
199 request.resolve(data);
200 pendingRequests.delete(requestId || 'default');
201 }
202 };
203
204 (window as any).__dumber_history_error = (error: string, requestId?: string) => {
205 const request = pendingRequests.get(requestId || 'default');
206 if (request) {
207 request.reject(new Error(error));
208 pendingRequests.delete(requestId || 'default');
209 }
210 };
211
212 // Omnibox suggestions callback - forward to injected script via DOM event bridge
213 let _pendingSuggestions: any[] | null = null;
214 (window as any).__dumber_omnibox_suggestions = (suggestions: any[]) => {
215 try {
216 console.log('🎯 [HOMEPAGE] Received omnibox suggestions:', suggestions?.length || 0, 'items');
217 // Dispatch an event that injected-world listener can consume
218 const evt = new CustomEvent('dumber:omnibox-suggestions', {
219 detail: { suggestions }
220 });
221 document.dispatchEvent(evt);
222 _pendingSuggestions = null; // clear cache on successful dispatch
223 } catch (err) {
224 console.warn('⚠️ [HOMEPAGE] Failed to dispatch suggestions event, caching:', err);
225 _pendingSuggestions = suggestions;
226 }
227 };
228
229 // If omnibox gets ready later, re-send any pending suggestions
230 document.addEventListener('dumber:omnibox-ready', () => {
231 if (_pendingSuggestions && Array.isArray(_pendingSuggestions)) {
232 try {
233 const evt = new CustomEvent('dumber:omnibox-suggestions', {
234 detail: { suggestions: _pendingSuggestions }
235 });
236 document.dispatchEvent(evt);
237 _pendingSuggestions = null;
238 console.log('🔄 [HOMEPAGE] Re-dispatched pending suggestions after omnibox-ready');
239 } catch (e) {
240 console.warn('⚠️ [HOMEPAGE] Failed to re-dispatch pending suggestions:', e);
241 }
242 }
243 });
244
245 callbacksInitialized = true;
246 };
247
248 const sendHistoryRequest = (type: string, params: any): Promise<any[]> => {
249 initializeCallbacks();
250
251 const requestId = `${type}_${Date.now()}_${Math.random()}`;
252
253 return new Promise((resolve, reject) => {
254 pendingRequests.set(requestId, { resolve, reject });
255
256 // Timeout after 10 seconds
257 setTimeout(() => {
258 if (pendingRequests.has(requestId)) {
259 pendingRequests.delete(requestId);
260 reject(new Error('Request timeout'));
261 }
262 }, 10000);
263
264 try {
265 const bridge = (window as any).webkit?.messageHandlers?.dumber;
266 if (bridge && typeof bridge.postMessage === 'function') {
267 bridge.postMessage(JSON.stringify({
268 ...params,
269 type,
270 requestId
271 }));
272 } else {
273 reject(new Error('WebKit message handler not available'));
274 }
275 } catch (error) {
276 pendingRequests.delete(requestId);
277 reject(error);
278 }
279 });
280 };
281
282 // Fetch recent history via message bridge
283 const fetchHistory = async () => {
284 try {
285 const data = await sendHistoryRequest('history_recent', {
286 limit: 20,
287 offset: 0
288 });
289
290 historyItems = Array.isArray(data) ? data : [];
291 historyOffset = historyItems.length;
292 hasMoreHistory = data.length === 20;
293 } catch (error) {
294 console.error('Error fetching history:', error);
295 historyItems = [];
296 hasMoreHistory = false;
297 } finally {
298 historyLoading = false;
299 }
300 };
301
302 // Load more history items for infinite scroll
303 const loadMoreHistory = async () => {
304 if (loadingMoreHistory || !hasMoreHistory) return;
305
306 loadingMoreHistory = true;
307 try {
308 const data = await sendHistoryRequest('history_recent', {
309 limit: 20,
310 offset: historyOffset
311 });
312
313 if (Array.isArray(data) && data.length > 0) {
314 historyItems = [...historyItems, ...data];
315 historyOffset += data.length;
316 hasMoreHistory = data.length === 20; // If we got full batch, there might be more
317 } else {
318 hasMoreHistory = false;
319 }
320 } catch (error) {
321 console.error('Error fetching more history:', error);
322 hasMoreHistory = false;
323 } finally {
324 loadingMoreHistory = false;
325 }
326 };
327
328 // Navigate to a URL when history item is clicked
329 const navigateTo = (url: string) => {
330 window.location.href = url;
331 };
332
333 // Delete a history entry
334 const deleteHistoryEntry = async (id: number, event: Event) => {
335 event.stopPropagation(); // Prevent navigation
336
337 // Add to deleting array for animation
338 if (!deletingIds.includes(id)) {
339 deletingIds = [...deletingIds, id];
340 }
341
342 try {
343 // Set up response handlers
344 (window as any).__dumber_history_deleted = async () => {
345 // Wait a bit for the fade animation to complete
346 await new Promise(resolve => setTimeout(resolve, 200));
347
348 // Reset pagination and refresh history after successful deletion
349 historyOffset = 0;
350 hasMoreHistory = true;
351 await fetchHistory();
352 await fetchStats();
353 await fetchTopVisited();
354
355 // Remove from deleting array after refresh
356 deletingIds = deletingIds.filter(i => i !== id);
357 };
358
359 (window as any).__dumber_history_error = (error: string) => {
360 console.error('Error deleting history entry:', error);
361 // Remove from deleting array if failed
362 deletingIds = deletingIds.filter(i => i !== id);
363 };
364
365 // Send message to Go backend
366 const bridge = (window as any).webkit?.messageHandlers?.dumber;
367 if (bridge && typeof bridge.postMessage === 'function') {
368 bridge.postMessage(JSON.stringify({
369 type: 'history_delete',
370 historyId: id.toString()
371 }));
372 } else {
373 throw new Error('WebKit message handler not available');
374 }
375 } catch (error) {
376 console.error('Error sending delete request:', error);
377 // Remove from deleting array if failed
378 deletingIds = deletingIds.filter(i => i !== id);
379 }
380 };
381
382 // Get domain from URL
383 const getDomain = (url: string): string => {
384 try {
385 return new URL(url).hostname;
386 } catch {
387 return url;
388 }
389 };
390
391 const getDisplayTitle = (item: HistoryItem): string => {
392 if (!item) return '';
393 const trimmed = item.title?.trim();
394 if (trimmed) return trimmed;
395 const domain = getDomain(item.url);
396 return domain || item.url;
397 };
398
399 // Format relative time
400 const formatTime = (timestamp: string): string => {
401 try {
402 const date = new Date(timestamp);
403 const now = new Date();
404 const diff = now.getTime() - date.getTime();
405
406 const minutes = Math.floor(diff / (1000 * 60));
407 const hours = Math.floor(diff / (1000 * 60 * 60));
408 const days = Math.floor(diff / (1000 * 60 * 60 * 24));
409
410 if (minutes < 1) return 'Just now';
411 if (minutes < 60) return `${minutes}m ago`;
412 if (hours < 24) return `${hours}h ago`;
413 if (days < 7) return `${days}d ago`;
414
415 return date.toLocaleDateString();
416 } catch {
417 return '';
418 }
419 };
420
421 const formatCalendarDate = (timestamp?: string): string => {
422 if (!timestamp) return '—';
423 try {
424 const date = new Date(timestamp);
425 return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' });
426 } catch {
427 return '—';
428 }
429 };
430
431 const formatVisitLabel = (count: number): string => {
432 if (!count || count <= 0) return 'Open';
433 if (count >= 1000) {
434 const value = count / 1000;
435 const formatted = value >= 10 ? Math.round(value).toString() : value.toFixed(1).replace(/\.0$/, '');
436 return `${formatted}k visits`;
437 }
438 return `${count} ${count === 1 ? 'visit' : 'visits'}`;
439 };
440
441 const buildQuickAccess = (): HistoryItem[] => {
442 const picks: HistoryItem[] = [];
443 const seen = new Set<string>();
444
445 const pushItem = (item: HistoryItem | null | undefined) => {
446 if (!item) return;
447 const domain = getDomain(item.url);
448 const key = domain || item.url;
449 if (seen.has(key)) return;
450 seen.add(key);
451 picks.push(item);
452 };
453
454 for (const item of topVisited ?? []) {
455 pushItem(item);
456 if (picks.length >= MAX_QUICK_ACCESS_ITEMS) return picks;
457 }
458
459 for (const item of historyItems ?? []) {
460 pushItem(item);
461 if (picks.length >= MAX_QUICK_ACCESS_ITEMS) break;
462 }
463
464 return picks;
465 };
466
467 let quickAccess = $derived<HistoryItem[]>(buildQuickAccess());
468 let quickAccessLoading = $derived(topVisitedLoading || historyLoading);
469
470 const persistTheme = (mode: ThemeMode) => {
471 try {
472 localStorage.setItem(THEME_STORAGE_KEY, mode);
473 } catch (error) {
474 console.warn("[homepage] Failed to persist theme preference", error);
475 }
476 };
477
478 const applyTheme = (mode: ThemeMode) => {
479 themeMode = mode;
480
481 const manager = (window as any).__dumber_color_scheme_manager as
482 | { setUserPreference?: (theme: ThemeMode) => void }
483 | undefined;
484
485 if (manager?.setUserPreference) {
486 manager.setUserPreference(mode);
487 return;
488 }
489
490 if (window.__dumber_setTheme) {
491 window.__dumber_setTheme(mode);
492 persistTheme(mode);
493 return;
494 }
495
496 if (mode === "dark") {
497 document.documentElement.classList.add("dark");
498 } else {
499 document.documentElement.classList.remove("dark");
500 }
501 persistTheme(mode);
502 };
503
504 const toggleTheme = () => {
505 const nextMode = themeMode === "dark" ? "light" : "dark";
506 applyTheme(nextMode);
507 };
508
509 // Fetch history stats via message bridge
510 const fetchStats = async () => {
511 try {
512 // Set up response handler
513 (window as any).__dumber_history_stats = (data: any) => {
514 stats = data;
515 statsLoading = false;
516 };
517
518 (window as any).__dumber_history_error = (error: string) => {
519 console.error('Error fetching stats:', error);
520 stats = null;
521 statsLoading = false;
522 };
523
524 // Send message to Go backend
525 const bridge = (window as any).webkit?.messageHandlers?.dumber;
526 if (bridge && typeof bridge.postMessage === 'function') {
527 bridge.postMessage(JSON.stringify({
528 type: 'history_stats'
529 }));
530 } else {
531 throw new Error('WebKit message handler not available');
532 }
533 } catch (error) {
534 console.error('Error sending stats request:', error);
535 stats = null;
536 statsLoading = false;
537 }
538 };
539
540 // Fetch top visited sites via message bridge
541 const fetchTopVisited = async () => {
542 try {
543 // Set up response handler
544 (window as any).__dumber_history_search = (data: any[]) => {
545 // Sort by visit_count in descending order and take top 5
546 const sortedData = Array.isArray(data) ?
547 data.sort((a, b) => (b.visit_count || 0) - (a.visit_count || 0)).slice(0, 5) : [];
548 topVisited = sortedData;
549 topVisitedLoading = false;
550 };
551
552 (window as any).__dumber_history_error = (error: string) => {
553 console.error('Error fetching top visited:', error);
554 topVisited = [];
555 topVisitedLoading = false;
556 };
557
558 // Send message to Go backend
559 const bridge = (window as any).webkit?.messageHandlers?.dumber;
560 if (bridge && typeof bridge.postMessage === 'function') {
561 bridge.postMessage(JSON.stringify({
562 type: 'history_search',
563 q: '',
564 limit: 50 // Get more results to sort properly
565 }));
566 } else {
567 throw new Error('WebKit message handler not available');
568 }
569 } catch (error) {
570 console.error('Error sending top visited request:', error);
571 topVisited = [];
572 topVisitedLoading = false;
573 }
574 };
575
576 // Intersection Observer for infinite scroll
577 let historyScrollSentinel = $state<HTMLElement | null>(null);
578
579 const setupInfiniteScroll = () => {
580 if (!historyScrollSentinel) return;
581
582 const observer = new IntersectionObserver(
583 (entries) => {
584 const entry = entries[0];
585 if (entry && entry.isIntersecting && hasMoreHistory && !loadingMoreHistory) {
586 loadMoreHistory();
587 }
588 },
589 {
590 threshold: 0.1,
591 rootMargin: '50px'
592 }
593 );
594
595 observer.observe(historyScrollSentinel);
596
597 return () => observer.disconnect();
598 };
599
600 onMount(() => {
601 initializeShortcuts();
602 fetchHistory().then(() => {
603 // Setup infinite scroll after initial history is loaded
604 setTimeout(setupInfiniteScroll, 100);
605 });
606 fetchStats();
607 fetchTopVisited();
608
609 // Theme synchronization
610 const syncThemeState = () => {
611 themeMode = document.documentElement.classList.contains("dark")
612 ? "dark"
613 : "light";
614 };
615 syncThemeState();
616 themeObserver = new MutationObserver(syncThemeState);
617 themeObserver.observe(document.documentElement, {
618 attributes: true,
619 attributeFilter: ["class"],
620 });
621
622 window.addEventListener('resize', scheduleHistoryPanelSync);
623
624 return () => {
625 themeObserver?.disconnect();
626 themeObserver = null;
627 window.removeEventListener('resize', scheduleHistoryPanelSync);
628 if (resizeSyncRaf) {
629 cancelAnimationFrame(resizeSyncRaf);
630 resizeSyncRaf = 0;
631 }
632 };
633 });
634
635 $effect(() => {
636 historyItems;
637 topVisited;
638 stats;
639 shortcuts;
640 scheduleHistoryPanelSync();
641 });
642
643 $effect(() => {
644 historyHeaderRef;
645 historyBodyRef;
646 insightsPanelRef;
647 scheduleHistoryPanelSync();
648 });
649
650</script>
651
652<svelte:head>
653 <title>Dumber Browser - Homepage</title>
654 <meta name="description" content="Fast Wayland Browser - Your browsing patterns at a glance" />
655</svelte:head>
656
657<div class="homepage-shell">
658 <div class="homepage-content">
659 <div class="top-bar">
660 <button
661 class="theme-toggle-button"
662 type="button"
663 aria-pressed={themeMode === "dark"}
664 title={themeMode === "dark" ? "Switch to light mode" : "Switch to dark mode"}
665 onclick={toggleTheme}
666 >
667 <span class="sr-only">
668 {themeMode === "dark" ? "Switch to light mode" : "Switch to dark mode"}
669 </span>
670 {#if themeMode === "dark"}
671 <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
672 <path
673 fill="currentColor"
674 d="M21 12.79A9 9 0 0111.21 3 7 7 0 0012 17a7 7 0 009-4.21z"
675 />
676 </svg>
677 {:else}
678 <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
679 <circle cx="12" cy="12" r="5" fill="currentColor" />
680 <g stroke="currentColor" stroke-width="2" stroke-linecap="round">
681 <line x1="12" y1="1" x2="12" y2="4" />
682 <line x1="12" y1="20" x2="12" y2="23" />
683 <line x1="4.22" y1="4.22" x2="6.34" y2="6.34" />
684 <line x1="17.66" y1="17.66" x2="19.78" y2="19.78" />
685 <line x1="1" y1="12" x2="4" y2="12" />
686 <line x1="20" y1="12" x2="23" y2="12" />
687 <line x1="4.22" y1="19.78" x2="6.34" y2="17.66" />
688 <line x1="17.66" y1="6.34" x2="19.78" y2="4.22" />
689 </g>
690 </svg>
691 {/if}
692 </button>
693 </div>
694
695 <section class="hero-panel brutal-panel">
696 <div class="hero-heading">
697 <h1 class="hero-title">Jump back in</h1>
698 <p class="hero-subtitle">Direct access to the sites you visit the most, plus what you just explored.</p>
699 </div>
700 <div class="quick-access-wrapper">
701 {#if quickAccessLoading}
702 <div class="loading quick-access-loading">Preparing your shortcuts...</div>
703 {:else if quickAccess.length === 0}
704 <div class="empty-state quick-access-empty">
705 <h3>No quick links yet</h3>
706 <p>Browse a few sites and we’ll surface your frequent destinations here.</p>
707 </div>
708 {:else}
709 <div class="quick-access-grid">
710 {#each quickAccess as item (item.id)}
711 <div
712 class="quick-access-item"
713 role="button"
714 tabindex="0"
715 onclick={() => navigateTo(item.url)}
716 onkeydown={(e) => e.key === 'Enter' && navigateTo(item.url)}
717 >
718 <div class="quick-access-favicon">
719 {#if item.favicon_url}
720 <img
721 src={item.favicon_url}
722 alt=""
723 class="quick-access-favicon-img"
724 onerror={(e) => {
725 const target = e.target as HTMLImageElement;
726 if (target) {
727 target.style.display = 'none';
728 const fallback = target.nextElementSibling as HTMLElement;
729 if (fallback) fallback.style.display = 'flex';
730 }
731 }}
732 onload={(e) => {
733 const target = e.target as HTMLImageElement;
734 if (target) {
735 const fallback = target.nextElementSibling as HTMLElement;
736 if (fallback) fallback.style.display = 'none';
737 }
738 }}
739 />
740 <div class="quick-access-fallback" style="display: none;">
741 <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
742 <circle cx="12" cy="12" r="10" />
743 <path d="M2 12h20" />
744 </svg>
745 </div>
746 {:else}
747 <div class="quick-access-fallback">
748 <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
749 <circle cx="12" cy="12" r="10" />
750 <path d="M2 12h20" />
751 </svg>
752 </div>
753 {/if}
754 </div>
755 <div class="quick-access-meta">
756 <div class="quick-access-title">{getDisplayTitle(item)}</div>
757 <div class="quick-access-domain">{getDomain(item.url)}</div>
758 </div>
759 <div class="quick-access-visits">{formatVisitLabel(item.visit_count)}</div>
760 </div>
761 {/each}
762 </div>
763 {/if}
764 </div>
765 </section>
766
767 <div class="main-panels">
768 <section class="history-panel brutal-panel">
769 <div class="panel-header" bind:this={historyHeaderRef}>
770 <h2 class="panel-title">Recent History</h2>
771 <p class="panel-subtitle">Scroll to revisit anything from your latest sessions.</p>
772 </div>
773 <div class="panel-body history-body" bind:this={historyBodyRef}>
774 {#if historyLoading}
775 <div class="loading">Loading history...</div>
776 {:else if historyItems.length === 0}
777 <div class="empty-state">
778 <h3>No history yet</h3>
779 <p>Start browsing to see your recent history here.</p>
780 </div>
781 {:else}
782 <div class="history-list">
783 {#each historyItems as item (item.id)}
784 <div
785 class="history-item"
786 class:deleting={isDeleting(item.id)}
787 role="button"
788 tabindex="0"
789 onclick={() => navigateTo(item.url)}
790 onkeydown={(e) => e.key === 'Enter' && navigateTo(item.url)}
791 >
792 <div class="history-item-leading">
793 {#if item.favicon_url}
794 <img
795 src={item.favicon_url}
796 alt=""
797 class="history-favicon-img"
798 onerror={(e) => {
799 const target = e.target as HTMLImageElement;
800 if (target) {
801 target.style.display = 'none';
802 const fallback = target.nextElementSibling as HTMLElement;
803 if (fallback) fallback.style.display = 'flex';
804 }
805 }}
806 onload={(e) => {
807 const target = e.target as HTMLImageElement;
808 if (target) {
809 const fallback = target.nextElementSibling as HTMLElement;
810 if (fallback) fallback.style.display = 'none';
811 }
812 }}
813 />
814 <div class="history-favicon-fallback" style="display: none;">
815 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
816 <circle cx="12" cy="12" r="10" />
817 <path d="M2 12h20" />
818 </svg>
819 </div>
820 {:else}
821 <div class="history-favicon-fallback">
822 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
823 <circle cx="12" cy="12" r="10" />
824 <path d="M2 12h20" />
825 </svg>
826 </div>
827 {/if}
828 </div>
829 <div class="history-item-content">
830 <div class="history-item-top">
831 <div class="history-title">{getDisplayTitle(item)}</div>
832 <div class="history-time">{formatTime(item.last_visited)}</div>
833 </div>
834 <div class="history-item-bottom">
835 <div class="history-domain">{getDomain(item.url)}</div>
836 <div class="history-url">{item.url}</div>
837 </div>
838 </div>
839 <div
840 class="history-delete"
841 role="button"
842 tabindex="0"
843 onclick={(e) => deleteHistoryEntry(item.id, e)}
844 onkeydown={(e) => e.key === 'Enter' && deleteHistoryEntry(item.id, e)}
845 title="Delete this entry"
846 >
847 <span aria-hidden="true">×</span>
848 </div>
849 </div>
850 {/each}
851
852 {#if hasMoreHistory}
853 <div bind:this={historyScrollSentinel} class="scroll-sentinel"></div>
854 {#if loadingMoreHistory}
855 <div class="loading-more">Loading more history...</div>
856 {/if}
857 {/if}
858 </div>
859 {/if}
860 </div>
861 </section>
862
863 <section class="insights-panel brutal-panel" bind:this={insightsPanelRef}>
864 <div class="panel-header">
865 <h2 class="panel-title">Usage Insights</h2>
866 <p class="panel-subtitle">Your browsing patterns at a glance.</p>
867 </div>
868 <div class="panel-body insights-body">
869 <div class="insight-block">
870 <h3 class="block-title">Top sites</h3>
871 {#if topVisitedLoading}
872 <div class="loading">Loading usage insights...</div>
873 {:else if topVisited.length === 0}
874 <div class="empty-state compact">
875 <p>No visit data available</p>
876 </div>
877 {:else}
878 <div class="insights-list">
879 {#each topVisited as item, index (item.id)}
880 <div
881 class="insight-item"
882 role="button"
883 tabindex="0"
884 onclick={() => navigateTo(item.url)}
885 onkeydown={(e) => e.key === 'Enter' && navigateTo(item.url)}
886 >
887 <div class="insight-rank">{index + 1}</div>
888 <div class="insight-body">
889 <div class="insight-title">{getDisplayTitle(item)}</div>
890 <div class="insight-domain">{getDomain(item.url)}</div>
891 </div>
892 <div class="insight-visits">{formatVisitLabel(item.visit_count)}</div>
893 </div>
894 {/each}
895 </div>
896 {/if}
897 </div>
898
899 <div class="insight-block">
900 <h3 class="block-title">History snapshot</h3>
901 {#if statsLoading}
902 <div class="loading">Loading stats...</div>
903 {:else if stats}
904 <div class="stats-grid">
905 <div class="stat-item">
906 <div class="stat-value">{stats.total_entries}</div>
907 <div class="stat-label">Entries Stored</div>
908 </div>
909 <div class="stat-item">
910 <div class="stat-value">{stats.recent_count}</div>
911 <div class="stat-label">Recent Window</div>
912 </div>
913 {#if stats.newest_entry}
914 <div class="stat-item">
915 <div class="stat-value">{formatTime(stats.newest_entry)}</div>
916 <div class="stat-label">Newest Visit</div>
917 </div>
918 {/if}
919 {#if stats.oldest_entry}
920 <div class="stat-item">
921 <div class="stat-value">{formatCalendarDate(stats.oldest_entry)}</div>
922 <div class="stat-label">Oldest Entry Loaded</div>
923 </div>
924 {/if}
925 </div>
926 {:else}
927 <div class="empty-state compact">
928 <p>No statistics available</p>
929 </div>
930 {/if}
931 </div>
932 </div>
933 </section>
934 </div>
935
936 <section class="shortcuts-panel brutal-panel">
937 <div class="panel-header">
938 <h2 class="panel-title">Keyboard Shortcuts</h2>
939 <p class="panel-subtitle">Stay quick with the essentials.</p>
940 </div>
941 <div class="panel-body">
942 {#if shortcutsLoading}
943 <div class="loading">Loading shortcuts...</div>
944 {:else}
945 <div class="shortcuts-list">
946 {#each shortcuts as shortcut (shortcut.key)}
947 <div class="shortcut-item">
948 <div class="shortcut-key-badge">{shortcut.key}</div>
949 <div class="shortcut-desc">{shortcut.description}</div>
950 </div>
951 {/each}
952 </div>
953 {/if}
954 </div>
955 </section>
956 </div>
957
958 <Footer />
959</div>
960
961<style>
962/* Homepage Layout Styles */
963.homepage-shell {
964 min-height: 100vh;
965 background-color: var(--dynamic-bg);
966 color: var(--dynamic-text);
967 display: flex;
968 flex-direction: column;
969 padding: clamp(1.75rem, 3vw, 3rem);
970 font-family: "Fira Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
971}
972
973.homepage-shell * {
974 box-sizing: border-box;
975}
976
977.homepage-content {
978 flex: 1;
979 width: 100%;
980 max-width: 1180px;
981 margin: 0 auto;
982 display: flex;
983 flex-direction: column;
984 gap: clamp(1.75rem, 2.6vw, 2.75rem);
985}
986
987.top-bar {
988 display: flex;
989 justify-content: flex-end;
990}
991
992.theme-toggle-button {
993 width: 3rem;
994 height: 3rem;
995 display: inline-flex;
996 align-items: center;
997 justify-content: center;
998 border: 2px solid var(--dynamic-border);
999 border-radius: 0;
1000 background-color: var(--dynamic-surface);
1001 color: var(--dynamic-text);
1002 transition:
1003 background-color 200ms ease,
1004 color 200ms ease,
1005 border-color 200ms ease;
1006 cursor: pointer;
1007}
1008
1009.theme-toggle-button:hover,
1010.theme-toggle-button:focus-visible {
1011 border-color: var(--dynamic-accent);
1012 color: var(--dynamic-accent);
1013 outline: none;
1014}
1015
1016.theme-toggle-button svg {
1017 width: 1.5rem;
1018 height: 1.5rem;
1019}
1020
1021.sr-only {
1022 position: absolute;
1023 width: 1px;
1024 height: 1px;
1025 padding: 0;
1026 margin: -1px;
1027 overflow: hidden;
1028 clip: rect(0, 0, 0, 0);
1029 white-space: nowrap;
1030 border: 0;
1031}
1032
1033.brutal-panel {
1034 border: 2px solid var(--dynamic-border);
1035 background-color: var(--dynamic-surface);
1036 border-radius: 0;
1037 display: flex;
1038 flex-direction: column;
1039 gap: 0;
1040}
1041
1042.hero-panel {
1043 gap: clamp(1.25rem, 2vw, 1.75rem);
1044 padding: clamp(1.75rem, 2.2vw, 2.5rem);
1045}
1046
1047.hero-heading {
1048 display: flex;
1049 flex-direction: column;
1050 gap: 0.5rem;
1051 max-width: 36rem;
1052}
1053
1054.hero-title {
1055 font-size: clamp(2rem, 3vw, 2.75rem);
1056 font-weight: 700;
1057 letter-spacing: -0.01em;
1058 margin: 0;
1059}
1060
1061.hero-subtitle {
1062 font-size: 1rem;
1063 line-height: 1.6;
1064 color: var(--dynamic-muted);
1065 margin: 0;
1066}
1067
1068.quick-access-wrapper {
1069 display: flex;
1070 flex-direction: column;
1071 gap: 1rem;
1072}
1073
1074.quick-access-grid {
1075 display: flex;
1076 flex-wrap: wrap;
1077 gap: 1rem;
1078}
1079
1080.quick-access-item {
1081 flex: 1 1 min(16rem, 100%);
1082 min-width: 12rem;
1083 display: flex;
1084 align-items: center;
1085 gap: 1rem;
1086 border: 2px solid var(--dynamic-border);
1087 background-color: var(--dynamic-surface-variant);
1088 padding: 0.9rem 1.1rem;
1089 cursor: pointer;
1090 border-radius: 0;
1091 transition:
1092 border-color 200ms ease,
1093 background-color 200ms ease;
1094}
1095
1096.quick-access-item:hover,
1097.quick-access-item:focus-visible {
1098 border-color: var(--dynamic-accent);
1099 outline: none;
1100}
1101
1102.quick-access-favicon {
1103 width: 48px;
1104 height: 48px;
1105 border: 2px solid var(--dynamic-border);
1106 background-color: var(--dynamic-surface);
1107 display: flex;
1108 align-items: center;
1109 justify-content: center;
1110 flex-shrink: 0;
1111 border-radius: 0;
1112}
1113
1114.quick-access-favicon-img {
1115 width: 70%;
1116 height: 70%;
1117 object-fit: contain;
1118}
1119
1120.quick-access-fallback {
1121 width: 100%;
1122 height: 100%;
1123 display: flex;
1124 align-items: center;
1125 justify-content: center;
1126 color: var(--dynamic-muted);
1127}
1128
1129.quick-access-meta {
1130 flex: 1;
1131 min-width: 0;
1132 display: flex;
1133 flex-direction: column;
1134 gap: 0.125rem;
1135}
1136
1137.quick-access-title {
1138 font-size: 1rem;
1139 font-weight: 600;
1140 color: var(--dynamic-text);
1141 white-space: nowrap;
1142 overflow: hidden;
1143 text-overflow: ellipsis;
1144}
1145
1146.quick-access-domain {
1147 font-size: 0.85rem;
1148 color: var(--dynamic-muted);
1149 white-space: nowrap;
1150 overflow: hidden;
1151 text-overflow: ellipsis;
1152}
1153
1154.quick-access-visits {
1155 font-size: 0.82rem;
1156 color: var(--dynamic-accent);
1157 text-transform: uppercase;
1158 letter-spacing: 0.05em;
1159 font-weight: 600;
1160 flex-shrink: 0;
1161}
1162
1163.loading {
1164 padding: 1.25rem;
1165 text-align: center;
1166 color: var(--dynamic-muted);
1167 border: 2px dashed var(--dynamic-border);
1168 background-color: var(--dynamic-bg);
1169}
1170
1171.empty-state {
1172 border: 2px dashed var(--dynamic-border);
1173 padding: 1.75rem;
1174 display: flex;
1175 flex-direction: column;
1176 gap: 0.5rem;
1177 text-align: center;
1178 align-items: center;
1179 color: var(--dynamic-muted);
1180 background-color: var(--dynamic-bg);
1181}
1182
1183.empty-state h3 {
1184 margin: 0;
1185 color: var(--dynamic-text);
1186 font-size: 1.1rem;
1187}
1188
1189.empty-state p {
1190 margin: 0;
1191 color: var(--dynamic-muted);
1192}
1193
1194.empty-state.compact {
1195 padding: 1.25rem;
1196}
1197
1198/* Main panels with height matching */
1199.main-panels {
1200 display: flex;
1201 flex-wrap: wrap;
1202 gap: clamp(1.5rem, 2vw, 2rem);
1203 align-items: flex-start;
1204}
1205
1206.history-panel {
1207 flex: 1 1 55%;
1208 min-width: min(32rem, 100%);
1209 display: flex;
1210 flex-direction: column;
1211 min-height: 0;
1212}
1213
1214.insights-panel {
1215 flex: 1 1 35%;
1216 min-width: min(24rem, 100%);
1217 display: flex;
1218 flex-direction: column;
1219 min-height: 0;
1220}
1221
1222.panel-header {
1223 display: flex;
1224 flex-direction: column;
1225 gap: 0.4rem;
1226 padding: clamp(1.25rem, 2vw, 1.75rem) clamp(1.5rem, 2.5vw, 2rem);
1227 border-bottom: 2px solid var(--dynamic-border);
1228 background-color: var(--dynamic-surface);
1229}
1230
1231.panel-title {
1232 margin: 0;
1233 font-size: 1.3rem;
1234 font-weight: 600;
1235}
1236
1237.panel-subtitle {
1238 margin: 0;
1239 font-size: 0.95rem;
1240 color: var(--dynamic-muted);
1241}
1242
1243.panel-body {
1244 padding: clamp(1.25rem, 2vw, 1.75rem) clamp(1.5rem, 2.5vw, 2rem);
1245 display: flex;
1246 flex-direction: column;
1247 gap: 1.1rem;
1248 min-height: 0;
1249 overflow: hidden;
1250}
1251
1252.history-body {
1253 gap: 1rem;
1254 display: flex;
1255 flex-direction: column;
1256 min-height: 0;
1257 overflow: hidden;
1258}
1259
1260.history-list {
1261 display: flex;
1262 flex-direction: column;
1263 gap: 0.85rem;
1264 flex: 1;
1265 min-height: 0;
1266 overflow-y: auto;
1267 padding-right: 0.5rem;
1268}
1269
1270.history-item {
1271 display: flex;
1272 align-items: flex-start;
1273 gap: 1rem;
1274 border: 2px solid var(--dynamic-border);
1275 background-color: var(--dynamic-surface-variant);
1276 padding: 1rem 1.1rem;
1277 cursor: pointer;
1278 min-height: 80px;
1279 border-radius: 0;
1280 transition:
1281 border-color 200ms ease,
1282 background-color 200ms ease;
1283}
1284
1285.history-item:hover,
1286.history-item:focus-visible {
1287 border-color: var(--dynamic-accent);
1288 background-color: var(--dynamic-bg);
1289 outline: none;
1290}
1291
1292.history-item.deleting {
1293 opacity: 0.4;
1294 pointer-events: none;
1295 transform: translateX(6px);
1296}
1297
1298.history-item-leading {
1299 width: 44px;
1300 height: 44px;
1301 border: 2px solid var(--dynamic-border);
1302 background-color: var(--dynamic-surface);
1303 display: flex;
1304 align-items: center;
1305 justify-content: center;
1306 flex-shrink: 0;
1307 border-radius: 0;
1308}
1309
1310.history-favicon-img {
1311 width: 60%;
1312 height: 60%;
1313 object-fit: contain;
1314}
1315
1316.history-favicon-fallback {
1317 display: flex;
1318 align-items: center;
1319 justify-content: center;
1320 width: 100%;
1321 height: 100%;
1322 color: var(--dynamic-muted);
1323}
1324
1325.history-item-content {
1326 flex: 1;
1327 min-width: 0;
1328 display: flex;
1329 flex-direction: column;
1330 gap: 0.125rem;
1331 justify-content: center;
1332}
1333
1334.history-item-top {
1335 display: flex;
1336 align-items: flex-start;
1337 justify-content: space-between;
1338 gap: 1rem;
1339}
1340
1341.history-title {
1342 font-weight: 600;
1343 color: var(--dynamic-text);
1344 white-space: nowrap;
1345 overflow: hidden;
1346 text-overflow: ellipsis;
1347}
1348
1349.history-time {
1350 font-size: 0.85rem;
1351 color: var(--dynamic-muted);
1352 flex-shrink: 0;
1353}
1354
1355.history-item-bottom {
1356 display: flex;
1357 align-items: baseline;
1358 gap: 0.5rem;
1359 overflow: hidden;
1360 position: relative;
1361}
1362
1363.history-domain {
1364 font-size: 0.9rem;
1365 font-weight: 500;
1366 color: var(--dynamic-text);
1367 white-space: nowrap;
1368 flex-shrink: 0;
1369}
1370
1371.history-url {
1372 font-size: 0.8rem;
1373 color: var(--dynamic-muted);
1374 white-space: nowrap;
1375 font-family:
1376 "JetBrains Mono",
1377 "SFMono-Regular",
1378 Menlo,
1379 monospace;
1380 overflow: hidden;
1381 flex: 1;
1382 position: relative;
1383 mask-image: linear-gradient(to right, black 70%, transparent 100%);
1384 -webkit-mask-image: linear-gradient(to right, black 70%, transparent 100%);
1385}
1386
1387.history-delete {
1388 display: flex;
1389 align-items: center;
1390 justify-content: center;
1391 width: 38px;
1392 border-left: 2px solid var(--dynamic-border);
1393 background-color: var(--dynamic-surface);
1394 color: var(--dynamic-muted);
1395 font-size: 1.1rem;
1396 transition: background-color 200ms ease, color 200ms ease;
1397 flex-shrink: 0;
1398}
1399
1400.history-delete:hover,
1401.history-delete:focus-visible {
1402 color: var(--dynamic-accent);
1403 background-color: var(--dynamic-bg);
1404 outline: none;
1405}
1406
1407.scroll-sentinel {
1408 width: 100%;
1409 height: 1px;
1410}
1411
1412.loading-more {
1413 padding: 0.75rem;
1414 text-align: center;
1415 color: var(--dynamic-muted);
1416 font-size: 0.85rem;
1417}
1418
1419.insights-body {
1420 gap: 1.5rem;
1421}
1422
1423.insight-block {
1424 display: flex;
1425 flex-direction: column;
1426 gap: 0.9rem;
1427}
1428
1429.block-title {
1430 font-size: 0.85rem;
1431 text-transform: uppercase;
1432 letter-spacing: 0.08em;
1433 color: var(--dynamic-muted);
1434 margin: 0;
1435}
1436
1437.insights-list {
1438 display: flex;
1439 flex-direction: column;
1440 gap: 0.75rem;
1441}
1442
1443.insight-item {
1444 display: flex;
1445 align-items: center;
1446 gap: 0.75rem;
1447 border: 2px solid var(--dynamic-border);
1448 background-color: var(--dynamic-surface-variant);
1449 padding: 0.85rem 1rem;
1450 cursor: pointer;
1451 border-radius: 0;
1452 transition:
1453 border-color 200ms ease,
1454 background-color 200ms ease;
1455}
1456
1457.insight-item:hover,
1458.insight-item:focus-visible {
1459 border-color: var(--dynamic-accent);
1460 outline: none;
1461}
1462
1463.insight-rank {
1464 width: 2rem;
1465 height: 2rem;
1466 border: 2px solid var(--dynamic-border);
1467 background-color: var(--dynamic-surface);
1468 display: flex;
1469 align-items: center;
1470 justify-content: center;
1471 font-weight: 600;
1472 font-size: 0.9rem;
1473 color: var(--dynamic-accent);
1474 flex-shrink: 0;
1475 border-radius: 0;
1476}
1477
1478.insight-body {
1479 flex: 1;
1480 min-width: 0;
1481 display: flex;
1482 flex-direction: column;
1483 gap: 0.125rem;
1484}
1485
1486.insight-title {
1487 font-weight: 600;
1488 color: var(--dynamic-text);
1489 white-space: nowrap;
1490 overflow: hidden;
1491 text-overflow: ellipsis;
1492}
1493
1494.insight-domain {
1495 font-size: 0.85rem;
1496 color: var(--dynamic-muted);
1497 white-space: nowrap;
1498 overflow: hidden;
1499 text-overflow: ellipsis;
1500}
1501
1502.insight-visits {
1503 font-size: 0.78rem;
1504 color: var(--dynamic-accent);
1505 text-transform: uppercase;
1506 letter-spacing: 0.05em;
1507 font-weight: 600;
1508 flex-shrink: 0;
1509}
1510
1511.stats-grid {
1512 display: flex;
1513 flex-wrap: wrap;
1514 gap: 0.85rem;
1515}
1516
1517.stat-item {
1518 flex: 1 1 160px;
1519 border: 2px solid var(--dynamic-border);
1520 background-color: var(--dynamic-surface-variant);
1521 padding: 1rem;
1522 display: flex;
1523 flex-direction: column;
1524 gap: 0.35rem;
1525 border-radius: 0;
1526}
1527
1528.stat-value {
1529 font-size: 1.4rem;
1530 font-weight: 600;
1531 color: var(--dynamic-text);
1532}
1533
1534.stat-label {
1535 font-size: 0.8rem;
1536 color: var(--dynamic-muted);
1537 text-transform: uppercase;
1538 letter-spacing: 0.08em;
1539}
1540
1541.shortcuts-panel {
1542 gap: 0;
1543}
1544
1545.shortcuts-list {
1546 display: flex;
1547 flex-wrap: wrap;
1548 gap: 0.85rem;
1549}
1550
1551.shortcut-item {
1552 display: flex;
1553 align-items: center;
1554 gap: 0.75rem;
1555 border: 2px solid var(--dynamic-border);
1556 background-color: var(--dynamic-bg);
1557 padding: 0.75rem 1rem;
1558 border-radius: 0;
1559}
1560
1561.shortcut-key-badge {
1562 font-family:
1563 "JetBrains Mono",
1564 "SFMono-Regular",
1565 Menlo,
1566 monospace;
1567 font-size: 0.85rem;
1568 text-transform: uppercase;
1569 letter-spacing: 0.08em;
1570 padding: 0.35rem 0.65rem;
1571 border: 2px solid var(--dynamic-border);
1572 background-color: var(--dynamic-surface-variant);
1573 color: var(--dynamic-muted);
1574 font-weight: 500;
1575 border-radius: 0;
1576}
1577
1578.shortcut-desc {
1579 font-size: 0.95rem;
1580 color: var(--dynamic-text);
1581}
1582
1583.shortcuts-panel .loading {
1584 border-style: solid;
1585}
1586
1587/* Responsive Design */
1588@media (max-width: 1200px) {
1589 .history-panel,
1590 .insights-panel {
1591 min-width: min(28rem, 100%);
1592 }
1593}
1594
1595@media (max-width: 960px) {
1596 .homepage-shell {
1597 padding: clamp(1.25rem, 5vw, 2rem);
1598 }
1599
1600 .main-panels {
1601 flex-direction: column;
1602 }
1603
1604 .history-panel,
1605 .insights-panel {
1606 min-width: 100%;
1607 }
1608
1609 .quick-access-item {
1610 flex: 1 1 calc(50% - 1rem);
1611 }
1612}
1613
1614@media (max-width: 640px) {
1615 .theme-toggle-button {
1616 width: 2.75rem;
1617 height: 2.75rem;
1618 }
1619
1620 .panel-body,
1621 .panel-header,
1622 .hero-panel {
1623 padding-left: 1.25rem;
1624 padding-right: 1.25rem;
1625 }
1626
1627 .quick-access-item {
1628 flex: 1 1 100%;
1629 }
1630
1631 .history-item {
1632 flex-direction: column;
1633 align-items: flex-start;
1634 }
1635
1636 .history-delete {
1637 width: 100%;
1638 border-left: 0;
1639 border-top: 2px solid var(--dynamic-border);
1640 align-self: stretch;
1641 }
1642
1643 .history-item-top {
1644 flex-direction: column;
1645 align-items: flex-start;
1646 gap: 0.35rem;
1647 }
1648
1649 .history-time {
1650 order: 3;
1651 }
1652}
1653</style>