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:
143
pkg/tui/modal/focus.go
Normal file
143
pkg/tui/modal/focus.go
Normal file
@@ -0,0 +1,143 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user