feat(tui): add 13 preloaded themes matching web design system

Port all web CSS token themes to TUI via shared vocabulary (accent, dim,
muted, ok, todo, event, remind, danger). Styles rebuild from active
theme on switch. Press T to cycle, persists to ~/.nib/theme. Glamour
markdown renderer respects light/dark per theme.
This commit is contained in:
2026-05-20 20:13:21 -04:00
parent db1dc135d2
commit a96c1a52f4
8 changed files with 196 additions and 146 deletions
+1 -1
View File
@@ -115,7 +115,7 @@ func (d detailModel) previewView(width int) string {
bodyWidth = 20 bodyWidth = 20
} }
r, _ := glamour.NewTermRenderer( r, _ := glamour.NewTermRenderer(
glamour.WithStylePath("dark"), glamour.WithStylePath(glamourStyle()),
glamour.WithWordWrap(bodyWidth), glamour.WithWordWrap(bodyWidth),
) )
rendered, err := r.Render(e.Body) rendered, err := r.Render(e.Body)
+1
View File
@@ -33,6 +33,7 @@ func renderHelp(width, height int) string {
{"2", "cards view"}, {"2", "cards view"},
{"s", "cycle sort (cards)"}, {"s", "cycle sort (cards)"},
{"i", "cycle intent (cards)"}, {"i", "cycle intent (cards)"},
{"T", "cycle theme"},
}}, }},
{"Actions", [][2]string{ {"Actions", [][2]string{
{"d", "delete (with confirm)"}, {"d", "delete (with confirm)"},
+2
View File
@@ -34,6 +34,7 @@ type keyMap struct {
Tab key.Binding Tab key.Binding
ToggleRail key.Binding ToggleRail key.Binding
Stumble key.Binding Stumble key.Binding
Theme key.Binding
} }
var keys = keyMap{ var keys = keyMap{
@@ -68,4 +69,5 @@ var keys = keyMap{
Tab: key.NewBinding(key.WithKeys("tab"), key.WithHelp("tab", "focus cycle")), Tab: key.NewBinding(key.WithKeys("tab"), key.WithHelp("tab", "focus cycle")),
ToggleRail: key.NewBinding(key.WithKeys("ctrl+b"), key.WithHelp("ctrl+b", "toggle tag rail")), ToggleRail: key.NewBinding(key.WithKeys("ctrl+b"), key.WithHelp("ctrl+b", "toggle tag rail")),
Stumble: key.NewBinding(key.WithKeys("S"), key.WithHelp("S", "stumble")), Stumble: key.NewBinding(key.WithKeys("S"), key.WithHelp("S", "stumble")),
Theme: key.NewBinding(key.WithKeys("T"), key.WithHelp("T", "theme")),
} }
+6
View File
@@ -105,6 +105,8 @@ type model struct {
} }
func newModel(store *db.Store) model { func newModel(store *db.Store) model {
loadTheme()
applyTheme()
inp := newInputModel() inp := newInputModel()
inp.ti.Focus() inp.ti.Focus()
return model{ return model{
@@ -565,6 +567,10 @@ func (m model) updateBrowse(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
} }
return m, nil return m, nil
case "T":
t := cycleTheme()
return m, m.setStatus("theme: " + t.Name)
case "i": case "i":
if m.mode == modeCards && m.state == stateList { if m.mode == modeCards && m.state == stateList {
m.cards.setIntent(m.cards.intent.next()) m.cards.setIntent(m.cards.intent.next())
+1 -1
View File
@@ -111,7 +111,7 @@ func (s stumbleModel) view() string {
bodyWidth = 20 bodyWidth = 20
} }
r, _ := glamour.NewTermRenderer( r, _ := glamour.NewTermRenderer(
glamour.WithStylePath("dark"), glamour.WithStylePath(glamourStyle()),
glamour.WithWordWrap(bodyWidth), glamour.WithWordWrap(bodyWidth),
) )
rendered, err := r.Render(e.Body) rendered, err := r.Render(e.Body)
+93 -143
View File
@@ -3,147 +3,97 @@ package tui
import "github.com/charmbracelet/lipgloss" import "github.com/charmbracelet/lipgloss"
var ( var (
subtle = lipgloss.AdaptiveColor{Light: "#D9DCCF", Dark: "#383838"} titleStyle lipgloss.Style
highlight = lipgloss.AdaptiveColor{Light: "#874BFD", Dark: "#7D56F4"} statusStyle lipgloss.Style
dim = lipgloss.AdaptiveColor{Light: "#A49FA5", Dark: "#777777"} listItemStyle lipgloss.Style
selectedItemStyle lipgloss.Style
titleStyle = lipgloss.NewStyle(). glyphStyle lipgloss.Style
Bold(true). completedGlyphStyle lipgloss.Style
Foreground(highlight). tagStyle lipgloss.Style
PaddingLeft(1) idStyle lipgloss.Style
inputPromptStyle lipgloss.Style
statusStyle = lipgloss.NewStyle(). detailHeaderStyle lipgloss.Style
Foreground(dim). detailBodyStyle lipgloss.Style
PaddingLeft(1) helpStyle lipgloss.Style
errorStyle lipgloss.Style
listItemStyle = lipgloss.NewStyle(). dateHeaderStyle lipgloss.Style
PaddingLeft(4) pinnedStyle lipgloss.Style
filterPillStyle lipgloss.Style
selectedItemStyle = lipgloss.NewStyle(). helpKeyStyle lipgloss.Style
PaddingLeft(1). helpDescStyle lipgloss.Style
Bold(true). affordanceStyle lipgloss.Style
Foreground(highlight). useCountStyle lipgloss.Style
SetString("") modeStyle lipgloss.Style
detailLabelStyle lipgloss.Style
glyphStyle = lipgloss.NewStyle(). detailValueStyle lipgloss.Style
Width(2) checkDoneStyle lipgloss.Style
checkPendingStyle lipgloss.Style
completedGlyphStyle = lipgloss.NewStyle(). searchPillStyle lipgloss.Style
Width(2). gutterStyle lipgloss.Style
Foreground(dim) drawerBorderStyle lipgloss.Style
drawerHintsStyle lipgloss.Style
tagStyle = lipgloss.NewStyle(). drawerPreviewStyle lipgloss.Style
Foreground(lipgloss.AdaptiveColor{Light: "#43BF6D", Dark: "#73F59F"}) separatorStyle lipgloss.Style
hintKeyStyle lipgloss.Style
idStyle = lipgloss.NewStyle(). hintDescStyle lipgloss.Style
Foreground(dim) railHeaderStyle lipgloss.Style
railTagStyle lipgloss.Style
inputPromptStyle = lipgloss.NewStyle(). railActiveTagStyle lipgloss.Style
Foreground(highlight). railCountStyle lipgloss.Style
Bold(true) stumbleAgeStyle lipgloss.Style
detailHeaderStyle = lipgloss.NewStyle().
Bold(true).
Foreground(highlight).
MarginBottom(1)
detailBodyStyle = lipgloss.NewStyle().
PaddingLeft(2).
PaddingTop(1)
helpStyle = lipgloss.NewStyle().
Foreground(dim).
PaddingLeft(1)
errorStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#FF0000")).
PaddingLeft(1)
dateHeaderStyle = lipgloss.NewStyle().
Foreground(dim).
PaddingLeft(1)
pinnedStyle = lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#D4A017", Dark: "#FFD700"})
filterPillStyle = lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#43BF6D", Dark: "#73F59F"}).
Bold(true)
helpKeyStyle = lipgloss.NewStyle().
Foreground(highlight).
Bold(true).
Width(18)
helpDescStyle = lipgloss.NewStyle().
Foreground(dim)
affordanceStyle = lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#5B8EF0", Dark: "#7AAFFF"}).
Bold(true)
useCountStyle = lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#B07D3A", Dark: "#D4A54A"})
modeStyle = lipgloss.NewStyle().
Foreground(dim).
Bold(true)
detailLabelStyle = lipgloss.NewStyle().
Foreground(highlight).
Bold(true)
detailValueStyle = lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#555555", Dark: "#BBBBBB"})
checkDoneStyle = lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#43BF6D", Dark: "#73F59F"})
checkPendingStyle = lipgloss.NewStyle().
Foreground(dim)
searchPillStyle = lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#E06C75", Dark: "#E06C75"}).
Bold(true)
gutterStyle = lipgloss.NewStyle().
Foreground(dim)
drawerBorderStyle = lipgloss.NewStyle().
Foreground(dim)
drawerHintsStyle = lipgloss.NewStyle().
Foreground(dim).
PaddingLeft(2)
drawerPreviewStyle = lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#555555", Dark: "#AAAAAA"}).
PaddingLeft(2)
separatorStyle = lipgloss.NewStyle().
Foreground(dim)
hintKeyStyle = lipgloss.NewStyle().
Foreground(highlight).
Bold(true)
hintDescStyle = lipgloss.NewStyle().
Foreground(dim)
railHeaderStyle = lipgloss.NewStyle().
Bold(true).
Foreground(dim)
railTagStyle = lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#43BF6D", Dark: "#73F59F"})
railActiveTagStyle = lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#43BF6D", Dark: "#73F59F"}).
Bold(true)
railCountStyle = lipgloss.NewStyle().
Foreground(dim)
stumbleAgeStyle = lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#cc4400", Dark: "#fab387"})
) )
func init() {
applyTheme()
}
func applyTheme() {
t := activeTheme()
accent := lipgloss.Color(t.Accent)
dim := lipgloss.Color(t.Dim)
muted := lipgloss.Color(t.Muted)
ok := lipgloss.Color(t.Ok)
todo := lipgloss.Color(t.Todo)
event := lipgloss.Color(t.Event)
remind := lipgloss.Color(t.Remind)
danger := lipgloss.Color(t.Danger)
titleStyle = lipgloss.NewStyle().Bold(true).Foreground(accent).PaddingLeft(1)
statusStyle = lipgloss.NewStyle().Foreground(dim).PaddingLeft(1)
listItemStyle = lipgloss.NewStyle().PaddingLeft(4)
selectedItemStyle = lipgloss.NewStyle().PaddingLeft(1).Bold(true).Foreground(accent).SetString("")
glyphStyle = lipgloss.NewStyle().Width(2)
completedGlyphStyle = lipgloss.NewStyle().Width(2).Foreground(dim)
tagStyle = lipgloss.NewStyle().Foreground(ok)
idStyle = lipgloss.NewStyle().Foreground(dim)
inputPromptStyle = lipgloss.NewStyle().Foreground(accent).Bold(true)
detailHeaderStyle = lipgloss.NewStyle().Bold(true).Foreground(accent).MarginBottom(1)
detailBodyStyle = lipgloss.NewStyle().PaddingLeft(2).PaddingTop(1)
helpStyle = lipgloss.NewStyle().Foreground(dim).PaddingLeft(1)
errorStyle = lipgloss.NewStyle().Foreground(danger).PaddingLeft(1)
dateHeaderStyle = lipgloss.NewStyle().Foreground(dim).PaddingLeft(1)
pinnedStyle = lipgloss.NewStyle().Foreground(todo)
filterPillStyle = lipgloss.NewStyle().Foreground(ok).Bold(true)
helpKeyStyle = lipgloss.NewStyle().Foreground(accent).Bold(true).Width(18)
helpDescStyle = lipgloss.NewStyle().Foreground(dim)
affordanceStyle = lipgloss.NewStyle().Foreground(event).Bold(true)
useCountStyle = lipgloss.NewStyle().Foreground(remind)
modeStyle = lipgloss.NewStyle().Foreground(dim).Bold(true)
detailLabelStyle = lipgloss.NewStyle().Foreground(accent).Bold(true)
detailValueStyle = lipgloss.NewStyle().Foreground(muted)
checkDoneStyle = lipgloss.NewStyle().Foreground(ok)
checkPendingStyle = lipgloss.NewStyle().Foreground(dim)
searchPillStyle = lipgloss.NewStyle().Foreground(danger).Bold(true)
gutterStyle = lipgloss.NewStyle().Foreground(dim)
drawerBorderStyle = lipgloss.NewStyle().Foreground(dim)
drawerHintsStyle = lipgloss.NewStyle().Foreground(dim).PaddingLeft(2)
drawerPreviewStyle = lipgloss.NewStyle().Foreground(muted).PaddingLeft(2)
separatorStyle = lipgloss.NewStyle().Foreground(dim)
hintKeyStyle = lipgloss.NewStyle().Foreground(accent).Bold(true)
hintDescStyle = lipgloss.NewStyle().Foreground(dim)
railHeaderStyle = lipgloss.NewStyle().Bold(true).Foreground(dim)
railTagStyle = lipgloss.NewStyle().Foreground(ok)
railActiveTagStyle = lipgloss.NewStyle().Foreground(ok).Bold(true)
railCountStyle = lipgloss.NewStyle().Foreground(dim)
stumbleAgeStyle = lipgloss.NewStyle().Foreground(remind)
}
+2 -1
View File
@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/charmbracelet/lipgloss"
"github.com/lerko/nib/internal/db" "github.com/lerko/nib/internal/db"
) )
@@ -78,7 +79,7 @@ func (r tagRailModel) view(focused bool) string {
headerStyle := railHeaderStyle headerStyle := railHeaderStyle
if focused { if focused {
headerStyle = headerStyle.Foreground(highlight) headerStyle = headerStyle.Foreground(lipgloss.Color(activeTheme().Accent))
} }
b.WriteString(headerStyle.Render("tags")) b.WriteString(headerStyle.Render("tags"))
b.WriteString("\n") b.WriteString("\n")
+90
View File
@@ -0,0 +1,90 @@
package tui
import (
"os"
"path/filepath"
"strings"
)
type Theme struct {
Name string
Dark bool
Accent string
Dim string
Muted string
Ok string
Todo string
Event string
Remind string
Danger string
}
var themes = []Theme{
{Name: "dark", Dark: true, Accent: "#c8942a", Dim: "#504840", Muted: "#8c8070", Ok: "#7aab72", Todo: "#d4a84b", Event: "#6898c8", Remind: "#c8784a", Danger: "#b85858"},
{Name: "tinycard", Dark: true, Accent: "#ad8ee6", Dim: "#555a6a", Muted: "#8b90a0", Ok: "#4ade80", Todo: "#fbbf24", Event: "#22d3ee", Remind: "#e8845a", Danger: "#ef4444"},
{Name: "catppuccin", Dark: true, Accent: "#cba6f7", Dim: "#6c7086", Muted: "#a6adc8", Ok: "#a6e3a1", Todo: "#f9e2af", Event: "#89b4fa", Remind: "#fab387", Danger: "#f38ba8"},
{Name: "nord", Dark: true, Accent: "#88c0d0", Dim: "#4c566a", Muted: "#d8dee9", Ok: "#a3be8c", Todo: "#ebcb8b", Event: "#81a1c1", Remind: "#d08770", Danger: "#bf616a"},
{Name: "dracula", Dark: true, Accent: "#bd93f9", Dim: "#6272a4", Muted: "#bfbfbf", Ok: "#50fa7b", Todo: "#f1fa8c", Event: "#8be9fd", Remind: "#ffb86c", Danger: "#ff5555"},
{Name: "gruvbox", Dark: true, Accent: "#fabd2f", Dim: "#665c54", Muted: "#a89984", Ok: "#b8bb26", Todo: "#fabd2f", Event: "#83a598", Remind: "#fe8019", Danger: "#fb4934"},
{Name: "rosepine", Dark: true, Accent: "#c4a7e7", Dim: "#6e6a86", Muted: "#908caa", Ok: "#a6da95", Todo: "#f6c177", Event: "#31748f", Remind: "#ea9a97", Danger: "#eb6f92"},
{Name: "tokyonight", Dark: true, Accent: "#7aa2f7", Dim: "#565f89", Muted: "#a9b1d6", Ok: "#9ece6a", Todo: "#e0af68", Event: "#7aa2f7", Remind: "#ff9e64", Danger: "#f7768e"},
{Name: "solarized", Dark: true, Accent: "#268bd2", Dim: "#586e75", Muted: "#657b83", Ok: "#859900", Todo: "#b58900", Event: "#268bd2", Remind: "#cb4b16", Danger: "#dc322f"},
{Name: "paper", Dark: false, Accent: "#8a6018", Dim: "#a09080", Muted: "#6a5e50", Ok: "#2a6828", Todo: "#7a5c00", Event: "#245890", Remind: "#984020", Danger: "#882030"},
{Name: "catppuccin-latte", Dark: false, Accent: "#8839ef", Dim: "#9ca0b0", Muted: "#6c6f85", Ok: "#40a02b", Todo: "#df8e1d", Event: "#1e66f5", Remind: "#fe640b", Danger: "#d20f39"},
{Name: "rosepine-dawn", Dark: false, Accent: "#907aa9", Dim: "#9893a5", Muted: "#797593", Ok: "#56949f", Todo: "#ea9d34", Event: "#286983", Remind: "#d7827e", Danger: "#b4637a"},
{Name: "solarized-light", Dark: false, Accent: "#268bd2", Dim: "#93a1a1", Muted: "#586e75", Ok: "#859900", Todo: "#b58900", Event: "#268bd2", Remind: "#cb4b16", Danger: "#dc322f"},
}
var activeThemeIndex int
func activeTheme() Theme {
return themes[activeThemeIndex]
}
func cycleTheme() Theme {
activeThemeIndex = (activeThemeIndex + 1) % len(themes)
applyTheme()
saveTheme()
return themes[activeThemeIndex]
}
func glamourStyle() string {
if themes[activeThemeIndex].Dark {
return "dark"
}
return "light"
}
func themePath() string {
home, err := os.UserHomeDir()
if err != nil {
return ""
}
return filepath.Join(home, ".nib", "theme")
}
func loadTheme() {
p := themePath()
if p == "" {
return
}
data, err := os.ReadFile(p)
if err != nil {
return
}
name := strings.TrimSpace(string(data))
for i, t := range themes {
if t.Name == name {
activeThemeIndex = i
return
}
}
}
func saveTheme() {
p := themePath()
if p == "" {
return
}
_ = os.WriteFile(p, []byte(themes[activeThemeIndex].Name+"\n"), 0o644)
}