Files
LazyPM/pkg/tui/modal/focus.go
Robin Olsen 7facfb6afe 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.
2026-03-20 16:28:10 +01:00

144 lines
3.5 KiB
Go

package modal
// FocusArea represents a distinct focusable area in the UI
type FocusArea int
const (
FocusNone FocusArea = iota
FocusList
FocusDetail
FocusColumn1
FocusColumn2
FocusColumn3
FocusColumn4
)
// FocusManager handles focus state across different UI areas.
// It provides a clean separation of focus concerns from modal state.
type FocusManager struct {
currentArea FocusArea
areas map[FocusArea]bool
}
// NewFocusManager creates a new focus manager with all areas disabled by default
func NewFocusManager() *FocusManager {
return &FocusManager{
currentArea: FocusNone,
areas: make(map[FocusArea]bool),
}
}
// SetCurrent sets the currently focused area
func (f *FocusManager) SetCurrent(area FocusArea) {
f.currentArea = area
}
// Current returns the currently focused area
func (f *FocusManager) Current() FocusArea {
return f.currentArea
}
// IsFocused returns true if the given area is currently focused
func (f *FocusManager) IsFocused(area FocusArea) bool {
return f.currentArea == area
}
// IsListFocused returns true if any list area is focused
func (f *FocusManager) IsListFocused() bool {
return f.currentArea == FocusList ||
f.currentArea == FocusColumn1 ||
f.currentArea == FocusColumn2 ||
f.currentArea == FocusColumn3 ||
f.currentArea == FocusColumn4
}
// IsDetailFocused returns true if the detail area is focused
func (f *FocusManager) IsDetailFocused() bool {
return f.currentArea == FocusDetail
}
// EnableArea marks an area as available for focus
func (f *FocusManager) EnableArea(area FocusArea) {
f.areas[area] = true
}
// DisableArea marks an area as unavailable for focus
func (f *FocusManager) DisableArea(area FocusArea) {
f.areas[area] = false
if f.currentArea == area {
f.currentArea = FocusNone
}
}
// IsAreaEnabled returns true if the area is enabled
func (f *FocusManager) IsAreaEnabled(area FocusArea) bool {
return f.areas[area]
}
// Next moves focus to the next enabled area
func (f *FocusManager) Next() {
areas := []FocusArea{FocusList, FocusDetail}
f.cycleFocus(areas)
}
// Previous moves focus to the previous enabled area
func (f *FocusManager) Previous() {
areas := []FocusArea{FocusDetail, FocusList}
f.cycleFocus(areas)
}
// NextColumn moves focus to the next column (for kanban)
func (f *FocusManager) NextColumn() {
areas := []FocusArea{FocusColumn1, FocusColumn2, FocusColumn3, FocusColumn4}
f.cycleFocus(areas)
}
// PreviousColumn moves focus to the previous column (for kanban)
func (f *FocusManager) PreviousColumn() {
areas := []FocusArea{FocusColumn4, FocusColumn3, FocusColumn2, FocusColumn1}
f.cycleFocus(areas)
}
// cycleFocus finds the next enabled area in the given order
func (f *FocusManager) cycleFocus(areas []FocusArea) {
// Find current position
startIdx := -1
for i, area := range areas {
if area == f.currentArea {
startIdx = i
break
}
}
// Search for next enabled area
for i := 1; i <= len(areas); i++ {
idx := (startIdx + i) % len(areas)
if idx < 0 {
idx += len(areas)
}
if f.areas[areas[idx]] {
f.currentArea = areas[idx]
return
}
}
}
// ToggleDetail toggles between list and detail focus
func (f *FocusManager) ToggleDetail() {
if f.currentArea == FocusDetail {
f.currentArea = FocusList
} else {
f.currentArea = FocusDetail
}
}
// Reset clears the current focus
func (f *FocusManager) Reset() {
f.currentArea = FocusNone
}
// CanHandleKey returns true if the current focus area can handle keyboard input
func (f *FocusManager) CanHandleKey() bool {
return f.currentArea != FocusNone
}