# 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"}}
{{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}} {{.}} {{end}} {{range .DisabledCategories}} {{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.