feat(tui): apply visual polish to dashboard tabs

Update all tab renderers to leverage the expanded theme palette and
polished components:

tab_overview.go:
- Use PanelCard (accent-bordered variant) for daily token chart
- Multi-color model bars: BlueBright, Cyan, Magenta, Yellow, Green
  for visual distinction between models
- Pre-compute styles outside loops for better performance
- Use Cyan for today's hourly chart, Magenta for last-hour chart

tab_breakdown.go:
- Apply consistent background styling
- Use new accent variants for visual hierarchy

tab_costs.go:
- Proper background fill on cost tables
- Accent coloring for cost highlights

tab_sessions.go:
- Background continuity in session list
- Visual improvements to session detail view

tab_settings.go:
- Consistent styling with other tabs

The result is a dashboard where each tab feels visually cohesive,
with color providing semantic meaning (different colors for different
models, metrics, and states) rather than arbitrary decoration.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
teernisse
2026-02-28 00:05:39 -05:00
parent c15dc8b487
commit 901090f921
5 changed files with 322 additions and 216 deletions

View File

@@ -21,7 +21,7 @@ func (a App) renderOverviewTab(cw int) string {
models := a.models
var b strings.Builder
// Row 1: Metric cards
// Row 1: Metric cards with colored values
costDelta := ""
if prev.CostPerDay > 0 {
costDelta = fmt.Sprintf("%s/day (%s)", cli.FormatCost(stats.CostPerDay), cli.FormatDelta(stats.CostPerDay, prev.CostPerDay))
@@ -54,7 +54,7 @@ func (a App) renderOverviewTab(cw int) string {
b.WriteString(components.MetricCardRow(cards, cw))
b.WriteString("\n")
// Row 2: Daily token usage chart
// Row 2: Daily token usage chart - use PanelCard for emphasis
if len(days) > 0 {
chartVals := make([]float64, len(days))
chartLabels := chartDateLabels(days)
@@ -62,9 +62,9 @@ func (a App) renderOverviewTab(cw int) string {
chartVals[len(days)-1-i] = float64(d.InputTokens + d.OutputTokens + d.CacheCreation5m + d.CacheCreation1h)
}
chartInnerW := components.CardInnerWidth(cw)
b.WriteString(components.ContentCard(
b.WriteString(components.PanelCard(
fmt.Sprintf("Daily Token Usage (%dd)", a.days),
components.BarChart(chartVals, chartLabels, t.Blue, chartInnerW, 10),
components.BarChart(chartVals, chartLabels, t.BlueBright, chartInnerW, 10),
cw,
))
b.WriteString("\n")
@@ -88,7 +88,7 @@ func (a App) renderOverviewTab(cw int) string {
}
todayCard = components.ContentCard(
fmt.Sprintf("Today (%s)", cli.FormatTokens(todayTotal)),
components.BarChart(hourVals, hourLabels24(), t.Blue, components.CardInnerWidth(liveHalves[0]), liveChartH),
components.BarChart(hourVals, hourLabels24(), t.Cyan, components.CardInnerWidth(liveHalves[0]), liveChartH),
liveHalves[0],
)
}
@@ -104,7 +104,7 @@ func (a App) renderOverviewTab(cw int) string {
}
lastHourCard = components.ContentCard(
fmt.Sprintf("Last Hour (%s)", cli.FormatTokens(hourTotal)),
components.BarChart(minVals, minuteLabels(), t.Accent, components.CardInnerWidth(liveHalves[1]), liveChartH),
components.BarChart(minVals, minuteLabels(), t.Magenta, components.CardInnerWidth(liveHalves[1]), liveChartH),
liveHalves[1],
)
}
@@ -127,10 +127,7 @@ func (a App) renderOverviewTab(cw int) string {
halves := components.LayoutRow(cw, 2)
innerW := components.CardInnerWidth(halves[0])
nameStyle := lipgloss.NewStyle().Foreground(t.TextPrimary)
barStyle := lipgloss.NewStyle().Foreground(t.Accent)
pctStyle := lipgloss.NewStyle().Foreground(t.TextDim)
// Model split with colored bars per model
var modelBody strings.Builder
limit := 5
if len(models) < limit {
@@ -150,18 +147,36 @@ func (a App) renderOverviewTab(cw int) string {
if barMaxLen < 1 {
barMaxLen = 1
}
for _, ms := range models[:limit] {
// Color palette for models - pre-compute styles to avoid allocation in loop
modelColors := []lipgloss.Color{t.BlueBright, t.Cyan, t.Magenta, t.Yellow, t.Green}
sepStyle := lipgloss.NewStyle().Background(t.Surface)
nameStyle := lipgloss.NewStyle().Foreground(t.TextPrimary).Background(t.Surface)
// Pre-compute bar and percent styles for each color
barStyles := make([]lipgloss.Style, len(modelColors))
pctStyles := make([]lipgloss.Style, len(modelColors))
for i, color := range modelColors {
barStyles[i] = lipgloss.NewStyle().Foreground(color).Background(t.Surface)
pctStyles[i] = lipgloss.NewStyle().Foreground(color).Background(t.Surface).Bold(true)
}
for i, ms := range models[:limit] {
barLen := 0
if maxShare > 0 {
barLen = int(ms.SharePercent / maxShare * float64(barMaxLen))
}
fmt.Fprintf(&modelBody, "%s %s %s\n",
nameStyle.Render(fmt.Sprintf("%-*s", nameW, shortModel(ms.Model))),
barStyle.Render(strings.Repeat("", barLen)),
pctStyle.Render(fmt.Sprintf("%.0f%%", ms.SharePercent)))
colorIdx := i % len(modelColors)
modelBody.WriteString(nameStyle.Render(fmt.Sprintf("%-*s", nameW, shortModel(ms.Model))))
modelBody.WriteString(sepStyle.Render(" "))
modelBody.WriteString(barStyles[colorIdx].Render(strings.Repeat("█", barLen)))
modelBody.WriteString(sepStyle.Render(" "))
modelBody.WriteString(pctStyles[colorIdx].Render(fmt.Sprintf("%3.0f%%", ms.SharePercent)))
modelBody.WriteString("\n")
}
// Compact activity: aggregate prompts into 4-hour buckets
// Activity patterns with time-of-day coloring
now := time.Now()
since := now.AddDate(0, 0, -a.days)
hours := pipeline.AggregateHourly(a.filtered, since, now)
@@ -172,11 +187,11 @@ func (a App) renderOverviewTab(cw int) string {
color lipgloss.Color
}
buckets := []actBucket{
{"Night 00-03", 0, t.Red},
{"Early 04-07", 0, t.Yellow},
{"Morning 08-11", 0, t.Green},
{"Night 00-03", 0, t.Magenta},
{"Early 04-07", 0, t.Orange},
{"Morning 08-11", 0, t.GreenBright},
{"Midday 12-15", 0, t.Green},
{"Evening 16-19", 0, t.Green},
{"Evening 16-19", 0, t.Cyan},
{"Late 20-23", 0, t.Yellow},
}
for _, h := range hours {
@@ -196,31 +211,34 @@ func (a App) renderOverviewTab(cw int) string {
actInnerW := components.CardInnerWidth(halves[1])
// Compute number column width from actual data so bars never overflow.
// Compute number column width
maxNumW := 5
for _, bk := range buckets {
if nw := len(cli.FormatNumber(int64(bk.total))); nw > maxNumW {
maxNumW = nw
}
}
// prefix = 13 (label) + 1 (space) + maxNumW (number) + 1 (space)
actBarMax := actInnerW - 15 - maxNumW
if actBarMax < 1 {
actBarMax = 1
}
numStyle := lipgloss.NewStyle().Foreground(t.TextMuted)
labelStyle := lipgloss.NewStyle().Foreground(t.TextMuted).Background(t.Surface)
numStyle := lipgloss.NewStyle().Foreground(t.TextPrimary).Background(t.Surface)
var actBody strings.Builder
for _, bk := range buckets {
bl := 0
if maxBucket > 0 {
bl = bk.total * actBarMax / maxBucket
}
bar := lipgloss.NewStyle().Foreground(bk.color).Render(strings.Repeat("█", bl))
fmt.Fprintf(&actBody, "%s %s %s\n",
numStyle.Render(bk.label),
numStyle.Render(fmt.Sprintf("%*s", maxNumW, cli.FormatNumber(int64(bk.total)))),
bar)
barStyle := lipgloss.NewStyle().Foreground(bk.color).Background(t.Surface)
actBody.WriteString(labelStyle.Render(bk.label))
actBody.WriteString(sepStyle.Render(" "))
actBody.WriteString(numStyle.Render(fmt.Sprintf("%*s", maxNumW, cli.FormatNumber(int64(bk.total)))))
actBody.WriteString(sepStyle.Render(" "))
actBody.WriteString(barStyle.Render(strings.Repeat("█", bl)))
actBody.WriteString("\n")
}
modelCard := components.ContentCard("Model Split", modelBody.String(), halves[0])
@@ -236,7 +254,7 @@ func (a App) renderOverviewTab(cw int) string {
return b.String()
}
// hourLabels24 returns X-axis labels for 24 hourly buckets (one per hour).
// hourLabels24 returns X-axis labels for 24 hourly buckets.
func hourLabels24() []string {
labels := make([]string, 24)
for i := 0; i < 24; i++ {
@@ -253,8 +271,7 @@ func hourLabels24() []string {
return labels
}
// minuteLabels returns X-axis labels for 12 five-minute buckets (one per bucket).
// Bucket 0 is oldest (55-60 min ago), bucket 11 is newest (0-5 min ago).
// minuteLabels returns X-axis labels for 12 five-minute buckets.
func minuteLabels() []string {
return []string{"-55", "-50", "-45", "-40", "-35", "-30", "-25", "-20", "-15", "-10", "-5", "now"}
}