diff --git a/pkg/tui/components/modals.go b/pkg/tui/components/modals.go index 1ca6ee1..b41e3bb 100644 --- a/pkg/tui/components/modals.go +++ b/pkg/tui/components/modals.go @@ -6,10 +6,25 @@ import ( "github.com/charmbracelet/lipgloss" ) +// modalBoxWidth returns a clamped width for modal content. Never returns a value < 1. +func modalBoxWidth(maxWidth, width int) int { + if width < 5 { + return 1 + } + w := min(maxWidth, width-4) + if w < 1 { + return 1 + } + return w +} + // components contains reusable TUI modal renderers for issue actions. func RenderEditTitle(width, height int, inputView string) string { - editBoxWidth := min(60, width-4) + if width < 5 || height < 5 { + return "" + } + editBoxWidth := modalBoxWidth(60, width) editContent := lipgloss.JoinVertical(lipgloss.Left, styles.LabelStyle.Render("Edit title (Enter to save, Esc to cancel):"), inputView, @@ -22,7 +37,10 @@ func RenderEditTitle(width, height int, inputView string) string { } func RenderEditDescription(width, height int, inputView string) string { - editBoxWidth := min(60, width-4) + if width < 5 || height < 5 { + return "" + } + editBoxWidth := modalBoxWidth(60, width) editContent := lipgloss.JoinVertical(lipgloss.Left, styles.LabelStyle.Render("Edit description (Ctrl+S to save, Esc to cancel):"), inputView, @@ -35,7 +53,10 @@ func RenderEditDescription(width, height int, inputView string) string { } func RenderCreateIssue(width, height int, inputView string) string { - createBoxWidth := min(60, width-4) + if width < 5 || height < 5 { + return "" + } + createBoxWidth := modalBoxWidth(60, width) createContent := lipgloss.JoinVertical(lipgloss.Left, styles.LabelStyle.Render("New issue (Enter to create, Esc to cancel):"), inputView, @@ -48,11 +69,14 @@ func RenderCreateIssue(width, height int, inputView string) string { } func RenderConfirmDelete(width, height int, issueID string) string { + if width < 5 || height < 5 { + return "" + } 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) + confirmBoxWidth := modalBoxWidth(50, width) confirmBox := styles.ContainerStyle. Width(confirmBoxWidth). BorderForeground(styles.PrimaryBorder). @@ -61,12 +85,15 @@ func RenderConfirmDelete(width, height int, issueID string) string { } func RenderChooseStatus(width, height int, issueID string) string { + if width < 5 || height < 5 { + return "" + } 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) + statusBoxWidth := modalBoxWidth(50, width) statusBox := styles.ContainerStyle. Width(statusBoxWidth). BorderForeground(styles.PrimaryBorder). @@ -75,12 +102,15 @@ func RenderChooseStatus(width, height int, issueID string) string { } func RenderChoosePriority(width, height int, issueID string) string { + if width < 5 || height < 5 { + return "" + } 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) + priorityBoxWidth := modalBoxWidth(60, width) priorityBox := styles.ContainerStyle. Width(priorityBoxWidth). BorderForeground(styles.PrimaryBorder). @@ -89,12 +119,15 @@ func RenderChoosePriority(width, height int, issueID string) string { } func RenderChooseType(width, height int, issueID string) string { + if width < 5 || height < 5 { + return "" + } 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) + typeBoxWidth := modalBoxWidth(65, width) typeBox := styles.ContainerStyle. Width(typeBoxWidth). BorderForeground(styles.PrimaryBorder). diff --git a/pkg/tui/views/kanban/keys.go b/pkg/tui/views/kanban/keys.go index 47227ce..bcaa08c 100644 --- a/pkg/tui/views/kanban/keys.go +++ b/pkg/tui/views/kanban/keys.go @@ -14,10 +14,15 @@ type KanbanKeyMap struct { MoveColumnRight key.Binding MoveIssueLeft key.Binding MoveIssueRight key.Binding + SubmitValidation key.Binding } var defaultKanbanKeyMap = KanbanKeyMap{ CommonKeyMap: components.DefaultCommonKeyMap(), + SubmitValidation: key.NewBinding( + key.WithKeys("S"), + key.WithHelp("S", "submit validation"), + ), SwitchToDashboard: key.NewBinding( key.WithKeys("v"), key.WithHelp("v", "dashboard 1"), @@ -44,6 +49,13 @@ func (d *Model) handleKeyMsg(msg tea.KeyMsg) tea.Cmd { var cmd tea.Cmd switch { + case key.Matches(msg, d.keyMap.SubmitValidation): + if d.submitChan != nil { + select { + case d.submitChan <- struct{}{}: + default: + } + } case key.Matches(msg, d.keyMap.Help): d.helpBar.ToggleHelp() case key.Matches(msg, d.keyMap.Quit): diff --git a/pkg/tui/views/kanban/model.go b/pkg/tui/views/kanban/model.go index 639e432..6ead0ec 100644 --- a/pkg/tui/views/kanban/model.go +++ b/pkg/tui/views/kanban/model.go @@ -55,11 +55,12 @@ type Model struct { typeIssueID string feedbackChan chan models.ValidationFeedback quitChan chan bool + submitChan chan<- struct{} currentFeedback models.ValidationFeedback showComplete bool } -func NewDashboard(app *app.App, feedbackChan chan models.ValidationFeedback, quitChan chan bool) *Model { +func NewDashboard(app *app.App, feedbackChan chan models.ValidationFeedback, quitChan chan bool, submitChan chan<- struct{}) *Model { m := &Model{ header: components.NewHeader("Kanban Board"), keyMap: defaultKanbanKeyMap, @@ -70,6 +71,7 @@ func NewDashboard(app *app.App, feedbackChan chan models.ValidationFeedback, qui focusOnDetail: false, feedbackChan: feedbackChan, quitChan: quitChan, + submitChan: submitChan, } allIssues, _ := app.Issues.SearchIssues(context.Background(), "", models.IssueFilter{}) diff --git a/pkg/tui/views/root.go b/pkg/tui/views/root.go index 9908040..4920610 100644 --- a/pkg/tui/views/root.go +++ b/pkg/tui/views/root.go @@ -14,6 +14,7 @@ type RootModel struct { app *app.App feedbackChan chan models.ValidationFeedback quitChan chan bool + submitChan chan<- struct{} lastSize tea.WindowSizeMsg hasSize bool } @@ -25,6 +26,7 @@ func NewRootView(app *app.App, feedbackChan chan models.ValidationFeedback, quit app: app, feedbackChan: feedbackChan, quitChan: quitChan, + submitChan: submitChan, } } @@ -44,7 +46,7 @@ func (r *RootModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return r, cmd case msgs.SwitchToDashboardMsg: // switch back to dashboard 1 and apply the last known size. - r.currentView = dashboard.NewDashboard(r.app, r.feedbackChan, r.quitChan, r.app.SubmitChan) + r.currentView = dashboard.NewDashboard(r.app, r.feedbackChan, r.quitChan, r.submitChan) var cmds []tea.Cmd if r.hasSize { // check if there is a size, and then update it @@ -59,7 +61,7 @@ func (r *RootModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return r, tea.Batch(cmds...) case msgs.SwitchToKanbanBoardMsg: // switch to kanban board and apply the last known size. - r.currentView = kanban.NewDashboard(r.app, r.feedbackChan, r.quitChan) + r.currentView = kanban.NewDashboard(r.app, r.feedbackChan, r.quitChan, r.submitChan) var cmds []tea.Cmd if r.hasSize { // check if there is a size, and then update it