feat(tui): stable detail panel with scroll, pinned footer, and 60/40 split #165

Merged
lerko merged 6 commits from fix/sla-sidebar-height into main 2026-07-01 01:27:52 +00:00
4 changed files with 38 additions and 11 deletions
Showing only changes of commit 0badc2ddf5 - Show all commits
+12
View File
@@ -6,6 +6,18 @@ import (
"github.com/charmbracelet/lipgloss"
)
func (m Model) titledPanelH(title, content string, width, height int, focused bool) string {
s := m.titledPanel(title, content, width, focused)
if height <= 0 {
return s
}
lines := strings.Split(s, "\n")
if len(lines) <= height {
return s
}
return strings.Join(lines[:height-1], "\n") + "\n" + lines[len(lines)-1]
}
func (m Model) titledPanel(title, content string, width int, focused bool) string {
borderColor := m.theme.Border
titleColor := m.theme.Muted
+1 -1
View File
@@ -142,7 +142,7 @@ func TestDetailLoad_CachesAndViewDoesNoIO(t *testing.T) {
}
for i := 0; i < 3; i++ {
_ = m.viewDetailInline(80)
_ = m.viewDetailInline(80, 30)
}
if ms.stateChangeCalls != 1 {
t.Errorf("View performed DB IO: store hit %d times (want 1, from the Cmd only)", ms.stateChangeCalls)
+3 -2
View File
@@ -190,8 +190,9 @@ func (m Model) viewMonitorsLayout() string {
case detailHistory:
title = "History · " + title
}
detail := m.viewDetailInline(detailW - 2)
detailPanel := m.zones.Mark("panel-detail", m.titledPanel(title, detail, detailW, m.focusedPanel == panelDetail))
monHeight := lipgloss.Height(monPanel)
detail := m.viewDetailInline(detailW-2, monHeight)
detailPanel := m.zones.Mark("panel-detail", m.titledPanelH(title, detail, detailW, monHeight, m.focusedPanel == panelDetail))
topParts = append(topParts, detailPanel)
}
+22 -8
View File
@@ -10,23 +10,23 @@ import (
"github.com/charmbracelet/lipgloss"
)
func (m Model) viewDetailInline(width int) string {
func (m Model) viewDetailInline(width, height int) string {
if m.cursor >= len(m.sites) {
return ""
}
switch m.detailMode {
case detailSLA:
return m.viewSLASidebar(width)
return m.viewSLASidebar(width, height)
case detailHistory:
return m.viewHistorySidebar(width)
return m.viewHistorySidebar(width, height)
default:
site := m.sites[m.cursor]
hist, _ := m.engine.GetHistory(site.ID)
return m.viewDetailSidebar(site, hist, width)
return m.viewDetailSidebar(site, hist, width, height)
}
}
func (m Model) viewDetailSidebar(site models.Site, hist monitor.SiteHistory, width int) string {
func (m Model) viewDetailSidebar(site models.Site, hist monitor.SiteHistory, width, _ int) string {
dot := m.st.subtleStyle.Render(" · ")
label := m.st.subtleStyle
var b strings.Builder
@@ -235,7 +235,7 @@ func (m Model) fmtStatusWord(status string) string {
}
}
func (m Model) viewSLASidebar(width int) string {
func (m Model) viewSLASidebar(width, height int) string {
var b strings.Builder
label := m.st.subtleStyle
innerW := width - 4
@@ -277,7 +277,21 @@ func (m Model) viewSLASidebar(width int) string {
if dayBarW < 10 {
dayBarW = 10
}
for _, day := range m.slaDailyBreakdown {
slaChrome := 14
if r.OutageCount > 0 {
slaChrome += 3
}
maxDaily := len(m.slaDailyBreakdown)
if height > 0 && height-slaChrome < maxDaily {
maxDaily = height - slaChrome
if maxDaily < 1 {
maxDaily = 1
}
}
for i, day := range m.slaDailyBreakdown {
if i >= maxDaily {
break
}
dateStr := day.Date.Format("Jan 02")
dayBar := m.uptimeBar(day.UptimePct, dayBarW)
pctStr := fmtPct(day.UptimePct) + "%"
@@ -309,7 +323,7 @@ func (m Model) viewSLASidebar(width int) string {
return lipgloss.NewStyle().Width(width).MaxWidth(width).Render(b.String())
}
func (m Model) viewHistorySidebar(width int) string {
func (m Model) viewHistorySidebar(width, _ int) string {
var b strings.Builder
label := m.st.subtleStyle
innerW := width - 4