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.
103 lines
2.0 KiB
Go
103 lines
2.0 KiB
Go
package modal
|
|
|
|
import (
|
|
tea "charm.land/bubbletea/v2"
|
|
"charm.land/lipgloss/v2"
|
|
"github.com/LazyBachelor/LazyPM/internal/style"
|
|
)
|
|
|
|
// BaseModal provides common functionality for all modals.
|
|
// Embed this struct to get default implementations of the Modal interface.
|
|
type BaseModal struct {
|
|
id string
|
|
modType ModalType
|
|
active bool
|
|
width int
|
|
height int
|
|
}
|
|
|
|
// NewBaseModal creates a new base modal with the given properties
|
|
func NewBaseModal(id string, modType ModalType) BaseModal {
|
|
return BaseModal{
|
|
id: id,
|
|
modType: modType,
|
|
width: 80,
|
|
height: 20,
|
|
}
|
|
}
|
|
|
|
// ID returns the modal's unique identifier
|
|
func (b *BaseModal) ID() string {
|
|
return b.id
|
|
}
|
|
|
|
// Type returns the modal type
|
|
func (b *BaseModal) Type() ModalType {
|
|
return b.modType
|
|
}
|
|
|
|
// IsActive returns true if the modal is currently active
|
|
func (b *BaseModal) IsActive() bool {
|
|
return b.active
|
|
}
|
|
|
|
// SetSize updates the modal dimensions
|
|
func (b *BaseModal) SetSize(width, height int) {
|
|
if width < 1 {
|
|
width = 1
|
|
}
|
|
if height < 1 {
|
|
height = 1
|
|
}
|
|
b.width = width
|
|
b.height = height
|
|
}
|
|
|
|
// activate marks the modal as active
|
|
func (b *BaseModal) activate() tea.Cmd {
|
|
b.active = true
|
|
return nil
|
|
}
|
|
|
|
// deactivate marks the modal as inactive
|
|
func (b *BaseModal) deactivate() {
|
|
b.active = false
|
|
}
|
|
|
|
// Width returns the modal width
|
|
func (b *BaseModal) Width() int {
|
|
return b.width
|
|
}
|
|
|
|
// Height returns the modal height
|
|
func (b *BaseModal) Height() int {
|
|
return b.height
|
|
}
|
|
|
|
// ModalFrame renders content within a standard modal frame without full-screen placement
|
|
func ModalFrame(content string, width int) string {
|
|
if width < 5 {
|
|
return ""
|
|
}
|
|
|
|
boxWidth := max(min(80, width-4), 1)
|
|
|
|
return style.ModalContainerStyle.
|
|
Width(boxWidth).
|
|
Render(content)
|
|
}
|
|
|
|
// ModalWithLabel renders a modal with a label and content
|
|
func ModalWithLabel(label, content string, width int) string {
|
|
if width < 5 {
|
|
return ""
|
|
}
|
|
|
|
fullContent := lipgloss.JoinVertical(lipgloss.Left,
|
|
style.LabelStyle.Render(label),
|
|
content,
|
|
)
|
|
|
|
return ModalFrame(fullContent, width)
|
|
}
|