feat(ui): dark theme redesign, fix image search and defaults

- Inline CSS in base.html (Inter, dark mode, sticky search, tabs, results)
- Remove HTMX/JS from templates; pagination via GET links
- Atmospheric side gradients + grid; wider column on large viewports
- Parse ?category= for HTML tabs (fixes Images category routing)
- Include bing_images, ddg_images, qwant_images in local_ported defaults
- Default listen port 5355; update Docker, compose, flake, README
- Favicon img uses /favicon/ proxy; preferences without inline JS

Made-with: Cursor
This commit is contained in:
ashisgreat22 2026-03-23 22:49:41 +01:00
parent bdc3dae4f5
commit 518215f62e
16 changed files with 1107 additions and 106 deletions

File diff suppressed because it is too large Load diff

View file

@ -2,9 +2,9 @@
<a class="image-result" href="{{.URL}}" target="_blank" rel="noopener noreferrer">
<div class="image-thumb">
{{if .Thumbnail}}
<img src="{{.Thumbnail}}" alt="{{.Title}}" loading="lazy" onerror="this.parentElement.classList.add('image-error')">
<img src="{{.Thumbnail}}" alt="{{.Title}}" loading="lazy">
{{else}}
<div class="image-placeholder">🖼️</div>
<div class="image-placeholder" aria-hidden="true">🖼️</div>
{{end}}
</div>
<div class="image-meta">

View file

@ -9,7 +9,7 @@
<h2 class="pref-section-title">Appearance</h2>
<div class="pref-row">
<label for="theme-select">Theme</label>
<select name="theme" id="theme-select" onchange="this.form.submit()">
<select name="theme" id="theme-select">
<option value="light" {{if eq .Theme "light"}}selected{{end}}>Light</option>
<option value="dark" {{if eq .Theme "dark"}}selected{{end}}>Dark</option>
</select>
@ -70,7 +70,7 @@
</div>
<div class="pref-row">
<div class="pref-row-info">
<label>Favicon Service</label>
<label for="pref-favicon">Favicon Service</label>
<p class="pref-desc">Fetch favicons for result URLs. "None" is most private.</p>
</div>
<select name="favicon" id="pref-favicon">
@ -105,26 +105,4 @@
</div>
</form>
</div>
<script>
(function() {
// Load saved engine preferences
var savedEngines = JSON.parse(localStorage.getItem('samsa-engines') || 'null');
if (savedEngines) {
savedEngines.forEach(function(engine) {
var checkbox = document.querySelector('input[name="engine"][value="' + engine.id + '"]');
if (checkbox) checkbox.checked = engine.enabled;
});
}
// Save on submit
document.querySelector('.preferences-form').addEventListener('submit', function() {
var engines = [];
document.querySelectorAll('input[name="engine"]').forEach(function(cb) {
engines.push({ id: cb.value, enabled: cb.checked });
});
localStorage.setItem('samsa-engines', JSON.stringify(engines));
});
})();
</script>
{{end}}

View file

@ -4,7 +4,9 @@
<a href="{{.URL}}" target="_blank" rel="noopener noreferrer">{{.SafeTitle}}</a>
</div>
<div class="result_url">
<img class="result-favicon" data-domain="{{.Domain}}" src="" alt="" loading="lazy" style="display:none">
{{if .Domain}}
<img class="result-favicon" src="/favicon/{{.Domain}}" alt="" loading="lazy" width="14" height="14">
{{end}}
<a href="{{.URL}}" target="_blank" rel="noopener noreferrer">{{.URL}}</a>
<span class="engine-badge" data-engine="{{.Engine}}">{{.Engine}}</span>
</div>

View file

@ -2,33 +2,38 @@
{{define "content"}}
<div class="results-container">
<div class="results-header">
<a href="/" class="results-logo">
<svg 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>samsa</span>
</a>
<div class="results-header-inner">
<a href="/" class="results-logo">
<svg 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>samsa</span>
</a>
<form class="header-search" method="GET" action="/search" role="search" hx-get="/search" hx-target="#urls" hx-swap="innerHTML" hx-select="#urls">
<div class="search-box">
<input type="text" name="q" value="{{.Query}}" placeholder="Search…" autocomplete="off">
<button type="submit" class="search-btn" aria-label="Search">
<svg width="16" height="16" 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>
</div>
</form>
<form class="header-search" method="GET" action="/search" role="search">
{{if and .ActiveCategory (ne .ActiveCategory "all")}}
<input type="hidden" name="category" value="{{.ActiveCategory}}">
{{end}}
<div class="search-box">
<input type="text" name="q" value="{{.Query}}" placeholder="Search…" autocomplete="off">
<button type="submit" class="search-btn" aria-label="Search">
<svg width="16" height="16" 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>
</div>
</form>
</div>
</div>
<div class="category-tabs" role="tablist">
<a href="/search?q={{.Query | urlquery}}&category=" class="category-tab {{if eq .ActiveCategory ""}}active{{end}}">All</a>
<a href="/search?q={{.Query | urlquery}}&category=general" class="category-tab {{if eq .ActiveCategory "general"}}active{{end}}">General</a>
<a href="/search?q={{.Query | urlquery}}&category=it" class="category-tab {{if eq .ActiveCategory "it"}}active{{end}}">IT</a>
<a href="/search?q={{.Query | urlquery}}&category=news" class="category-tab {{if eq .ActiveCategory "news"}}active{{end}}">News</a>
<a href="/search?q={{.Query | urlquery}}&category=images" class="category-tab {{if eq .ActiveCategory "images"}}active{{end}}">Images</a>
<a href="/search?q={{.Query | urlquery}}&amp;category=" class="category-tab {{if or (eq .ActiveCategory "") (eq .ActiveCategory "all")}}active{{end}}">All</a>
<a href="/search?q={{.Query | urlquery}}&amp;category=general" class="category-tab {{if eq .ActiveCategory "general"}}active{{end}}">General</a>
<a href="/search?q={{.Query | urlquery}}&amp;category=it" class="category-tab {{if eq .ActiveCategory "it"}}active{{end}}">IT</a>
<a href="/search?q={{.Query | urlquery}}&amp;category=news" class="category-tab {{if eq .ActiveCategory "news"}}active{{end}}">News</a>
<a href="/search?q={{.Query | urlquery}}&amp;category=images" class="category-tab {{if eq .ActiveCategory "images"}}active{{end}}">Images</a>
</div>
<div class="results-content">

View file

@ -1,6 +1,6 @@
{{define "results_inner"}}
{{if .Corrections}}
<div id="corrections" class="correction" hx-swap-oob="true">{{range .Corrections}}{{.}} {{end}}</div>
<div id="corrections" class="correction">{{range .Corrections}}{{.}} {{end}}</div>
{{end}}
{{if or .Answers .Infoboxes}}
@ -11,13 +11,13 @@
</div>
{{end}}
<div class="results-meta" id="results-meta" hx-swap-oob="true">
<div class="results-meta" id="results-meta">
{{if .NumberOfResults}}
<span>{{.NumberOfResults}} results</span>
{{end}}
</div>
<div id="urls" role="main" hx-select="#urls" hx-swap="innerHTML" hx-target="#urls">
<div id="urls" role="main">
{{if .Results}}
{{if .IsImageSearch}}
<div class="image-grid">
@ -40,7 +40,7 @@
{{end}}
{{else if not .Answers}}
<div class="no-results">
<div class="no-results-icon">🔍</div>
<div class="no-results-icon" aria-hidden="true">🔍</div>
<h2>No results found</h2>
<p>Try different keywords or check your spelling.</p>
</div>
@ -48,42 +48,26 @@
</div>
{{if .Pageno}}
<nav class="pagination" role="navigation">
<nav class="pagination" role="navigation" aria-label="Pagination">
{{if gt .Pageno 1}}
<button type="button" class="paginate-btn" data-q="{{.Query}}" data-page="{{.PrevPage}}">← Prev</button>
<a class="pag-link" href="/search?q={{.Query | urlquery}}&amp;pageno={{.PrevPage}}{{if and .ActiveCategory (ne .ActiveCategory "all")}}&amp;category={{.ActiveCategory | urlquery}}{{end}}">← Prev</a>
{{end}}
{{range .PageNumbers}}
{{if .IsCurrent}}
<span class="page-current">{{.Num}}</span>
<span class="page-current" aria-current="page">{{.Num}}</span>
{{else}}
<button type="button" class="paginate-btn" data-q="{{$.Query}}" data-page="{{.Num}}">{{.Num}}</button>
<a class="pag-link" href="/search?q={{$.Query | urlquery}}&amp;pageno={{.Num}}{{if and $.ActiveCategory (ne $.ActiveCategory "all")}}&amp;category={{$.ActiveCategory | urlquery}}{{end}}">{{.Num}}</a>
{{end}}
{{end}}
{{if .HasNext}}
<button type="button" class="paginate-btn" data-q="{{.Query}}" data-page="{{.NextPage}}">Next →</button>
<a class="pag-link" href="/search?q={{.Query | urlquery}}&amp;pageno={{.NextPage}}{{if and .ActiveCategory (ne .ActiveCategory "all")}}&amp;category={{.ActiveCategory | urlquery}}{{end}}">Next →</a>
{{end}}
</nav>
{{end}}
<div class="back-to-top" id="backToTop">
<a href="#">↑ Back to top</a>
<div class="back-to-top">
<a href="#top">↑ Back to top</a>
</div>
<div class="htmx-indicator">Searching…</div>
<script>
(function() {
document.body.addEventListener('click', function(e) {
var btn = e.target.closest('.paginate-btn');
if (!btn) return;
var q = btn.getAttribute('data-q');
var page = btn.getAttribute('data-page');
if (!q || !page) return;
var url = '/search?q=' + encodeURIComponent(q) + '&pageno=' + encodeURIComponent(page);
htmx.ajax(url, {target: '#urls', swap: 'innerHTML', select: '#urls'});
});
})();
</script>
{{end}}

View file

@ -1,5 +1,5 @@
{{define "video_item"}}
<article class="result video-result">
<article class="result video-result" data-engine="{{.Engine}}">
{{if .Thumbnail}}
<div class="result_thumbnail">
<a href="{{.URL}}" target="_blank" rel="noopener noreferrer">
@ -9,13 +9,16 @@
{{end}}
<div class="result_content_wrapper">
<div class="result_header">
<a href="{{.URL}}" target="_blank" rel="noopener noreferrer">{{.Title}}</a>
<a href="{{.URL}}" target="_blank" rel="noopener noreferrer">{{.SafeTitle}}</a>
</div>
<div class="result_url">
<span class="engine-badge">youtube</span>
{{if .URL}}
<a href="{{.URL}}" target="_blank" rel="noopener noreferrer">{{.URL}}</a>
{{end}}
<span class="engine-badge" data-engine="{{.Engine}}">{{.Engine}}</span>
</div>
{{if .Content}}
<p class="result_content">{{.Content}}</p>
<p class="result_content">{{.SafeContent}}</p>
{{end}}
</div>
</article>