feat: complete UI redesign — modern, clean search interface

- New CSS: complete design system with CSS variables, modern color palette
- Homepage: full-viewport hero with centered search, logo, tagline
- Result cards: rounded, shadowed, with favicons via Google Favicon API
- Layout: sidebar + results grid, responsive
- Typography: proper font stack, variable weights
- Settings panel: polished popover with animations
- Autocomplete: modern dropdown with keyboard nav
- Dark mode: full color palette via data-theme attribute
- Favicon: clean search icon SVG
This commit is contained in:
Franz Kafka 2026-03-22 08:06:31 +00:00
parent 471b9798e1
commit ea9bae88b0
7 changed files with 940 additions and 755 deletions

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{{define "base"}}
<!DOCTYPE html>
<html lang="en">
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
@ -13,31 +13,43 @@
<link rel="icon" href="/static/img/favicon.svg" type="image/svg+xml">
<link title="kafka" type="application/opensearchdescription+xml" rel="search" href="/opensearch.xml">
</head>
<body class="{{if .Query}}search_on_results{{end}}">
<body>
<header class="site-header">
<span class="site-title">kafka</span>
<!-- Desktop trigger (hidden on mobile via CSS) -->
<button id="settings-trigger" class="settings-trigger settings-trigger-desktop"
aria-label="Preferences" aria-expanded="false" aria-controls="settings-popover">&#9881;</button>
<a href="/" class="site-logo">
<svg class="site-logo-mark" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="11" cy="11" r="8"/>
<path d="m21 21-4.35-4.35"/>
</svg>
<span class="site-name">kafka</span>
</a>
<button id="settings-trigger" class="settings-trigger" aria-label="Preferences" aria-expanded="false" aria-controls="settings-popover">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/>
<circle cx="12" cy="12" r="3"/>
</svg>
</button>
</header>
<!-- Mobile FAB trigger (shown only on mobile via CSS) -->
<button id="settings-trigger-mobile" class="settings-trigger settings-trigger-mobile"
aria-label="Preferences" aria-expanded="false" aria-controls="settings-popover"
style="display:none;">&#9881;</button>
<main>
<main class="{{if .Query}}page-results{{else}}page-home{{end}}">
{{template "content" .}}
</main>
<footer>
<p>Powered by <a href="https://git.ashisgreat.xyz/penal-colony/kafka">kafka</a> — a privacy-respecting, open metasearch engine</p>
</footer>
<script src="/static/js/settings.js"></script>
<div id="settings-popover" data-open="false" role="dialog" aria-label="Preferences" aria-modal="true">
<div class="settings-popover-header">
Preferences
<button class="settings-popover-close" aria-label="Close">&#215;</button>
<button class="settings-popover-close" aria-label="Close">×</button>
</div>
<div class="settings-popover-body"></div>
</div>
<div id="autocomplete-dropdown"></div>
<script>
(function () {
'use strict';
@ -50,12 +62,10 @@
var activeIndex = -1;
var fetchController = null;
// Escape regex special chars for highlight matching
function escapeRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
// Highlight matching prefix
function highlight(text, query) {
if (!query) return text;
var re = new RegExp('^(' + escapeRegex(query) + ')', 'i');
@ -72,7 +82,7 @@
var escaped = highlight(suggestions[i], input.value);
html += '<div class="autocomplete-suggestion" data-index="' + i + '">' + escaped + '</div>';
}
html += '<div class="autocomplete-footer">Press <kbd></kbd><kbd></kbd> to navigate, Enter to select, Esc to close</div>';
html += '<div class="autocomplete-footer">↑↓ navigate · Enter select · Esc close</div>';
dropdown.innerHTML = html;
dropdown.classList.add('open');
activeIndex = -1;
@ -141,14 +151,13 @@
});
input.addEventListener('blur', function () {
// Delay to allow click events on suggestions
setTimeout(closeDropdown, 150);
});
dropdown.addEventListener('mousedown', function (e) {
var item = e.target.closest('.autocomplete-suggestion');
if (item) {
e.preventDefault(); // prevent blur from firing before select
e.preventDefault();
var idx = parseInt(item.getAttribute('data-index'), 10);
selectSuggestion(idx);
}

View file

@ -1,15 +1,23 @@
{{define "title"}}{{end}}
{{define "content"}}
<div class="index">
<div class="title"><h1>kafka</h1></div>
<div id="search">
<div class="search-hero">
<div class="hero-logo">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="11" cy="11" r="8"/>
<path d="m21 21-4.35-4.35"/>
</svg>
</div>
<p class="hero-tagline">Search the web privately, without tracking or censorship.</p>
<div class="search-box">
<form method="GET" action="/search" role="search" id="search-form">
<input type="text" name="q" id="q" placeholder="Search…" autocomplete="off" autofocus
hx-get="/search" hx-target="#results" hx-trigger="keyup changed delay:500ms" hx-include="this">
<button type="submit">Search</button>
<input type="hidden" name="engines" id="engines-input" value="">
<input type="text" name="q" id="q" placeholder="Search the web…" autocomplete="off" autofocus>
<button type="submit" class="search-box-submit" aria-label="Search">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"/>
<path d="m21 21-4.35-4.35"/>
</svg>
</button>
</form>
<div id="autocomplete-dropdown"></div>
</div>
</div>
<div id="results"></div>

View file

@ -1,16 +1,15 @@
{{define "result_item"}}
<article class="result">
<h3 class="result_header">
<div class="result_header">
<a href="{{.URL}}" target="_blank" rel="noopener noreferrer">{{.Title}}</a>
</h3>
</div>
<div class="result_url">
<img class="result-favicon" src="https://www.google.com/s2/favicons?domain={{.URL | urlquery}}&sz=32" alt="" loading="lazy" onerror="this.style.display='none'">
<a href="{{.URL}}" target="_blank" rel="noopener noreferrer">{{.URL}}</a>
<span class="engine-badge">{{.Engine}}</span>
</div>
{{if .Content}}
<p class="result_content">{{.Content}}</p>
{{end}}
{{if .Engine}}
<div class="result_engine"><span class="engine">{{.Engine}}</span></div>
{{end}}
</article>
{{end}}

View file

@ -1,15 +1,46 @@
{{define "title"}}{{if .Query}}{{.Query}} — {{end}}{{end}}
{{define "content"}}
<div id="search">
<form method="GET" action="/search" role="search">
<input type="text" name="q" id="q" value="{{.Query}}" autocomplete="off"
hx-get="/search" hx-target="#results" hx-trigger="keyup changed delay:500ms" hx-include="this">
<button type="submit">Search</button>
<input type="hidden" name="engines" id="engines-input" value="">
</form>
</div>
<div class="results-layout">
<!-- Compact search bar -->
<div class="search-compact">
<div class="search-box">
<form method="GET" action="/search" role="search" id="search-form">
<input type="text" name="q" id="q" value="{{.Query}}" autocomplete="off" autofocus
hx-get="/search" hx-target="#results" hx-trigger="keyup changed delay:500ms" hx-include="this">
<button type="submit" class="search-box-submit" aria-label="Search">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"/>
<path d="m21 21-4.35-4.35"/>
</svg>
</button>
</form>
</div>
</div>
<div id="results">
{{template "results_inner" .}}
<!-- Results -->
<div class="results-column" id="results">
{{template "results_inner" .}}
</div>
<!-- Sidebar -->
<aside class="sidebar" id="sidebar">
{{if .Suggestions}}
<div class="sidebar-card">
<div class="sidebar-title">Suggestions</div>
<div class="suggestion-list">
{{range .Suggestions}}<span class="suggestion"><a href="/search?q={{. | urlquery}}">{{.}}</a></span>{{end}}
</div>
</div>
{{end}}
{{if .UnresponsiveEngines}}
<div class="sidebar-card">
<div class="sidebar-title">Engines with issues</div>
<ul class="unresponsive-engines">
{{range .UnresponsiveEngines}}<li>{{index . 0}}: {{index . 1}}</li>{{end}}
</ul>
</div>
{{end}}
</aside>
</div>
{{end}}

View file

@ -1,51 +1,26 @@
{{define "results_inner"}}
{{if .Corrections}}
<div class="corrections">
{{range .Corrections}}<span class="correction">{{.}}</span>{{end}}
</div>
<div class="correction">{{range .Corrections}}{{.}} {{end}}</div>
{{end}}
{{if .Answers}}
{{if or .Answers .Infoboxes}}
<div id="answers">
{{range .Answers}}
<div class="answer">{{.}}</div>
<div class="dialog-error">{{.}}</div>
{{end}}
{{range .Infoboxes}}
<div class="infobox">
{{if .title}}<div class="title">{{.title}}</div>{{end}}
{{if .content}}<div>{{.content}}</div>{{end}}
{{if .img_src}}<img src="{{.img_src}}" alt="{{.title}}">{{end}}
</div>
{{end}}
</div>
{{end}}
<div id="sidebar">
<div class="results-meta">
{{if .NumberOfResults}}
<p id="result_count"><small>{{.NumberOfResults}} results</small></p>
{{end}}
{{if .Infoboxes}}
<div id="infoboxes">
{{range .Infoboxes}}
<div class="infobox">
{{if .title}}<div class="title">{{.title}}</div>{{end}}
{{if .content}}<div class="content">{{.content}}</div>{{end}}
{{if .img_src}}<img src="{{.img_src}}" alt="{{.title}}" loading="lazy">{{end}}
</div>
{{end}}
</div>
{{end}}
{{if .Suggestions}}
<div id="suggestions">
<small>Suggestions:</small>
<div>
{{range .Suggestions}}<span class="suggestion"><a href="/search?q={{.}}">{{.}}</a></span>{{end}}
</div>
</div>
{{end}}
{{if .UnresponsiveEngines}}
<div class="unresponsive_engines">
<small>Unresponsive engines:</small>
<ul>
{{range .UnresponsiveEngines}}<li>{{index . 0}}: {{index . 1}}</li>{{end}}
</ul>
</div>
<span>{{.NumberOfResults}} results</span>
{{end}}
</div>
@ -59,51 +34,47 @@
{{end}}
{{end}}
{{else if not .Answers}}
<div class="no_results">
<p>No results found.</p>
{{if .Query}}<p>Try different keywords or check your spelling.</p>{{end}}
<div class="no-results">
<div class="no-results-icon">🔍</div>
<h2>No results found</h2>
<p>Try different keywords or check your spelling.</p>
</div>
{{end}}
</div>
{{if .Pageno}}
<nav id="pagination" role="navigation">
<nav class="pagination" role="navigation">
{{if gt .Pageno 1}}
<form method="GET" action="/search" class="previous_page">
<form method="GET" action="/search" class="prev-next">
<input type="hidden" name="q" value="{{.Query}}">
<input type="hidden" name="pageno" value="{{.PrevPage}}">
<input type="hidden" name="format" value="html">
<button type="submit" role="link">← Previous</button>
<button type="submit">← Prev</button>
</form>
{{end}}
<div class="numbered_pagination">
{{range .PageNumbers}}
{{if .IsCurrent}}
<span class="page_number_current">{{.Num}}</span>
{{else}}
<form method="GET" action="/search" class="page_number">
<input type="hidden" name="q" value="{{$.Query}}">
<input type="hidden" name="pageno" value="{{.Num}}">
<input type="hidden" name="format" value="html">
<button type="submit" role="link">{{.Num}}</button>
</form>
{{end}}
{{end}}
</div>
{{range .PageNumbers}}
{{if .IsCurrent}}
<span class="page-current">{{.Num}}</span>
{{else}}
<form method="GET" action="/search" class="page-link">
<input type="hidden" name="q" value="{{$.Query}}">
<input type="hidden" name="pageno" value="{{.Num}}">
<button type="submit">{{.Num}}</button>
</form>
{{end}}
{{end}}
{{if .HasNext}}
<form method="GET" action="/search" class="next_page">
<form method="GET" action="/search" class="prev-next">
<input type="hidden" name="q" value="{{.Query}}">
<input type="hidden" name="pageno" value="{{.NextPage}}">
<input type="hidden" name="format" value="html">
<button type="submit" role="link">Next →</button>
<button type="submit">Next →</button>
</form>
{{end}}
</nav>
{{end}}
<div id="backToTop">
<div class="back-to-top" id="backToTop">
<a href="#">↑ Back to top</a>
</div>

View file

@ -8,15 +8,15 @@
</div>
{{end}}
<div class="result_content_wrapper">
<h3 class="result_header">
<div class="result_header">
<a href="{{.URL}}" target="_blank" rel="noopener noreferrer">{{.Title}}</a>
</h3>
</div>
<div class="result_url">
<span class="engine-badge">youtube</span>
</div>
{{if .Content}}
<p class="result_content">{{.Content}}</p>
{{end}}
{{if .Engine}}
<div class="result_engine"><span class="engine">{{.Engine}}</span></div>
{{end}}
</div>
</article>
{{end}}