LPM-140 The Sprinting Update Adds sprint support across the web UI (board + issue detail), TUI kanban, CLI/REPL, and the Beads-backed storage layer. Changes: Introduces sprint entities in storage and exposes sprint operations via IssueService. Updates web board to support backlog vs. sprint columns, sprint selection, and sprint creation routes. Updates TUI kanban to show backlog + sprint columns and adds sprint selection/creation actions.
146 lines
3.6 KiB
Go
146 lines
3.6 KiB
Go
package modal
|
|
|
|
// FocusArea represents a distinct focusable area in the UI
|
|
type FocusArea int
|
|
|
|
const (
|
|
FocusNone FocusArea = iota
|
|
FocusList
|
|
FocusDetail
|
|
FocusColumn0 // Backlog
|
|
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 == FocusColumn0 ||
|
|
f.currentArea == FocusColumn1 ||
|
|
f.currentArea == FocusColumn2 ||
|
|
f.currentArea == FocusColumn3 ||
|
|
f.currentArea == FocusColumn4
|
|
}
|
|
|
|
// NextColumn moves focus to the next column (for kanban)
|
|
func (f *FocusManager) NextColumn() {
|
|
areas := []FocusArea{FocusColumn0, 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, FocusColumn0}
|
|
f.cycleFocus(areas)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// 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
|
|
}
|