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

@@ -3,6 +3,7 @@
package ui
import (
"fmt"
"strings"
"github.com/charmbracelet/lipgloss"
@@ -11,8 +12,9 @@ import (
// Ayu theme color palette
// Dark: https://terminalcolors.com/themes/ayu/dark/
// Light: https://terminalcolors.com/themes/ayu/light/
// Source: https://github.com/ayu-theme/ayu-colors
var (
// Semantic status colors (Ayu theme - adaptive light/dark)
// Core semantic colors (Ayu theme - adaptive light/dark)
ColorPass = lipgloss.AdaptiveColor{
Light: "#86b300", // ayu light bright green
Dark: "#c2d94c", // ayu dark bright green
@@ -33,9 +35,86 @@ var (
Light: "#399ee6", // ayu light bright blue
Dark: "#59c2ff", // ayu dark bright blue
}
// === Workflow Status Colors ===
// Only actionable states get color - open/closed match standard text
ColorStatusOpen = lipgloss.AdaptiveColor{
Light: "", // standard text color
Dark: "",
}
ColorStatusInProgress = lipgloss.AdaptiveColor{
Light: "#f2ae49", // yellow - active work, very visible
Dark: "#ffb454",
}
ColorStatusClosed = lipgloss.AdaptiveColor{
Light: "#9099a1", // slightly dimmed - visually shows "done"
Dark: "#8090a0",
}
ColorStatusBlocked = lipgloss.AdaptiveColor{
Light: "#f07171", // red - needs attention
Dark: "#f26d78",
}
ColorStatusPinned = lipgloss.AdaptiveColor{
Light: "#d2a6ff", // purple - special/elevated
Dark: "#d2a6ff",
}
// === Priority Colors ===
// Only P0/P1 get color - P2/P3/P4 match standard text
ColorPriorityP0 = lipgloss.AdaptiveColor{
Light: "#f07171", // bright red - critical
Dark: "#f07178",
}
ColorPriorityP1 = lipgloss.AdaptiveColor{
Light: "#ff8f40", // orange - high urgency
Dark: "#ff8f40",
}
ColorPriorityP2 = lipgloss.AdaptiveColor{
Light: "", // standard text color
Dark: "",
}
ColorPriorityP3 = lipgloss.AdaptiveColor{
Light: "", // standard text color
Dark: "",
}
ColorPriorityP4 = lipgloss.AdaptiveColor{
Light: "", // standard text color
Dark: "",
}
// === Issue Type Colors ===
// Bugs and epics get color - they need attention
// All other types use standard text
ColorTypeBug = lipgloss.AdaptiveColor{
Light: "#f07171", // bright red - bugs are problems
Dark: "#f26d78",
}
ColorTypeFeature = lipgloss.AdaptiveColor{
Light: "", // standard text color
Dark: "",
}
ColorTypeTask = lipgloss.AdaptiveColor{
Light: "", // standard text color
Dark: "",
}
ColorTypeEpic = lipgloss.AdaptiveColor{
Light: "#d2a6ff", // purple - larger scope work
Dark: "#d2a6ff",
}
ColorTypeChore = lipgloss.AdaptiveColor{
Light: "", // standard text color
Dark: "",
}
// === Issue ID Color ===
// IDs use standard text color - subtle, not attention-grabbing
ColorID = lipgloss.AdaptiveColor{
Light: "", // standard text color
Dark: "",
}
)
// Status styles - consistent across all commands
// Core styles - consistent across all commands
var (
PassStyle = lipgloss.NewStyle().Foreground(ColorPass)
WarnStyle = lipgloss.NewStyle().Foreground(ColorWarn)
@@ -44,6 +123,36 @@ var (
AccentStyle = lipgloss.NewStyle().Foreground(ColorAccent)
)
// Issue ID style
var IDStyle = lipgloss.NewStyle().Foreground(ColorID)
// Status styles for workflow states
var (
StatusOpenStyle = lipgloss.NewStyle().Foreground(ColorStatusOpen)
StatusInProgressStyle = lipgloss.NewStyle().Foreground(ColorStatusInProgress)
StatusClosedStyle = lipgloss.NewStyle().Foreground(ColorStatusClosed)
StatusBlockedStyle = lipgloss.NewStyle().Foreground(ColorStatusBlocked)
StatusPinnedStyle = lipgloss.NewStyle().Foreground(ColorStatusPinned)
)
// Priority styles
var (
PriorityP0Style = lipgloss.NewStyle().Foreground(ColorPriorityP0).Bold(true)
PriorityP1Style = lipgloss.NewStyle().Foreground(ColorPriorityP1)
PriorityP2Style = lipgloss.NewStyle().Foreground(ColorPriorityP2)
PriorityP3Style = lipgloss.NewStyle().Foreground(ColorPriorityP3)
PriorityP4Style = lipgloss.NewStyle().Foreground(ColorPriorityP4)
)
// Type styles for issue categories
var (
TypeBugStyle = lipgloss.NewStyle().Foreground(ColorTypeBug)
TypeFeatureStyle = lipgloss.NewStyle().Foreground(ColorTypeFeature)
TypeTaskStyle = lipgloss.NewStyle().Foreground(ColorTypeTask)
TypeEpicStyle = lipgloss.NewStyle().Foreground(ColorTypeEpic)
TypeChoreStyle = lipgloss.NewStyle().Foreground(ColorTypeChore)
)
// CategoryStyle for section headers - bold with accent color
var CategoryStyle = lipgloss.NewStyle().Bold(true).Foreground(ColorAccent)
@@ -128,3 +237,125 @@ func RenderSkipIcon() string {
func RenderInfoIcon() string {
return AccentStyle.Render(IconInfo)
}
// === Issue Component Renderers ===
// RenderID renders an issue ID with semantic styling
func RenderID(id string) string {
return IDStyle.Render(id)
}
// RenderStatus renders a status with semantic styling
// in_progress/blocked/pinned get color; open/closed use standard text
func RenderStatus(status string) string {
switch status {
case "in_progress":
return StatusInProgressStyle.Render(status)
case "blocked":
return StatusBlockedStyle.Render(status)
case "pinned":
return StatusPinnedStyle.Render(status)
case "closed":
return StatusClosedStyle.Render(status)
default: // open and others
return StatusOpenStyle.Render(status)
}
}
// RenderPriority renders a priority level with semantic styling
// P0/P1 get color; P2/P3/P4 use standard text
func RenderPriority(priority int) string {
label := fmt.Sprintf("P%d", priority)
switch priority {
case 0:
return PriorityP0Style.Render(label)
case 1:
return PriorityP1Style.Render(label)
case 2:
return PriorityP2Style.Render(label)
case 3:
return PriorityP3Style.Render(label)
case 4:
return PriorityP4Style.Render(label)
default:
return label
}
}
// RenderType renders an issue type with semantic styling
// bugs get color; all other types use standard text
func RenderType(issueType string) string {
switch issueType {
case "bug":
return TypeBugStyle.Render(issueType)
case "feature":
return TypeFeatureStyle.Render(issueType)
case "task":
return TypeTaskStyle.Render(issueType)
case "epic":
return TypeEpicStyle.Render(issueType)
case "chore":
return TypeChoreStyle.Render(issueType)
default:
return issueType
}
}
// RenderIssueCompact renders a compact one-line issue summary
// Format: ID [Priority] [Type] Status - Title
// When status is "closed", the entire line is dimmed to show it's done
func RenderIssueCompact(id string, priority int, issueType, status, title string) string {
line := fmt.Sprintf("%s [P%d] [%s] %s - %s",
id, priority, issueType, status, title)
if status == "closed" {
// Entire line is dimmed - visually shows "done"
return StatusClosedStyle.Render(line)
}
return fmt.Sprintf("%s [%s] [%s] %s - %s",
RenderID(id),
RenderPriority(priority),
RenderType(issueType),
RenderStatus(status),
title,
)
}
// RenderPriorityForStatus renders priority with color only if not closed
func RenderPriorityForStatus(priority int, status string) string {
if status == "closed" {
return fmt.Sprintf("P%d", priority)
}
return RenderPriority(priority)
}
// RenderTypeForStatus renders type with color only if not closed
func RenderTypeForStatus(issueType, status string) string {
if status == "closed" {
return issueType
}
return RenderType(issueType)
}
// RenderClosedLine renders an entire line in the closed/dimmed style
func RenderClosedLine(line string) string {
return StatusClosedStyle.Render(line)
}
// BoldStyle for emphasis
var BoldStyle = lipgloss.NewStyle().Bold(true)
// CommandStyle for command names - subtle contrast, not attention-grabbing
var CommandStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{
Light: "#5c6166", // slightly darker than standard
Dark: "#bfbdb6", // slightly brighter than standard
})
// RenderBold renders text in bold
func RenderBold(s string) string {
return BoldStyle.Render(s)
}
// RenderCommand renders a command name with subtle styling
func RenderCommand(s string) string {
return CommandStyle.Render(s)
}