diff --git a/.forgejo/workflows/test.yml b/.forgejo/workflows/test.yml
index 5f7efb4..bd05693 100644
--- a/.forgejo/workflows/test.yml
+++ b/.forgejo/workflows/test.yml
@@ -11,15 +11,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: https://github.com/actions/checkout@v5
+ uses: https://github.com/actions/checkout@v4
- name: Set up Go
uses: https://github.com/actions/setup-go@v5
with:
go-version-file: go.mod
- - name: Clean vendor
- run: rm -rf vendor
-
- name: Test
run: go test -race -v ./...
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
deleted file mode 100644
index 47cc920..0000000
--- a/.github/workflows/test.yml
+++ /dev/null
@@ -1,25 +0,0 @@
-name: Tests
-
-on:
- pull_request:
- branches: [main]
- push:
- branches: [main]
-
-jobs:
- test:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@v5
-
- - name: Set up Go
- uses: actions/setup-go@v5
- with:
- go-version-file: go.mod
-
- - name: Clean vendor
- run: rm -rf vendor
-
- - name: Test
- run: go test -race -v ./...
diff --git a/.gitignore b/.gitignore
index 6cea500..a5388c7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,11 +1,5 @@
node_modules/
.agent/
-internal/spa/dist/
-frontend/node_modules/
-frontend/dist/
-frontend/bun.lock
-frontend/bun.lockb
-frontend/package-lock.json
*.exe
*.exe~
*.dll
diff --git a/README.md b/README.md
index 25c1c29..c03019e 100644
--- a/README.md
+++ b/README.md
@@ -221,4 +221,4 @@ Includes Valkey 8 with health checks out of the box.
## License
-[AGPLv3](https://www.gnu.org/licenses/agpl-3.0.html)
+MIT
diff --git a/cmd/kafka/main.go b/cmd/kafka/main.go
index 29ab620..cdc81b5 100644
--- a/cmd/kafka/main.go
+++ b/cmd/kafka/main.go
@@ -19,6 +19,7 @@ package main
import (
"flag"
"fmt"
+ "io/fs"
"log"
"log/slog"
"net/http"
@@ -30,7 +31,7 @@ import (
"github.com/metamorphosis-dev/kafka/internal/httpapi"
"github.com/metamorphosis-dev/kafka/internal/middleware"
"github.com/metamorphosis-dev/kafka/internal/search"
- "github.com/metamorphosis-dev/kafka/internal/spa"
+ "github.com/metamorphosis-dev/kafka/internal/views"
)
func main() {
@@ -79,20 +80,22 @@ func main() {
h := httpapi.NewHandler(svc, acSvc.Suggestions, cfg.Server.SourceURL)
mux := http.NewServeMux()
-
- // API routes - handled by Go
+ mux.HandleFunc("/", h.Index)
mux.HandleFunc("/healthz", h.Healthz)
mux.HandleFunc("/search", h.Search)
mux.HandleFunc("/autocompleter", h.Autocompleter)
mux.HandleFunc("/opensearch.xml", h.OpenSearch(cfg.Server.BaseURL))
- // SPA handler - serves React app for all other routes
- spaHandler := spa.NewHandler()
- mux.Handle("/", spaHandler)
+ // Serve embedded static files (CSS, JS, images).
+ staticFS, err := views.StaticFS()
+ if err != nil {
+ log.Fatalf("failed to load static files: %v", err)
+ }
+ var subFS fs.FS = staticFS
+ mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(subFS))))
- // Apply middleware: global rate limit → burst rate limit → per-IP rate limit → CORS → security headers → handler.
+ // Apply middleware: global rate limit → burst rate limit → per-IP rate limit → CORS → handler.
var handler http.Handler = mux
- handler = middleware.SecurityHeaders(middleware.SecurityHeadersConfig{})(handler)
handler = middleware.CORS(middleware.CORSConfig{
AllowedOrigins: cfg.CORS.AllowedOrigins,
AllowedMethods: cfg.CORS.AllowedMethods,
@@ -104,7 +107,6 @@ func main() {
Requests: cfg.RateLimit.Requests,
Window: cfg.RateLimitWindow(),
CleanupInterval: cfg.RateLimitCleanupInterval(),
- TrustedProxies: cfg.RateLimit.TrustedProxies,
}, logger)(handler)
handler = middleware.GlobalRateLimit(middleware.GlobalRateLimitConfig{
Requests: cfg.GlobalRateLimit.Requests,
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
deleted file mode 100644
index 28b98a1..0000000
--- a/docs/superpowers/plans/2026-03-22-brave-search-frontend-redesign.md
+++ /dev/null
@@ -1,1222 +0,0 @@
-# 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}}
-
-{{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.
-
-
-
-
-
-
-{{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"}}
-
-
-
-
-
-
-
-
- Search
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Privacy
-
-
-
-
Block trackers and scripts that follow you across the web.
-
-
-
-
-
-
-
Ask websites not to track you.
-
-
-
-
-
-
-
- Tabs
-
-
-
-
Choose what happens when you open a new tab.
-
-
-
-
-
-
-
- Appearance
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Content
-
-
-
-
Hide explicit content from search results (SafeSearch).
-
-
-
-
-
-
-
Automatically play video content when visible.
-
-
-
-
-
-
-
- Languages
-
-
-
-
-
-
-
-
-
-
-
-
- Regional
-
-
-
-
-
-
-
-
-
-
-
-{{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.
diff --git a/docs/superpowers/plans/2026-03-22-frontend-replacement.md b/docs/superpowers/plans/2026-03-22-frontend-replacement.md
deleted file mode 100644
index 1cb475e..0000000
--- a/docs/superpowers/plans/2026-03-22-frontend-replacement.md
+++ /dev/null
@@ -1,358 +0,0 @@
-# Frontend Replacement 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:** Replace the Go template-based frontend with the search-zen-50 React SPA, embedded in the Go binary as a single deployment.
-
-**Architecture:** Build React app → embed in Go binary via `//go:embed` → serve via Go HTTP server with SPA fallback routing. React calls `/search?format=json` and `/autocompleter?q=` APIs.
-
-**Tech Stack:** Go (embed), React 18, Vite, TailwindCSS, React Router, @tanstack/react-query
-
----
-
-## File Map
-
-| File | Action |
-|------|--------|
-| `cmd/kafka/main.go` | Modify - replace template handlers with SPA handler |
-| `internal/spa/spa.go` | Create - embed React build, serve static files, SPA fallback |
-| `internal/spa/dist/` | Build output - React build artifacts (gitignored) |
-| `src/hooks/use-search.ts` | Modify - replace mock with real API calls |
-| `src/lib/mock-data.ts` | Keep types, remove MOCK_RESPONSE usage |
-
----
-
-## Task 1: Build React App
-
-**Files:**
-- Build: `/tmp/search-zen-50/dist/` (output directory)
-
-- [ ] **Step 1: Install dependencies and build**
-
-```bash
-cd /tmp/search-zen-50 && bun install && bun run build
-```
-
-Expected: `dist/` directory created with `index.html`, `assets/` folder containing JS/CSS bundles
-
-- [ ] **Step 2: Verify dist contents**
-
-```bash
-ls /tmp/search-zen-50/dist/ && ls /tmp/search-zen-50/dist/assets/ | head -10
-```
-
-Expected: `index.html` exists, `assets/` contains `.js` and `.css` files
-
----
-
-## Task 2: Create SPA Go Package
-
-**Files:**
-- Create: `internal/spa/spa.go`
-
-```go
-package spa
-
-import (
- "embed"
- "io/fs"
- "net/http"
- "path"
-)
-
-//go:embed all:dist
-var distFS embed.FS
-
-// DistFS returns the embedded dist directory as an fs.FS.
-func DistFS() (fs.FS, error) {
- return fs.Sub(distFS, "dist")
-}
-
-// NewHandler returns an HTTP handler that:
-// - Serves static files from the embedded dist/ directory
-// - Falls back to index.html for SPA routing (any non-API path)
-func NewHandler() http.Handler {
- dist, err := DistFS()
- if err != nil {
- panic("spa: embedded dist not found: " + err.Error())
- }
- return &spaHandler{dist: dist}
-}
-
-type spaHandler struct {
- dist fs.FS
-}
-
-func (h *spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- // API paths are handled by Go API handlers - this should never be reached
- // since Go mux dispatches to specific handlers first. But if reached,
- // pass through to FileServer which will return 404 for unknown paths.
-
- // Try to serve the requested file first
- filePath := path.Clean(r.URL.Path)
- f, err := h.dist.Open(filePath)
- if err == nil {
- f.Close()
- // File exists - serve it via FileServer
- http.FileServer(http.FS(h.dist)).ServeHTTP(w, r)
- return
- }
-
- // Fallback to index.html for SPA routing
- indexFile, err := h.dist.Open("index.html")
- if err != nil {
- http.Error(w, "index.html not found in embedded files", http.StatusInternalServerError)
- return
- }
- indexFile.Close()
- http.FileServer(http.FS(h.dist)).ServeHTTP(w, r)
-}
-
-```
-
----
-
-## Task 3: Wire SPA Handler in main.go
-
-**Files:**
-- Modify: `cmd/kafka/main.go`
-
-- [ ] **Step 1: Replace handlers with SPA**
-
-In `main.go`, find and replace the `mux.HandleFunc` section (lines 82-88) and the static file serving section (lines 90-96).
-
-Old code (lines 82-96):
-```go
-mux := http.NewServeMux()
-mux.HandleFunc("/", h.Index)
-mux.HandleFunc("/healthz", h.Healthz)
-mux.HandleFunc("/search", h.Search)
-mux.HandleFunc("/autocompleter", h.Autocompleter)
-mux.HandleFunc("/preferences", h.Preferences)
-mux.HandleFunc("/opensearch.xml", h.OpenSearch(cfg.Server.BaseURL))
-
-// Serve embedded static files (CSS, JS, images).
-staticFS, err := views.StaticFS()
-if err != nil {
- log.Fatalf("failed to load static files: %v", err)
-}
-var subFS fs.FS = staticFS
-mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(subFS))))
-```
-
-New code:
-```go
-mux := http.NewServeMux()
-
-// API routes - handled by Go
-mux.HandleFunc("/healthz", h.Healthz)
-mux.HandleFunc("/search", h.Search)
-mux.HandleFunc("/autocompleter", h.Autocompleter)
-mux.HandleFunc("/opensearch.xml", h.OpenSearch(cfg.Server.BaseURL))
-
-// SPA handler - serves React app for all other routes
-spaHandler := spa.NewHandler()
-mux.Handle("/", spaHandler)
-```
-
-- [ ] **Step 2: Add spa import**
-
-Add to imports (after `"github.com/metamorphosis-dev/kafka/internal/search"`):
-```go
-"github.com/metamorphosis-dev/kafka/internal/spa"
-```
-
-- [ ] **Step 3: Remove unused views import if needed**
-
-If `views` is only used for `StaticFS()`, remove the import. The template rendering functions (`RenderIndex`, etc.) won't be needed anymore.
-
-- [ ] **Step 4: Verify build**
-
-```bash
-cd /home/ashie/git/kafka && go build ./cmd/kafka/
-```
-
-Expected: Builds successfully (may fail on embed if dist not found - continue to next task)
-
----
-
-## Task 4: Wire React to Real API
-
-**Files:**
-- Modify: `src/hooks/use-search.ts` in `/tmp/search-zen-50/`
-
-- [ ] **Step 1: Replace mock search with real API call**
-
-Replace the `search` function in `use-search.ts`:
-
-Old code (lines 23-36):
-```typescript
-const search = useCallback(async (query: string) => {
- if (!query.trim()) return;
-
- setState((prev) => ({ ...prev, query, isLoading: true, error: null, hasSearched: true }));
-
- // Simulate network delay
- await new Promise((r) => setTimeout(r, 800));
-
- setState((prev) => ({
- ...prev,
- isLoading: false,
- results: { ...MOCK_RESPONSE, query },
- }));
-}, []);
-```
-
-New code:
-```typescript
-const search = useCallback(async (query: string) => {
- if (!query.trim()) return;
-
- setState((prev) => ({ ...prev, query, isLoading: true, error: null, hasSearched: true }));
-
- try {
- const response = await fetch(`/search?format=json&q=${encodeURIComponent(query)}`);
- if (!response.ok) {
- throw new Error(`HTTP ${response.status}`);
- }
- const data = await response.json();
- setState((prev) => ({
- ...prev,
- isLoading: false,
- results: data,
- }));
- } catch (err) {
- setState((prev) => ({
- ...prev,
- isLoading: false,
- error: err instanceof Error ? err.message : "Search failed",
- }));
- }
-}, []);
-```
-
-- [ ] **Step 2: Remove mock data import**
-
-Remove the mock import line (should be near line 2):
-```typescript
-import { MOCK_RESPONSE, type SearXNGResponse, type Category } from "@/lib/mock-data";
-```
-
-Replace with:
-```typescript
-import type { SearXNGResponse, Category } from "@/lib/mock-data";
-```
-
-- [ ] **Step 3: Keep the CATEGORIES export**
-
-Ensure `mock-data.ts` still exports `CATEGORIES` and `Category` type. The file should look like:
-
-```typescript
-// Keep these exports - used by CategoryTabs and preferences
-export const CATEGORIES = ["general", "it", "images", "news"] as const;
-export type Category = typeof CATEGORIES[number];
-
-// Keep interfaces
-export interface SearchResult {
- url: string;
- title: string;
- content: string;
- engine: string;
- parsed_url: [string, string, string, string, string];
- engines: string[];
- positions: number[];
- score: number;
- category: string;
- pretty_url: string;
- img_src?: string;
- thumbnail?: string;
- publishedDate?: string;
-}
-
-export interface SearXNGResponse {
- query: string;
- number_of_results: number;
- results: SearchResult[];
- answers: string[];
- corrections: string[];
- infoboxes: any[];
- suggestions: string[];
- unresponsive_engines: string[];
-}
-```
-
----
-
-## Task 5: Rebuild React and Verify
-
-**Files:**
-- Build: `/tmp/search-zen-50/dist/`
-
-- [ ] **Step 1: Rebuild with changes**
-
-```bash
-cd /tmp/search-zen-50 && bun run build
-```
-
-- [ ] **Step 2: Copy dist to kafka**
-
-```bash
-rm -rf /home/ashie/git/kafka/internal/spa/dist
-cp -r /tmp/search-zen-50/dist /home/ashie/git/kafka/internal/spa/dist
-```
-
-- [ ] **Step 3: Verify Go build**
-
-```bash
-cd /home/ashie/git/kafka && go build ./cmd/kafka/ && echo "Build successful"
-```
-
-Expected: "Build successful"
-
----
-
-## Task 6: Test the Integration
-
-- [ ] **Step 1: Start the server**
-
-```bash
-cd /home/ashie/git/kafka && ./kafka -config config.toml &
-sleep 2
-```
-
-- [ ] **Step 2: Test homepage**
-
-```bash
-curl -s http://localhost:8080/ | head -20
-```
-
-Expected: HTML with `` from React app
-
-- [ ] **Step 3: Test API**
-
-```bash
-curl -s "http://localhost:8080/search?format=json&q=test" | head -50
-```
-
-Expected: JSON search response
-
-- [ ] **Step 4: Clean up**
-
-```bash
-pkill -f "./kafka" 2>/dev/null; echo "Done"
-```
-
----
-
-## Dependencies
-
-- Node.js/Bun for building React app
-- Go 1.24+ for embed functionality
-- No new Go dependencies
-
-## Notes
-
-- The `internal/spa/dist/` folder should be gitignored (build artifact)
-- The `internal/spa/dist/` copy is needed for the embed to work at compile time
-- Preferences page is entirely client-side (localStorage) - no backend needed
-- Autocomplete can be added later by modifying `SearchInput.tsx` to call `/autocompleter`
diff --git a/docs/superpowers/specs/2026-03-22-brave-search-frontend-redesign.md b/docs/superpowers/specs/2026-03-22-brave-search-frontend-redesign.md
deleted file mode 100644
index d30ab99..0000000
--- a/docs/superpowers/specs/2026-03-22-brave-search-frontend-redesign.md
+++ /dev/null
@@ -1,328 +0,0 @@
-# Brave Search Frontend Redesign — Design Specification
-
-## Overview
-
-Redesign the kafka 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/kafka.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
diff --git a/docs/superpowers/specs/2026-03-22-frontend-replacement-design.md b/docs/superpowers/specs/2026-03-22-frontend-replacement-design.md
deleted file mode 100644
index 65c294a..0000000
--- a/docs/superpowers/specs/2026-03-22-frontend-replacement-design.md
+++ /dev/null
@@ -1,74 +0,0 @@
-# Frontend Replacement: search-zen-50 Integration
-
-## Status
-Approved
-
-## Overview
-
-Replace the current Go template-based frontend (HTMX + Go templates) with the search-zen-50 React SPA. The React app is built statically and embedded into the Go binary, serving as a single binary deployment.
-
-## Architecture
-
-- **Build**: React/Vite app builds to `dist/` directory
-- **Embed**: Go's `//go:embed` embeds the dist folder into the binary
-- **Serve**: Go HTTP server serves static files and handles API routes
-- **SPA routing**: Non-API routes serve `index.html` for React Router
-
-## Changes
-
-### Go Side
-
-1. **Create `internal/spa/spa.go`**
- - Embeds the React build (`dist/`) using `//go:embed`
- - Serves static files (JS, CSS, images)
- - Handles SPA fallback: serves `index.html` for all non-API routes
- - Provides `SPAHandler` that wraps API routes
-
-2. **Modify `cmd/kafka/main.go`**
- - Import the embedded SPA files
- - Route `/`, `/preferences`, and unknown routes to SPA handler
- - Keep existing API routes: `/search`, `/autocompleter`, `/healthz`, `/opensearch.xml`
-
-### React Side
-
-1. **Modify `use-search.ts`**
- - Replace mock data with real API call: `fetch("/search?format=json&q=${encodeURIComponent(query)}")`
- - Map response to existing `SearXNGResponse` type (already matches)
-
-2. **Add autocomplete** (optional enhancement)
- - Call `/autocompleter?q=${encodeURIComponent(query)}`
- - Display suggestions while typing
-
-3. **Keep unchanged**
- - All UI components
- - Preferences page (localStorage-based)
- - Routing (React Router)
-
-## Data Flow
-
-```
-Browser → GET / → Go serves embedded index.html
-Browser → GET /search?format=json&q=... → Go search handler → JSON
-Browser → React renders results via use-search hook
-```
-
-## API Compatibility
-
-The existing kafka API (`/search?format=json`) already matches the expected `SearXNGResponse` interface in the React code:
-- `query: string`
-- `number_of_results: number`
-- `results: SearchResult[]`
-- `suggestions: string[]`
-- `unresponsive_engines: string[][]`
-
-## File Changes
-
-- **New**: `internal/spa/spa.go`
-- **Modified**: `cmd/kafka/main.go` (wire SPA handler)
-- **Modified**: `src/hooks/use-search.ts` (use real API)
-- **Build step**: `npm run build` or `bun run build` in search-zen-50
-
-## Dependencies
-
-- React app uses `@tanstack/react-query` for API calls (already in package.json)
-- No new Go dependencies needed
diff --git a/flake.nix b/flake.nix
index 3552728..d143495 100644
--- a/flake.nix
+++ b/flake.nix
@@ -21,16 +21,13 @@
version = "0.1.0";
src = ./.;
- vendorHash = "sha256-8wlKD+33s97oorCJTfHKAgE2Xp1HKXV+bSr6z29KrKM=";
+ vendorHash = "sha256-NbAa4QM/TI3BTuZs4glx9k3ZjSl2/2LQfKlQ7izR8Ho=";
# Run: nix build .#packages.x86_64-linux.default
- # It will fail with the correct hash. Replace vendorHash with it.
+ # It will fail with the correct hash. Replace it here.
# Embed the templates and static files at build time.
ldflags = [ "-s" "-w" ];
- # Remove stale vendor directory before buildGoModule deletes it.
- preConfigure = "rm -rf vendor || true";
-
nativeCheckInputs = with pkgs; [ ];
# Tests require network; they run in CI instead.
diff --git a/frontend/.gitignore b/frontend/.gitignore
deleted file mode 100644
index a547bf3..0000000
--- a/frontend/.gitignore
+++ /dev/null
@@ -1,24 +0,0 @@
-# Logs
-logs
-*.log
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-pnpm-debug.log*
-lerna-debug.log*
-
-node_modules
-dist
-dist-ssr
-*.local
-
-# Editor directories and files
-.vscode/*
-!.vscode/extensions.json
-.idea
-.DS_Store
-*.suo
-*.ntvs*
-*.njsproj
-*.sln
-*.sw?
diff --git a/frontend/README.md b/frontend/README.md
deleted file mode 100644
index a125fd6..0000000
--- a/frontend/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Welcome to your Lovable project
-
-TODO: Document your project here
diff --git a/frontend/components.json b/frontend/components.json
deleted file mode 100644
index 62e1011..0000000
--- a/frontend/components.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "$schema": "https://ui.shadcn.com/schema.json",
- "style": "default",
- "rsc": false,
- "tsx": true,
- "tailwind": {
- "config": "tailwind.config.ts",
- "css": "src/index.css",
- "baseColor": "slate",
- "cssVariables": true,
- "prefix": ""
- },
- "aliases": {
- "components": "@/components",
- "utils": "@/lib/utils",
- "ui": "@/components/ui",
- "lib": "@/lib",
- "hooks": "@/hooks"
- }
-}
diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js
deleted file mode 100644
index 40f72cc..0000000
--- a/frontend/eslint.config.js
+++ /dev/null
@@ -1,26 +0,0 @@
-import js from "@eslint/js";
-import globals from "globals";
-import reactHooks from "eslint-plugin-react-hooks";
-import reactRefresh from "eslint-plugin-react-refresh";
-import tseslint from "typescript-eslint";
-
-export default tseslint.config(
- { ignores: ["dist"] },
- {
- extends: [js.configs.recommended, ...tseslint.configs.recommended],
- files: ["**/*.{ts,tsx}"],
- languageOptions: {
- ecmaVersion: 2020,
- globals: globals.browser,
- },
- plugins: {
- "react-hooks": reactHooks,
- "react-refresh": reactRefresh,
- },
- rules: {
- ...reactHooks.configs.recommended.rules,
- "react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
- "@typescript-eslint/no-unused-vars": "off",
- },
- },
-);
diff --git a/frontend/index.html b/frontend/index.html
deleted file mode 100644
index c1ff5ee..0000000
--- a/frontend/index.html
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
-
- kafka — Private Meta-Search
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/frontend/package.json b/frontend/package.json
deleted file mode 100644
index e90cada..0000000
--- a/frontend/package.json
+++ /dev/null
@@ -1,90 +0,0 @@
-{
- "name": "vite_react_shadcn_ts",
- "private": true,
- "version": "0.0.0",
- "type": "module",
- "scripts": {
- "dev": "vite",
- "build": "vite build",
- "build:dev": "vite build --mode development",
- "lint": "eslint .",
- "preview": "vite preview",
- "test": "vitest run",
- "test:watch": "vitest"
- },
- "dependencies": {
- "@hookform/resolvers": "^3.10.0",
- "@radix-ui/react-accordion": "^1.2.11",
- "@radix-ui/react-alert-dialog": "^1.1.14",
- "@radix-ui/react-aspect-ratio": "^1.1.7",
- "@radix-ui/react-avatar": "^1.1.10",
- "@radix-ui/react-checkbox": "^1.3.2",
- "@radix-ui/react-collapsible": "^1.1.11",
- "@radix-ui/react-context-menu": "^2.2.15",
- "@radix-ui/react-dialog": "^1.1.14",
- "@radix-ui/react-dropdown-menu": "^2.1.15",
- "@radix-ui/react-hover-card": "^1.1.14",
- "@radix-ui/react-label": "^2.1.7",
- "@radix-ui/react-menubar": "^1.1.15",
- "@radix-ui/react-navigation-menu": "^1.2.13",
- "@radix-ui/react-popover": "^1.1.14",
- "@radix-ui/react-progress": "^1.1.7",
- "@radix-ui/react-radio-group": "^1.3.7",
- "@radix-ui/react-scroll-area": "^1.2.9",
- "@radix-ui/react-select": "^2.2.5",
- "@radix-ui/react-separator": "^1.1.7",
- "@radix-ui/react-slider": "^1.3.5",
- "@radix-ui/react-slot": "^1.2.3",
- "@radix-ui/react-switch": "^1.2.5",
- "@radix-ui/react-tabs": "^1.1.12",
- "@radix-ui/react-toast": "^1.2.14",
- "@radix-ui/react-toggle": "^1.1.9",
- "@radix-ui/react-toggle-group": "^1.1.10",
- "@radix-ui/react-tooltip": "^1.2.7",
- "@tanstack/react-query": "^5.83.0",
- "class-variance-authority": "^0.7.1",
- "clsx": "^2.1.1",
- "cmdk": "^1.1.1",
- "date-fns": "^3.6.0",
- "embla-carousel-react": "^8.6.0",
- "input-otp": "^1.4.2",
- "lucide-react": "^0.462.0",
- "next-themes": "^0.3.0",
- "react": "^18.3.1",
- "react-day-picker": "^8.10.1",
- "react-dom": "^18.3.1",
- "react-hook-form": "^7.61.1",
- "react-resizable-panels": "^2.1.9",
- "react-router-dom": "^6.30.1",
- "recharts": "^2.15.4",
- "sonner": "^1.7.4",
- "tailwind-merge": "^2.6.0",
- "tailwindcss-animate": "^1.0.7",
- "vaul": "^0.9.9",
- "zod": "^3.25.76"
- },
- "devDependencies": {
- "@eslint/js": "^9.32.0",
- "@playwright/test": "^1.57.0",
- "@tailwindcss/typography": "^0.5.16",
- "@testing-library/jest-dom": "^6.6.0",
- "@testing-library/react": "^16.0.0",
- "@types/node": "^22.16.5",
- "@types/react": "^18.3.23",
- "@types/react-dom": "^18.3.7",
- "@vitejs/plugin-react-swc": "^3.11.0",
- "autoprefixer": "^10.4.21",
- "eslint": "^9.32.0",
- "eslint-plugin-react-hooks": "^5.2.0",
- "eslint-plugin-react-refresh": "^0.4.20",
- "globals": "^15.15.0",
- "jsdom": "^20.0.3",
- "lovable-tagger": "^1.1.13",
- "postcss": "^8.5.6",
- "tailwindcss": "^3.4.17",
- "typescript": "^5.8.3",
- "typescript-eslint": "^8.38.0",
- "vite": "^5.4.19",
- "vitest": "^3.2.4"
- }
-}
diff --git a/frontend/playwright-fixture.ts b/frontend/playwright-fixture.ts
deleted file mode 100644
index 7d471c1..0000000
--- a/frontend/playwright-fixture.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-// Re-export the base fixture from the package
-// Override or extend test/expect here if needed
-export { test, expect } from "lovable-agent-playwright-config/fixture";
diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts
deleted file mode 100644
index ec19e95..0000000
--- a/frontend/playwright.config.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { createLovableConfig } from "lovable-agent-playwright-config/config";
-
-export default createLovableConfig({
- // Add your custom playwright configuration overrides here
- // Example:
- // timeout: 60000,
- // use: {
- // baseURL: 'http://localhost:3000',
- // },
-});
diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js
deleted file mode 100644
index 2aa7205..0000000
--- a/frontend/postcss.config.js
+++ /dev/null
@@ -1,6 +0,0 @@
-export default {
- plugins: {
- tailwindcss: {},
- autoprefixer: {},
- },
-};
diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico
deleted file mode 100644
index 3c01d69..0000000
Binary files a/frontend/public/favicon.ico and /dev/null differ
diff --git a/frontend/public/placeholder.svg b/frontend/public/placeholder.svg
deleted file mode 100644
index ea950de..0000000
--- a/frontend/public/placeholder.svg
+++ /dev/null
@@ -1,40 +0,0 @@
-
diff --git a/frontend/public/robots.txt b/frontend/public/robots.txt
deleted file mode 100644
index 6018e70..0000000
--- a/frontend/public/robots.txt
+++ /dev/null
@@ -1,14 +0,0 @@
-User-agent: Googlebot
-Allow: /
-
-User-agent: Bingbot
-Allow: /
-
-User-agent: Twitterbot
-Allow: /
-
-User-agent: facebookexternalhit
-Allow: /
-
-User-agent: *
-Allow: /
diff --git a/frontend/src/App.css b/frontend/src/App.css
deleted file mode 100644
index b9d355d..0000000
--- a/frontend/src/App.css
+++ /dev/null
@@ -1,42 +0,0 @@
-#root {
- max-width: 1280px;
- margin: 0 auto;
- padding: 2rem;
- text-align: center;
-}
-
-.logo {
- height: 6em;
- padding: 1.5em;
- will-change: filter;
- transition: filter 300ms;
-}
-.logo:hover {
- filter: drop-shadow(0 0 2em #646cffaa);
-}
-.logo.react:hover {
- filter: drop-shadow(0 0 2em #61dafbaa);
-}
-
-@keyframes logo-spin {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
-}
-
-@media (prefers-reduced-motion: no-preference) {
- a:nth-of-type(2) .logo {
- animation: logo-spin infinite 20s linear;
- }
-}
-
-.card {
- padding: 2em;
-}
-
-.read-the-docs {
- color: #888;
-}
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
deleted file mode 100644
index f1ed102..0000000
--- a/frontend/src/App.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
-import { BrowserRouter, Route, Routes } from "react-router-dom";
-import { Toaster as Sonner } from "@/components/ui/sonner";
-import { Toaster } from "@/components/ui/toaster";
-import { TooltipProvider } from "@/components/ui/tooltip";
-import { PreferencesProvider } from "@/contexts/PreferencesContext";
-import Index from "./pages/Index.tsx";
-import Preferences from "./pages/Preferences.tsx";
-import NotFound from "./pages/NotFound.tsx";
-
-const queryClient = new QueryClient();
-
-const App = () => (
-
-
-
-
-
-
-
- } />
- } />
- } />
-
-
-
-
-
-);
-
-export default App;
diff --git a/frontend/src/components/CategoryTabs.tsx b/frontend/src/components/CategoryTabs.tsx
deleted file mode 100644
index 1be1eda..0000000
--- a/frontend/src/components/CategoryTabs.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import { Globe, Code, Image, Newspaper } from "lucide-react";
-import { CATEGORIES, type Category } from "@/lib/mock-data";
-
-const CATEGORY_META: Record = {
- general: { label: "General", icon: Globe },
- it: { label: "IT", icon: Code },
- images: { label: "Images", icon: Image },
- news: { label: "News", icon: Newspaper },
-};
-
-interface CategoryTabsProps {
- active: Category;
- onChange: (c: Category) => void;
-}
-
-export function CategoryTabs({ active, onChange }: CategoryTabsProps) {
- return (
-
- {CATEGORIES.map((cat) => {
- const { label, icon: Icon } = CATEGORY_META[cat];
- const isActive = cat === active;
- return (
-
- );
- })}
-
- );
-}
diff --git a/frontend/src/components/NavLink.tsx b/frontend/src/components/NavLink.tsx
deleted file mode 100644
index a561a95..0000000
--- a/frontend/src/components/NavLink.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import { NavLink as RouterNavLink, NavLinkProps } from "react-router-dom";
-import { forwardRef } from "react";
-import { cn } from "@/lib/utils";
-
-interface NavLinkCompatProps extends Omit {
- className?: string;
- activeClassName?: string;
- pendingClassName?: string;
-}
-
-const NavLink = forwardRef(
- ({ className, activeClassName, pendingClassName, to, ...props }, ref) => {
- return (
-
- cn(className, isActive && activeClassName, isPending && pendingClassName)
- }
- {...props}
- />
- );
- },
-);
-
-NavLink.displayName = "NavLink";
-
-export { NavLink };
diff --git a/frontend/src/components/ResultCard.tsx b/frontend/src/components/ResultCard.tsx
deleted file mode 100644
index 62209eb..0000000
--- a/frontend/src/components/ResultCard.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import type { SearchResult } from "@/lib/mock-data";
-
-interface ResultCardProps {
- result: SearchResult;
-}
-
-export function ResultCard({ result }: ResultCardProps) {
- const domain = result.parsed_url[1];
- const faviconUrl = `https://www.google.com/s2/favicons?domain=${domain}&sz=32`;
-
- return (
-
-
-

-
{result.pretty_url}
- {result.engines.length > 1 && (
-
- {result.engines.length} engines
-
- )}
-
-
- {result.title}
-
-
- {result.content}
-
- {result.publishedDate && (
-
- {new Date(result.publishedDate).toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" })}
-
- )}
-
- );
-}
diff --git a/frontend/src/components/ResultSkeleton.tsx b/frontend/src/components/ResultSkeleton.tsx
deleted file mode 100644
index 0119f4c..0000000
--- a/frontend/src/components/ResultSkeleton.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-export function ResultSkeleton() {
- return (
-
- {Array.from({ length: 5 }).map((_, i) => (
-
- ))}
-
- );
-}
diff --git a/frontend/src/components/SearchInput.tsx b/frontend/src/components/SearchInput.tsx
deleted file mode 100644
index 9d1cfe5..0000000
--- a/frontend/src/components/SearchInput.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-import { Search } from "lucide-react";
-import { Input } from "@/components/ui/input";
-import { Button } from "@/components/ui/button";
-import { FormEvent, useRef, useEffect } from "react";
-
-interface SearchInputProps {
- query: string;
- onQueryChange: (q: string) => void;
- onSearch: (q: string) => void;
- compact?: boolean;
- autoFocus?: boolean;
-}
-
-export function SearchInput({ query, onQueryChange, onSearch, compact, autoFocus }: SearchInputProps) {
- const inputRef = useRef(null);
-
- useEffect(() => {
- if (autoFocus) inputRef.current?.focus();
- }, [autoFocus]);
-
- const handleSubmit = (e: FormEvent) => {
- e.preventDefault();
- onSearch(query);
- };
-
- return (
-
- );
-}
diff --git a/frontend/src/components/ui/accordion.tsx b/frontend/src/components/ui/accordion.tsx
deleted file mode 100644
index 1e7878c..0000000
--- a/frontend/src/components/ui/accordion.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-import * as React from "react";
-import * as AccordionPrimitive from "@radix-ui/react-accordion";
-import { ChevronDown } from "lucide-react";
-
-import { cn } from "@/lib/utils";
-
-const Accordion = AccordionPrimitive.Root;
-
-const AccordionItem = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-));
-AccordionItem.displayName = "AccordionItem";
-
-const AccordionTrigger = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, children, ...props }, ref) => (
-
- svg]:rotate-180",
- className,
- )}
- {...props}
- >
- {children}
-
-
-
-));
-AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
-
-const AccordionContent = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, children, ...props }, ref) => (
-
- {children}
-
-));
-
-AccordionContent.displayName = AccordionPrimitive.Content.displayName;
-
-export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
diff --git a/frontend/src/components/ui/alert-dialog.tsx b/frontend/src/components/ui/alert-dialog.tsx
deleted file mode 100644
index 6dfbfb4..0000000
--- a/frontend/src/components/ui/alert-dialog.tsx
+++ /dev/null
@@ -1,104 +0,0 @@
-import * as React from "react";
-import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
-
-import { cn } from "@/lib/utils";
-import { buttonVariants } from "@/components/ui/button";
-
-const AlertDialog = AlertDialogPrimitive.Root;
-
-const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
-
-const AlertDialogPortal = AlertDialogPrimitive.Portal;
-
-const AlertDialogOverlay = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-));
-AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
-
-const AlertDialogContent = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-
-
-
-));
-AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
-
-const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes) => (
-
-);
-AlertDialogHeader.displayName = "AlertDialogHeader";
-
-const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes) => (
-
-);
-AlertDialogFooter.displayName = "AlertDialogFooter";
-
-const AlertDialogTitle = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-));
-AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
-
-const AlertDialogDescription = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-));
-AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName;
-
-const AlertDialogAction = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-));
-AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
-
-const AlertDialogCancel = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-));
-AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
-
-export {
- AlertDialog,
- AlertDialogPortal,
- AlertDialogOverlay,
- AlertDialogTrigger,
- AlertDialogContent,
- AlertDialogHeader,
- AlertDialogFooter,
- AlertDialogTitle,
- AlertDialogDescription,
- AlertDialogAction,
- AlertDialogCancel,
-};
diff --git a/frontend/src/components/ui/alert.tsx b/frontend/src/components/ui/alert.tsx
deleted file mode 100644
index 2efc3c8..0000000
--- a/frontend/src/components/ui/alert.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-import * as React from "react";
-import { cva, type VariantProps } from "class-variance-authority";
-
-import { cn } from "@/lib/utils";
-
-const alertVariants = cva(
- "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
- {
- variants: {
- variant: {
- default: "bg-background text-foreground",
- destructive: "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
- },
- },
- defaultVariants: {
- variant: "default",
- },
- },
-);
-
-const Alert = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes & VariantProps
->(({ className, variant, ...props }, ref) => (
-
-));
-Alert.displayName = "Alert";
-
-const AlertTitle = React.forwardRef>(
- ({ className, ...props }, ref) => (
-
- ),
-);
-AlertTitle.displayName = "AlertTitle";
-
-const AlertDescription = React.forwardRef>(
- ({ className, ...props }, ref) => (
-
- ),
-);
-AlertDescription.displayName = "AlertDescription";
-
-export { Alert, AlertTitle, AlertDescription };
diff --git a/frontend/src/components/ui/aspect-ratio.tsx b/frontend/src/components/ui/aspect-ratio.tsx
deleted file mode 100644
index c9e6f4b..0000000
--- a/frontend/src/components/ui/aspect-ratio.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio";
-
-const AspectRatio = AspectRatioPrimitive.Root;
-
-export { AspectRatio };
diff --git a/frontend/src/components/ui/avatar.tsx b/frontend/src/components/ui/avatar.tsx
deleted file mode 100644
index 68d21bb..0000000
--- a/frontend/src/components/ui/avatar.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import * as React from "react";
-import * as AvatarPrimitive from "@radix-ui/react-avatar";
-
-import { cn } from "@/lib/utils";
-
-const Avatar = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-));
-Avatar.displayName = AvatarPrimitive.Root.displayName;
-
-const AvatarImage = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-));
-AvatarImage.displayName = AvatarPrimitive.Image.displayName;
-
-const AvatarFallback = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-));
-AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
-
-export { Avatar, AvatarImage, AvatarFallback };
diff --git a/frontend/src/components/ui/badge.tsx b/frontend/src/components/ui/badge.tsx
deleted file mode 100644
index 0853c44..0000000
--- a/frontend/src/components/ui/badge.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import * as React from "react";
-import { cva, type VariantProps } from "class-variance-authority";
-
-import { cn } from "@/lib/utils";
-
-const badgeVariants = cva(
- "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
- {
- variants: {
- variant: {
- default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
- secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
- destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
- outline: "text-foreground",
- },
- },
- defaultVariants: {
- variant: "default",
- },
- },
-);
-
-export interface BadgeProps extends React.HTMLAttributes, VariantProps {}
-
-function Badge({ className, variant, ...props }: BadgeProps) {
- return ;
-}
-
-export { Badge, badgeVariants };
diff --git a/frontend/src/components/ui/breadcrumb.tsx b/frontend/src/components/ui/breadcrumb.tsx
deleted file mode 100644
index ca91ff5..0000000
--- a/frontend/src/components/ui/breadcrumb.tsx
+++ /dev/null
@@ -1,90 +0,0 @@
-import * as React from "react";
-import { Slot } from "@radix-ui/react-slot";
-import { ChevronRight, MoreHorizontal } from "lucide-react";
-
-import { cn } from "@/lib/utils";
-
-const Breadcrumb = React.forwardRef<
- HTMLElement,
- React.ComponentPropsWithoutRef<"nav"> & {
- separator?: React.ReactNode;
- }
->(({ ...props }, ref) => );
-Breadcrumb.displayName = "Breadcrumb";
-
-const BreadcrumbList = React.forwardRef>(
- ({ className, ...props }, ref) => (
-
- ),
-);
-BreadcrumbList.displayName = "BreadcrumbList";
-
-const BreadcrumbItem = React.forwardRef>(
- ({ className, ...props }, ref) => (
-
- ),
-);
-BreadcrumbItem.displayName = "BreadcrumbItem";
-
-const BreadcrumbLink = React.forwardRef<
- HTMLAnchorElement,
- React.ComponentPropsWithoutRef<"a"> & {
- asChild?: boolean;
- }
->(({ asChild, className, ...props }, ref) => {
- const Comp = asChild ? Slot : "a";
-
- return ;
-});
-BreadcrumbLink.displayName = "BreadcrumbLink";
-
-const BreadcrumbPage = React.forwardRef>(
- ({ className, ...props }, ref) => (
-
- ),
-);
-BreadcrumbPage.displayName = "BreadcrumbPage";
-
-const BreadcrumbSeparator = ({ children, className, ...props }: React.ComponentProps<"li">) => (
- svg]:size-3.5", className)} {...props}>
- {children ?? }
-
-);
-BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
-
-const BreadcrumbEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => (
-
-
- More
-
-);
-BreadcrumbEllipsis.displayName = "BreadcrumbElipssis";
-
-export {
- Breadcrumb,
- BreadcrumbList,
- BreadcrumbItem,
- BreadcrumbLink,
- BreadcrumbPage,
- BreadcrumbSeparator,
- BreadcrumbEllipsis,
-};
diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx
deleted file mode 100644
index cdedd4f..0000000
--- a/frontend/src/components/ui/button.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import * as React from "react";
-import { Slot } from "@radix-ui/react-slot";
-import { cva, type VariantProps } from "class-variance-authority";
-
-import { cn } from "@/lib/utils";
-
-const buttonVariants = cva(
- "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
- {
- variants: {
- variant: {
- default: "bg-primary text-primary-foreground hover:bg-primary/90",
- destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
- outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
- secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
- ghost: "hover:bg-accent hover:text-accent-foreground",
- link: "text-primary underline-offset-4 hover:underline",
- },
- size: {
- default: "h-10 px-4 py-2",
- sm: "h-9 rounded-md px-3",
- lg: "h-11 rounded-md px-8",
- icon: "h-10 w-10",
- },
- },
- defaultVariants: {
- variant: "default",
- size: "default",
- },
- },
-);
-
-export interface ButtonProps
- extends React.ButtonHTMLAttributes,
- VariantProps {
- asChild?: boolean;
-}
-
-const Button = React.forwardRef(
- ({ className, variant, size, asChild = false, ...props }, ref) => {
- const Comp = asChild ? Slot : "button";
- return ;
- },
-);
-Button.displayName = "Button";
-
-export { Button, buttonVariants };
diff --git a/frontend/src/components/ui/calendar.tsx b/frontend/src/components/ui/calendar.tsx
deleted file mode 100644
index 900a69e..0000000
--- a/frontend/src/components/ui/calendar.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-import * as React from "react";
-import { ChevronLeft, ChevronRight } from "lucide-react";
-import { DayPicker } from "react-day-picker";
-
-import { cn } from "@/lib/utils";
-import { buttonVariants } from "@/components/ui/button";
-
-export type CalendarProps = React.ComponentProps;
-
-function Calendar({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) {
- return (
- ,
- IconRight: ({ ..._props }) => ,
- }}
- {...props}
- />
- );
-}
-Calendar.displayName = "Calendar";
-
-export { Calendar };
diff --git a/frontend/src/components/ui/card.tsx b/frontend/src/components/ui/card.tsx
deleted file mode 100644
index e282748..0000000
--- a/frontend/src/components/ui/card.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-import * as React from "react";
-
-import { cn } from "@/lib/utils";
-
-const Card = React.forwardRef>(({ className, ...props }, ref) => (
-
-));
-Card.displayName = "Card";
-
-const CardHeader = React.forwardRef>(
- ({ className, ...props }, ref) => (
-
- ),
-);
-CardHeader.displayName = "CardHeader";
-
-const CardTitle = React.forwardRef>(
- ({ className, ...props }, ref) => (
-
- ),
-);
-CardTitle.displayName = "CardTitle";
-
-const CardDescription = React.forwardRef>(
- ({ className, ...props }, ref) => (
-
- ),
-);
-CardDescription.displayName = "CardDescription";
-
-const CardContent = React.forwardRef>(
- ({ className, ...props }, ref) => ,
-);
-CardContent.displayName = "CardContent";
-
-const CardFooter = React.forwardRef>(
- ({ className, ...props }, ref) => (
-
- ),
-);
-CardFooter.displayName = "CardFooter";
-
-export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
diff --git a/frontend/src/components/ui/carousel.tsx b/frontend/src/components/ui/carousel.tsx
deleted file mode 100644
index 3aa0f31..0000000
--- a/frontend/src/components/ui/carousel.tsx
+++ /dev/null
@@ -1,224 +0,0 @@
-import * as React from "react";
-import useEmblaCarousel, { type UseEmblaCarouselType } from "embla-carousel-react";
-import { ArrowLeft, ArrowRight } from "lucide-react";
-
-import { cn } from "@/lib/utils";
-import { Button } from "@/components/ui/button";
-
-type CarouselApi = UseEmblaCarouselType[1];
-type UseCarouselParameters = Parameters;
-type CarouselOptions = UseCarouselParameters[0];
-type CarouselPlugin = UseCarouselParameters[1];
-
-type CarouselProps = {
- opts?: CarouselOptions;
- plugins?: CarouselPlugin;
- orientation?: "horizontal" | "vertical";
- setApi?: (api: CarouselApi) => void;
-};
-
-type CarouselContextProps = {
- carouselRef: ReturnType[0];
- api: ReturnType[1];
- scrollPrev: () => void;
- scrollNext: () => void;
- canScrollPrev: boolean;
- canScrollNext: boolean;
-} & CarouselProps;
-
-const CarouselContext = React.createContext(null);
-
-function useCarousel() {
- const context = React.useContext(CarouselContext);
-
- if (!context) {
- throw new Error("useCarousel must be used within a ");
- }
-
- return context;
-}
-
-const Carousel = React.forwardRef & CarouselProps>(
- ({ orientation = "horizontal", opts, setApi, plugins, className, children, ...props }, ref) => {
- const [carouselRef, api] = useEmblaCarousel(
- {
- ...opts,
- axis: orientation === "horizontal" ? "x" : "y",
- },
- plugins,
- );
- const [canScrollPrev, setCanScrollPrev] = React.useState(false);
- const [canScrollNext, setCanScrollNext] = React.useState(false);
-
- const onSelect = React.useCallback((api: CarouselApi) => {
- if (!api) {
- return;
- }
-
- setCanScrollPrev(api.canScrollPrev());
- setCanScrollNext(api.canScrollNext());
- }, []);
-
- const scrollPrev = React.useCallback(() => {
- api?.scrollPrev();
- }, [api]);
-
- const scrollNext = React.useCallback(() => {
- api?.scrollNext();
- }, [api]);
-
- const handleKeyDown = React.useCallback(
- (event: React.KeyboardEvent) => {
- if (event.key === "ArrowLeft") {
- event.preventDefault();
- scrollPrev();
- } else if (event.key === "ArrowRight") {
- event.preventDefault();
- scrollNext();
- }
- },
- [scrollPrev, scrollNext],
- );
-
- React.useEffect(() => {
- if (!api || !setApi) {
- return;
- }
-
- setApi(api);
- }, [api, setApi]);
-
- React.useEffect(() => {
- if (!api) {
- return;
- }
-
- onSelect(api);
- api.on("reInit", onSelect);
- api.on("select", onSelect);
-
- return () => {
- api?.off("select", onSelect);
- };
- }, [api, onSelect]);
-
- return (
-
-
- {children}
-
-
- );
- },
-);
-Carousel.displayName = "Carousel";
-
-const CarouselContent = React.forwardRef>(
- ({ className, ...props }, ref) => {
- const { carouselRef, orientation } = useCarousel();
-
- return (
-
- );
- },
-);
-CarouselContent.displayName = "CarouselContent";
-
-const CarouselItem = React.forwardRef>(
- ({ className, ...props }, ref) => {
- const { orientation } = useCarousel();
-
- return (
-
- );
- },
-);
-CarouselItem.displayName = "CarouselItem";
-
-const CarouselPrevious = React.forwardRef>(
- ({ className, variant = "outline", size = "icon", ...props }, ref) => {
- const { orientation, scrollPrev, canScrollPrev } = useCarousel();
-
- return (
-
- );
- },
-);
-CarouselPrevious.displayName = "CarouselPrevious";
-
-const CarouselNext = React.forwardRef>(
- ({ className, variant = "outline", size = "icon", ...props }, ref) => {
- const { orientation, scrollNext, canScrollNext } = useCarousel();
-
- return (
-
- );
- },
-);
-CarouselNext.displayName = "CarouselNext";
-
-export { type CarouselApi, Carousel, CarouselContent, CarouselItem, CarouselPrevious, CarouselNext };
diff --git a/frontend/src/components/ui/chart.tsx b/frontend/src/components/ui/chart.tsx
deleted file mode 100644
index 08d40d9..0000000
--- a/frontend/src/components/ui/chart.tsx
+++ /dev/null
@@ -1,303 +0,0 @@
-import * as React from "react";
-import * as RechartsPrimitive from "recharts";
-
-import { cn } from "@/lib/utils";
-
-// Format: { THEME_NAME: CSS_SELECTOR }
-const THEMES = { light: "", dark: ".dark" } as const;
-
-export type ChartConfig = {
- [k in string]: {
- label?: React.ReactNode;
- icon?: React.ComponentType;
- } & ({ color?: string; theme?: never } | { color?: never; theme: Record });
-};
-
-type ChartContextProps = {
- config: ChartConfig;
-};
-
-const ChartContext = React.createContext(null);
-
-function useChart() {
- const context = React.useContext(ChartContext);
-
- if (!context) {
- throw new Error("useChart must be used within a ");
- }
-
- return context;
-}
-
-const ChartContainer = React.forwardRef<
- HTMLDivElement,
- React.ComponentProps<"div"> & {
- config: ChartConfig;
- children: React.ComponentProps["children"];
- }
->(({ id, className, children, config, ...props }, ref) => {
- const uniqueId = React.useId();
- const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
-
- return (
-
-
-
- {children}
-
-
- );
-});
-ChartContainer.displayName = "Chart";
-
-const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
- const colorConfig = Object.entries(config).filter(([_, config]) => config.theme || config.color);
-
- if (!colorConfig.length) {
- return null;
- }
-
- return (
-