feat(settings): add JS module for localStorage preferences and panel
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
4fe78c69ce
commit
2785b84939
1 changed files with 266 additions and 0 deletions
266
internal/views/static/js/settings.js
Normal file
266
internal/views/static/js/settings.js
Normal file
|
|
@ -0,0 +1,266 @@
|
||||||
|
var ALL_ENGINES = [
|
||||||
|
'wikipedia', 'arxiv', 'crossref', 'braveapi',
|
||||||
|
'qwant', 'duckduckgo', 'github', 'reddit', 'bing'
|
||||||
|
];
|
||||||
|
|
||||||
|
var DEFAULT_PREFS = {
|
||||||
|
theme: 'system',
|
||||||
|
engines: ALL_ENGINES.slice(),
|
||||||
|
safeSearch: 'moderate',
|
||||||
|
format: 'html'
|
||||||
|
};
|
||||||
|
|
||||||
|
var STORAGE_KEY = 'kafka_prefs';
|
||||||
|
|
||||||
|
function loadPrefs() {
|
||||||
|
var stored = localStorage.getItem(STORAGE_KEY);
|
||||||
|
var prefs = DEFAULT_PREFS;
|
||||||
|
if (stored) {
|
||||||
|
try {
|
||||||
|
var parsed = JSON.parse(stored);
|
||||||
|
prefs = {
|
||||||
|
theme: parsed.theme || DEFAULT_PREFS.theme,
|
||||||
|
engines: parsed.engines || DEFAULT_PREFS.engines.slice(),
|
||||||
|
safeSearch: parsed.safeSearch || DEFAULT_PREFS.safeSearch,
|
||||||
|
format: parsed.format || DEFAULT_PREFS.format
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
prefs = DEFAULT_PREFS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return prefs;
|
||||||
|
}
|
||||||
|
|
||||||
|
function savePrefs(prefs) {
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(prefs));
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTheme(theme) {
|
||||||
|
if (theme === 'system') {
|
||||||
|
document.documentElement.removeAttribute('data-theme');
|
||||||
|
} else {
|
||||||
|
document.documentElement.setAttribute('data-theme', theme);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncEngineInput(prefs) {
|
||||||
|
var input = document.getElementById('engines-input');
|
||||||
|
if (input) {
|
||||||
|
input.value = prefs.engines.join(',');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closePanel() {
|
||||||
|
var popover = document.getElementById('settings-popover');
|
||||||
|
var trigger = document.getElementById('settings-trigger');
|
||||||
|
if (popover) {
|
||||||
|
popover.setAttribute('data-open', 'false');
|
||||||
|
}
|
||||||
|
if (trigger) {
|
||||||
|
trigger.setAttribute('aria-expanded', 'false');
|
||||||
|
}
|
||||||
|
if (trigger) {
|
||||||
|
trigger.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openPanel() {
|
||||||
|
var popover = document.getElementById('settings-popover');
|
||||||
|
var trigger = document.getElementById('settings-trigger');
|
||||||
|
if (popover) {
|
||||||
|
popover.setAttribute('data-open', 'true');
|
||||||
|
}
|
||||||
|
if (trigger) {
|
||||||
|
trigger.setAttribute('aria-expanded', 'true');
|
||||||
|
}
|
||||||
|
var firstFocusable = popover ? popover.querySelector('button, input, select, textarea, a[href], [tabindex]:not([tabindex="-1"])') : null;
|
||||||
|
if (firstFocusable) {
|
||||||
|
firstFocusable.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(str) {
|
||||||
|
return str
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>');
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPanel(prefs) {
|
||||||
|
var themeIcons = { light: '☀️', dark: '🌙', system: '⌨' };
|
||||||
|
var safeOptions = ['moderate', 'strict', 'off'];
|
||||||
|
var formatOptions = ['html', 'json', 'csv', 'rss'];
|
||||||
|
|
||||||
|
var html = '<div id="settings-panel-inner">';
|
||||||
|
html += '<h2>Settings</h2>';
|
||||||
|
|
||||||
|
// Theme buttons
|
||||||
|
html += '<fieldset><legend>Theme</legend>';
|
||||||
|
html += '<div class="theme-buttons">';
|
||||||
|
['light', 'dark', 'system'].forEach(function(t) {
|
||||||
|
var checked = prefs.theme === t ? ' checked' : '';
|
||||||
|
var icon = escapeHtml(themeIcons[t]);
|
||||||
|
html += '<label><input type="radio" name="theme" value="' + escapeHtml(t) + '"' + checked + '> ' + icon + ' ' + escapeHtml(t.charAt(0).toUpperCase() + t.slice(1)) + '</label>';
|
||||||
|
});
|
||||||
|
html += '</div></fieldset>';
|
||||||
|
|
||||||
|
// Engine checkboxes
|
||||||
|
html += '<fieldset><legend>Engines</legend>';
|
||||||
|
html += '<div class="engine-grid">';
|
||||||
|
ALL_ENGINES.forEach(function(engine) {
|
||||||
|
var checked = prefs.engines.indexOf(engine) !== -1 ? ' checked' : '';
|
||||||
|
html += '<label><input type="checkbox" name="engine" value="' + escapeHtml(engine) + '"' + checked + '> ' + escapeHtml(engine) + '</label>';
|
||||||
|
});
|
||||||
|
html += '</div></fieldset>';
|
||||||
|
|
||||||
|
html += '<p class="notice">Engine changes apply to your next search.</p>';
|
||||||
|
|
||||||
|
// Safe search dropdown
|
||||||
|
html += '<label for="safe-search-select">Safe Search</label>';
|
||||||
|
html += '<select id="safe-search-select">';
|
||||||
|
safeOptions.forEach(function(opt) {
|
||||||
|
var selected = prefs.safeSearch === opt ? ' selected' : '';
|
||||||
|
html += '<option value="' + escapeHtml(opt) + '"' + selected + '>' + escapeHtml(opt.charAt(0).toUpperCase() + opt.slice(1)) + '</option>';
|
||||||
|
});
|
||||||
|
html += '</select>';
|
||||||
|
|
||||||
|
// Format dropdown
|
||||||
|
html += '<label for="format-select">Format</label>';
|
||||||
|
html += '<select id="format-select">';
|
||||||
|
formatOptions.forEach(function(opt) {
|
||||||
|
var selected = prefs.format === opt ? ' selected' : '';
|
||||||
|
html += '<option value="' + escapeHtml(opt) + '"' + selected + '>' + escapeHtml(opt.toUpperCase()) + '</option>';
|
||||||
|
});
|
||||||
|
html += '</select>';
|
||||||
|
|
||||||
|
html += '<button type="button" id="settings-close">Close</button>';
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
document.body.innerHTML = html;
|
||||||
|
|
||||||
|
// Attach listeners to theme buttons
|
||||||
|
var themeButtons = document.querySelectorAll('input[name="theme"]');
|
||||||
|
themeButtons.forEach(function(btn) {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
var newPrefs = loadPrefs();
|
||||||
|
newPrefs.theme = btn.value;
|
||||||
|
savePrefs(newPrefs);
|
||||||
|
applyTheme(newPrefs.theme);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Attach listeners to engine checkboxes
|
||||||
|
var engineCheckboxes = document.querySelectorAll('input[name="engine"]');
|
||||||
|
engineCheckboxes.forEach(function(cb) {
|
||||||
|
cb.addEventListener('change', function() {
|
||||||
|
var newPrefs = loadPrefs();
|
||||||
|
if (cb.checked) {
|
||||||
|
if (newPrefs.engines.indexOf(cb.value) === -1) {
|
||||||
|
newPrefs.engines.push(cb.value);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newPrefs.engines = newPrefs.engines.filter(function(e) { return e !== cb.value; });
|
||||||
|
}
|
||||||
|
savePrefs(newPrefs);
|
||||||
|
syncEngineInput(newPrefs);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Attach listener to safe search dropdown
|
||||||
|
var safeSelect = document.getElementById('safe-search-select');
|
||||||
|
if (safeSelect) {
|
||||||
|
safeSelect.addEventListener('change', function() {
|
||||||
|
var newPrefs = loadPrefs();
|
||||||
|
newPrefs.safeSearch = safeSelect.value;
|
||||||
|
savePrefs(newPrefs);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach listener to format dropdown
|
||||||
|
var formatSelect = document.getElementById('format-select');
|
||||||
|
if (formatSelect) {
|
||||||
|
formatSelect.addEventListener('change', function() {
|
||||||
|
var newPrefs = loadPrefs();
|
||||||
|
newPrefs.format = formatSelect.value;
|
||||||
|
savePrefs(newPrefs);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach listener to close button
|
||||||
|
var closeBtn = document.getElementById('settings-close');
|
||||||
|
if (closeBtn) {
|
||||||
|
closeBtn.addEventListener('click', closePanel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initSettings() {
|
||||||
|
var prefs = loadPrefs();
|
||||||
|
applyTheme(prefs.theme);
|
||||||
|
syncEngineInput(prefs);
|
||||||
|
renderPanel(prefs);
|
||||||
|
|
||||||
|
var trigger = document.getElementById('settings-trigger');
|
||||||
|
var triggerMobile = document.getElementById('settings-trigger-mobile');
|
||||||
|
|
||||||
|
function togglePanel() {
|
||||||
|
var popover = document.getElementById('settings-popover');
|
||||||
|
if (popover && popover.getAttribute('data-open') === 'true') {
|
||||||
|
closePanel();
|
||||||
|
} else {
|
||||||
|
openPanel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trigger) {
|
||||||
|
trigger.addEventListener('click', togglePanel);
|
||||||
|
}
|
||||||
|
if (triggerMobile) {
|
||||||
|
triggerMobile.addEventListener('click', togglePanel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escape key handler
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
var popover = document.getElementById('settings-popover');
|
||||||
|
if (popover && popover.getAttribute('data-open') === 'true') {
|
||||||
|
closePanel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click outside handler
|
||||||
|
document.addEventListener('click', function(e) {
|
||||||
|
var popover = document.getElementById('settings-popover');
|
||||||
|
var trigger = document.getElementById('settings-trigger');
|
||||||
|
if (popover && popover.getAttribute('data-open') === 'true') {
|
||||||
|
if (!popover.contains(e.target) && (!trigger || !trigger.contains(e.target))) {
|
||||||
|
closePanel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Focus trap
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Tab') {
|
||||||
|
var popover = document.getElementById('settings-popover');
|
||||||
|
if (popover && popover.getAttribute('data-open') === 'true') {
|
||||||
|
var focusableElements = popover.querySelectorAll('button, input, select, textarea, a[href], [tabindex]:not([tabindex="-1"])');
|
||||||
|
var firstEl = focusableElements[0];
|
||||||
|
var lastEl = focusableElements[focusableElements.length - 1];
|
||||||
|
if (e.shiftKey && document.activeElement === firstEl) {
|
||||||
|
e.preventDefault();
|
||||||
|
lastEl.focus();
|
||||||
|
} else if (!e.shiftKey && document.activeElement === lastEl) {
|
||||||
|
e.preventDefault();
|
||||||
|
firstEl.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', initSettings);
|
||||||
|
} else {
|
||||||
|
initSettings();
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue