Merge pull request #73 from LazyBachelor/LPM-138
LPM-138 Refactor Tui with composable modals and use canvas Refactors the TUI (dashboard + kanban) to use a shared, composable modal system with canvas-based overlay rendering, while consolidating styling into internal/style and expanding issue interactions (e.g., comments). Changes: Introduces a new pkg/tui/modal system (manager/stack + multiple modal types) and overlays modals using Lipgloss compositor layers. Refactors dashboard/kanban views and input handling to use the modal manager + focus manager instead of per-modal boolean state. Consolidates TUI styling by moving from pkg/tui/styles to internal/style and updates components to use the new style package; adds a shared footer renderer.
This commit is contained in:
@@ -2,30 +2,98 @@ package style
|
||||
|
||||
import (
|
||||
"charm.land/lipgloss/v2"
|
||||
)
|
||||
|
||||
// Color palette
|
||||
var (
|
||||
PrimaryColor = lipgloss.Color("6")
|
||||
SecondaryColor = lipgloss.Color("2")
|
||||
AccentColor = lipgloss.Color("7")
|
||||
TextColor = lipgloss.Color("15")
|
||||
|
||||
BorderColor = lipgloss.Color("8")
|
||||
"charm.land/lipgloss/v2/compat"
|
||||
)
|
||||
|
||||
var (
|
||||
AppStyle = lipgloss.NewStyle().Padding(1, 2).Foreground(TextColor)
|
||||
Primary = compat.AdaptiveColor{Light: lipgloss.Color("#5A56E0"), Dark: lipgloss.Color("#7571F9")}
|
||||
Secondary = compat.AdaptiveColor{Light: lipgloss.Color("#02BA84"), Dark: lipgloss.Color("#02BF87")}
|
||||
|
||||
Success = compat.AdaptiveColor{Light: lipgloss.Color("#02BA84"), Dark: lipgloss.Color("#02BF87")}
|
||||
Warning = compat.AdaptiveColor{Light: lipgloss.Color("#F59E0B"), Dark: lipgloss.Color("#F59E0B")}
|
||||
Error = compat.AdaptiveColor{Light: lipgloss.Color("#FE5F86"), Dark: lipgloss.Color("#FE5F86")}
|
||||
|
||||
PrimaryText = compat.AdaptiveColor{Light: lipgloss.Color("#1A1A1A"), Dark: lipgloss.Color("#E0E0E0")}
|
||||
SecondaryText = compat.AdaptiveColor{Light: lipgloss.Color("#666666"), Dark: lipgloss.Color("#999999")}
|
||||
FaintText = compat.AdaptiveColor{Light: lipgloss.Color("#999999"), Dark: lipgloss.Color("#666666")}
|
||||
|
||||
PrimaryBorder = compat.AdaptiveColor{Light: lipgloss.Color("#5A56E0"), Dark: lipgloss.Color("#7571F9")}
|
||||
SecondaryBorder = compat.AdaptiveColor{Light: lipgloss.Color("#CCCCCC"), Dark: lipgloss.Color("#444444")}
|
||||
|
||||
SelectedBackground = compat.AdaptiveColor{Light: lipgloss.Color("#E8E8E8"), Dark: lipgloss.Color("#333333")}
|
||||
)
|
||||
|
||||
const (
|
||||
ListViewRatio = 52 // Percentage of total width allocated to the list view
|
||||
LabelWidth = 14
|
||||
MarginBottomSmall = 1
|
||||
)
|
||||
|
||||
var DefaultBorder = lipgloss.ThickBorder()
|
||||
|
||||
var (
|
||||
HeaderStyle = lipgloss.NewStyle().Foreground(Primary).Padding(0, 1).Bold(true)
|
||||
HeaderTitleStyle = lipgloss.NewStyle().Foreground(Primary).Bold(true).Padding(0)
|
||||
)
|
||||
|
||||
var ContainerStyle = lipgloss.NewStyle().
|
||||
Border(DefaultBorder, true, false, false, false).
|
||||
BorderForeground(SecondaryBorder).
|
||||
Padding(1)
|
||||
|
||||
var ModalContainerStyle = lipgloss.NewStyle().
|
||||
Border(DefaultBorder).
|
||||
BorderForeground(PrimaryBorder).
|
||||
Padding(2, 3)
|
||||
|
||||
var DetailsContainerStyle = lipgloss.NewStyle().
|
||||
Border(DefaultBorder, true, false, false, true).
|
||||
BorderForeground(SecondaryBorder).
|
||||
Padding(1)
|
||||
|
||||
var (
|
||||
RowStyle = lipgloss.NewStyle().MarginBottom(MarginBottomSmall)
|
||||
TitleStyle = lipgloss.NewStyle().Foreground(Primary).Bold(true)
|
||||
LabelStyle = lipgloss.NewStyle().Foreground(SecondaryText)
|
||||
ValueStyle = lipgloss.NewStyle().Foreground(PrimaryText)
|
||||
IssueTypeStyle = lipgloss.NewStyle().Foreground(Primary).Bold(true)
|
||||
)
|
||||
|
||||
var (
|
||||
DefaultBorder = lipgloss.NormalBorder()
|
||||
BorderStyle = lipgloss.NewStyle().Border(DefaultBorder).BorderForeground(BorderColor)
|
||||
FilterStyle = lipgloss.NewStyle().Foreground(Primary).Bold(true).Padding(0, 1)
|
||||
FilterInputStyle = lipgloss.NewStyle().Foreground(PrimaryText).Padding(0, 1)
|
||||
FilterPromptStyle = lipgloss.NewStyle().Foreground(Primary).Bold(true)
|
||||
)
|
||||
|
||||
func StatusStyle(status string) lipgloss.Style {
|
||||
style := lipgloss.NewStyle().Bold(true)
|
||||
switch status {
|
||||
case "open":
|
||||
return style.Foreground(Secondary)
|
||||
case "closed":
|
||||
return style.Foreground(FaintText)
|
||||
case "in_progress":
|
||||
return style.Foreground(Warning)
|
||||
case "blocked":
|
||||
return style.Foreground(Error)
|
||||
default:
|
||||
return style.Foreground(SecondaryText)
|
||||
}
|
||||
}
|
||||
|
||||
func HighlightKey(key string) string {
|
||||
return lipgloss.NewStyle().
|
||||
Foreground(Primary).
|
||||
Bold(true).
|
||||
Padding(0, 1).
|
||||
Render(key)
|
||||
}
|
||||
|
||||
var (
|
||||
TitleStyle = lipgloss.NewStyle().Foreground(PrimaryColor).Bold(true)
|
||||
TextStyle = lipgloss.NewStyle().Foreground(TextColor)
|
||||
HelpStyle = lipgloss.NewStyle().Align(lipgloss.Center).Foreground(AccentColor)
|
||||
ErrorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("1")).Bold(true) // Red color for errors
|
||||
TextStyle = lipgloss.NewStyle().Foreground(PrimaryText)
|
||||
BorderStyle = lipgloss.NewStyle().Border(lipgloss.NormalBorder()).BorderForeground(SecondaryBorder)
|
||||
ErrorStyle = lipgloss.NewStyle().Foreground(Error).Bold(true)
|
||||
HelpStyle = lipgloss.NewStyle().Align(lipgloss.Center).Foreground(Secondary)
|
||||
)
|
||||
|
||||
var SecondaryColor = lipgloss.Color("#02BA84")
|
||||
|
||||
43
internal/utils/truncate/trunc.go
Normal file
43
internal/utils/truncate/trunc.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package truncate
|
||||
|
||||
import "charm.land/lipgloss/v2"
|
||||
|
||||
// TruncateToWidth trims the given text so that its rendered width does not
|
||||
// exceed maxWidth. If truncation occurs and there is room, an ellipsis is
|
||||
// appended to indicate that the text was shortened.
|
||||
func TruncateToWidth(text string, maxWidth int) string {
|
||||
if maxWidth <= 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
if lipgloss.Width(text) <= maxWidth {
|
||||
return text
|
||||
}
|
||||
|
||||
const ellipsis = "…"
|
||||
ellipsisWidth := lipgloss.Width(ellipsis)
|
||||
|
||||
if ellipsisWidth > maxWidth {
|
||||
runes := []rune(text)
|
||||
if len(runes) > 0 {
|
||||
firstChar := string(runes[0])
|
||||
if lipgloss.Width(firstChar) <= maxWidth {
|
||||
return firstChar
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
runes := []rune(text)
|
||||
lastSafe := 0
|
||||
for i := range runes {
|
||||
candidate := string(runes[:i+1])
|
||||
if lipgloss.Width(candidate)+ellipsisWidth > maxWidth {
|
||||
break
|
||||
}
|
||||
lastSafe = i + 1
|
||||
}
|
||||
|
||||
current := string(runes[:lastSafe])
|
||||
return current + ellipsis
|
||||
}
|
||||
19
internal/utils/user/user.go
Normal file
19
internal/utils/user/user.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/user"
|
||||
)
|
||||
|
||||
func GetOsUsername() string {
|
||||
if u, err := user.Current(); err == nil && u.Username != "" {
|
||||
return u.Username
|
||||
}
|
||||
if s := os.Getenv("USER"); s != "" {
|
||||
return s
|
||||
}
|
||||
if s := os.Getenv("USERNAME"); s != "" {
|
||||
return s
|
||||
}
|
||||
return "user"
|
||||
}
|
||||
Reference in New Issue
Block a user