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" "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 { func (m Model) titledPanel(title, content string, width int, focused bool) string {
borderColor := m.theme.Border borderColor := m.theme.Border
titleColor := m.theme.Muted titleColor := m.theme.Muted
+1 -1
View File
@@ -142,7 +142,7 @@ func TestDetailLoad_CachesAndViewDoesNoIO(t *testing.T) {
} }
for i := 0; i < 3; i++ { for i := 0; i < 3; i++ {
_ = m.viewDetailInline(80) _ = m.viewDetailInline(80, 30)
} }
if ms.stateChangeCalls != 1 { if ms.stateChangeCalls != 1 {
t.Errorf("View performed DB IO: store hit %d times (want 1, from the Cmd only)", ms.stateChangeCalls) 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: case detailHistory:
title = "History · " + title title = "History · " + title
} }
detail := m.viewDetailInline(detailW - 2) monHeight := lipgloss.Height(monPanel)
detailPanel := m.zones.Mark("panel-detail", m.titledPanel(title, detail, detailW, m.focusedPanel == panelDetail)) 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) topParts = append(topParts, detailPanel)
} }
+22 -8
View File
@@ -10,23 +10,23 @@ import (
"github.com/charmbracelet/lipgloss" "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) { if m.cursor >= len(m.sites) {
return "" return ""
} }
switch m.detailMode { switch m.detailMode {
case detailSLA: case detailSLA:
return m.viewSLASidebar(width) return m.viewSLASidebar(width, height)
case detailHistory: case detailHistory:
return m.viewHistorySidebar(width) return m.viewHistorySidebar(width, height)
default: default:
site := m.sites[m.cursor] site := m.sites[m.cursor]
hist, _ := m.engine.GetHistory(site.ID) 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(" · ") dot := m.st.subtleStyle.Render(" · ")
label := m.st.subtleStyle label := m.st.subtleStyle
var b strings.Builder 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 var b strings.Builder
label := m.st.subtleStyle label := m.st.subtleStyle
innerW := width - 4 innerW := width - 4
@@ -277,7 +277,21 @@ func (m Model) viewSLASidebar(width int) string {
if dayBarW < 10 { if dayBarW < 10 {
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") dateStr := day.Date.Format("Jan 02")
dayBar := m.uptimeBar(day.UptimePct, dayBarW) dayBar := m.uptimeBar(day.UptimePct, dayBarW)
pctStr := fmtPct(day.UptimePct) + "%" 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()) 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 var b strings.Builder
label := m.st.subtleStyle label := m.st.subtleStyle
innerW := width - 4 innerW := width - 4