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

@@ -10,11 +10,11 @@ import (
"regexp"
"strings"
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/rpc"
"github.com/steveyegge/beads/internal/storage/sqlite"
"github.com/steveyegge/beads/internal/types"
"github.com/steveyegge/beads/internal/ui"
)
// deleteViaDaemon uses the RPC daemon to delete issues
@@ -62,19 +62,17 @@ func deleteViaDaemon(issueIDs []string, force, dryRun, cascade bool, jsonOutput
deletedCount := int(result["deleted_count"].(float64))
totalCount := int(result["total_count"].(float64))
green := color.New(color.FgGreen).SprintFunc()
if deletedCount > 0 {
if deletedCount == 1 {
fmt.Printf("%s Deleted %s\n", green("✓"), issueIDs[0])
fmt.Printf("%s Deleted %s\n", ui.RenderPass("✓"), issueIDs[0])
} else {
fmt.Printf("%s Deleted %d issue(s)\n", green("✓"), deletedCount)
fmt.Printf("%s Deleted %d issue(s)\n", ui.RenderPass("✓"), deletedCount)
}
}
if errors, ok := result["errors"].([]interface{}); ok && len(errors) > 0 {
yellow := color.New(color.FgYellow).SprintFunc()
fmt.Printf("\n%s Warnings:\n", yellow("⚠"))
fmt.Printf("\n%s Warnings:\n", ui.RenderWarn("⚠"))
for _, e := range errors {
fmt.Printf(" %s\n", e)
}
@@ -85,8 +83,9 @@ func deleteViaDaemon(issueIDs []string, force, dryRun, cascade bool, jsonOutput
}
var deleteCmd = &cobra.Command{
Use: "delete <issue-id> [issue-id...]",
Short: "Delete one or more issues and clean up references",
Use: "delete <issue-id> [issue-id...]",
GroupID: "issues",
Short: "Delete one or more issues and clean up references",
Long: `Delete one or more issues and clean up all references to them.
This command will:
1. Remove all dependency links (any type, both directions) involving the issues
@@ -215,9 +214,7 @@ the issues will not resurrect from remote branches.`,
replacementText := `$1[deleted:` + issueID + `]$3`
// Preview mode
if !force {
red := color.New(color.FgRed).SprintFunc()
yellow := color.New(color.FgYellow).SprintFunc()
fmt.Printf("\n%s\n", red("⚠️ DELETE PREVIEW"))
fmt.Printf("\n%s\n", ui.RenderFail("⚠️ DELETE PREVIEW"))
fmt.Printf("\nIssue to delete:\n")
fmt.Printf(" %s: %s\n", issueID, issue.Title)
totalDeps := len(depRecords) + len(dependents)
@@ -248,8 +245,8 @@ the issues will not resurrect from remote branches.`,
fmt.Printf(" (none have text references)\n")
}
}
fmt.Printf("\n%s\n", yellow("This operation cannot be undone!"))
fmt.Printf("To proceed, run: %s\n\n", yellow("bd delete "+issueID+" --force"))
fmt.Printf("\n%s\n", ui.RenderWarn("This operation cannot be undone!"))
fmt.Printf("To proceed, run: %s\n\n", ui.RenderWarn("bd delete "+issueID+" --force"))
return
}
// Actually delete
@@ -323,8 +320,7 @@ the issues will not resurrect from remote branches.`,
"references_updated": updatedIssueCount,
})
} else {
green := color.New(color.FgGreen).SprintFunc()
fmt.Printf("%s Deleted %s\n", green("✓"), issueID)
fmt.Printf("%s Deleted %s\n", ui.RenderPass("✓"), issueID)
fmt.Printf(" Removed %d dependency link(s)\n", totalDepsRemoved)
fmt.Printf(" Updated text references in %d issue(s)\n", updatedIssueCount)
}
@@ -476,14 +472,13 @@ func deleteBatch(_ *cobra.Command, issueIDs []string, force bool, dryRun bool, c
if dryRun {
fmt.Printf("\n(Dry-run mode - no changes made)\n")
} else {
yellow := color.New(color.FgYellow).SprintFunc()
fmt.Printf("\n%s\n", yellow("This operation cannot be undone!"))
fmt.Printf("\n%s\n", ui.RenderWarn("This operation cannot be undone!"))
if cascade {
fmt.Printf("To proceed with cascade deletion, run: %s\n",
yellow("bd delete "+strings.Join(issueIDs, " ")+" --cascade --force"))
ui.RenderWarn("bd delete "+strings.Join(issueIDs, " ")+" --cascade --force"))
} else {
fmt.Printf("To proceed, run: %s\n",
yellow("bd delete "+strings.Join(issueIDs, " ")+" --force"))
ui.RenderWarn("bd delete "+strings.Join(issueIDs, " ")+" --force"))
}
}
return
@@ -527,7 +522,7 @@ func deleteBatch(_ *cobra.Command, issueIDs []string, force bool, dryRun bool, c
// Use 'bd cleanup --hard' after syncing to fully purge old tombstones.
if hardDelete {
if !jsonOutput {
fmt.Println(color.YellowString("⚠️ HARD DELETE MODE: Pruning tombstones from JSONL"))
fmt.Println(ui.RenderWarn("⚠️ HARD DELETE MODE: Pruning tombstones from JSONL"))
fmt.Println(" Note: Tombstones kept in DB to prevent resurrection. Run 'bd sync' then 'bd cleanup --hard' to fully purge.")
}
// Prune tombstones from JSONL using negative TTL (immediate expiration)
@@ -555,24 +550,20 @@ func deleteBatch(_ *cobra.Command, issueIDs []string, force bool, dryRun bool, c
"orphaned_issues": result.OrphanedIssues,
})
} else {
green := color.New(color.FgGreen).SprintFunc()
fmt.Printf("%s Deleted %d issue(s)\n", green("✓"), result.DeletedCount)
fmt.Printf("%s Deleted %d issue(s)\n", ui.RenderPass("✓"), result.DeletedCount)
fmt.Printf(" Removed %d dependency link(s)\n", result.DependenciesCount)
fmt.Printf(" Removed %d label(s)\n", result.LabelsCount)
fmt.Printf(" Removed %d event(s)\n", result.EventsCount)
fmt.Printf(" Updated text references in %d issue(s)\n", updatedCount)
if len(result.OrphanedIssues) > 0 {
yellow := color.New(color.FgYellow).SprintFunc()
fmt.Printf(" %s Orphaned %d issue(s): %s\n",
yellow("⚠"), len(result.OrphanedIssues), strings.Join(result.OrphanedIssues, ", "))
ui.RenderWarn("⚠"), len(result.OrphanedIssues), strings.Join(result.OrphanedIssues, ", "))
}
}
}
// showDeletionPreview shows what would be deleted
func showDeletionPreview(issueIDs []string, issues map[string]*types.Issue, cascade bool, depError error) {
red := color.New(color.FgRed).SprintFunc()
yellow := color.New(color.FgYellow).SprintFunc()
fmt.Printf("\n%s\n", red("⚠️ DELETE PREVIEW"))
fmt.Printf("\n%s\n", ui.RenderFail("⚠️ DELETE PREVIEW"))
fmt.Printf("\nIssues to delete (%d):\n", len(issueIDs))
for _, id := range issueIDs {
if issue := issues[id]; issue != nil {
@@ -580,10 +571,10 @@ func showDeletionPreview(issueIDs []string, issues map[string]*types.Issue, casc
}
}
if cascade {
fmt.Printf("\n%s Cascade mode enabled - will also delete all dependent issues\n", yellow("⚠"))
fmt.Printf("\n%s Cascade mode enabled - will also delete all dependent issues\n", ui.RenderWarn("⚠"))
}
if depError != nil {
fmt.Printf("\n%s\n", red(depError.Error()))
fmt.Printf("\n%s\n", ui.RenderFail(depError.Error()))
}
}
// updateTextReferencesInIssues updates text references to deleted issues in pre-collected connected issues