feat: Phase 2 UX Polish — theme system, config profiles, error banner, tests
- Add Theme struct with 3 predefined themes (dark/light/druntime) - Add Profile struct for named configuration profiles - Add ErrorBanner with severity levels and auto-dismiss - Add unit tests for theme, error banner, and models - Update CHANGELOG.md and PROJECT_STATE.md
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
// ErrorSeverity indicates the level of an error message
|
||||
type ErrorSeverity int
|
||||
|
||||
const (
|
||||
SevError ErrorSeverity = iota
|
||||
SevWarning
|
||||
SevInfo
|
||||
)
|
||||
|
||||
// ErrorBanner displays a structured error message with title, details, and hints
|
||||
type ErrorBanner struct {
|
||||
Title string
|
||||
Detail string
|
||||
Hints []string
|
||||
Severity ErrorSeverity
|
||||
AutoDismiss bool
|
||||
DismissAfter time.Duration
|
||||
createdAt time.Time
|
||||
visible bool
|
||||
}
|
||||
|
||||
// NewErrorBanner creates a new error banner with the given severity
|
||||
func NewErrorBanner(severity ErrorSeverity) *ErrorBanner {
|
||||
return &ErrorBanner{
|
||||
Severity: severity,
|
||||
AutoDismiss: true,
|
||||
DismissAfter: 5 * time.Second,
|
||||
visible: false,
|
||||
}
|
||||
}
|
||||
|
||||
// Show displays the error banner with the given message
|
||||
func (b *ErrorBanner) Show(title, detail string, hints ...string) {
|
||||
b.Title = title
|
||||
b.Detail = detail
|
||||
b.Hints = hints
|
||||
b.visible = true
|
||||
b.createdAt = time.Now()
|
||||
}
|
||||
|
||||
// Hide hides the error banner
|
||||
func (b *ErrorBanner) Hide() {
|
||||
b.visible = false
|
||||
}
|
||||
|
||||
// IsVisible returns whether the banner is currently visible
|
||||
func (b *ErrorBanner) IsVisible() bool {
|
||||
return b.visible
|
||||
}
|
||||
|
||||
// Update checks if auto-dismiss time has elapsed
|
||||
func (b *ErrorBanner) Update() {
|
||||
if b.visible && b.AutoDismiss && time.Since(b.createdAt) > b.DismissAfter {
|
||||
b.visible = false
|
||||
}
|
||||
}
|
||||
|
||||
// View renders the error banner
|
||||
func (b *ErrorBanner) View(width int) string {
|
||||
if !b.visible {
|
||||
return ""
|
||||
}
|
||||
|
||||
var (
|
||||
titleStyle lipgloss.Style
|
||||
borderColor lipgloss.Color
|
||||
prefix string
|
||||
)
|
||||
|
||||
switch b.Severity {
|
||||
case SevError:
|
||||
titleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#ea6962")).Bold(true)
|
||||
borderColor = lipgloss.Color("#ea6962")
|
||||
prefix = "✖"
|
||||
case SevWarning:
|
||||
titleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#d8a657")).Bold(true)
|
||||
borderColor = lipgloss.Color("#d8a657")
|
||||
prefix = "⚠"
|
||||
case SevInfo:
|
||||
titleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#7daea3")).Bold(true)
|
||||
borderColor = lipgloss.Color("#7daea3")
|
||||
prefix = "ℹ"
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
|
||||
// Title line with prefix
|
||||
sb.WriteString(titleStyle.Render(fmt.Sprintf("%s %s", prefix, b.Title)))
|
||||
|
||||
// Detail line
|
||||
if b.Detail != "" {
|
||||
sb.WriteString("\n")
|
||||
sb.WriteString(strings.Repeat(" ", len(prefix)+1))
|
||||
detailStyle := lipgloss.NewStyle().Foreground(activeTheme.Fg)
|
||||
sb.WriteString(detailStyle.Render(b.Detail))
|
||||
}
|
||||
|
||||
// Hints
|
||||
if len(b.Hints) > 0 {
|
||||
sb.WriteString("\n")
|
||||
sb.WriteString(strings.Repeat(" ", len(prefix)+1))
|
||||
hintStyle := lipgloss.NewStyle().Foreground(activeTheme.FgMute)
|
||||
sb.WriteString(hintStyle.Render("Hints:"))
|
||||
for i, hint := range b.Hints {
|
||||
sb.WriteString("\n")
|
||||
sb.WriteString(strings.Repeat(" ", len(prefix)+2))
|
||||
sb.WriteString(hintStyle.Render(fmt.Sprintf("%d. %s", i+1, hint)))
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap in a styled box
|
||||
borderStyle := lipgloss.NewStyle().
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(borderColor).
|
||||
Padding(0, 1).
|
||||
Width(min(width-2, 80))
|
||||
|
||||
return borderStyle.Render(sb.String())
|
||||
}
|
||||
|
||||
// min returns the smaller of two integers
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
Reference in New Issue
Block a user