refactor(ui): standardize on lipgloss semantic color system

Replace all fatih/color usages with internal/ui package that provides:
- Semantic color tokens (Pass, Warn, Fail, Accent, Muted)
- Adaptive light/dark mode support via Lipgloss AdaptiveColor
- Ayu theme colors for consistent, accessible output
- Tufte-inspired data-ink ratio principles

Files migrated: 35 command files in cmd/bd/

Add docs/ui-philosophy.md documenting:
- Semantic token usage guidelines
- Light/dark terminal optimization rationale
- Tufte and perceptual UI/UX theory application
- When to use (and not use) color in CLI output
This commit is contained in:
Ryan Snodgrass
2025-12-20 12:59:17 -08:00
parent fb1dff4f56
commit 6ca141712c
40 changed files with 887 additions and 646 deletions

View File

@@ -8,11 +8,11 @@ import (
"sort"
"strings"
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/rpc"
"github.com/steveyegge/beads/internal/storage"
"github.com/steveyegge/beads/internal/types"
"github.com/steveyegge/beads/internal/ui"
"github.com/steveyegge/beads/internal/utils"
)
@@ -33,8 +33,9 @@ type GraphLayout struct {
}
var graphCmd = &cobra.Command{
Use: "graph <issue-id>",
Short: "Display issue dependency graph",
Use: "graph <issue-id>",
GroupID: "deps",
Short: "Display issue dependency graph",
Long: `Display an ASCII visualization of an issue's dependency graph.
For epics, shows all children and their dependencies.
@@ -283,8 +284,7 @@ func renderGraph(layout *GraphLayout, subgraph *TemplateSubgraph) {
return
}
cyan := color.New(color.FgCyan).SprintFunc()
fmt.Printf("\n%s Dependency graph for %s:\n\n", cyan("📊"), layout.RootID)
fmt.Printf("\n%s Dependency graph for %s:\n\n", ui.RenderAccent("📊"), layout.RootID)
// Calculate box width based on longest title
maxTitleLen := 0
@@ -370,33 +370,34 @@ func renderGraph(layout *GraphLayout, subgraph *TemplateSubgraph) {
func renderNodeBox(node *GraphNode, width int) string {
// Status indicator
var statusIcon string
var colorFn func(a ...interface{}) string
var titleStr string
title := truncateTitle(node.Issue.Title, width-4)
switch node.Issue.Status {
case types.StatusOpen:
statusIcon = "○"
colorFn = color.New(color.FgWhite).SprintFunc()
titleStr = padRight(title, width-4)
case types.StatusInProgress:
statusIcon = "◐"
colorFn = color.New(color.FgYellow).SprintFunc()
titleStr = ui.RenderWarn(padRight(title, width-4))
case types.StatusBlocked:
statusIcon = "●"
colorFn = color.New(color.FgRed).SprintFunc()
titleStr = ui.RenderFail(padRight(title, width-4))
case types.StatusClosed:
statusIcon = "✓"
colorFn = color.New(color.FgGreen).SprintFunc()
titleStr = ui.RenderPass(padRight(title, width-4))
default:
statusIcon = "?"
colorFn = color.New(color.FgWhite).SprintFunc()
titleStr = padRight(title, width-4)
}
title := truncateTitle(node.Issue.Title, width-4)
id := node.Issue.ID
// Build the box
topBottom := " ┌" + strings.Repeat("─", width) + "┐"
middle := fmt.Sprintf(" │ %s %s │", statusIcon, colorFn(padRight(title, width-4)))
idLine := fmt.Sprintf(" │ %s │", color.New(color.FgHiBlack).Sprint(padRight(id, width-2)))
middle := fmt.Sprintf(" │ %s %s │", statusIcon, titleStr)
idLine := fmt.Sprintf(" │ %s │", ui.RenderMuted(padRight(id, width-2)))
bottom := " └" + strings.Repeat("─", width) + "┘"
return topBottom + "\n" + middle + "\n" + idLine + "\n" + bottom
@@ -446,27 +447,28 @@ func computeDependencyCounts(subgraph *TemplateSubgraph) (blocks map[string]int,
func renderNodeBoxWithDeps(node *GraphNode, width int, blocksCount int, blockedByCount int) string {
// Status indicator
var statusIcon string
var colorFn func(a ...interface{}) string
var titleStr string
title := truncateTitle(node.Issue.Title, width-4)
switch node.Issue.Status {
case types.StatusOpen:
statusIcon = "○"
colorFn = color.New(color.FgWhite).SprintFunc()
titleStr = padRight(title, width-4)
case types.StatusInProgress:
statusIcon = "◐"
colorFn = color.New(color.FgYellow).SprintFunc()
titleStr = ui.RenderWarn(padRight(title, width-4))
case types.StatusBlocked:
statusIcon = "●"
colorFn = color.New(color.FgRed).SprintFunc()
titleStr = ui.RenderFail(padRight(title, width-4))
case types.StatusClosed:
statusIcon = "✓"
colorFn = color.New(color.FgGreen).SprintFunc()
titleStr = ui.RenderPass(padRight(title, width-4))
default:
statusIcon = "?"
colorFn = color.New(color.FgWhite).SprintFunc()
titleStr = padRight(title, width-4)
}
title := truncateTitle(node.Issue.Title, width-4)
id := node.Issue.ID
// Build dependency info string
@@ -484,12 +486,12 @@ func renderNodeBoxWithDeps(node *GraphNode, width int, blocksCount int, blockedB
// Build the box
topBottom := " ┌" + strings.Repeat("─", width) + "┐"
middle := fmt.Sprintf(" │ %s %s │", statusIcon, colorFn(padRight(title, width-4)))
idLine := fmt.Sprintf(" │ %s │", color.New(color.FgHiBlack).Sprint(padRight(id, width-2)))
middle := fmt.Sprintf(" │ %s %s │", statusIcon, titleStr)
idLine := fmt.Sprintf(" │ %s │", ui.RenderMuted(padRight(id, width-2)))
var result string
if depInfo != "" {
depLine := fmt.Sprintf(" │ %s │", color.New(color.FgCyan).Sprint(padRight(depInfo, width-2)))
depLine := fmt.Sprintf(" │ %s │", ui.RenderAccent(padRight(depInfo, width-2)))
bottom := " └" + strings.Repeat("─", width) + "┘"
result = topBottom + "\n" + middle + "\n" + idLine + "\n" + depLine + "\n" + bottom
} else {