chore: remove stale design and implementation plan docs
This commit is contained in:
parent
015f8b357a
commit
1e81eea28e
4 changed files with 0 additions and 2377 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -1,747 +0,0 @@
|
|||
# Settings UI Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** A preferences popover panel (top-right on desktop, bottom sheet on mobile) that lets users set theme, enabled engines, safe search, and default format. All changes auto-save to `localStorage` and apply immediately to the DOM.
|
||||
|
||||
**Architecture:** Pure client-side JS + CSS added alongside existing templates. No Go changes. Settings persist via `localStorage` key `samsa_prefs`. Theme applies via `data-theme` attribute on `<html>`.
|
||||
|
||||
**Tech Stack:** Vanilla JS (no framework), existing `samsa.css` custom properties, HTMX for search.
|
||||
|
||||
---
|
||||
|
||||
## File Map
|
||||
|
||||
| Action | File |
|
||||
|--------|------|
|
||||
| Create | `internal/views/static/js/settings.js` |
|
||||
| Modify | `internal/views/static/css/samsa.css` |
|
||||
| Modify | `internal/views/templates/base.html` |
|
||||
| Modify | `internal/views/templates/index.html` |
|
||||
| Modify | `internal/views/templates/results.html` |
|
||||
| Modify | `internal/views/views.go` |
|
||||
|
||||
**Key insight on engine preferences:** `ParseSearchRequest` reads `engines` as a CSV form value (`r.FormValue("engines")`). The search forms in `index.html` and `results.html` will get a hidden `#engines-input` field that is kept in sync with localStorage. On submit, the engines preference is sent as a normal form field. HTMX `hx-include="this"` already includes the form element, so the hidden input is automatically included in the request.
|
||||
|
||||
---
|
||||
|
||||
## Task 1: CSS — Popover, toggles, bottom sheet
|
||||
|
||||
**Files:**
|
||||
- Modify: `internal/views/static/css/samsa.css`
|
||||
|
||||
- [ ] **Step 1: Add CSS for popover, triggers, toggles, bottom sheet**
|
||||
|
||||
Append the following to `samsa.css`:
|
||||
|
||||
```css
|
||||
/* ============================================
|
||||
Settings Panel
|
||||
============================================ */
|
||||
|
||||
/* Header */
|
||||
.site-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.6rem 1rem;
|
||||
background: var(--color-header-background);
|
||||
border-bottom: 1px solid var(--color-header-border);
|
||||
}
|
||||
.site-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-base-font);
|
||||
}
|
||||
|
||||
/* Gear trigger button */
|
||||
.settings-trigger {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.1rem;
|
||||
cursor: pointer;
|
||||
padding: 0.3rem 0.5rem;
|
||||
border-radius: var(--radius);
|
||||
color: var(--color-base-font);
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s, background 0.2s;
|
||||
line-height: 1;
|
||||
}
|
||||
.settings-trigger:hover,
|
||||
.settings-trigger[aria-expanded="true"] {
|
||||
opacity: 1;
|
||||
background: var(--color-sidebar-background);
|
||||
}
|
||||
|
||||
/* Popover panel */
|
||||
.settings-popover {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
width: 280px;
|
||||
max-height: 420px;
|
||||
overflow-y: auto;
|
||||
background: var(--color-base-background);
|
||||
border: 1px solid var(--color-sidebar-border);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
z-index: 200;
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
}
|
||||
.settings-popover[data-open="true"] {
|
||||
display: flex;
|
||||
animation: settings-slide-in 0.2s ease;
|
||||
}
|
||||
@keyframes settings-slide-in {
|
||||
from { opacity: 0; transform: translateY(-8px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.settings-popover-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--color-sidebar-border);
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.settings-popover-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.2rem;
|
||||
cursor: pointer;
|
||||
color: var(--color-base-font);
|
||||
opacity: 0.6;
|
||||
padding: 0 0.25rem;
|
||||
line-height: 1;
|
||||
}
|
||||
.settings-popover-close:hover { opacity: 1; }
|
||||
|
||||
.settings-popover-body {
|
||||
padding: 0.8rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.settings-section-title {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--color-suggestion);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* Theme buttons */
|
||||
.theme-buttons {
|
||||
display: flex;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
.theme-btn {
|
||||
flex: 1;
|
||||
padding: 0.35rem 0.5rem;
|
||||
border: 1px solid var(--color-sidebar-border);
|
||||
border-radius: var(--radius);
|
||||
background: var(--color-btn-background);
|
||||
color: var(--color-base-font);
|
||||
cursor: pointer;
|
||||
font-size: 0.75rem;
|
||||
text-align: center;
|
||||
transition: background 0.15s, border-color 0.15s;
|
||||
}
|
||||
.theme-btn:hover { background: var(--color-btn-hover); }
|
||||
.theme-btn.active {
|
||||
background: var(--color-link);
|
||||
color: #fff;
|
||||
border-color: var(--color-link);
|
||||
}
|
||||
|
||||
/* Engine toggles — 2-column grid */
|
||||
.engine-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
.engine-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.3rem 0.5rem;
|
||||
border-radius: var(--radius);
|
||||
background: var(--color-sidebar-background);
|
||||
font-size: 0.78rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.engine-toggle input[type="checkbox"] {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
accent-color: var(--color-link);
|
||||
}
|
||||
.engine-toggle span {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Search defaults */
|
||||
.setting-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.4rem;
|
||||
}
|
||||
.setting-row label {
|
||||
font-size: 0.85rem;
|
||||
flex: 1;
|
||||
}
|
||||
.setting-row select {
|
||||
width: 110px;
|
||||
padding: 0.3rem 0.4rem;
|
||||
font-size: 0.8rem;
|
||||
border: 1px solid var(--color-sidebar-border);
|
||||
border-radius: var(--radius);
|
||||
background: var(--color-base-background);
|
||||
color: var(--color-base-font);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Mid-search notice */
|
||||
.settings-notice {
|
||||
font-size: 0.72rem;
|
||||
color: var(--color-suggestion);
|
||||
margin-top: 0.3rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Dark theme via data-theme attribute */
|
||||
html[data-theme="dark"] {
|
||||
--color-base: #222;
|
||||
--color-base-font: #dcdcdc;
|
||||
--color-base-background: #2b2b2b;
|
||||
--color-header-background: #333;
|
||||
--color-header-border: #444;
|
||||
--color-search-border: #555;
|
||||
--color-search-focus: #5dade2;
|
||||
--color-result-url: #8ab4f8;
|
||||
--color-result-url-visited: #b39ddb;
|
||||
--color-result-content: #b0b0b0;
|
||||
--color-result-title: #8ab4f8;
|
||||
--color-result-title-visited: #b39ddb;
|
||||
--color-result-engine: #999;
|
||||
--color-result-border: #3a3a3a;
|
||||
--color-link: #5dade2;
|
||||
--color-link-visited: #b39ddb;
|
||||
--color-sidebar-background: #333;
|
||||
--color-sidebar-border: #444;
|
||||
--color-infobox-background: #333;
|
||||
--color-infobox-border: #444;
|
||||
--color-pagination-current: #5dade2;
|
||||
--color-pagination-border: #444;
|
||||
--color-error: #e74c3c;
|
||||
--color-error-background: #3b1a1a;
|
||||
--color-suggestion: #999;
|
||||
--color-footer: #666;
|
||||
--color-btn-background: #333;
|
||||
--color-btn-border: #555;
|
||||
--color-btn-hover: #444;
|
||||
}
|
||||
|
||||
/* Mobile: Bottom sheet + FAB trigger */
|
||||
@media (max-width: 768px) {
|
||||
/* Hide desktop trigger, show FAB */
|
||||
.settings-trigger-desktop {
|
||||
display: none;
|
||||
}
|
||||
.settings-trigger-mobile {
|
||||
display: block;
|
||||
}
|
||||
.settings-popover {
|
||||
position: fixed;
|
||||
top: auto;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
max-height: 70vh;
|
||||
border-radius: var(--radius) var(--radius) 0 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
/* FAB: fixed bottom-right button visible only on mobile */
|
||||
.settings-trigger-mobile {
|
||||
display: block;
|
||||
position: fixed;
|
||||
bottom: 1.5rem;
|
||||
right: 1.5rem;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-link);
|
||||
color: #fff;
|
||||
border: none;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
||||
font-size: 1.2rem;
|
||||
z-index: 199;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note: The existing `:root` and `@media (prefers-color-scheme: dark)` blocks provide the "system" theme. `html[data-theme="dark"]` overrides only apply when the user explicitly picks dark mode. When `theme === 'system'`, the `data-theme` attribute is removed and the browser's `prefers-color-scheme` media query kicks in via the existing CSS.
|
||||
|
||||
- [ ] **Step 2: Verify existing tests still pass**
|
||||
|
||||
Run: `go test ./...`
|
||||
Expected: all pass
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add internal/views/static/css/samsa.css
|
||||
git commit -m "feat(settings): add popover, toggle, and bottom-sheet CSS"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: JS — Settings logic
|
||||
|
||||
**Files:**
|
||||
- Create: `internal/views/static/js/settings.js`
|
||||
|
||||
- [ ] **Step 1: Write the settings JS module**
|
||||
|
||||
Create `internal/views/static/js/settings.js`:
|
||||
|
||||
```javascript
|
||||
'use strict';
|
||||
|
||||
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 = 'samsa_prefs';
|
||||
|
||||
// ── Persistence ──────────────────────────────────────────────────────────────
|
||||
|
||||
function loadPrefs() {
|
||||
try {
|
||||
var raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) return { theme: DEFAULT_PREFS.theme, engines: DEFAULT_PREFS.engines.slice(), safeSearch: DEFAULT_PREFS.safeSearch, format: DEFAULT_PREFS.format };
|
||||
var saved = JSON.parse(raw);
|
||||
return { theme: saved.theme || DEFAULT_PREFS.theme, engines: saved.engines || DEFAULT_PREFS.engines.slice(), safeSearch: saved.safeSearch || DEFAULT_PREFS.safeSearch, format: saved.format || DEFAULT_PREFS.format };
|
||||
} catch (e) {
|
||||
return { theme: DEFAULT_PREFS.theme, engines: DEFAULT_PREFS.engines.slice(), safeSearch: DEFAULT_PREFS.safeSearch, format: DEFAULT_PREFS.format };
|
||||
}
|
||||
}
|
||||
|
||||
function savePrefs(prefs) {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify({ theme: prefs.theme, engines: prefs.engines, safeSearch: prefs.safeSearch, format: prefs.format }));
|
||||
} catch (e) { /* quota or private mode */ }
|
||||
}
|
||||
|
||||
// ── Theme application ────────────────────────────────────────────────────────
|
||||
|
||||
function applyTheme(theme) {
|
||||
if (theme === 'system') {
|
||||
document.documentElement.removeAttribute('data-theme');
|
||||
} else {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Engine input sync ─────────────────────────────────────────────────────────
|
||||
|
||||
function syncEngineInput(prefs) {
|
||||
var input = document.getElementById('engines-input');
|
||||
if (input) input.value = prefs.engines.join(',');
|
||||
}
|
||||
|
||||
// ── Panel open / close ────────────────────────────────────────────────────────
|
||||
|
||||
function closePanel() {
|
||||
var panel = document.getElementById('settings-popover');
|
||||
var trigger = document.getElementById('settings-trigger');
|
||||
if (!panel) return;
|
||||
panel.setAttribute('data-open', 'false');
|
||||
if (trigger) trigger.setAttribute('aria-expanded', 'false');
|
||||
if (trigger) trigger.focus();
|
||||
}
|
||||
|
||||
function openPanel() {
|
||||
var panel = document.getElementById('settings-popover');
|
||||
var trigger = document.getElementById('settings-trigger');
|
||||
if (!panel) return;
|
||||
panel.setAttribute('data-open', 'true');
|
||||
if (trigger) trigger.setAttribute('aria-expanded', 'true');
|
||||
var focusable = panel.querySelector('button, input, select');
|
||||
if (focusable) focusable.focus();
|
||||
}
|
||||
|
||||
// ── Escape key ───────────────────────────────────────────────────────────────
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key !== 'Escape') return;
|
||||
var panel = document.getElementById('settings-popover');
|
||||
if (!panel || panel.getAttribute('data-open') !== 'true') return;
|
||||
closePanel();
|
||||
});
|
||||
|
||||
// ── Click outside ─────────────────────────────────────────────────────────────
|
||||
|
||||
document.addEventListener('click', function(e) {
|
||||
var panel = document.getElementById('settings-popover');
|
||||
var trigger = document.getElementById('settings-trigger');
|
||||
if (!panel || panel.getAttribute('data-open') !== 'true') return;
|
||||
if (!panel.contains(e.target) && (!trigger || !trigger.contains(e.target))) {
|
||||
closePanel();
|
||||
}
|
||||
});
|
||||
|
||||
// ── Focus trap ────────────────────────────────────────────────────────────────
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key !== 'Tab') return;
|
||||
var panel = document.getElementById('settings-popover');
|
||||
if (!panel || panel.getAttribute('data-open') !== 'true') return;
|
||||
var focusable = Array.prototype.slice.call(panel.querySelectorAll('button, input, select, [tabindex]:not([tabindex="-1"])'));
|
||||
if (!focusable.length) return;
|
||||
var first = focusable[0];
|
||||
var last = focusable[focusable.length - 1];
|
||||
if (e.shiftKey) {
|
||||
if (document.activeElement === first) { e.preventDefault(); last.focus(); }
|
||||
} else {
|
||||
if (document.activeElement === last) { e.preventDefault(); first.focus(); }
|
||||
}
|
||||
});
|
||||
|
||||
// ── Render ────────────────────────────────────────────────────────────────────
|
||||
|
||||
function escapeHtml(str) {
|
||||
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
function renderPanel(prefs) {
|
||||
var panel = document.getElementById('settings-popover');
|
||||
if (!panel) return;
|
||||
var body = panel.querySelector('.settings-popover-body');
|
||||
if (!body) return;
|
||||
|
||||
var themeBtns = '';
|
||||
['light', 'dark', 'system'].forEach(function(t) {
|
||||
var icons = { light: '\u2600', dark: '\u263D', system: '\u2318' };
|
||||
var labels = { light: 'Light', dark: 'Dark', system: 'System' };
|
||||
var active = prefs.theme === t ? ' active' : '';
|
||||
themeBtns += '<button class="theme-btn' + active + '" data-theme="' + t + '">' + icons[t] + ' ' + labels[t] + '</button>';
|
||||
});
|
||||
|
||||
var engineToggles = '';
|
||||
ALL_ENGINES.forEach(function(name) {
|
||||
var checked = prefs.engines.indexOf(name) !== -1 ? ' checked' : '';
|
||||
engineToggles += '<label class="engine-toggle"><input type="checkbox" value="' + escapeHtml(name) + '"' + checked + '><span>' + escapeHtml(name) + '</span></label>';
|
||||
});
|
||||
|
||||
var ssOptions = [
|
||||
{ val: 'moderate', label: 'Moderate' },
|
||||
{ val: 'strict', label: 'Strict' },
|
||||
{ val: 'off', label: 'Off' }
|
||||
];
|
||||
var fmtOptions = [
|
||||
{ val: 'html', label: 'HTML' },
|
||||
{ val: 'json', label: 'JSON' },
|
||||
{ val: 'csv', label: 'CSV' },
|
||||
{ val: 'rss', label: 'RSS' }
|
||||
];
|
||||
var ssOptionsHtml = '';
|
||||
var fmtOptionsHtml = '';
|
||||
ssOptions.forEach(function(o) {
|
||||
var sel = prefs.safeSearch === o.val ? ' selected' : '';
|
||||
ssOptionsHtml += '<option value="' + o.val + '"' + sel + '>' + o.label + '</option>';
|
||||
});
|
||||
fmtOptions.forEach(function(o) {
|
||||
var sel = prefs.format === o.val ? ' selected' : '';
|
||||
fmtOptionsHtml += '<option value="' + o.val + '"' + sel + '>' + o.label + '</option>';
|
||||
});
|
||||
|
||||
body.innerHTML =
|
||||
'<div class="settings-section">' +
|
||||
'<div class="settings-section-title">Appearance</div>' +
|
||||
'<div class="theme-buttons">' + themeBtns + '</div>' +
|
||||
'</div>' +
|
||||
'<div class="settings-section">' +
|
||||
'<div class="settings-section-title">Engines</div>' +
|
||||
'<div class="engine-grid">' + engineToggles + '</div>' +
|
||||
'<p class="settings-notice">Engine changes apply to your next search.</p>' +
|
||||
'</div>' +
|
||||
'<div class="settings-section">' +
|
||||
'<div class="settings-section-title">Search Defaults</div>' +
|
||||
'<div class="setting-row">' +
|
||||
'<label for="pref-safesearch">Safe search</label>' +
|
||||
'<select id="pref-safesearch">' + ssOptionsHtml + '</select>' +
|
||||
'</div>' +
|
||||
'<div class="setting-row">' +
|
||||
'<label for="pref-format">Default format</label>' +
|
||||
'<select id="pref-format">' + fmtOptionsHtml + '</select>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
// Theme buttons
|
||||
var themeBtnEls = panel.querySelectorAll('.theme-btn');
|
||||
for (var i = 0; i < themeBtnEls.length; i++) {
|
||||
themeBtnEls[i].addEventListener('click', (function(btn) {
|
||||
return function() {
|
||||
prefs.theme = btn.getAttribute('data-theme');
|
||||
savePrefs(prefs);
|
||||
applyTheme(prefs.theme);
|
||||
syncEngineInput(prefs);
|
||||
renderPanel(prefs);
|
||||
};
|
||||
})(themeBtnEls[i]));
|
||||
}
|
||||
|
||||
// Engine checkboxes
|
||||
var checkboxes = panel.querySelectorAll('.engine-toggle input[type="checkbox"]');
|
||||
for (var j = 0; j < checkboxes.length; j++) {
|
||||
checkboxes[j].addEventListener('change', (function(cb) {
|
||||
return function() {
|
||||
var checked = Array.prototype.slice.call(panel.querySelectorAll('.engine-toggle input[type="checkbox"]:checked')).map(function(el) { return el.value; });
|
||||
if (checked.length === 0) { cb.checked = true; return; }
|
||||
prefs.engines = checked;
|
||||
savePrefs(prefs);
|
||||
syncEngineInput(prefs);
|
||||
};
|
||||
})(checkboxes[j]));
|
||||
}
|
||||
|
||||
// Safe search
|
||||
var ssEl = panel.querySelector('#pref-safesearch');
|
||||
if (ssEl) {
|
||||
ssEl.addEventListener('change', function() {
|
||||
prefs.safeSearch = ssEl.value;
|
||||
savePrefs(prefs);
|
||||
});
|
||||
}
|
||||
|
||||
// Format
|
||||
var fmtEl = panel.querySelector('#pref-format');
|
||||
if (fmtEl) {
|
||||
fmtEl.addEventListener('change', function() {
|
||||
prefs.format = fmtEl.value;
|
||||
savePrefs(prefs);
|
||||
});
|
||||
}
|
||||
|
||||
// Close button
|
||||
var closeBtn = panel.querySelector('.settings-popover-close');
|
||||
if (closeBtn) closeBtn.addEventListener('click', closePanel);
|
||||
}
|
||||
|
||||
// ── Init ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
function initSettings() {
|
||||
var prefs = loadPrefs();
|
||||
applyTheme(prefs.theme);
|
||||
syncEngineInput(prefs);
|
||||
|
||||
var panel = document.getElementById('settings-popover');
|
||||
var trigger = document.getElementById('settings-trigger');
|
||||
var mobileTrigger = document.getElementById('settings-trigger-mobile');
|
||||
|
||||
if (panel) {
|
||||
renderPanel(prefs);
|
||||
|
||||
function togglePanel() {
|
||||
var isOpen = panel.getAttribute('data-open') === 'true';
|
||||
if (isOpen) closePanel(); else openPanel();
|
||||
}
|
||||
|
||||
if (trigger) trigger.addEventListener('click', togglePanel);
|
||||
if (mobileTrigger) mobileTrigger.addEventListener('click', togglePanel);
|
||||
}
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initSettings);
|
||||
} else {
|
||||
initSettings();
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify JS syntax**
|
||||
|
||||
Run: `node --check internal/views/static/js/settings.js`
|
||||
Expected: no output (exit 0)
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add internal/views/static/js/settings.js
|
||||
git commit -m "feat(settings): add JS module for localStorage preferences and panel"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: HTML — Gear trigger, panel markup, header in base
|
||||
|
||||
**Files:**
|
||||
- Modify: `internal/views/templates/base.html`
|
||||
- Modify: `internal/views/views.go`
|
||||
|
||||
- [ ] **Step 1: Add ShowHeader to PageData**
|
||||
|
||||
In `views.go`, add `ShowHeader bool` to `PageData` struct.
|
||||
|
||||
- [ ] **Step 2: Set ShowHeader in render functions**
|
||||
|
||||
In `RenderIndex` and `RenderSearch`, set `PageData.ShowHeader = true`.
|
||||
|
||||
- [ ] **Step 3: Update base.html — add header and settings markup**
|
||||
|
||||
In `base.html`, update the `<body>` to:
|
||||
|
||||
```html
|
||||
<body class="{{if .Query}}search_on_results{{end}}">
|
||||
{{if .ShowHeader}}
|
||||
<header class="site-header">
|
||||
<span class="site-title">samsa</span>
|
||||
<!-- Desktop trigger (hidden on mobile) -->
|
||||
<button id="settings-trigger" class="settings-trigger settings-trigger-desktop"
|
||||
aria-label="Preferences" aria-expanded="false" aria-controls="settings-popover">⚙</button>
|
||||
</header>
|
||||
<!-- Mobile FAB trigger (hidden on desktop, shown via CSS on mobile) -->
|
||||
<button id="settings-trigger-mobile" class="settings-trigger settings-trigger-mobile"
|
||||
aria-label="Preferences" aria-expanded="false" aria-controls="settings-popover"
|
||||
style="display:none;">⚙</button>
|
||||
{{end}}
|
||||
<main>
|
||||
{{template "content" .}}
|
||||
</main>
|
||||
<footer>
|
||||
<p>Powered by <a href="https://git.ashisgreat.xyz/penal-colony/samsa">samsa</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">×</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;
|
||||
// ... existing autocomplete JS stays unchanged ...
|
||||
}());
|
||||
</script>
|
||||
</body>
|
||||
```
|
||||
|
||||
**Note:** The existing autocomplete `<script>` block is preserved as-is. Only the body wrapper and settings elements are added.
|
||||
|
||||
- [ ] **Step 4: Run tests**
|
||||
|
||||
Run: `go test ./...`
|
||||
Expected: all pass
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add internal/views/templates/base.html internal/views/views.go
|
||||
git commit -m "feat(settings): add gear trigger and panel markup to base template"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Search form — Inject engine preferences
|
||||
|
||||
**Files:**
|
||||
- Modify: `internal/views/templates/index.html`
|
||||
- Modify: `internal/views/templates/results.html`
|
||||
|
||||
- [ ] **Step 1: Add hidden engines input to both search forms**
|
||||
|
||||
In `index.html`, add inside the `<form>`:
|
||||
|
||||
```html
|
||||
<input type="hidden" name="engines" id="engines-input" value="">
|
||||
```
|
||||
|
||||
In `results.html`, add inside the `<form>`:
|
||||
|
||||
```html
|
||||
<input type="hidden" name="engines" id="engines-input" value="">
|
||||
```
|
||||
|
||||
The `value` is populated by `syncEngineInput(prefs)` on page load. When the form submits (regular GET or HTMX), the `engines` parameter is included as a CSV string, which `ParseSearchRequest` reads correctly via `r.FormValue("engines")`.
|
||||
|
||||
- [ ] **Step 2: Verify existing search works**
|
||||
|
||||
Run: `go run ./cmd/samsa -config config.toml`
|
||||
Open: `http://localhost:8080`
|
||||
Search for "golang" — results should appear as normal.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add internal/views/templates/index.html internal/views/templates/results.html
|
||||
git commit -m "feat(settings): add hidden engines input to search forms"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: End-to-end verification
|
||||
|
||||
- [ ] **Step 1: Start server**
|
||||
|
||||
Run: `go run ./cmd/samsa -config config.toml`
|
||||
Open: `http://localhost:8080`
|
||||
|
||||
- [ ] **Step 2: Verify gear icon and panel**
|
||||
|
||||
Click the gear icon in the header — panel drops down from top-right with Appearance, Engines, and Search Defaults sections.
|
||||
|
||||
- [ ] **Step 3: Verify theme persistence**
|
||||
|
||||
Click Dark → page colors change immediately. Refresh → dark theme persists.
|
||||
|
||||
- [ ] **Step 4: Verify engine toggle persistence**
|
||||
|
||||
Uncheck "wikipedia", refresh → "wikipedia" stays unchecked.
|
||||
|
||||
- [ ] **Step 5: Verify engines appear in search query**
|
||||
|
||||
With wikipedia unchecked, open DevTools → Network tab, search "golang". Verify request URL includes `&engines=arxiv,crossref,...` (no wikipedia).
|
||||
|
||||
- [ ] **Step 6: Verify mobile bottom sheet**
|
||||
|
||||
Resize to <768px or use mobile device emulation. Click gear → full-width sheet slides up from bottom.
|
||||
|
||||
- [ ] **Step 7: Final commit**
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "feat: complete settings UI — popover, auto-save, theme, engines, mobile bottom-sheet"
|
||||
```
|
||||
|
|
@ -1,328 +0,0 @@
|
|||
# Brave Search Frontend Redesign — Design Specification
|
||||
|
||||
## Overview
|
||||
|
||||
Redesign the samsa frontend to match Brave Search's clean, functional aesthetic with emphasis on layout changes: three-column results page, category tiles on homepage, and a hybrid preferences system with full-page `/preferences` route.
|
||||
|
||||
## Design Principles
|
||||
|
||||
1. **Brave-like layout** — Three-column results, full-page preferences, homepage tiles
|
||||
2. **Preserve existing design tokens** — Keep current CSS variables (colors, spacing, radii)
|
||||
3. **CSS Grid for layout** — Three-column grid for results, flexible layouts elsewhere
|
||||
4. **Hybrid preferences** — Quick popover for common settings (theme + engines), full `/preferences` page for all options
|
||||
5. **Minimal HTML changes** — Restructure templates where needed for layout, reuse existing partials
|
||||
6. **localStorage-only preferences** — No server-side persistence; all preferences stored in browser localStorage
|
||||
|
||||
---
|
||||
|
||||
## 1. Homepage Redesign
|
||||
|
||||
### Current State
|
||||
- Centered hero with logo, tagline, and search box
|
||||
- No visual categorization of search types
|
||||
|
||||
### New Layout
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ [Logo] [⚙ Preferences]│
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ [🔍 Search Box] │
|
||||
│ │
|
||||
│ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │
|
||||
│ │ News │ │ Images │ │ Videos │ │ Maps │ ... │
|
||||
│ └────────┘ └────────┘ └────────┘ └────────┘ │
|
||||
│ │
|
||||
│ "Search the web privately..." │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Implementation
|
||||
- **File:** `internal/views/templates/index.html`
|
||||
- **Structure:** Search hero + category tiles grid
|
||||
- **Tiles:** Static links to `/search?q=...` with category parameter (e.g., `&category=images`)
|
||||
- **Styling:** Grid of icon+label cards below search box, subtle hover effects
|
||||
|
||||
### Category Tiles
|
||||
| Category | Icon | Notes |
|
||||
|----------|------|-------|
|
||||
| All | 🌐 | Default, no category param |
|
||||
| News | 📰 | |
|
||||
| Images | 🖼️ | |
|
||||
| Videos | 🎬 | |
|
||||
| Maps | 🗺️ | |
|
||||
| Shopping | 🛒 | Future: connect to shopping engine |
|
||||
| Music | 🎵 | Future: connect to music engine |
|
||||
| Weather | 🌤️ | Future: connect to weather API |
|
||||
| Sports | ⚽ | Future |
|
||||
| Cryptocurrency | ₿ | Future |
|
||||
|
||||
Categories marked "Future" are included in the UI but may not have backend support yet. Category tiles that lack backend support display grayed out with a "Coming soon" tooltip.
|
||||
|
||||
---
|
||||
|
||||
## 2. Results Page — Three-Column Layout
|
||||
|
||||
### Current State
|
||||
- Two columns: compact search bar spanning top, main results + right sidebar
|
||||
|
||||
### New Layout
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ [Logo] [⚙ Preferences]│
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ ┌─────────┐ ┌────────────────────────────┐ ┌──────────┐│
|
||||
│ │ Nav │ │ 🔍 [ Search Input ] │ │ Related ││
|
||||
│ │ ─────── │ └────────────────────────────┘ │ Searches ││
|
||||
│ │ All │ About 1,240 results (0.42s) │ ││
|
||||
│ │ Images │ ┌──────────────────────────┐ │ │ ─────── ││
|
||||
│ │ Videos │ │ Result Card │ │ │ Suggestions│
|
||||
│ │ News │ │ Title, URL, Description │ │ │ ││
|
||||
│ │ Maps │ └──────────────────────────┘ │ └──────────┘│
|
||||
│ │ Shopping│ ┌──────────────────────────┐ │ │
|
||||
│ │ ... │ │ Result Card │ │ │
|
||||
│ │ │ │ ... │ │ │
|
||||
│ │ ─────── │ └──────────────────────────┘ │ │
|
||||
│ │ Filters │ ... │ │
|
||||
│ │ Time │ │ │
|
||||
│ │ Type │ [Pagination] │ │
|
||||
│ └─────────┘ │ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Implementation
|
||||
- **Files:** `internal/views/templates/results.html`, `internal/views/templates/base.html`
|
||||
- **Left Sidebar (desktop, sticky):**
|
||||
- Category navigation links (All, Images, Videos, News, Maps, Shopping, Music, Weather)
|
||||
- Filters section (Time range, Result type) — collapsible
|
||||
- Hidden on mobile (< 768px)
|
||||
|
||||
- **Center Column:**
|
||||
- Compact search bar
|
||||
- Results count meta: "About {n} results ({time}s)"
|
||||
- Result cards (unchanged markup)
|
||||
- Pagination
|
||||
|
||||
- **Right Sidebar:**
|
||||
- Related searches (existing suggestions)
|
||||
- Additional panels as needed
|
||||
|
||||
### Filters
|
||||
**Time Range Options:**
|
||||
| Label | Query Param |
|
||||
|-------|-------------|
|
||||
| Any time | (none) |
|
||||
| Past hour | `&time=h` |
|
||||
| Past 24 hours | `&time=d` |
|
||||
| Past week | `&time=w` |
|
||||
| Past month | `&time=m` |
|
||||
| Past year | `&time=y` |
|
||||
|
||||
**Result Type Options:**
|
||||
| Label | Query Param |
|
||||
|-------|-------------|
|
||||
| All results | (none) |
|
||||
| News | `&type=news` |
|
||||
| Videos | `&type=video` |
|
||||
| Images | `&type=image` |
|
||||
|
||||
Filter state persists in URL query params and is preserved across HTMX navigation via `hx-include`.
|
||||
|
||||
### Mobile Behavior
|
||||
| Breakpoint | Layout |
|
||||
|------------|--------|
|
||||
| < 768px | Single column, no left sidebar |
|
||||
| 768px - 1024px | Two columns (center + right sidebar), no left nav |
|
||||
| > 1024px | Full three columns |
|
||||
|
||||
On mobile (< 768px):
|
||||
- Category filters accessible via a horizontal scrollable chip row above results
|
||||
- Both sidebars hidden
|
||||
- Search bar full-width
|
||||
|
||||
---
|
||||
|
||||
## 3. Preferences Page — Full-Page Hybrid
|
||||
|
||||
### Current State
|
||||
- Popover triggered by gear icon in header
|
||||
- JavaScript-rendered from localStorage
|
||||
- Sections: Appearance, Engines, Search Defaults
|
||||
|
||||
### New Layout
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ [Logo] [⚙ Preferences]│
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ ┌────────────────┐ ┌─────────────────────────────────────┐│
|
||||
│ │ Nav │ │ Content ││
|
||||
│ │ ───────────── │ │ ││
|
||||
│ │ Search │ │ [Section Content] ││
|
||||
│ │ Privacy │ │ ││
|
||||
│ │ Tabs │ │ ││
|
||||
│ │ Appearance │ │ ││
|
||||
│ │ Sidebar │ │ ││
|
||||
│ │ Content │ │ ││
|
||||
│ │ Languages │ │ ││
|
||||
│ │ Regional │ │ ││
|
||||
│ └────────────────┘ └─────────────────────────────────────┘│
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Sections (Brave-style)
|
||||
1. **Search** — Default engine, safe search, language
|
||||
2. **Privacy** — Tracking protection toggle (UI only, always on), request DNT header toggle
|
||||
3. **Tabs** — New tab behavior (placeholder section)
|
||||
4. **Appearance** — Theme (Light/Dark/System), results font size
|
||||
5. **Sidebar** — Sidebar visibility toggle
|
||||
6. **Content** — Filter explicit results (SafeSearch), auto-play media toggle
|
||||
7. **Languages** — UI language (English only for now), search language
|
||||
8. **Regional** — Region/Country, timezone (placeholder)
|
||||
|
||||
### Implementation
|
||||
- **Route:** Add `GET /preferences` and `POST /preferences` to `internal/httpapi/`
|
||||
- **Template:** `internal/views/templates/preferences.html`
|
||||
- **Storage:** localStorage-only. GET handler renders page shell, JavaScript populates form values from localStorage. POST handler receives form data, writes to localStorage, re-renders page.
|
||||
- **Quick Settings Popover:** Keep existing popover for **theme toggle and engine toggles only** (lightweight, localStorage). SafeSearch and Format settings move exclusively to full preferences page.
|
||||
- **Styling:** Match existing design tokens, section headers, form controls
|
||||
|
||||
### Preferences Nav (Mobile)
|
||||
- Horizontal scrollable nav on mobile (< 768px)
|
||||
- Active section highlighted
|
||||
|
||||
---
|
||||
|
||||
## 4. Component Changes
|
||||
|
||||
### Header
|
||||
- Logo + site name (unchanged)
|
||||
- Preferences button (unchanged)
|
||||
|
||||
### Search Box
|
||||
- Homepage: Larger, prominent, centered
|
||||
- Results page: Compact, full-width within center column
|
||||
|
||||
### Result Cards
|
||||
- Keep existing structure
|
||||
- Consider subtle styling improvements (spacing, typography)
|
||||
|
||||
### Category Tiles (Homepage)
|
||||
- Icon + label per category
|
||||
- Hover: slight scale + shadow
|
||||
|
||||
### Left Sidebar (Results Page)
|
||||
- Sticky positioning (`position: sticky; top: calc(var(--header-height) + 1rem)`)
|
||||
- Category links with active state indicator
|
||||
- Collapsible filter sections
|
||||
|
||||
### Preferences Nav
|
||||
- Vertical nav with section icons
|
||||
- Active state indicator
|
||||
- Mobile: horizontal scroll
|
||||
|
||||
---
|
||||
|
||||
## 5. CSS Architecture
|
||||
|
||||
### Existing (Retain)
|
||||
- CSS custom properties (design tokens)
|
||||
- Component-level styles
|
||||
- Dark mode via `[data-theme="dark"]`
|
||||
|
||||
### New
|
||||
|
||||
**Layout Grid for three-column results:**
|
||||
```css
|
||||
.results-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 200px 1fr 240px;
|
||||
gap: 2rem;
|
||||
align-items: start;
|
||||
}
|
||||
```
|
||||
|
||||
**Sticky Left Sidebar:**
|
||||
```css
|
||||
.results-layout .left-sidebar {
|
||||
position: sticky;
|
||||
top: calc(var(--header-height) + 1.5rem);
|
||||
max-height: calc(100vh - var(--header-height) - 3rem);
|
||||
overflow-y: auto;
|
||||
}
|
||||
```
|
||||
|
||||
**Preferences page layout:**
|
||||
```css
|
||||
.preferences-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 200px 1fr;
|
||||
gap: 2rem;
|
||||
}
|
||||
```
|
||||
|
||||
**Category tiles grid:**
|
||||
```css
|
||||
.category-tiles {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
```
|
||||
|
||||
**Mobile breakpoints:**
|
||||
```css
|
||||
@media (max-width: 768px) {
|
||||
.results-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.results-layout .left-sidebar,
|
||||
.results-layout .right-sidebar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.results-layout {
|
||||
grid-template-columns: 1fr 220px;
|
||||
}
|
||||
.results-layout .left-sidebar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Files to Modify
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `internal/views/templates/index.html` | Add category tiles |
|
||||
| `internal/views/templates/results.html` | Add left sidebar, restructure for three columns |
|
||||
| `internal/views/templates/base.html` | Minimal changes (no structural changes needed) |
|
||||
| `internal/views/templates/preferences.html` | **New** — full preferences page |
|
||||
| `internal/views/static/css/samsa.css` | Add layout grids, category tiles, sidebar styles, sticky positioning, mobile breakpoints |
|
||||
| `internal/views/static/js/settings.js` | Reduce popover to theme + engines; add preferences page JS |
|
||||
| `internal/httpapi/httpapi.go` | Add `/preferences` route (GET + POST) |
|
||||
| `internal/views/views.go` | Add preferences template rendering |
|
||||
|
||||
---
|
||||
|
||||
## 7. Priority Order
|
||||
|
||||
1. **Phase 1:** CSS layout framework (three-column grid, new variables, breakpoints)
|
||||
2. **Phase 2:** Results page three-column layout
|
||||
3. **Phase 3:** Homepage category tiles
|
||||
4. **Phase 4:** Preferences page (quick popover first, then full page)
|
||||
5. **Phase 5:** Polish and mobile responsiveness
|
||||
|
||||
---
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Backend search logic changes
|
||||
- New engine implementations (category tiles for future engines are UI placeholders only)
|
||||
- Caching or performance improvements
|
||||
- User authentication/account system
|
||||
- Server-side preference storage
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
# Settings UI Design — samsa
|
||||
|
||||
**Date:** 2026-03-22
|
||||
**Status:** Approved
|
||||
|
||||
## Overview
|
||||
|
||||
A lightweight preferences popover anchored to the top-right, just below the header. Triggered by a gear icon, it lets users adjust theme, enabled engines, and search defaults without leaving their current page. All changes auto-save to `localStorage` on every interaction.
|
||||
|
||||
## Layout & Structure
|
||||
|
||||
- **Trigger**: Gear icon (⚙️) in the top-right header, aligned with the header's right edge
|
||||
- **Panel**: 280px wide, max-height 420px, scrollable internally, rounded corners, subtle shadow, anchored top-right (drops down from trigger, like a dropdown)
|
||||
- **Close**: × button in panel header, click outside the panel, or pressing Escape
|
||||
- **No Save button** — every interaction immediately writes to `localStorage`
|
||||
|
||||
## Interaction Flow
|
||||
|
||||
1. User clicks ⚙️ → panel drops down from top-right (200ms ease)
|
||||
2. User toggles/clicks → changes apply instantly to DOM + write to `localStorage`
|
||||
3. User clicks × or outside or Escape → panel closes, settings persist
|
||||
4. **Accessibility**: Focus is trapped within the panel while open. Trigger button uses `aria-expanded` and `aria-controls`. Escape key closes the panel.
|
||||
|
||||
## Mid-Search Changes
|
||||
|
||||
When opened during an active search on `results.html`:
|
||||
- Engine toggles update `localStorage` immediately, but **current results remain unchanged**
|
||||
- A subtle inline note below the engines section: *"Engine changes apply to your next search"*
|
||||
|
||||
## Sections
|
||||
|
||||
### Appearance
|
||||
|
||||
- Three theme buttons: ☀️ Light / 🌙 Dark / 💻 System
|
||||
- Clicking immediately applies via `document.body.classList` + writes to localStorage
|
||||
- "System" reads `prefers-color-scheme` and updates on change
|
||||
|
||||
### Engines
|
||||
|
||||
- 2-column grid of toggle switches for all 9 engines
|
||||
- Each row: engine name + toggle switch
|
||||
- Enabled = filled accent color; Disabled = gray outline
|
||||
|
||||
### Search Defaults
|
||||
|
||||
- Safe search: dropdown (Moderate / Strict / Off)
|
||||
- Default format: dropdown (HTML / JSON / CSV)
|
||||
|
||||
## Default State
|
||||
|
||||
```js
|
||||
const DEFAULT_PREFS = {
|
||||
theme: "system",
|
||||
engines: ["wikipedia", "arxiv", "crossref", "braveapi", "qwant", "duckduckgo", "github", "reddit", "bing"],
|
||||
safeSearch: "moderate",
|
||||
format: "html"
|
||||
};
|
||||
```
|
||||
|
||||
## Persistence
|
||||
|
||||
```js
|
||||
// Written on every interaction
|
||||
localStorage.setItem('samsa_prefs', JSON.stringify({ ... }));
|
||||
|
||||
// Read on page load — merge with DEFAULT_PREFS
|
||||
const saved = JSON.parse(localStorage.getItem('samsa_prefs') || '{}');
|
||||
const prefs = { ...DEFAULT_PREFS, ...saved };
|
||||
```
|
||||
|
||||
## Responsive Behavior
|
||||
|
||||
- **Mobile (<768px)**: Panel becomes a **bottom sheet** — 100% width, slides up from the bottom, top corners rounded, max-height 70vh. Trigger moves to a fixed bottom-right FAB button.
|
||||
- Panel never covers the search input
|
||||
|
||||
## Applied to Existing Code
|
||||
|
||||
- `base.html` — add gear button in header, panel markup at end of `<body>`
|
||||
- `samsa.css` — popover styles, toggle switch styles, bottom sheet styles for mobile
|
||||
- `settings.js` — localStorage read/write, theme application, panel toggle, aria attributes, focus trap
|
||||
Loading…
Add table
Add a link
Reference in a new issue