kafka/internal/views/templates/base.html
ashisgreat22 84777211f8 feat(settings): add gear trigger and panel markup to base template
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 03:00:12 +01:00

160 lines
5.8 KiB
HTML

{{define "base"}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="referrer" content="no-referrer">
<meta name="robots" content="noarchive">
<meta name="description" content="kafka — a privacy-respecting, open metasearch engine">
<title>{{template "title" .}}kafka</title>
<link rel="stylesheet" href="/static/css/kafka.css">
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
<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}}">
<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>
</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>
{{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>
</div>
<div class="settings-popover-body"></div>
</div>
<script>
(function () {
'use strict';
var input = document.getElementById('q');
var dropdown = document.getElementById('autocomplete-dropdown');
var form = document.getElementById('search-form');
var debounceTimer = null;
var suggestions = [];
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');
return text.replace(re, '<mark>$1</mark>');
}
function renderDropdown() {
if (suggestions.length === 0) {
dropdown.classList.remove('open');
return;
}
var html = '';
for (var i = 0; i < suggestions.length; i++) {
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>';
dropdown.innerHTML = html;
dropdown.classList.add('open');
activeIndex = -1;
}
function closeDropdown() {
dropdown.classList.remove('open');
suggestions = [];
activeIndex = -1;
}
function selectSuggestion(index) {
if (index < 0 || index >= suggestions.length) return;
input.value = suggestions[index];
closeDropdown();
form.submit();
}
function updateActive(newIndex) {
var items = dropdown.querySelectorAll('.autocomplete-suggestion');
items.forEach(function (el) { el.classList.remove('active'); });
if (newIndex >= 0 && newIndex < items.length) {
items[newIndex].classList.add('active');
items[newIndex].scrollIntoView({ block: 'nearest' });
}
activeIndex = newIndex;
}
function fetchSuggestions(query) {
if (fetchController) fetchController.abort();
fetchController = new AbortController();
fetch('/autocompleter?q=' + encodeURIComponent(query), { signal: fetchController.signal })
.then(function (r) { return r.json(); })
.then(function (data) {
suggestions = data || [];
renderDropdown();
})
.catch(function (e) {
if (e.name !== 'AbortError') suggestions = [];
dropdown.classList.remove('open');
});
}
input.addEventListener('input', function () {
clearTimeout(debounceTimer);
var q = input.value.trim();
if (q.length < 2) { closeDropdown(); return; }
debounceTimer = setTimeout(function () { fetchSuggestions(q); }, 250);
});
input.addEventListener('keydown', function (e) {
if (!dropdown.classList.contains('open')) return;
var items = dropdown.querySelectorAll('.autocomplete-suggestion');
if (e.key === 'ArrowDown') {
e.preventDefault();
updateActive(Math.min(activeIndex + 1, items.length - 1));
} else if (e.key === 'ArrowUp') {
e.preventDefault();
updateActive(Math.max(activeIndex - 1, -1));
} else if (e.key === 'Enter' && activeIndex >= 0) {
e.preventDefault();
selectSuggestion(activeIndex);
} else if (e.key === 'Escape') {
closeDropdown();
}
});
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
var idx = parseInt(item.getAttribute('data-index'), 10);
selectSuggestion(idx);
}
});
}());
</script>
</body>
</html>
{{end}}