# 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 Go compilation** Run: `go build ./...` Expected: No errors Note: CSS is embedded as static files and not processed by the Go compiler. CSS changes must be tested manually in a browser. - [ ] **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: Add FilterOption struct and update PageData struct** Add `FilterOption` struct at package level in `views.go` (near `PageNumber` struct): ```go // FilterOption represents a filter radio option for the sidebar. type FilterOption struct { Label string Value string } ``` Then update `PageData` struct to include new fields: ```go type PageData struct { // ... existing fields (SourceURL, Query, Pageno, etc.) ... // New fields for three-column layout Categories []string CategoryIcons map[string]string DisabledCategories []string ActiveCategory string TimeFilters []FilterOption TypeFilters []FilterOption ActiveTime string ActiveType string } ``` - [ ] **Step 4: Update FromResponse signature and body** Update `FromResponse` signature to accept filter params and set defaults: ```go func FromResponse(resp contracts.SearchResponse, query string, pageno int, activeCategory, activeTime, activeType string) PageData { // Set defaults if activeCategory == "" { activeCategory = "all" } pd := PageData{ // ... existing initialization (NumberOfResults, Results, etc.) ... // New: categories with icons Categories: []string{"all", "news", "images", "videos", "maps"}, DisabledCategories: []string{"shopping", "music", "weather"}, CategoryIcons: map[string]string{ "all": "๐ŸŒ", "news": "๐Ÿ“ฐ", "images": "๐Ÿ–ผ๏ธ", "videos": "๐ŸŽฌ", "maps": "๐Ÿ—บ๏ธ", "shopping": "๐Ÿ›’", "music": "๐ŸŽต", "weather": "๐ŸŒค๏ธ", }, ActiveCategory: activeCategory, // 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: activeTime, // Type filters TypeFilters: []FilterOption{ {Label: "All results", Value: ""}, {Label: "News", Value: "news"}, {Label: "Videos", Value: "video"}, {Label: "Images", Value: "image"}, }, ActiveType: activeType, } // ... rest of function ... } ``` Update the `Search` handler in `handlers.go` to pass filter params: ```go pd := views.FromResponse(resp, req.Query, req.Pageno, r.FormValue("category"), r.FormValue("time"), r.FormValue("type")) ``` - [ ] **Step 5: Update results.html sidebar to show disabled state** Update the sidebar category loop to conditionally apply `disabled` class: ```html {{range .Categories}} {{index $.CategoryIcons .}} {{.}} {{end}} {{range .DisabledCategories}} {{index $.CategoryIcons .}} {{.}} {{end}} ``` - [ ] **Step 6: Test compilation** Run: `go build ./...` Expected: No errors - [ ] **Step 7: 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: Register preferences template in views.go** Add `tmplPreferences` variable and initialize it in `init()`. Also add `RenderPreferences` function: ```go // In views.go, add to var block: var ( tmplFull *template.Template tmplIndex *template.Template tmplFragment *template.Template tmplPreferences *template.Template ) // In init(), after existing template parsing, add: tmplPreferences = template.Must(template.New("").Funcs(funcMap).ParseFS(tmplFS, "base.html", "preferences.html", )) // Add RenderPreferences function: 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 4: Commit** ```bash git add internal/views/templates/preferences.html internal/views/static/css/kafka.css internal/views/views.go 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. // NOTE: This is a no-op. All preferences are stored in localStorage on the client // via JavaScript. This handler exists only for form submission completeness (e.g., // if a form POSTs without JS). The JavaScript in settings.js handles all saves. func (h *Handler) PreferencesPOST(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/preferences" { http.NotFound(w, r) return } 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.