detached 4b59d50

feat(ui): brutal homepage redesign with enhanced styling

- Complete homepage redesign with brutal aesthetic and component-scoped styling
- Implement dynamic height matching with better responsive behavior
- Add inline domain-URL layout with sophisticated fade effects
- Optimize spacing and typography for improved visual hierarchy
- Update logo design with squared borders and refined color palette
- Enhance Tailwind configuration with custom spacing and effects
commit
4b59d50ba080da3a6032e6dadff825790cc70398
author
bamen · · 2025-09-23 17:20 UTC
committer
bamen · · 2025-09-23 21:45 UTC
parent
147e404
signature
unsigned
4 files · +1165 -530
M assets/logo.svg +6 -6
@@ -2,8 +2,8 @@
22 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
33 <defs>
44 <linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
5- <stop offset="0%" stop-color="#121212"/>
6- <stop offset="100%" stop-color="#1e1e1e"/>
5+ <stop offset="0%" stop-color="#111827"/>
6+ <stop offset="100%" stop-color="#1f2937"/>
77 </linearGradient>
88 <filter id="inset" x="-20%" y="-20%" width="140%" height="140%">
99 <feOffset dx="0" dy="1"/>
@@ -13,18 +13,18 @@
1313 <feBlend in="SourceGraphic" mode="normal"/>
1414 </filter>
1515 </defs>
16- <rect x="4" y="4" width="56" height="56" rx="14" ry="14" fill="url(#bg)" filter="url(#inset)"/>
17- <rect x="4.5" y="4.5" width="55" height="55" rx="13.5" ry="13.5" fill="none" stroke="#252a2f" stroke-width="1"/>
16+ <rect x="4" y="4" width="56" height="56" rx="0" ry="0" fill="url(#bg)" filter="url(#inset)"/>
17+ <rect x="4.5" y="4.5" width="55" height="55" rx="0" ry="0" fill="none" stroke="#374151" stroke-width="1"/>
1818 <!-- D glyph (slightly larger and wider) -->
1919 <path d="M20 16 L20 48 M20 16 H34 q12 0 12 16 t-12 16 H20"
2020 fill="none"
21- stroke="#e9eef3"
21+ stroke="#f3f4f6"
2222 stroke-width="4.5"
2323 stroke-linecap="round"
2424 stroke-linejoin="round"
2525 transform="translate(33,32) scale(1.12,1.10) translate(-33,-32)"/>
2626 <!-- Speed bolt accent (significantly larger, centered) -->
2727 <path d="M36 22 L32 34 h6 l-8 16 4-12 h-6 z"
28- fill="#ff9800" opacity="0.95"
28+ fill="#60a5fa" opacity="0.95"
2929 transform="translate(33,32) scale(1.8) translate(-33,-32) translate(0,-3)"/>
3030 </svg>
@@ -2,8 +2,8 @@
2 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">2 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
3 <defs>3 <defs>
4 <linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">4 <linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
5- <stop offset="0%" stop-color="#121212"/>5+ <stop offset="0%" stop-color="#111827"/>
6- <stop offset="100%" stop-color="#1e1e1e"/>6+ <stop offset="100%" stop-color="#1f2937"/>
7 </linearGradient>7 </linearGradient>
8 <filter id="inset" x="-20%" y="-20%" width="140%" height="140%">8 <filter id="inset" x="-20%" y="-20%" width="140%" height="140%">
9 <feOffset dx="0" dy="1"/>9 <feOffset dx="0" dy="1"/>
@@ -13,18 +13,18 @@
13 <feBlend in="SourceGraphic" mode="normal"/>13 <feBlend in="SourceGraphic" mode="normal"/>
14 </filter>14 </filter>
15 </defs>15 </defs>
16- <rect x="4" y="4" width="56" height="56" rx="14" ry="14" fill="url(#bg)" filter="url(#inset)"/>16+ <rect x="4" y="4" width="56" height="56" rx="0" ry="0" fill="url(#bg)" filter="url(#inset)"/>
17- <rect x="4.5" y="4.5" width="55" height="55" rx="13.5" ry="13.5" fill="none" stroke="#252a2f" stroke-width="1"/>17+ <rect x="4.5" y="4.5" width="55" height="55" rx="0" ry="0" fill="none" stroke="#374151" stroke-width="1"/>
18 <!-- D glyph (slightly larger and wider) -->18 <!-- D glyph (slightly larger and wider) -->
19 <path d="M20 16 L20 48 M20 16 H34 q12 0 12 16 t-12 16 H20"19 <path d="M20 16 L20 48 M20 16 H34 q12 0 12 16 t-12 16 H20"
20 fill="none"20 fill="none"
21- stroke="#e9eef3"21+ stroke="#f3f4f6"
22 stroke-width="4.5"22 stroke-width="4.5"
23 stroke-linecap="round"23 stroke-linecap="round"
24 stroke-linejoin="round"24 stroke-linejoin="round"
25 transform="translate(33,32) scale(1.12,1.10) translate(-33,-32)"/>25 transform="translate(33,32) scale(1.12,1.10) translate(-33,-32)"/>
26 <!-- Speed bolt accent (significantly larger, centered) -->26 <!-- Speed bolt accent (significantly larger, centered) -->
27 <path d="M36 22 L32 34 h6 l-8 16 4-12 h-6 z"27 <path d="M36 22 L32 34 h6 l-8 16 4-12 h-6 z"
28- fill="#ff9800" opacity="0.95"28+ fill="#60a5fa" opacity="0.95"
29 transform="translate(33,32) scale(1.8) translate(-33,-32) translate(0,-3)"/>29 transform="translate(33,32) scale(1.8) translate(-33,-32) translate(0,-3)"/>
30 </svg>30 </svg>
M gui/src/pages/Homepage.svelte +1134 -511
@@ -38,6 +38,50 @@
3838 let historyOffset = $state(0);
3939 let hasMoreHistory = $state(true);
4040 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+ };
4185
4286 // Function for checking if an item is being deleted
4387 const isDeleting = (id: number) => deletingIds.includes(id);
@@ -344,6 +388,14 @@
344388 }
345389 };
346390
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+
347399 // Format relative time
348400 const formatTime = (timestamp: string): string => {
349401 try {
@@ -366,6 +418,94 @@
366418 }
367419 };
368420
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+
369509 // Fetch history stats via message bridge
370510 const fetchStats = async () => {
371511 try {
@@ -465,566 +605,1049 @@
465605 });
466606 fetchStats();
467607 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();
468648 });
469649
470650 </script>
471651
472-<div class="homepage-container">
473-<div class="container">
474- <div class="history-section">
475- <h2 class="section-title">Recent History</h2>
476- <div class="history-list">
477- {#if historyLoading}
478- <div class="loading">Loading history...</div>
479- {:else if historyItems.length === 0}
480- <div class="empty-state">
481- <h3>No history yet</h3>
482- <p>Start browsing to see your recent history here.</p>
483- </div>
484- {:else}
485- {#each historyItems as item (item.url)}
486- <div
487- class="history-item"
488- class:deleting={isDeleting(item.id)}
489- role="button"
490- tabindex="0"
491- onclick={() => navigateTo(item.url)}
492- onkeydown={(e) => e.key === 'Enter' && navigateTo(item.url)}
493- >
494- <div class="history-line">
495- <div class="history-favicon-chip">
496- {#if item.favicon_url}
497- <img
498- src={item.favicon_url}
499- alt=""
500- class="history-favicon-img"
501- onerror={(e) => {
502- const target = e.target as HTMLImageElement;
503- if (target) {
504- target.style.display='none';
505- const fallback = target.nextElementSibling as HTMLElement;
506- if (fallback) fallback.style.display='block';
507- }
508- }}
509- onload={(e) => {
510- const target = e.target as HTMLImageElement;
511- if (target) {
512- const fallback = target.nextElementSibling as HTMLElement;
513- if (fallback) fallback.style.display='none';
514- }
515- }}
516- />
517- <div class="history-favicon-fallback" style="display: none;">
518- <!-- inline globe placeholder -->
519- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
520- <circle cx="12" cy="12" r="10" />
521- <path d="M2 12h20" />
522- </svg>
523- </div>
524- {:else}
525- <div class="history-favicon-fallback">
526- <!-- inline globe placeholder -->
527- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
528- <circle cx="12" cy="12" r="10" />
529- <path d="M2 12h20" />
530- </svg>
531- </div>
532- {/if}
533- </div>
534- <div class="history-title">{item.title || 'Untitled'}</div>
535- <div class="history-sep">•</div>
536- <div class="history-domain">{getDomain(item.url)}</div>
537- <div class="history-sep">•</div>
538- <div class="history-url">{item.url}</div>
539- <div class="history-time">{formatTime(item.last_visited)}</div>
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)}
540711 <div
541- class="history-delete"
712+ class="quick-access-item"
542713 role="button"
543714 tabindex="0"
544- onclick={(e) => deleteHistoryEntry(item.id, e)}
545- onkeydown={(e) => e.key === 'Enter' && deleteHistoryEntry(item.id, e)}
546- title="Delete this entry"
715+ onclick={() => navigateTo(item.url)}
716+ onkeydown={(e) => e.key === 'Enter' && navigateTo(item.url)}
547717 >
548- <!-- inline delete placeholder -->
549- <span aria-hidden="true" style="display:inline-block;line-height:1;">×</span>
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>
550760 </div>
551- </div>
761+ {/each}
552762 </div>
553- {/each}
554-
555- <!-- Infinite scroll sentinel and loading indicator -->
556- {#if hasMoreHistory}
557- <div bind:this={historyScrollSentinel} class="scroll-sentinel"></div>
558- {#if loadingMoreHistory}
559- <div class="loading-more">Loading more history...</div>
560- {/if}
561763 {/if}
562- {/if}
563- </div>
564- </div>
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}
565851
566- <div class="shortcuts-section">
567- <h2 class="section-title">Keyboard Shortcuts</h2>
568- <div class="shortcuts-container">
569- {#if shortcutsLoading}
570- <div class="loading">Loading shortcuts...</div>
571- {:else}
572- <div class="shortcuts-card">
573- {#each shortcuts as shortcut, index (shortcut.key)}
574- <div class="shortcut-item">
575- <div class="shortcut-key-badge">{shortcut.key}</div>
576- <div class="shortcut-desc">{shortcut.description}</div>
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}
577858 </div>
578- {#if index < shortcuts.length - 1}
579- <div class="shortcut-separator"></div>
580- {/if}
581- {/each}
859+ {/if}
582860 </div>
583- {/if}
584- </div>
861+ </section>
585862
586- <h2 class="section-title top-visited-title">Top 5 Most Visited</h2>
587- <div class="top-visited-container">
588- {#if topVisitedLoading}
589- <div class="loading">Loading top visited...</div>
590- {:else if topVisited.length === 0}
591- <div class="empty-state">
592- <p>No visit data available</p>
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>
593867 </div>
594- {:else}
595- <div class="top-visited-list">
596- {#each topVisited as item, index (item.url)}
597- <div
598- class="top-visited-item"
599- role="button"
600- tabindex="0"
601- onclick={() => navigateTo(item.url)}
602- onkeydown={(e) => e.key === 'Enter' && navigateTo(item.url)}
603- >
604- <div class="rank">{index + 1}</div>
605- <div class="site-info">
606- <div class="site-title">{item.title || 'Untitled'}</div>
607- <div class="site-domain">{getDomain(item.url)}</div>
608- </div>
609- <div class="visit-count">{item.visit_count}</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}
610931 </div>
611- {/each}
612932 </div>
613- {/if}
933+ </section>
614934 </div>
615935
616- <h2 class="section-title stats-title">Statistics</h2>
617- <div class="stats-container">
618- {#if statsLoading}
619- <div class="loading">Loading stats...</div>
620- {:else if stats}
621- <div class="stats-grid">
622- <div class="stat-item">
623- <div class="stat-value">{stats.total_entries}</div>
624- <div class="stat-label">History Entries</div>
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}
625952 </div>
626- </div>
627- {:else}
628- <div class="empty-state">
629- <p>No statistics available</p>
630- </div>
631- {/if}
632- </div>
953+ {/if}
954+ </div>
955+ </section>
633956 </div>
634-</div>
635957
636-<Footer />
958+ <Footer />
637959 </div>
638960
639961 <style>
640- /* Remove default browser margins/padding on the page */
641- :global(html), :global(body) {
642- margin: 0;
643- padding: 0;
644- background: #1a1a1a;
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%);
6451592 }
1593+}
6461594
647- /* Scoped homepage container to prevent global CSS conflicts */
648- .homepage-container {
649- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
650- background: #1a1a1a;
651- color: #ffffff;
652- min-height: 100vh;
653- overflow-x: hidden;
654- display: flex;
655- flex-direction: column;
656- margin: 0;
657- padding: 0;
658- box-sizing: border-box;
1595+@media (max-width: 960px) {
1596+ .homepage-shell {
1597+ padding: clamp(1.25rem, 5vw, 2rem);
6591598 }
6601599
661- /* Reset styles for children, but scoped to homepage only */
662- .homepage-container * {
663- margin: 0;
664- padding: 0;
665- box-sizing: border-box;
666- }
667-
668- .container {
669- flex: 1;
670- display: flex;
671- align-items: stretch;
672- max-width: 1200px;
673- margin: 0 auto;
674- width: 100%;
675- padding: 2rem;
676- gap: 2rem;
677- overflow-x: hidden;
678- }
679-
680- .history-section {
681- flex: 2;
682- min-height: 0;
683- min-width: 0;
684- display: flex;
1600+ .main-panels {
6851601 flex-direction: column;
6861602 }
6871603
688- .shortcuts-section {
689- flex: 1;
690- min-height: 0;
691- min-width: 0;
692- }
693-
694- .section-title {
695- font-size: 1.5rem;
696- margin-bottom: 1rem;
697- color: #ffffff;
698- border-bottom: 2px solid #404040;
699- padding-bottom: 0.5rem;
700- }
701-
702- .history-list {
703- overflow-y: auto;
704- overflow-x: hidden;
705- max-width: 100%;
706- max-height: calc(100vh - 8rem);
707- }
708-
709- .history-item {
710- padding: 0.75rem;
711- margin-bottom: 0.5rem;
712- background: #2d2d2d;
713- border-radius: 6px;
714- cursor: pointer;
715- transition: all 0.2s ease-in-out;
716- border-left: 3px solid #404040;
717- overflow: hidden;
718- max-width: 100%;
719- position: relative;
720- opacity: 1;
721- transform: translateX(0);
722- }
723-
724- .history-item:hover {
725- background: #3d3d3d;
726- border-left-color: #0066cc;
727- }
728-
729- .history-item:hover .history-delete {
730- opacity: 1;
731- visibility: visible;
732- }
733-
734- .history-item.deleting {
735- opacity: 0;
736- transform: translateX(-20px);
737- pointer-events: none;
738- }
739-
740- .history-line {
741- display: flex;
742- gap: 0.5rem;
743- white-space: nowrap;
744- align-items: center;
745- overflow: hidden;
746- width: 100%;
747- min-width: 0;
748- }
749-
750- .history-favicon-chip {
751- flex: 0 0 20px;
752- width: 20px;
753- height: 20px;
754- border-radius: 50%;
755- background: #ccc;
756- border: 1px solid rgba(0,0,0,.12);
757- box-shadow: 0 1px 2px rgba(0,0,0,.12);
758- display: flex;
759- align-items: center;
760- justify-content: center;
761- }
762-
763- .history-favicon-img {
764- width: 16px;
765- height: 16px;
766- filter: brightness(1.06) contrast(1.03);
767- image-rendering: -webkit-optimize-contrast;
768- }
769-
770- .history-favicon-fallback {
771- font-size: 12px;
772- display: flex;
773- align-items: center;
774- justify-content: center;
775- color: #888;
776- }
777-
778- .history-title {
779- font-size: 0.95rem;
780- color: #e6e6e6;
781- flex: 0 0 auto;
782- }
783-
784- .history-domain {
785- font-size: 0.9rem;
786- color: #a5a5a5;
787- flex: 0 0 auto;
788- }
789-
790- .history-sep {
791- color: #666;
792- flex: 0 0 auto;
793- }
794-
795- .history-url {
796- font-size: 0.9rem;
797- color: #7a7a7a;
798- flex: 1 1 auto;
799- min-width: 0;
800- overflow: hidden;
801- text-overflow: ellipsis;
802- -webkit-mask-image: linear-gradient(to right, rgba(0,0,0,1) 85%, rgba(0,0,0,0) 100%);
803- mask-image: linear-gradient(to right, rgba(0,0,0,1) 85%, rgba(0,0,0,0) 100%);
804- }
805-
806- .history-time {
807- font-size: 0.85rem;
808- color: #666;
809- flex: 0 0 auto;
810- margin-left: auto;
811- }
812-
813- .history-delete {
814- position: absolute;
815- right: 0.75rem;
816- top: 50%;
817- transform: translateY(-50%);
818- opacity: 0;
819- visibility: hidden;
820- transition: opacity 0.2s, visibility 0.2s, transform 0.2s;
821- cursor: pointer;
822- background: #1a1a1a;
823- border-radius: 3px;
824- padding: 0.25rem;
825- border: 1px solid #404040;
826- display: flex;
827- align-items: center;
828- justify-content: center;
829- width: 2rem;
830- height: 2rem;
831- color: #888;
832- }
833-
834- .history-delete:hover {
835- background: #ff4444;
836- border-color: #ff6666;
837- color: #fff;
838- transform: translateY(-50%) scale(1.1);
839- }
840-
841- .shortcuts-container {
842- /* Remove height limit to show all shortcuts */
1604+ .history-panel,
1605+ .insights-panel {
1606+ min-width: 100%;
8431607 }
8441608
845- .shortcuts-card {
846- background: #2d2d2d;
847- border-radius: 8px;
848- border: 1px solid #404040;
849- padding: 1rem;
1609+ .quick-access-item {
1610+ flex: 1 1 calc(50% - 1rem);
8501611 }
1612+}
8511613
852- .shortcut-item {
853- display: flex;
854- align-items: center;
855- gap: 0.75rem;
856- padding: 0.5rem 0;
857- transition: all 0.2s ease;
1614+@media (max-width: 640px) {
1615+ .theme-toggle-button {
1616+ width: 2.75rem;
1617+ height: 2.75rem;
8581618 }
8591619
860- .shortcut-item:hover {
861- background: rgba(255, 255, 255, 0.02);
862- border-radius: 4px;
863- padding: 0.5rem;
864- margin: 0 -0.5rem;
1620+ .panel-body,
1621+ .panel-header,
1622+ .hero-panel {
1623+ padding-left: 1.25rem;
1624+ padding-right: 1.25rem;
8651625 }
8661626
867- .shortcut-separator {
868- height: 1px;
869- background: #404040;
870- margin: 0.25rem 0;
1627+ .quick-access-item {
1628+ flex: 1 1 100%;
8711629 }
8721630
873- .shortcut-key-badge {
874- background: linear-gradient(135deg, #0066cc, #004499);
875- color: #ffffff;
876- padding: 0.25rem 0.5rem;
877- border-radius: 4px;
878- font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
879- font-size: 0.75rem;
880- font-weight: 600;
881- white-space: nowrap;
882- box-shadow: 0 1px 2px rgba(0, 102, 204, 0.2);
883- border: 1px solid rgba(255, 255, 255, 0.1);
884- min-width: fit-content;
885- }
886-
887- .shortcut-desc {
888- color: #e6e6e6;
889- font-size: 0.9rem;
890- flex: 1;
891- }
892-
893- .stats-title {
894- margin-top: 2rem;
895- }
896-
897- .stats-container {
898- margin-top: 1rem;
899- }
900-
901- .stats-grid {
902- display: flex;
903- gap: 1rem;
904- }
905-
906- .stat-item {
907- background: #2d2d2d;
908- border-radius: 6px;
909- padding: 1rem;
910- text-align: center;
911- flex: 1;
912- border: 1px solid #404040;
913- }
914-
915- .stat-value {
916- font-size: 1.5rem;
917- font-weight: bold;
918- color: #0066cc;
919- margin-bottom: 0.25rem;
920- }
921-
922- .stat-label {
923- font-size: 0.8rem;
924- color: #888;
925- text-transform: uppercase;
926- letter-spacing: 0.5px;
927- }
928-
929- .top-visited-title {
930- margin-top: 2rem;
931- }
932-
933- .top-visited-container {
934- margin-top: 1rem;
935- }
936-
937- .top-visited-list {
938- display: flex;
1631+ .history-item {
9391632 flex-direction: column;
940- gap: 0.5rem;
941- }
942-
943- .top-visited-item {
944- display: flex;
945- align-items: center;
946- padding: 0.75rem;
947- background: #2d2d2d;
948- border-radius: 6px;
949- cursor: pointer;
950- transition: background 0.2s;
951- border: 1px solid #404040;
952- gap: 1rem;
1633+ align-items: flex-start;
9531634 }
9541635
955- .top-visited-item:hover {
956- background: #3d3d3d;
957- border-color: #0066cc;
958- }
959-
960- .rank {
961- font-weight: bold;
962- color: #0066cc;
963- font-size: 1.1rem;
964- min-width: 1.5rem;
965- text-align: center;
966- }
967-
968- .site-info {
969- flex: 1;
970- min-width: 0;
971- }
972-
973- .site-title {
974- font-size: 0.9rem;
975- color: #e6e6e6;
976- margin-bottom: 0.2rem;
977- white-space: nowrap;
978- overflow: hidden;
979- text-overflow: ellipsis;
980- }
981-
982- .site-domain {
983- font-size: 0.8rem;
984- color: #888;
985- }
986-
987- .visit-count {
988- font-size: 0.85rem;
989- color: #0066cc;
990- font-weight: bold;
991- min-width: 2rem;
992- text-align: center;
993- }
994-
995- .loading {
996- padding: 2rem;
997- text-align: center;
998- color: #888;
999- }
1000-
1001- .scroll-sentinel {
1002- height: 1px;
1636+ .history-delete {
10031637 width: 100%;
1638+ border-left: 0;
1639+ border-top: 2px solid var(--dynamic-border);
1640+ align-self: stretch;
10041641 }
10051642
1006- .loading-more {
1007- padding: 1rem;
1008- text-align: center;
1009- color: #888;
1010- font-size: 0.9rem;
1011- }
1012-
1013- .empty-state {
1014- padding: 3rem 2rem;
1015- text-align: center;
1016- color: #666;
1017- }
1018-
1019- .empty-state h3 {
1020- margin-bottom: 1rem;
1021- color: #888;
1643+ .history-item-top {
1644+ flex-direction: column;
1645+ align-items: flex-start;
1646+ gap: 0.35rem;
10221647 }
10231648
1024- @media (max-width: 768px) {
1025- .container {
1026- flex-direction: column;
1027- padding: 1rem;
1028- }
1649+ .history-time {
1650+ order: 3;
10291651 }
1030-</style>⏎̸
1652+}
1653+</style>
@@ -38,6 +38,50 @@
38 let historyOffset = $state(0);38 let historyOffset = $state(0);
39 let hasMoreHistory = $state(true);39 let hasMoreHistory = $state(true);
40 let loadingMoreHistory = $state(false);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+ };
41 85
42 // Function for checking if an item is being deleted86 // Function for checking if an item is being deleted
43 const isDeleting = (id: number) => deletingIds.includes(id);87 const isDeleting = (id: number) => deletingIds.includes(id);
@@ -344,6 +388,14 @@
344 }388 }
345 };389 };
346 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+
347 // Format relative time399 // Format relative time
348 const formatTime = (timestamp: string): string => {400 const formatTime = (timestamp: string): string => {
349 try {401 try {
@@ -366,6 +418,94 @@
366 }418 }
367 };419 };
368 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+
369 // Fetch history stats via message bridge509 // Fetch history stats via message bridge
370 const fetchStats = async () => {510 const fetchStats = async () => {
371 try {511 try {
@@ -465,566 +605,1049 @@
465 });605 });
466 fetchStats();606 fetchStats();
467 fetchTopVisited();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();
468 });648 });
469 649
470 </script>650 </script>
471 651
472-<div class="homepage-container">652+<svelte:head>
473-<div class="container">653+ <title>Dumber Browser - Homepage</title>
474- <div class="history-section">654+ <meta name="description" content="Fast Wayland Browser - Your browsing patterns at a glance" />
475- <h2 class="section-title">Recent History</h2>655+</svelte:head>
476- <div class="history-list">656+
477- {#if historyLoading}657+<div class="homepage-shell">
478- <div class="loading">Loading history...</div>658+ <div class="homepage-content">
479- {:else if historyItems.length === 0}659+ <div class="top-bar">
480- <div class="empty-state">660+ <button
481- <h3>No history yet</h3>661+ class="theme-toggle-button"
482- <p>Start browsing to see your recent history here.</p>662+ type="button"
483- </div>663+ aria-pressed={themeMode === "dark"}
484- {:else}664+ title={themeMode === "dark" ? "Switch to light mode" : "Switch to dark mode"}
485- {#each historyItems as item (item.url)}665+ onclick={toggleTheme}
486- <div666+ >
487- class="history-item"667+ <span class="sr-only">
488- class:deleting={isDeleting(item.id)}668+ {themeMode === "dark" ? "Switch to light mode" : "Switch to dark mode"}
489- role="button"669+ </span>
490- tabindex="0"670+ {#if themeMode === "dark"}
491- onclick={() => navigateTo(item.url)}671+ <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
492- onkeydown={(e) => e.key === 'Enter' && navigateTo(item.url)}672+ <path
493- >673+ fill="currentColor"
494- <div class="history-line">674+ d="M21 12.79A9 9 0 0111.21 3 7 7 0 0012 17a7 7 0 009-4.21z"
495- <div class="history-favicon-chip">675+ />
496- {#if item.favicon_url}676+ </svg>
497- <img677+ {:else}
498- src={item.favicon_url}678+ <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
499- alt=""679+ <circle cx="12" cy="12" r="5" fill="currentColor" />
500- class="history-favicon-img"680+ <g stroke="currentColor" stroke-width="2" stroke-linecap="round">
501- onerror={(e) => {681+ <line x1="12" y1="1" x2="12" y2="4" />
502- const target = e.target as HTMLImageElement;682+ <line x1="12" y1="20" x2="12" y2="23" />
503- if (target) {683+ <line x1="4.22" y1="4.22" x2="6.34" y2="6.34" />
504- target.style.display='none';684+ <line x1="17.66" y1="17.66" x2="19.78" y2="19.78" />
505- const fallback = target.nextElementSibling as HTMLElement;685+ <line x1="1" y1="12" x2="4" y2="12" />
506- if (fallback) fallback.style.display='block';686+ <line x1="20" y1="12" x2="23" y2="12" />
507- }687+ <line x1="4.22" y1="19.78" x2="6.34" y2="17.66" />
508- }}688+ <line x1="17.66" y1="6.34" x2="19.78" y2="4.22" />
509- onload={(e) => {689+ </g>
510- const target = e.target as HTMLImageElement;690+ </svg>
511- if (target) {691+ {/if}
512- const fallback = target.nextElementSibling as HTMLElement;692+ </button>
513- if (fallback) fallback.style.display='none';693+ </div>
514- }694+
515- }}695+ <section class="hero-panel brutal-panel">
516- />696+ <div class="hero-heading">
517- <div class="history-favicon-fallback" style="display: none;">697+ <h1 class="hero-title">Jump back in</h1>
518- <!-- inline globe placeholder -->698+ <p class="hero-subtitle">Direct access to the sites you visit the most, plus what you just explored.</p>
519- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">699+ </div>
520- <circle cx="12" cy="12" r="10" />700+ <div class="quick-access-wrapper">
521- <path d="M2 12h20" />701+ {#if quickAccessLoading}
522- </svg>702+ <div class="loading quick-access-loading">Preparing your shortcuts...</div>
523- </div>703+ {:else if quickAccess.length === 0}
524- {:else}704+ <div class="empty-state quick-access-empty">
525- <div class="history-favicon-fallback">705+ <h3>No quick links yet</h3>
526- <!-- inline globe placeholder -->706+ <p>Browse a few sites and we’ll surface your frequent destinations here.</p>
527- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">707+ </div>
528- <circle cx="12" cy="12" r="10" />708+ {:else}
529- <path d="M2 12h20" />709+ <div class="quick-access-grid">
530- </svg>710+ {#each quickAccess as item (item.id)}
531- </div>
532- {/if}
533- </div>
534- <div class="history-title">{item.title || 'Untitled'}</div>
535- <div class="history-sep">•</div>
536- <div class="history-domain">{getDomain(item.url)}</div>
537- <div class="history-sep">•</div>
538- <div class="history-url">{item.url}</div>
539- <div class="history-time">{formatTime(item.last_visited)}</div>
540 <div711 <div
541- class="history-delete"712+ class="quick-access-item"
542 role="button"713 role="button"
543 tabindex="0"714 tabindex="0"
544- onclick={(e) => deleteHistoryEntry(item.id, e)}715+ onclick={() => navigateTo(item.url)}
545- onkeydown={(e) => e.key === 'Enter' && deleteHistoryEntry(item.id, e)}716+ onkeydown={(e) => e.key === 'Enter' && navigateTo(item.url)}
546- title="Delete this entry"
547 >717 >
548- <!-- inline delete placeholder -->718+ <div class="quick-access-favicon">
549- <span aria-hidden="true" style="display:inline-block;line-height:1;">×</span>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>
550 </div>760 </div>
551- </div>761+ {/each}
552 </div>762 </div>
553- {/each}
554-
555- <!-- Infinite scroll sentinel and loading indicator -->
556- {#if hasMoreHistory}
557- <div bind:this={historyScrollSentinel} class="scroll-sentinel"></div>
558- {#if loadingMoreHistory}
559- <div class="loading-more">Loading more history...</div>
560- {/if}
561 {/if}763 {/if}
562- {/if}764+ </div>
563- </div>765+ </section>
564- </div>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}
565 851
566- <div class="shortcuts-section">852+ {#if hasMoreHistory}
567- <h2 class="section-title">Keyboard Shortcuts</h2>853+ <div bind:this={historyScrollSentinel} class="scroll-sentinel"></div>
568- <div class="shortcuts-container">854+ {#if loadingMoreHistory}
569- {#if shortcutsLoading}855+ <div class="loading-more">Loading more history...</div>
570- <div class="loading">Loading shortcuts...</div>856+ {/if}
571- {:else}857+ {/if}
572- <div class="shortcuts-card">
573- {#each shortcuts as shortcut, index (shortcut.key)}
574- <div class="shortcut-item">
575- <div class="shortcut-key-badge">{shortcut.key}</div>
576- <div class="shortcut-desc">{shortcut.description}</div>
577 </div>858 </div>
578- {#if index < shortcuts.length - 1}859+ {/if}
579- <div class="shortcut-separator"></div>
580- {/if}
581- {/each}
582 </div>860 </div>
583- {/if}861+ </section>
584- </div>
585 862
586- <h2 class="section-title top-visited-title">Top 5 Most Visited</h2>863+ <section class="insights-panel brutal-panel" bind:this={insightsPanelRef}>
587- <div class="top-visited-container">864+ <div class="panel-header">
588- {#if topVisitedLoading}865+ <h2 class="panel-title">Usage Insights</h2>
589- <div class="loading">Loading top visited...</div>866+ <p class="panel-subtitle">Your browsing patterns at a glance.</p>
590- {:else if topVisited.length === 0}
591- <div class="empty-state">
592- <p>No visit data available</p>
593 </div>867 </div>
594- {:else}868+ <div class="panel-body insights-body">
595- <div class="top-visited-list">869+ <div class="insight-block">
596- {#each topVisited as item, index (item.url)}870+ <h3 class="block-title">Top sites</h3>
597- <div871+ {#if topVisitedLoading}
598- class="top-visited-item"872+ <div class="loading">Loading usage insights...</div>
599- role="button"873+ {:else if topVisited.length === 0}
600- tabindex="0"874+ <div class="empty-state compact">
601- onclick={() => navigateTo(item.url)}875+ <p>No visit data available</p>
602- onkeydown={(e) => e.key === 'Enter' && navigateTo(item.url)}876+ </div>
603- >877+ {:else}
604- <div class="rank">{index + 1}</div>878+ <div class="insights-list">
605- <div class="site-info">879+ {#each topVisited as item, index (item.id)}
606- <div class="site-title">{item.title || 'Untitled'}</div>880+ <div
607- <div class="site-domain">{getDomain(item.url)}</div>881+ class="insight-item"
608- </div>882+ role="button"
609- <div class="visit-count">{item.visit_count}</div>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}
610 </div>931 </div>
611- {/each}
612 </div>932 </div>
613- {/if}933+ </section>
614 </div>934 </div>
615 935
616- <h2 class="section-title stats-title">Statistics</h2>936+ <section class="shortcuts-panel brutal-panel">
617- <div class="stats-container">937+ <div class="panel-header">
618- {#if statsLoading}938+ <h2 class="panel-title">Keyboard Shortcuts</h2>
619- <div class="loading">Loading stats...</div>939+ <p class="panel-subtitle">Stay quick with the essentials.</p>
620- {:else if stats}940+ </div>
621- <div class="stats-grid">941+ <div class="panel-body">
622- <div class="stat-item">942+ {#if shortcutsLoading}
623- <div class="stat-value">{stats.total_entries}</div>943+ <div class="loading">Loading shortcuts...</div>
624- <div class="stat-label">History Entries</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}
625 </div>952 </div>
626- </div>953+ {/if}
627- {:else}954+ </div>
628- <div class="empty-state">955+ </section>
629- <p>No statistics available</p>
630- </div>
631- {/if}
632- </div>
633 </div>956 </div>
634-</div>
635 957
636-<Footer />958+ <Footer />
637 </div>959 </div>
638 960
639 <style>961 <style>
640- /* Remove default browser margins/padding on the page */962+/* Homepage Layout Styles */
641- :global(html), :global(body) {963+.homepage-shell {
642- margin: 0;964+ min-height: 100vh;
643- padding: 0;965+ background-color: var(--dynamic-bg);
644- background: #1a1a1a;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%);
645 }1592 }
1593+}
646 1594
647- /* Scoped homepage container to prevent global CSS conflicts */1595+@media (max-width: 960px) {
648- .homepage-container {1596+ .homepage-shell {
649- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;1597+ padding: clamp(1.25rem, 5vw, 2rem);
650- background: #1a1a1a;
651- color: #ffffff;
652- min-height: 100vh;
653- overflow-x: hidden;
654- display: flex;
655- flex-direction: column;
656- margin: 0;
657- padding: 0;
658- box-sizing: border-box;
659 }1598 }
660 1599
661- /* Reset styles for children, but scoped to homepage only */1600+ .main-panels {
662- .homepage-container * {
663- margin: 0;
664- padding: 0;
665- box-sizing: border-box;
666- }
667-
668- .container {
669- flex: 1;
670- display: flex;
671- align-items: stretch;
672- max-width: 1200px;
673- margin: 0 auto;
674- width: 100%;
675- padding: 2rem;
676- gap: 2rem;
677- overflow-x: hidden;
678- }
679-
680- .history-section {
681- flex: 2;
682- min-height: 0;
683- min-width: 0;
684- display: flex;
685 flex-direction: column;1601 flex-direction: column;
686 }1602 }
687 1603
688- .shortcuts-section {1604+ .history-panel,
689- flex: 1;1605+ .insights-panel {
690- min-height: 0;1606+ min-width: 100%;
691- min-width: 0;
692- }
693-
694- .section-title {
695- font-size: 1.5rem;
696- margin-bottom: 1rem;
697- color: #ffffff;
698- border-bottom: 2px solid #404040;
699- padding-bottom: 0.5rem;
700- }
701-
702- .history-list {
703- overflow-y: auto;
704- overflow-x: hidden;
705- max-width: 100%;
706- max-height: calc(100vh - 8rem);
707- }
708-
709- .history-item {
710- padding: 0.75rem;
711- margin-bottom: 0.5rem;
712- background: #2d2d2d;
713- border-radius: 6px;
714- cursor: pointer;
715- transition: all 0.2s ease-in-out;
716- border-left: 3px solid #404040;
717- overflow: hidden;
718- max-width: 100%;
719- position: relative;
720- opacity: 1;
721- transform: translateX(0);
722- }
723-
724- .history-item:hover {
725- background: #3d3d3d;
726- border-left-color: #0066cc;
727- }
728-
729- .history-item:hover .history-delete {
730- opacity: 1;
731- visibility: visible;
732- }
733-
734- .history-item.deleting {
735- opacity: 0;
736- transform: translateX(-20px);
737- pointer-events: none;
738- }
739-
740- .history-line {
741- display: flex;
742- gap: 0.5rem;
743- white-space: nowrap;
744- align-items: center;
745- overflow: hidden;
746- width: 100%;
747- min-width: 0;
748- }
749-
750- .history-favicon-chip {
751- flex: 0 0 20px;
752- width: 20px;
753- height: 20px;
754- border-radius: 50%;
755- background: #ccc;
756- border: 1px solid rgba(0,0,0,.12);
757- box-shadow: 0 1px 2px rgba(0,0,0,.12);
758- display: flex;
759- align-items: center;
760- justify-content: center;
761- }
762-
763- .history-favicon-img {
764- width: 16px;
765- height: 16px;
766- filter: brightness(1.06) contrast(1.03);
767- image-rendering: -webkit-optimize-contrast;
768- }
769-
770- .history-favicon-fallback {
771- font-size: 12px;
772- display: flex;
773- align-items: center;
774- justify-content: center;
775- color: #888;
776- }
777-
778- .history-title {
779- font-size: 0.95rem;
780- color: #e6e6e6;
781- flex: 0 0 auto;
782- }
783-
784- .history-domain {
785- font-size: 0.9rem;
786- color: #a5a5a5;
787- flex: 0 0 auto;
788- }
789-
790- .history-sep {
791- color: #666;
792- flex: 0 0 auto;
793- }
794-
795- .history-url {
796- font-size: 0.9rem;
797- color: #7a7a7a;
798- flex: 1 1 auto;
799- min-width: 0;
800- overflow: hidden;
801- text-overflow: ellipsis;
802- -webkit-mask-image: linear-gradient(to right, rgba(0,0,0,1) 85%, rgba(0,0,0,0) 100%);
803- mask-image: linear-gradient(to right, rgba(0,0,0,1) 85%, rgba(0,0,0,0) 100%);
804- }
805-
806- .history-time {
807- font-size: 0.85rem;
808- color: #666;
809- flex: 0 0 auto;
810- margin-left: auto;
811- }
812-
813- .history-delete {
814- position: absolute;
815- right: 0.75rem;
816- top: 50%;
817- transform: translateY(-50%);
818- opacity: 0;
819- visibility: hidden;
820- transition: opacity 0.2s, visibility 0.2s, transform 0.2s;
821- cursor: pointer;
822- background: #1a1a1a;
823- border-radius: 3px;
824- padding: 0.25rem;
825- border: 1px solid #404040;
826- display: flex;
827- align-items: center;
828- justify-content: center;
829- width: 2rem;
830- height: 2rem;
831- color: #888;
832- }
833-
834- .history-delete:hover {
835- background: #ff4444;
836- border-color: #ff6666;
837- color: #fff;
838- transform: translateY(-50%) scale(1.1);
839- }
840-
841- .shortcuts-container {
842- /* Remove height limit to show all shortcuts */
843 }1607 }
844 1608
845- .shortcuts-card {1609+ .quick-access-item {
846- background: #2d2d2d;1610+ flex: 1 1 calc(50% - 1rem);
847- border-radius: 8px;
848- border: 1px solid #404040;
849- padding: 1rem;
850 }1611 }
1612+}
851 1613
852- .shortcut-item {1614+@media (max-width: 640px) {
853- display: flex;1615+ .theme-toggle-button {
854- align-items: center;1616+ width: 2.75rem;
855- gap: 0.75rem;1617+ height: 2.75rem;
856- padding: 0.5rem 0;
857- transition: all 0.2s ease;
858 }1618 }
859 1619
860- .shortcut-item:hover {1620+ .panel-body,
861- background: rgba(255, 255, 255, 0.02);1621+ .panel-header,
862- border-radius: 4px;1622+ .hero-panel {
863- padding: 0.5rem;1623+ padding-left: 1.25rem;
864- margin: 0 -0.5rem;1624+ padding-right: 1.25rem;
865 }1625 }
866 1626
867- .shortcut-separator {1627+ .quick-access-item {
868- height: 1px;1628+ flex: 1 1 100%;
869- background: #404040;
870- margin: 0.25rem 0;
871 }1629 }
872 1630
873- .shortcut-key-badge {1631+ .history-item {
874- background: linear-gradient(135deg, #0066cc, #004499);
875- color: #ffffff;
876- padding: 0.25rem 0.5rem;
877- border-radius: 4px;
878- font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
879- font-size: 0.75rem;
880- font-weight: 600;
881- white-space: nowrap;
882- box-shadow: 0 1px 2px rgba(0, 102, 204, 0.2);
883- border: 1px solid rgba(255, 255, 255, 0.1);
884- min-width: fit-content;
885- }
886-
887- .shortcut-desc {
888- color: #e6e6e6;
889- font-size: 0.9rem;
890- flex: 1;
891- }
892-
893- .stats-title {
894- margin-top: 2rem;
895- }
896-
897- .stats-container {
898- margin-top: 1rem;
899- }
900-
901- .stats-grid {
902- display: flex;
903- gap: 1rem;
904- }
905-
906- .stat-item {
907- background: #2d2d2d;
908- border-radius: 6px;
909- padding: 1rem;
910- text-align: center;
911- flex: 1;
912- border: 1px solid #404040;
913- }
914-
915- .stat-value {
916- font-size: 1.5rem;
917- font-weight: bold;
918- color: #0066cc;
919- margin-bottom: 0.25rem;
920- }
921-
922- .stat-label {
923- font-size: 0.8rem;
924- color: #888;
925- text-transform: uppercase;
926- letter-spacing: 0.5px;
927- }
928-
929- .top-visited-title {
930- margin-top: 2rem;
931- }
932-
933- .top-visited-container {
934- margin-top: 1rem;
935- }
936-
937- .top-visited-list {
938- display: flex;
939 flex-direction: column;1632 flex-direction: column;
940- gap: 0.5rem;1633+ align-items: flex-start;
941- }
942-
943- .top-visited-item {
944- display: flex;
945- align-items: center;
946- padding: 0.75rem;
947- background: #2d2d2d;
948- border-radius: 6px;
949- cursor: pointer;
950- transition: background 0.2s;
951- border: 1px solid #404040;
952- gap: 1rem;
953 }1634 }
954 1635
955- .top-visited-item:hover {1636+ .history-delete {
956- background: #3d3d3d;
957- border-color: #0066cc;
958- }
959-
960- .rank {
961- font-weight: bold;
962- color: #0066cc;
963- font-size: 1.1rem;
964- min-width: 1.5rem;
965- text-align: center;
966- }
967-
968- .site-info {
969- flex: 1;
970- min-width: 0;
971- }
972-
973- .site-title {
974- font-size: 0.9rem;
975- color: #e6e6e6;
976- margin-bottom: 0.2rem;
977- white-space: nowrap;
978- overflow: hidden;
979- text-overflow: ellipsis;
980- }
981-
982- .site-domain {
983- font-size: 0.8rem;
984- color: #888;
985- }
986-
987- .visit-count {
988- font-size: 0.85rem;
989- color: #0066cc;
990- font-weight: bold;
991- min-width: 2rem;
992- text-align: center;
993- }
994-
995- .loading {
996- padding: 2rem;
997- text-align: center;
998- color: #888;
999- }
1000-
1001- .scroll-sentinel {
1002- height: 1px;
1003 width: 100%;1637 width: 100%;
1638+ border-left: 0;
1639+ border-top: 2px solid var(--dynamic-border);
1640+ align-self: stretch;
1004 }1641 }
1005 1642
1006- .loading-more {1643+ .history-item-top {
1007- padding: 1rem;1644+ flex-direction: column;
1008- text-align: center;1645+ align-items: flex-start;
1009- color: #888;1646+ gap: 0.35rem;
1010- font-size: 0.9rem;
1011- }
1012-
1013- .empty-state {
1014- padding: 3rem 2rem;
1015- text-align: center;
1016- color: #666;
1017- }
1018-
1019- .empty-state h3 {
1020- margin-bottom: 1rem;
1021- color: #888;
1022 }1647 }
1023 1648
1024- @media (max-width: 768px) {1649+ .history-time {
1025- .container {1650+ order: 3;
1026- flex-direction: column;
1027- padding: 1rem;
1028- }
1029 }1651 }
1030-</style>⏎̸1652+}
1653+</style>
M gui/src/pages/homepage.ts +1 -0
@@ -1,4 +1,5 @@
11 import { mount } from "svelte";
2+import "../styles/tailwind.css";
23 import Homepage from "./Homepage.svelte";
34
45 // Proactively unregister any Service Workers and clear caches that may have been
@@ -1,4 +1,5 @@
1 import { mount } from "svelte";1 import { mount } from "svelte";
2+import "../styles/tailwind.css";
2 import Homepage from "./Homepage.svelte";3 import Homepage from "./Homepage.svelte";
3 4
4 // Proactively unregister any Service Workers and clear caches that may have been5 // Proactively unregister any Service Workers and clear caches that may have been
M gui/src/styles/tailwind.css +24 -13
@@ -3,17 +3,19 @@
33 /* Define custom theme colors for v4 */
44 @theme {
55 /* Light theme colors (default) */
6- --color-browser-bg: rgb(255 255 255); /* white */
7- --color-browser-surface: rgb(249 250 251); /* gray-50 */
8- --color-browser-text: rgb(17 24 39); /* gray-900 */
9- --color-browser-muted: rgb(107 114 128); /* gray-500 */
10- --color-browser-accent: rgb(59 130 246); /* blue-500 */
11- --color-browser-border: rgb(229 231 235); /* gray-200 */
6+ --color-browser-bg: rgb(253 252 250);
7+ --color-browser-surface: rgb(245 245 240);
8+ --color-browser-surface-variant: rgb(253 250 245);
9+ --color-browser-text: rgb(17 24 39);
10+ --color-browser-muted: rgb(107 114 128);
11+ --color-browser-accent: rgb(59 130 246);
12+ --color-browser-border: rgb(229 231 235);
1213 }
1314
1415 /* Dynamic theme switching via CSS variables */
1516 :root {
1617 --dynamic-bg: var(--color-browser-bg);
18+ --dynamic-surface-variant: var(--color-browser-surface-variant);
1719 --dynamic-surface: var(--color-browser-surface);
1820 --dynamic-text: var(--color-browser-text);
1921 --dynamic-muted: var(--color-browser-muted);
@@ -23,12 +25,13 @@
2325
2426 /* Dark theme colors (applied via .dark class from GTK) */
2527 .dark {
26- --dynamic-bg: rgb(17 24 39); /* gray-900 */
27- --dynamic-surface: rgb(31 41 55); /* gray-800 */
28- --dynamic-text: rgb(243 244 246); /* gray-100 */
29- --dynamic-muted: rgb(156 163 175); /* gray-400 */
30- --dynamic-accent: rgb(96 165 250); /* blue-400 */
31- --dynamic-border: rgb(55 65 81); /* gray-700 */
28+ --dynamic-bg: rgb(17 24 39);
29+ --dynamic-surface-variant: rgb(21 30 50);
30+ --dynamic-surface: rgb(31 41 55);
31+ --dynamic-text: rgb(243 244 246);
32+ --dynamic-muted: rgb(156 163 175);
33+ --dynamic-accent: rgb(96 165 250);
34+ --dynamic-border: rgb(55 65 81);
3235 }
3336
3437 /* Base styles for browser UI components */
@@ -94,7 +97,6 @@
9497 background-color: var(--dynamic-bg);
9598 color: var(--dynamic-text);
9699 border: 2px solid var(--dynamic-border);
97- border-radius: 0.375rem;
98100 box-shadow:
99101 0 4px 6px -1px rgb(0 0 0 / 0.1),
100102 0 2px 4px -2px rgb(0 0 0 / 0.1);
@@ -271,3 +273,12 @@
271273 box-sizing: border-box;
272274 }
273275 }
276+
277+/* Global body styles */
278+body {
279+ background-color: var(--dynamic-bg);
280+ color: var(--dynamic-text);
281+ transition:
282+ background-color 200ms ease,
283+ color 200ms ease;
284+}
@@ -3,17 +3,19 @@
3 /* Define custom theme colors for v4 */3 /* Define custom theme colors for v4 */
4 @theme {4 @theme {
5 /* Light theme colors (default) */5 /* Light theme colors (default) */
6- --color-browser-bg: rgb(255 255 255); /* white */6+ --color-browser-bg: rgb(253 252 250);
7- --color-browser-surface: rgb(249 250 251); /* gray-50 */7+ --color-browser-surface: rgb(245 245 240);
8- --color-browser-text: rgb(17 24 39); /* gray-900 */8+ --color-browser-surface-variant: rgb(253 250 245);
9- --color-browser-muted: rgb(107 114 128); /* gray-500 */9+ --color-browser-text: rgb(17 24 39);
10- --color-browser-accent: rgb(59 130 246); /* blue-500 */10+ --color-browser-muted: rgb(107 114 128);
11- --color-browser-border: rgb(229 231 235); /* gray-200 */11+ --color-browser-accent: rgb(59 130 246);
12+ --color-browser-border: rgb(229 231 235);
12 }13 }
13 14
14 /* Dynamic theme switching via CSS variables */15 /* Dynamic theme switching via CSS variables */
15 :root {16 :root {
16 --dynamic-bg: var(--color-browser-bg);17 --dynamic-bg: var(--color-browser-bg);
18+ --dynamic-surface-variant: var(--color-browser-surface-variant);
17 --dynamic-surface: var(--color-browser-surface);19 --dynamic-surface: var(--color-browser-surface);
18 --dynamic-text: var(--color-browser-text);20 --dynamic-text: var(--color-browser-text);
19 --dynamic-muted: var(--color-browser-muted);21 --dynamic-muted: var(--color-browser-muted);
@@ -23,12 +25,13 @@
23 25
24 /* Dark theme colors (applied via .dark class from GTK) */26 /* Dark theme colors (applied via .dark class from GTK) */
25 .dark {27 .dark {
26- --dynamic-bg: rgb(17 24 39); /* gray-900 */28+ --dynamic-bg: rgb(17 24 39);
27- --dynamic-surface: rgb(31 41 55); /* gray-800 */29+ --dynamic-surface-variant: rgb(21 30 50);
28- --dynamic-text: rgb(243 244 246); /* gray-100 */30+ --dynamic-surface: rgb(31 41 55);
29- --dynamic-muted: rgb(156 163 175); /* gray-400 */31+ --dynamic-text: rgb(243 244 246);
30- --dynamic-accent: rgb(96 165 250); /* blue-400 */32+ --dynamic-muted: rgb(156 163 175);
31- --dynamic-border: rgb(55 65 81); /* gray-700 */33+ --dynamic-accent: rgb(96 165 250);
34+ --dynamic-border: rgb(55 65 81);
32 }35 }
33 36
34 /* Base styles for browser UI components */37 /* Base styles for browser UI components */
@@ -94,7 +97,6 @@
94 background-color: var(--dynamic-bg);97 background-color: var(--dynamic-bg);
95 color: var(--dynamic-text);98 color: var(--dynamic-text);
96 border: 2px solid var(--dynamic-border);99 border: 2px solid var(--dynamic-border);
97- border-radius: 0.375rem;
98 box-shadow:100 box-shadow:
99 0 4px 6px -1px rgb(0 0 0 / 0.1),101 0 4px 6px -1px rgb(0 0 0 / 0.1),
100 0 2px 4px -2px rgb(0 0 0 / 0.1);102 0 2px 4px -2px rgb(0 0 0 / 0.1);
@@ -271,3 +273,12 @@
271 box-sizing: border-box;273 box-sizing: border-box;
272 }274 }
273 }275 }
276+
277+/* Global body styles */
278+body {
279+ background-color: var(--dynamic-bg);
280+ color: var(--dynamic-text);
281+ transition:
282+ background-color 200ms ease,
283+ color 200ms ease;
284+}

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