Files
terminalShop-old/docs/theme-implementation.md
2026-04-09 17:39:59 +02:00

226 lines
7.6 KiB
Markdown

# Theme System Implementation Reference
> Use this document when implementing or modifying the theme system.
> Every decision and pattern is explained so context loss doesn't stall progress.
---
## What We're Doing
Replacing 40+ scattered `lipgloss.Color("#XXXXXX")` calls across 13 TUI files with a
centralized `Theme` struct in `pkg/tui/theme/`. Copied closely from the reference
implementation in `terminal-shop-source/packages/go/pkg/tui/theme/`.
---
## New Files to Create
```
pkg/tui/theme/theme.go — Theme struct, BasicTheme(), all color/style methods
pkg/tui/theme/huh.go — HuhTheme(), copyFieldStyles(), copyTextStyles()
```
---
## Files to Modify (in order)
| Step | File | Type of change |
|------|------|----------------|
| 1 | `pkg/tui/theme/theme.go` | Create |
| 2 | `pkg/tui/theme/huh.go` | Create |
| 3 | `pkg/tui/model.go` | Add `renderer` + `theme` fields; update constructors |
| 4 | `main.go` | Create renderer from SSH session; pass to NewModelWithAuth |
| 5 | `pkg/tui/views.go` | 3 color swaps |
| 6 | `pkg/tui/splash.go` | 2 color swaps |
| 7 | `pkg/tui/header.go` | 4 color swaps |
| 8 | `pkg/tui/footer.go` | 2 color swaps |
| 9 | `pkg/tui/breadcrumbs.go` | 3 color swaps |
| 10 | `pkg/tui/shop.go` | 6 color swaps |
| 11 | `pkg/tui/cart.go` | 9 color swaps |
| 12 | `pkg/tui/account.go` | 14 color swaps |
| 13 | `pkg/tui/menu.go` | 4 color swaps |
| 14 | `pkg/tui/help.go` | 5 color swaps |
| 15 | `pkg/tui/confirmation.go` | 5 color swaps |
| 16 | `pkg/tui/shipping_form.go` | 4 color swaps + `.WithTheme()` |
| 17 | `pkg/tui/payment_form.go` | 5 color swaps + `.WithTheme()` |
---
## Color Mapping (hardcoded → theme)
| Hardcoded | Semantic role | Text style method | Color accessor |
|-----------|--------------|-------------------|----------------|
| `#FFFFFF` | active / selected text | `m.theme.TextAccent()` | `m.theme.Accent()` |
| `#AAAAAA` | body / regular text | `m.theme.TextBody()` | `m.theme.Body()` |
| `#666666` | hints / inactive / dim text | `m.theme.TextDim()` | `m.theme.Dim()` |
| `#666666` | border foreground | — | `m.theme.Border()` |
| `#4682B4` | highlight / brand / steel blue | `m.theme.TextHighlight()` | `m.theme.Highlight()` |
| `#999999` | form label text | `m.theme.TextLabel()` | `m.theme.Label()` |
| `#00FF00` | success / order placed | `m.theme.TextSuccess()` | `m.theme.Success()` |
| `"196"` | error text | `m.theme.TextError()` | `m.theme.Error()` |
| `"205"` | loading / spinner | `m.theme.TextLoading()` | — |
| `"196"` + `"52"` | error banner bg+fg | `m.theme.PanelError()` | — |
| `"C6DDF0"` | user name (light blue) | `m.theme.TextHighlight()` | — |
| `#CCCCCC` | muted subtext | `m.theme.TextBody()` | — |
**Note:** `#666666` is used for BOTH dim text AND border foreground in this project.
Both `Dim()` and `Border()` return the same value. Use `Border()` in `BorderForeground()`,
use `Dim()` / `TextDim()` for text styling.
**Note:** Dynamic product colors (`coffee.Color` = `"#8B4513"` etc.) are NOT replaced.
Keep those as `lipgloss.Color(coffee.Color)`.
---
## Theme Colors (what BasicTheme sets)
```go
background = AdaptiveColor{Dark: "#000000", Light: "#FBFCFD"}
border = AdaptiveColor{Dark: "#666666", Light: "#D7DBDF"}
body = Color("#AAAAAA")
dim = Color("#666666")
accent = AdaptiveColor{Dark: "#FFFFFF", Light: "#11181C"}
highlight = Color("#4682B4")
brand = Color("#4682B4") // same as highlight for this project
label = Color("#999999")
success = Color("#00FF00")
error = Color("196")
```
---
## How the Renderer Works
The reference implementation creates a `*lipgloss.Renderer` **per SSH session**:
```go
// main.go — SSH handler
renderer := bubbletea.MakeRenderer(s) // s = ssh.Session
```
This lets lipgloss detect the correct color profile for each client's terminal
(e.g. TrueColor for Ghostty vs 256-color for basic terminals). All style creation
inside the theme uses `renderer.NewStyle()` instead of the global `lipgloss.NewStyle()`.
In this project:
- SSH sessions → renderer from `bubbletea.MakeRenderer(s)`
- Local / test mode → `lipgloss.DefaultRenderer()`
---
## Usage Pattern in Views
All view functions are methods on `Model`, so `m.theme` is always available.
**Text-only style (before → after):**
```go
// before
lipgloss.NewStyle().Foreground(lipgloss.Color("#FFFFFF")).Bold(true).Width(18)
// after
m.theme.TextAccent().Bold(true).Width(18)
```
**Border foreground (before → after):**
```go
// before
lipgloss.NewStyle().Border(lipgloss.NormalBorder()).BorderForeground(lipgloss.Color("#666666"))
// after
lipgloss.NewStyle().Border(lipgloss.NormalBorder()).BorderForeground(m.theme.Border())
```
**Selected item background (before → after):**
```go
// before
lipgloss.NewStyle().Background(lipgloss.Color("#4682B4")).Foreground(lipgloss.Color("#FFFFFF"))
// after
lipgloss.NewStyle().Background(m.theme.Highlight()).Foreground(m.theme.Accent())
```
**`lipgloss.NewStyle()` stays for layout-only styles** (Width, Margin, Padding, Align
with no color). Only color-setting calls move to the theme.
---
## Huh Form Theme
`HuhTheme()` creates a `*huh.Theme` applied to shipping and payment forms.
Applied by chaining `.WithTheme(m.theme.Form())` on the `huh.NewForm(...)` call:
```go
// shipping_form.go and payment_form.go — in buildShippingForm / buildPaymentForm
f := huh.NewForm(...).
WithShowErrors(false).
WithShowHelp(false).
WithTheme(m.theme.Form()) // ← add this line
```
Form theme styles:
- Focused border: thick left border in accent color (`#FFFFFF`)
- Field title + body text: body color (`#AAAAAA`)
- Cursor: highlight color (`#4682B4`)
- Placeholder text: dim color (`#666666`)
- Error: color `"196"`
- Blurred fields: hidden border (same styles, border invisible)
---
## Constructor Changes (model.go)
**Before:** `NewModel(username string)` initializes everything.
`NewModelWithAuth(fingerprint, pubKey, apiURL, secret, stripeKey)` calls NewModel.
**After:** A private `newModelWithRenderer(username, renderer)` does the heavy init.
Both public constructors call it:
```go
func NewModel(username string) Model {
return newModelWithRenderer(username, lipgloss.DefaultRenderer())
}
func NewModelWithAuth(renderer *lipgloss.Renderer, fingerprint, pubKey, apiURL, secret, stripeKey string) Model {
m := newModelWithRenderer("", renderer)
m.Fingerprint = fingerprint
// ... etc
return m
}
```
`NewModelWithAuth` signature change: **renderer moves to first parameter**.
Update the call in `main.go` accordingly.
---
## Theme Methods Quick Reference
```go
// Color accessors (lipgloss.TerminalColor — use in .Foreground() / .Background())
m.theme.Accent() // #FFFFFF / #11181C adaptive
m.theme.Body() // #AAAAAA
m.theme.Dim() // #666666
m.theme.Border() // #666666 / #D7DBDF adaptive
m.theme.Highlight() // #4682B4
m.theme.Brand() // #4682B4
m.theme.Label() // #999999
m.theme.Success() // #00FF00
m.theme.Error() // "196"
m.theme.Background() // #000000 / #FBFCFD adaptive
// Style helpers (lipgloss.Style — chain .Bold(), .Width(), .Padding() etc.)
m.theme.Base() // base style, body foreground
m.theme.TextAccent() // white text
m.theme.TextBody() // gray text (#AAAAAA)
m.theme.TextDim() // dim text (#666666)
m.theme.TextHighlight() // steel blue text (#4682B4)
m.theme.TextBrand() // same as TextHighlight
m.theme.TextLabel() // label text (#999999)
m.theme.TextSuccess() // green text (#00FF00)
m.theme.TextError() // error text ("196")
m.theme.TextLoading() // loading pink text ("205")
m.theme.PanelError() // error banner: dark red bg + red fg
m.theme.Form() // *huh.Theme for form styling
```