Files
cburn/internal/tui/components/tabbar.go
teernisse c15dc8b487 feat(tui): polish components with icons, gradients, and proper backgrounds
Comprehensive visual refresh of all TUI components:

card.go:
- Add semantic icons based on metric type (tokens=◈, sessions=◉,
  cost=◆, cache=◇)
- Color-code metric values using new theme colors (Cyan, Magenta,
  Green, Blue) for visual variety
- Add CardRow() helper that properly height-equalizes cards and
  fills background on shorter cards to prevent "punched out" gaps
- Set explicit background on all style components

chart.go:
- Add background styling to sparkline renderer
- Ensure bar chart respects theme.Active.Surface background

progress.go:
- Add color gradient based on progress (Cyan→Accent→AccentBright)
- Style percentage text with bold and matching color
- Fix background fill on empty bar segments

statusbar.go:
- Complete redesign with SurfaceHover background
- Style keyboard hints: dim brackets, bright keys, muted labels
- Proper spacing and background continuity across sections
- Styled refresh indicator with AccentBright

tabbar.go:
- Add TabVisualWidth() for accurate mouse hit detection
- Modern underline-style active indicator using ━ characters
- AccentBright for active tab, proper dim styling for inactive
- Consistent Surface background across all tab elements

These changes create a cohesive visual language where every element
respects the dark background, icons add visual interest without
clutter, and color coding provides semantic meaning.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-28 00:05:39 -05:00

137 lines
3.8 KiB
Go

package components
import (
"strings"
"github.com/theirongolddev/cburn/internal/tui/theme"
"github.com/charmbracelet/lipgloss"
)
// Tab represents a single tab in the tab bar.
type Tab struct {
Name string
Key rune
KeyPos int // position of the shortcut letter in the name (-1 if not in name)
}
// Tabs defines all available tabs.
var Tabs = []Tab{
{Name: "Overview", Key: 'o', KeyPos: 0},
{Name: "Costs", Key: 'c', KeyPos: 0},
{Name: "Sessions", Key: 's', KeyPos: 0},
{Name: "Breakdown", Key: 'b', KeyPos: 0},
{Name: "Settings", Key: 'x', KeyPos: -1},
}
// TabVisualWidth returns the rendered visual width of a tab.
// This must match RenderTabBar's rendering logic exactly for mouse hit detection.
func TabVisualWidth(tab Tab, isActive bool) int {
// Active tabs: just the name with padding (1 on each side)
if isActive {
return len(tab.Name) + 2
}
// Inactive tabs: name with padding, plus "[k]" suffix if shortcut not in name
w := len(tab.Name) + 2
if tab.KeyPos < 0 {
w += 3 // "[k]"
}
return w
}
// RenderTabBar renders a modern tab bar with underline-style active indicator.
func RenderTabBar(activeIdx int, width int) string {
t := theme.Active
// Container with bottom border
barStyle := lipgloss.NewStyle().
Background(t.Surface).
Width(width)
// Active tab: bright text with accent underline
activeTabStyle := lipgloss.NewStyle().
Foreground(t.AccentBright).
Background(t.Surface).
Bold(true).
Padding(0, 1)
// Inactive tab: muted text
inactiveTabStyle := lipgloss.NewStyle().
Foreground(t.TextMuted).
Background(t.Surface).
Padding(0, 1)
// Key highlight style
keyStyle := lipgloss.NewStyle().
Foreground(t.Accent).
Background(t.Surface)
dimStyle := lipgloss.NewStyle().
Foreground(t.TextDim).
Background(t.Surface)
// Separator between tabs
sepStyle := lipgloss.NewStyle().
Foreground(t.Border).
Background(t.Surface)
var tabParts []string
var underlineParts []string
for i, tab := range Tabs {
var tabContent string
var underline string
if i == activeIdx {
// Active tab - full name, bright
tabContent = activeTabStyle.Render(tab.Name)
// Accent underline
underline = lipgloss.NewStyle().
Foreground(t.AccentBright).
Background(t.Surface).
Render(strings.Repeat("━", lipgloss.Width(tabContent)))
} else {
// Inactive tab - show key hint
if tab.KeyPos >= 0 && tab.KeyPos < len(tab.Name) {
before := tab.Name[:tab.KeyPos]
key := string(tab.Name[tab.KeyPos])
after := tab.Name[tab.KeyPos+1:]
tabContent = lipgloss.NewStyle().Padding(0, 1).Background(t.Surface).Render(
dimStyle.Render(before) + keyStyle.Render(key) + dimStyle.Render(after))
} else {
tabContent = inactiveTabStyle.Render(tab.Name) +
dimStyle.Render("[") + keyStyle.Render(string(tab.Key)) + dimStyle.Render("]")
}
// Dim underline
underline = lipgloss.NewStyle().
Foreground(t.Border).
Background(t.Surface).
Render(strings.Repeat("─", lipgloss.Width(tabContent)))
}
tabParts = append(tabParts, tabContent)
underlineParts = append(underlineParts, underline)
// Add separator between tabs (not after last)
if i < len(Tabs)-1 {
tabParts = append(tabParts, sepStyle.Render(" "))
underlineParts = append(underlineParts, sepStyle.Render(" "))
}
}
// Combine tab row and underline row
tabRow := strings.Join(tabParts, "")
underlineRow := strings.Join(underlineParts, "")
// Fill remaining width with border
tabRowWidth := lipgloss.Width(tabRow)
if tabRowWidth < width {
padding := width - tabRowWidth
tabRow += lipgloss.NewStyle().Background(t.Surface).Render(strings.Repeat(" ", padding))
underlineRow += lipgloss.NewStyle().Foreground(t.Border).Background(t.Surface).Render(strings.Repeat("─", padding))
}
return barStyle.Render(tabRow + "\n" + underlineRow)
}