refactor(tui): move shared components (help bar, issue list, modals, etc.) into components folder. Also added kanban board view.

This commit is contained in:
Moira Daniella A Sebastian
2026-03-04 15:20:16 +01:00
parent 5ca4aa1c38
commit 31ed1148f9
26 changed files with 1313 additions and 1811 deletions

View File

@@ -0,0 +1,163 @@
package components
import (
"github.com/LazyBachelor/LazyPM/internal/models"
"github.com/LazyBachelor/LazyPM/pkg/tui/styles"
"github.com/charmbracelet/lipgloss"
)
// components contains reusable TUI modal renderers for issue actions.
func RenderEditTitle(width, height int, inputView string) string {
editBoxWidth := min(60, width-4)
editContent := lipgloss.JoinVertical(lipgloss.Left,
styles.LabelStyle.Render("Edit title (Enter to save, Esc to cancel):"),
inputView,
)
editBox := styles.ContainerStyle.
Width(editBoxWidth).
BorderForeground(styles.PrimaryBorder).
Render(editContent)
return lipgloss.Place(width, height, lipgloss.Center, lipgloss.Center, editBox)
}
func RenderEditDescription(width, height int, inputView string) string {
editBoxWidth := min(60, width-4)
editContent := lipgloss.JoinVertical(lipgloss.Left,
styles.LabelStyle.Render("Edit description (Ctrl+S to save, Esc to cancel):"),
inputView,
)
editBox := styles.ContainerStyle.
Width(editBoxWidth).
BorderForeground(styles.PrimaryBorder).
Render(editContent)
return lipgloss.Place(width, height, lipgloss.Center, lipgloss.Center, editBox)
}
func RenderCreateIssue(width, height int, inputView string) string {
createBoxWidth := min(60, width-4)
createContent := lipgloss.JoinVertical(lipgloss.Left,
styles.LabelStyle.Render("New issue (Enter to create, Esc to cancel):"),
inputView,
)
createBox := styles.ContainerStyle.
Width(createBoxWidth).
BorderForeground(styles.PrimaryBorder).
Render(createContent)
return lipgloss.Place(width, height, lipgloss.Center, lipgloss.Center, createBox)
}
func RenderConfirmDelete(width, height int, issueID string) string {
confirmContent := lipgloss.JoinVertical(lipgloss.Left,
styles.LabelStyle.Render("Delete issue "+issueID+"?"),
lipgloss.NewStyle().Foreground(styles.FaintText).Render("Press y to delete, n or Esc to cancel"),
)
confirmBoxWidth := min(50, width-4)
confirmBox := styles.ContainerStyle.
Width(confirmBoxWidth).
BorderForeground(styles.PrimaryBorder).
Render(confirmContent)
return lipgloss.Place(width, height, lipgloss.Center, lipgloss.Center, confirmBox)
}
func RenderChooseStatus(width, height int, issueID string) string {
statusContent := lipgloss.JoinVertical(lipgloss.Left,
styles.LabelStyle.Render("Change status for "+issueID+":"),
lipgloss.NewStyle().Foreground(styles.FaintText).Render("o = open i = in_progress c = closed"),
lipgloss.NewStyle().Foreground(styles.FaintText).Render("Esc = cancel"),
)
statusBoxWidth := min(50, width-4)
statusBox := styles.ContainerStyle.
Width(statusBoxWidth).
BorderForeground(styles.PrimaryBorder).
Render(statusContent)
return lipgloss.Place(width, height, lipgloss.Center, lipgloss.Center, statusBox)
}
func RenderChoosePriority(width, height int, issueID string) string {
priorityContent := lipgloss.JoinVertical(lipgloss.Left,
styles.LabelStyle.Render("Change priority for "+issueID+":"),
lipgloss.NewStyle().Foreground(styles.FaintText).Render("0 = irrelevant 1 = low 2 = normal 3 = high 4 = critical"),
lipgloss.NewStyle().Foreground(styles.FaintText).Render("Esc = cancel"),
)
priorityBoxWidth := min(60, width-4)
priorityBox := styles.ContainerStyle.
Width(priorityBoxWidth).
BorderForeground(styles.PrimaryBorder).
Render(priorityContent)
return lipgloss.Place(width, height, lipgloss.Center, lipgloss.Center, priorityBox)
}
func RenderChooseType(width, height int, issueID string) string {
typeContent := lipgloss.JoinVertical(lipgloss.Left,
styles.LabelStyle.Render("Change type for "+issueID+":"),
lipgloss.NewStyle().Foreground(styles.FaintText).Render("b = bug f = feature t = task e = epic c = chore"),
lipgloss.NewStyle().Foreground(styles.FaintText).Render("Esc = cancel"),
)
typeBoxWidth := min(65, width-4)
typeBox := styles.ContainerStyle.
Width(typeBoxWidth).
BorderForeground(styles.PrimaryBorder).
Render(typeContent)
return lipgloss.Place(width, height, lipgloss.Center, lipgloss.Center, typeBox)
}
// RenderModals wraps the common modal overlay logic used by different views.
// It returns either one of the modal overlays (edit title/description, create,
// confirm delete, choose status/priority/type) or the provided main view if
// no modal is active.
func RenderModals(
width, height int,
editingTitle bool, titleInputView string,
editingDescription bool, descriptionInputView string,
creatingIssue bool, createTitleInputView string,
confirmingDelete bool, deleteIssueID string,
choosingStatus bool, statusIssueID string,
choosingPriority bool, priorityIssueID string,
choosingType bool, typeIssueID string,
mainView string,
) string {
if editingTitle {
return RenderEditTitle(width, height, titleInputView)
}
if editingDescription {
return RenderEditDescription(width, height, descriptionInputView)
}
if creatingIssue {
return RenderCreateIssue(width, height, createTitleInputView)
}
if confirmingDelete {
return RenderConfirmDelete(width, height, deleteIssueID)
}
if choosingStatus {
return RenderChooseStatus(width, height, statusIssueID)
}
if choosingPriority {
return RenderChoosePriority(width, height, priorityIssueID)
}
if choosingType {
return RenderChooseType(width, height, typeIssueID)
}
return mainView
}
// RenderFooter renders the shared footer with the help bar and optional
// validation feedback message.
func RenderFooter(width int, helpBar *HelpBar, feedback models.ValidationFeedback) string {
feedbackStatus := feedback.Message
if feedbackStatus == "" {
return helpBar.View()
}
helpBar.SetWidth(width - lipgloss.Width(feedbackStatus))
return lipgloss.JoinHorizontal(lipgloss.Left, helpBar.View(), feedbackStatus)
}