Files
cburn/internal/tui/components/tabbar.go
teernisse 4d46977328 feat: expand component library with bar chart, content cards, and layout helpers
Add BarChart component that renders multi-row Unicode bar charts with
anchored Y-axis labels, automatic tick-step computation, sub-sampling
for narrow terminals, and optional X-axis date labels. The chart
gracefully degrades to a sparkline when width/height is too small.

Add ContentCard, CardRow, and CardInnerWidth utilities for consistent
bordered card layout across all dashboard tabs. ContentCard renders
a lipgloss-bordered card with optional bold title; CardRow joins
pre-rendered cards horizontally; CardInnerWidth computes the usable
text width after accounting for border and padding.

Add LayoutRow helper that distributes a total width into n integer
widths that sum exactly, absorbing the integer-division remainder
into the first items -- eliminates off-by-one pixel drift in multi-
column layouts.

Refactor MetricCard to accept an outerWidth parameter and derive the
content width internally by subtracting border, replacing the old
raw-width parameter that required callers to do the subtraction.
MetricCardRow now uses LayoutRow for exact width distribution.

Refine TabBar to render all tabs on a single row when they fit within
the terminal width, falling back to the two-row layout only when
they overflow.

Simplify StatusBar by removing the unused filterInfo append that was
cluttering the left section.
2026-02-19 15:01:26 -05:00

95 lines
2.4 KiB
Go

package components
import (
"strings"
"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: "Dashboard", Key: 'd', KeyPos: 0},
{Name: "Costs", Key: 'c', KeyPos: 0},
{Name: "Sessions", Key: 's', KeyPos: 0},
{Name: "Models", Key: 'm', KeyPos: 0},
{Name: "Projects", Key: 'p', KeyPos: 0},
{Name: "Trends", Key: 't', KeyPos: 0},
{Name: "Efficiency", Key: 'e', KeyPos: 0},
{Name: "Activity", Key: 'a', KeyPos: 0},
{Name: "Budget", Key: 'b', KeyPos: 0},
{Name: "Settings", Key: 'x', KeyPos: -1}, // x is not in "Settings"
}
// RenderTabBar renders the tab bar with the given active index.
func RenderTabBar(activeIdx int, width int) string {
t := theme.Active
activeStyle := lipgloss.NewStyle().
Foreground(t.Accent).
Bold(true)
inactiveStyle := lipgloss.NewStyle().
Foreground(t.TextMuted)
keyStyle := lipgloss.NewStyle().
Foreground(t.Accent).
Bold(true)
dimKeyStyle := lipgloss.NewStyle().
Foreground(t.TextDim)
var parts []string
for i, tab := range Tabs {
var rendered string
if i == activeIdx {
rendered = activeStyle.Render(tab.Name)
} else {
// Render with highlighted shortcut key
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:]
rendered = inactiveStyle.Render(before) +
dimKeyStyle.Render("[") + keyStyle.Render(key) + dimKeyStyle.Render("]") +
inactiveStyle.Render(after)
} else {
// Key not in name (e.g., "Settings" with 'x')
rendered = inactiveStyle.Render(tab.Name) +
dimKeyStyle.Render("[") + keyStyle.Render(string(tab.Key)) + dimKeyStyle.Render("]")
}
}
parts = append(parts, rendered)
}
// Single row if all tabs fit
full := " " + strings.Join(parts, " ")
if lipgloss.Width(full) <= width {
return full
}
// Fall back to two rows
row1 := strings.Join(parts[:5], " ")
row2 := strings.Join(parts[5:], " ")
return " " + row1 + "\n " + row2
}
// TabIdxByKey returns the tab index for a given key press, or -1.
func TabIdxByKey(key rune) int {
for i, tab := range Tabs {
if tab.Key == key {
return i
}
}
return -1
}