diff --git a/docs/superpowers/plans/2026-03-22-brave-search-frontend-redesign.md b/docs/superpowers/plans/2026-03-22-brave-search-frontend-redesign.md new file mode 100644 index 0000000..486cee4 --- /dev/null +++ b/docs/superpowers/plans/2026-03-22-brave-search-frontend-redesign.md @@ -0,0 +1,1204 @@ +# Brave Search Frontend Redesign โ€” 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:** Redesign the kafka frontend to match Brave Search's layout: three-column results page, category tiles on homepage, and a hybrid preferences system with full-page `/preferences` route. + +**Architecture:** CSS Grid for page-level layouts (three-column results, two-column preferences). JavaScript popover for quick settings (theme + engines only). Server-rendered full preferences page with localStorage persistence. Category tiles are static links with category query params. + +**Tech Stack:** Go (Go templates), CSS Grid/Flexbox, Vanilla JavaScript (HTMX for search), localStorage for preferences + +--- + +## File Map + +| File | Responsibility | +|------|----------------| +| `internal/views/static/css/kafka.css` | Add layout grids, category tiles, sidebar styles, mobile breakpoints | +| `internal/views/templates/index.html` | Add category tiles below search box | +| `internal/views/templates/results.html` | Add left sidebar, restructure for three-column grid | +| `internal/views/templates/preferences.html` | **New** โ€” full preferences page with nav | +| `internal/views/templates/base.html` | No structural changes needed | +| `internal/views/static/js/settings.js` | Reduce popover to theme + engines; add preferences page JS | +| `internal/httpapi/handlers.go` | Add `GET /preferences` and `POST /preferences` handlers | +| `internal/views/views.go` | Add `RenderPreferences` and `tmplPreferences` template | + +--- + +## PHASE 1: CSS Layout Framework + +### Task 1: Add CSS Grid Layouts and Breakpoints + +**Files:** +- Modify: `internal/views/static/css/kafka.css` + +- [ ] **Step 1: Add three-column results layout CSS** + +Append to end of `kafka.css`, before the `@media print` block: + +```css +/* ============================================================ + Three-Column Results Layout + ============================================================ */ + +.results-layout { + display: grid; + grid-template-columns: 200px 1fr 240px; + gap: 2rem; + align-items: start; +} + +.results-layout .left-sidebar { + position: sticky; + top: calc(var(--header-height) + 1.5rem); + max-height: calc(100vh - var(--header-height) - 3rem); + overflow-y: auto; +} + +.results-layout .right-sidebar { + position: sticky; + top: calc(var(--header-height) + 1.5rem); + max-height: calc(100vh - var(--header-height) - 3rem); + overflow-y: auto; +} + +.results-layout .results-column { + min-width: 0; +} +``` + +- [ ] **Step 2: Add mobile breakpoints** + +```css +/* Tablet: hide left sidebar, two columns */ +@media (min-width: 769px) and (max-width: 1024px) { + .results-layout { + grid-template-columns: 1fr 220px; + } + .results-layout .left-sidebar { + display: none; + } +} + +/* Mobile: single column, no sidebars */ +@media (max-width: 768px) { + .results-layout { + grid-template-columns: 1fr; + } + .results-layout .left-sidebar, + .results-layout .right-sidebar { + display: none; + } +} +``` + +- [ ] **Step 3: Add preferences page layout CSS** + +```css +/* ============================================================ + Preferences Page Layout + ============================================================ */ + +.preferences-layout { + display: grid; + grid-template-columns: 200px 1fr; + gap: 2rem; + align-items: start; + padding: 2rem 0; +} + +.preferences-nav { + position: sticky; + top: calc(var(--header-height) + 1.5rem); +} + +.preferences-nav-item { + display: flex; + align-items: center; + gap: 0.6rem; + padding: 0.6rem 0.75rem; + border-radius: var(--radius-sm); + color: var(--text-secondary); + text-decoration: none; + font-size: 0.9rem; + transition: background 0.15s, color 0.15s; + cursor: pointer; +} + +.preferences-nav-item:hover { + background: var(--bg-tertiary); + color: var(--text-primary); +} + +.preferences-nav-item.active { + background: var(--accent-soft); + color: var(--accent); + font-weight: 500; +} + +.preferences-content { + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: 1.5rem; +} + +@media (max-width: 768px) { + .preferences-layout { + grid-template-columns: 1fr; + } + .preferences-nav { + position: static; + display: flex; + overflow-x: auto; + gap: 0.5rem; + padding-bottom: 0.5rem; + } + .preferences-nav-item { + white-space: nowrap; + } +} +``` + +- [ ] **Step 4: Add category tiles CSS** + +```css +/* ============================================================ + Category Tiles + ============================================================ */ + +.category-tiles { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); + gap: 1rem; + margin-top: 2rem; +} + +.category-tile { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + padding: 1rem 0.5rem; + border-radius: var(--radius-md); + text-decoration: none; + color: var(--text-secondary); + font-size: 0.85rem; + transition: background 0.15s, color 0.15s, transform 0.15s, box-shadow 0.15s; +} + +.category-tile:hover { + background: var(--bg-tertiary); + color: var(--text-primary); + transform: translateY(-2px); + box-shadow: var(--shadow-sm); +} + +.category-tile-icon { + font-size: 1.5rem; + line-height: 1; +} + +.category-tile.disabled { + opacity: 0.5; + cursor: not-allowed; + pointer-events: none; +} + +@media (max-width: 768px) { + .category-tiles { + grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); + gap: 0.75rem; + } + .category-tile { + padding: 0.75rem 0.25rem; + font-size: 0.75rem; + } + .category-tile-icon { + font-size: 1.25rem; + } +} +``` + +- [ ] **Step 5: Add left sidebar navigation styles** + +```css +/* ============================================================ + Left Sidebar (Results Page) + ============================================================ */ + +.left-sidebar { + padding: 0; +} + +.sidebar-nav { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.sidebar-nav-title { + font-size: 0.7rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--text-muted); + padding: 0.5rem 0.75rem; + margin-top: 0.5rem; +} + +.sidebar-nav-item { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 0.75rem; + border-radius: var(--radius-sm); + color: var(--text-secondary); + text-decoration: none; + font-size: 0.875rem; + transition: background 0.15s, color 0.15s; +} + +.sidebar-nav-item:hover { + background: var(--bg-tertiary); + color: var(--text-primary); +} + +.sidebar-nav-item.active { + background: var(--accent-soft); + color: var(--accent); + font-weight: 500; +} + +.sidebar-nav-item-icon { + font-size: 1rem; + width: 20px; + text-align: center; +} + +.sidebar-filters { + margin-top: 1rem; + padding-top: 1rem; + border-top: 1px solid var(--border); +} + +.sidebar-filter-group { + margin-bottom: 0.75rem; +} + +.sidebar-filter-label { + font-size: 0.75rem; + font-weight: 500; + color: var(--text-muted); + padding: 0 0.75rem; + margin-bottom: 0.25rem; +} + +.sidebar-filter-option { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.35rem 0.75rem; + font-size: 0.8rem; + color: var(--text-secondary); + cursor: pointer; + border-radius: var(--radius-sm); + transition: background 0.15s; +} + +.sidebar-filter-option:hover { + background: var(--bg-tertiary); +} + +.sidebar-filter-option input[type="radio"] { + accent-color: var(--accent); +} + +/* Mobile filter chips */ +.mobile-filter-chips { + display: none; + overflow-x: auto; + gap: 0.5rem; + padding: 0.75rem 0; + -webkit-overflow-scrolling: touch; +} + +.mobile-filter-chips::-webkit-scrollbar { + display: none; +} + +.mobile-filter-chip { + display: inline-flex; + align-items: center; + padding: 0.4rem 0.75rem; + border: 1px solid var(--border); + border-radius: var(--radius-full); + font-size: 0.8rem; + color: var(--text-secondary); + white-space: nowrap; + text-decoration: none; + transition: background 0.15s, border-color 0.15s; +} + +.mobile-filter-chip:hover, +.mobile-filter-chip.active { + background: var(--accent-soft); + border-color: var(--accent); + color: var(--accent); +} + +@media (max-width: 768px) { + .mobile-filter-chips { + display: flex; + } +} +``` + +- [ ] **Step 6: Verify CSS changes compile** + +Run: `go build ./...` +Expected: No errors + +- [ ] **Step 7: Commit** + +```bash +git add internal/views/static/css/kafka.css +git commit -m "feat(frontend): add CSS layout framework for three-column results and preferences page" +``` + +--- + +## PHASE 2: Results Page Three-Column Layout + +### Task 2: Restructure Results Template + +**Files:** +- Modify: `internal/views/templates/results.html` +- Modify: `internal/views/views.go` + +- [ ] **Step 1: Read current results.html to understand exact content** + +Current structure has `.results-layout` grid with `.search-compact` spanning full width, `.results-column`, and `.sidebar`. Need to add left sidebar and restructure grid. + +- [ ] **Step 2: Replace results.html content** + +Replace the entire file content: + +```html +{{define "title"}}{{if .Query}}{{.Query}} โ€” {{end}}{{end}} +{{define "content"}} +
+ + + + +
+ +
+ +
+ + +
+ All + {{range .Categories}} + {{.}} + {{end}} +
+ + + {{template "results_inner" .}} +
+ + + +
+{{end}} +``` + +- [ ] **Step 3: Update PageData struct to include new fields** + +Modify `internal/views/views.go` โ€” add to `PageData` struct: + +```go +type PageData struct { + // ... existing fields ... + + // New fields for three-column layout + Categories []string + CategoryIcons map[string]string + ActiveCategory string + TimeFilters []FilterOption + TypeFilters []FilterOption + ActiveTime string + ActiveType string +} + +// FilterOption represents a filter radio option +type FilterOption struct { + Label string + Value string +} +``` + +- [ ] **Step 4: Update FromResponse to populate new fields** + +In `views.go`, update `FromResponse` to populate the new fields: + +```go +func FromResponse(resp contracts.SearchResponse, query string, pageno int) PageData { + pd := PageData{ + // ... existing initialization ... + + // New: categories with icons + Categories: []string{"all", "news", "images", "videos", "maps", "shopping", "music", "weather"}, + CategoryIcons: map[string]string{ + "all": "๐ŸŒ", + "news": "๐Ÿ“ฐ", + "images": "๐Ÿ–ผ๏ธ", + "videos": "๐ŸŽฌ", + "maps": "๐Ÿ—บ๏ธ", + "shopping": "๐Ÿ›’", + "music": "๐ŸŽต", + "weather": "๐ŸŒค๏ธ", + }, + ActiveCategory: "all", + + // Time filters + TimeFilters: []FilterOption{ + {Label: "Any time", Value: ""}, + {Label: "Past hour", Value: "h"}, + {Label: "Past 24 hours", Value: "d"}, + {Label: "Past week", Value: "w"}, + {Label: "Past month", Value: "m"}, + {Label: "Past year", Value: "y"}, + }, + ActiveTime: "", + + // Type filters + TypeFilters: []FilterOption{ + {Label: "All results", Value: ""}, + {Label: "News", Value: "news"}, + {Label: "Videos", Value: "video"}, + {Label: "Images", Value: "image"}, + }, + ActiveType: "", + } + // ... rest of function ... +} +``` + +- [ ] **Step 5: Register new preferences template** + +In `views.go`, add to the `init()` function and add `tmplPreferences`: + +```go +var ( + tmplFull *template.Template + tmplIndex *template.Template + tmplFragment *template.Template + tmplPreferences *template.Template +) + +func init() { + tmplFS, _ := fs.Sub(templatesFS, "templates") + + funcMap := template.FuncMap{ + "urlquery": template.URLQueryEscaper, + } + + tmplFull = template.Must(template.New("").Funcs(funcMap).ParseFS(tmplFS, + "base.html", "results.html", "results_inner.html", "result_item.html", "video_item.html", + )) + tmplIndex = template.Must(template.New("").Funcs(funcMap).ParseFS(tmplFS, + "base.html", "index.html", + )) + tmplFragment = template.Must(template.New("").Funcs(funcMap).ParseFS(tmplFS, + "results_inner.html", "result_item.html", "video_item.html", + )) + tmplPreferences = template.Must(template.New("").Funcs(funcMap).ParseFS(tmplFS, + "base.html", "preferences.html", + )) +} +``` + +- [ ] **Step 6: Add RenderPreferences function** + +Add to `views.go`: + +```go +// RenderPreferences renders the full preferences page. +func RenderPreferences(w http.ResponseWriter, sourceURL string) error { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + return tmplPreferences.ExecuteTemplate(w, "base", PageData{ShowHeader: true, SourceURL: sourceURL}) +} +``` + +- [ ] **Step 7: Test compilation** + +Run: `go build ./...` +Expected: No errors + +- [ ] **Step 8: Commit** + +```bash +git add internal/views/views.go internal/views/templates/results.html +git commit -m "feat(frontend): add three-column results layout with left sidebar navigation" +``` + +--- + +## PHASE 3: Homepage Category Tiles + +### Task 3: Add Category Tiles to Homepage + +**Files:** +- Modify: `internal/views/templates/index.html` + +- [ ] **Step 1: Read current index.html** + +- [ ] **Step 2: Replace index.html with tiles** + +```html +{{define "title"}}{{end}} +{{define "content"}} +
+ +

Search the web privately, without tracking or censorship.

+ + + +
+ + ๐ŸŒ + All + + + ๐Ÿ–ผ๏ธ + Images + + + ๐Ÿ“ฐ + News + + + ๐ŸŽฌ + Videos + + + ๐Ÿ—บ๏ธ + Maps + + + ๐Ÿ›’ + Shopping + + + ๐ŸŽต + Music + + + ๐ŸŒค๏ธ + Weather + +
+
+
+{{end}} +``` + +- [ ] **Step 3: Test compilation** + +Run: `go build ./...` +Expected: No errors + +- [ ] **Step 4: Commit** + +```bash +git add internal/views/templates/index.html +git commit -m "feat(frontend): add category tiles to homepage" +``` + +--- + +## PHASE 4: Preferences Page + +### Task 4: Create Preferences Template + +**Files:** +- Create: `internal/views/templates/preferences.html` + +- [ ] **Step 1: Create preferences.html** + +```html +{{define "title"}}Preferences{{end}} +{{define "content"}} +
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+
+{{end}} +``` + +- [ ] **Step 2: Add preferences section CSS styles** + +Append to `kafka.css`: + +```css +/* ============================================================ + Preferences Page Styles + ============================================================ */ + +.pref-section { + margin-bottom: 2rem; +} + +.pref-section:last-child { + margin-bottom: 0; +} + +.pref-section-title { + font-size: 1rem; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 1rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid var(--border); +} + +.pref-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 0.75rem 0; + border-bottom: 1px solid var(--border); +} + +.pref-row:last-child { + border-bottom: none; +} + +.pref-row label { + font-size: 0.9rem; + color: var(--text-primary); +} + +.pref-row-info { + flex: 1; +} + +.pref-row-info label { + font-weight: 500; +} + +.pref-desc { + font-size: 0.8rem; + color: var(--text-muted); + margin-top: 0.25rem; +} + +.pref-row select { + padding: 0.5rem 0.75rem; + font-size: 0.85rem; + font-family: inherit; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + background: var(--bg); + color: var(--text-primary); + cursor: pointer; + min-width: 150px; +} + +.pref-row select:focus { + outline: none; + border-color: var(--accent); +} + +.pref-row input[type="checkbox"] { + width: 18px; + height: 18px; + accent-color: var(--accent); + cursor: pointer; + flex-shrink: 0; +} + +.pref-row input[type="checkbox"]:disabled { + opacity: 0.6; + cursor: not-allowed; +} +``` + +- [ ] **Step 3: Commit** + +```bash +git add internal/views/templates/preferences.html internal/views/static/css/kafka.css +git commit -m "feat(frontend): add preferences page template and styles" +``` + +--- + +### Task 5: Add Preferences Route + +**Files:** +- Modify: `internal/httpapi/handlers.go` +- Modify: `cmd/kafka/main.go` + +- [ ] **Step 1: Add GET and POST handlers for /preferences** + +Add to `handlers.go`: + +```go +// Preferences renders the preferences page. +func (h *Handler) Preferences(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/preferences" { + http.NotFound(w, r) + return + } + if err := views.RenderPreferences(w, h.sourceURL); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +// PreferencesPOST handles form submission from the preferences page. +func (h *Handler) PreferencesPOST(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/preferences" { + http.NotFound(w, r) + return + } + // Preferences are stored in localStorage on the client. + // This handler exists for form submission completeness but + // the actual save happens via JavaScript. + // Redirect back to preferences page. + http.Redirect(w, r, "/preferences", http.StatusFound) +} +``` + +- [ ] **Step 2: Register the route in main** + +Find where routes are registered (likely in `cmd/kafka/main.go`) and add: + +```go +mux.HandleFunc("GET /preferences", handler.Preferences) +mux.HandleFunc("POST /preferences", handler.PreferencesPOST) +``` + +- [ ] **Step 3: Test compilation** + +Run: `go build ./...` +Expected: No errors + +- [ ] **Step 4: Commit** + +```bash +git add internal/httpapi/handlers.go cmd/kafka/main.go +git commit -m "feat: add GET and POST /preferences route" +``` + +--- + +### Task 6: Update Settings JavaScript + +**Files:** +- Modify: `internal/views/static/js/settings.js` + +- [ ] **Step 1: Reduce popover to theme + engines only** + +Update the `renderPanel` function to remove SafeSearch and Format options. Keep only theme buttons and engine toggles. + +- [ ] **Step 2: Add preferences page navigation JavaScript** + +Add to end of `settings.js`: + +```javascript +// Preferences page navigation +function initPreferences() { + var nav = document.getElementById('preferences-nav'); + if (!nav) return; + + var sections = document.querySelectorAll('.pref-section'); + var navItems = nav.querySelectorAll('.preferences-nav-item'); + + function showSection(id) { + sections.forEach(function(sec) { + sec.style.display = sec.id === 'section-' + id ? 'block' : 'none'; + }); + navItems.forEach(function(item) { + item.classList.toggle('active', item.getAttribute('data-section') === id); + }); + } + + navItems.forEach(function(item) { + item.addEventListener('click', function() { + showSection(item.getAttribute('data-section')); + }); + }); + + // Load saved preferences + var prefs = loadPrefs(); + var themeEl = document.getElementById('pref-theme'); + if (themeEl) themeEl.value = prefs.theme || 'system'; + + var ssEl = document.getElementById('pref-safesearch'); + if (ssEl) ssEl.value = prefs.safeSearch || 'moderate'; + + var fmtEl = document.getElementById('pref-format'); + if (fmtEl) fmtEl.value = prefs.format || 'html'; + + // Save handlers + if (themeEl) { + themeEl.addEventListener('change', function() { + prefs.theme = themeEl.value; + savePrefs(prefs); + applyTheme(prefs.theme); + }); + } + + if (ssEl) { + ssEl.addEventListener('change', function() { + prefs.safeSearch = ssEl.value; + savePrefs(prefs); + }); + } + + if (fmtEl) { + fmtEl.addEventListener('change', function() { + prefs.format = fmtEl.value; + savePrefs(prefs); + }); + } + + // Show first section by default + showSection('search'); +} + +document.addEventListener('DOMContentLoaded', initPreferences); +``` + +- [ ] **Step 3: Test with browser** + +Manual verification needed โ€” cannot test browser JS with `go build` + +- [ ] **Step 4: Commit** + +```bash +git add internal/views/static/js/settings.js +git commit -m "feat(frontend): reduce popover to theme+engines, add preferences page JS" +``` + +--- + +## PHASE 5: Polish and Mobile Responsiveness + +### Task 7: Mobile Filter Chips Integration + +**Files:** +- Modify: `internal/views/templates/results.html` + +- [ ] **Step 1: Ensure mobile filter chips have working category links** + +The current results.html has mobile filter chips with category links. These should preserve existing query params for pagination/HTMX navigation. + +- [ ] **Step 2: Add filter form submission via HTMX** + +Update the filter radio buttons to submit via HTMX when changed. + +- [ ] **Step 3: Commit** + +```bash +git add internal/views/templates/results.html +git commit -m "fix(frontend): add HTMX filter submission" +``` + +--- + +### Task 8: Final Mobile Responsiveness Audit + +**Files:** +- Review: `internal/views/static/css/kafka.css` + +- [ ] **Step 1: Test all breakpoints manually** + +- [ ] **Step 2: Fix any layout issues found** + +- [ ] **Step 3: Commit any fixes** + +```bash +git add internal/views/static/css/kafka.css +git commit -m "fix(frontend): improve mobile responsiveness" +``` + +--- + +## Summary + +| Phase | Task | Files | +|-------|------|-------| +| 1 | CSS Layout Framework | `kafka.css` | +| 2 | Results Three-Column | `results.html`, `views.go` | +| 3 | Homepage Tiles | `index.html` | +| 4 | Preferences Page | `preferences.html` (new), `handlers.go`, `settings.js` | +| 5 | Polish | Various | + +**Total: 8 tasks across 5 phases** + +Run `go test ./...` after each phase to verify nothing is broken.