feat(tui): stable detail panel with scroll, pinned footer, and 60/40 split #165
+19
-5
@@ -6,7 +6,7 @@ import (
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
func (m Model) titledPanelH(title, content string, width, height, scrollOffset int, focused bool) string {
|
||||
func (m Model) titledPanelH(title, content, footer string, width, height, scrollOffset int, focused bool) string {
|
||||
if height <= 0 {
|
||||
return m.titledPanel(title, content, width, focused)
|
||||
}
|
||||
@@ -40,7 +40,13 @@ func (m Model) titledPanelH(title, content string, width, height, scrollOffset i
|
||||
inner := contentStyle.Render(content)
|
||||
contentLines := strings.Split(inner, "\n")
|
||||
|
||||
bodyH := height - 2
|
||||
var footerLines []string
|
||||
if footer != "" {
|
||||
footerRendered := contentStyle.Render(footer)
|
||||
footerLines = strings.Split(footerRendered, "\n")
|
||||
}
|
||||
|
||||
bodyH := height - 2 - len(footerLines)
|
||||
if bodyH < 1 {
|
||||
bodyH = 1
|
||||
}
|
||||
@@ -58,13 +64,21 @@ func (m Model) titledPanelH(title, content string, width, height, scrollOffset i
|
||||
}
|
||||
visible := contentLines[scrollOffset:end]
|
||||
|
||||
borderLine := func(line string) string {
|
||||
return bc.Render("│") + line + strings.Repeat(" ", max(0, innerW-lipgloss.Width(line))) + bc.Render("│")
|
||||
}
|
||||
emptyLine := borderLine(strings.Repeat(" ", innerW))
|
||||
|
||||
var lines []string
|
||||
lines = append(lines, top)
|
||||
for _, line := range visible {
|
||||
lines = append(lines, bc.Render("│")+line+strings.Repeat(" ", max(0, innerW-lipgloss.Width(line)))+bc.Render("│"))
|
||||
lines = append(lines, borderLine(line))
|
||||
}
|
||||
for len(lines) < height-1 {
|
||||
lines = append(lines, bc.Render("│")+strings.Repeat(" ", innerW)+bc.Render("│"))
|
||||
for len(lines) < height-1-len(footerLines) {
|
||||
lines = append(lines, emptyLine)
|
||||
}
|
||||
for _, line := range footerLines {
|
||||
lines = append(lines, borderLine(line))
|
||||
}
|
||||
lines = append(lines, bottom)
|
||||
|
||||
|
||||
@@ -192,7 +192,8 @@ func (m Model) viewMonitorsLayout() string {
|
||||
}
|
||||
monHeight := lipgloss.Height(monPanel)
|
||||
detail := m.viewDetailInline(detailW-2, monHeight)
|
||||
detailPanel := m.zones.Mark("panel-detail", m.titledPanelH(title, detail, detailW, monHeight, m.detailScrollOffset, m.focusedPanel == panelDetail))
|
||||
footer := m.detailFooter(detailW - 2)
|
||||
detailPanel := m.zones.Mark("panel-detail", m.titledPanelH(title, detail, footer, detailW, monHeight, m.detailScrollOffset, m.focusedPanel == panelDetail))
|
||||
topParts = append(topParts, detailPanel)
|
||||
}
|
||||
|
||||
|
||||
@@ -143,8 +143,6 @@ func (m Model) viewDetailSidebar(site models.Site, hist monitor.SiteHistory, wid
|
||||
b.WriteString(" " + label.Render("No state changes") + "\n")
|
||||
}
|
||||
|
||||
b.WriteString("\n " + m.detailKeys() + "\n")
|
||||
|
||||
return lipgloss.NewStyle().Width(width).MaxWidth(width).Render(b.String())
|
||||
}
|
||||
|
||||
@@ -216,6 +214,33 @@ func (m Model) detailKeys() string {
|
||||
return m.st.subtleStyle.Render("[e] Edit [h] History [s] SLA [Esc] Back")
|
||||
}
|
||||
|
||||
func (m Model) detailFooter(width int) string {
|
||||
label := m.st.subtleStyle
|
||||
var lines []string
|
||||
|
||||
switch m.detailMode {
|
||||
case detailSLA:
|
||||
var keys []string
|
||||
for i, p := range slaPeriods {
|
||||
k := fmt.Sprintf("[%s] %s", p.key, p.label)
|
||||
if i == m.slaPeriodIdx {
|
||||
keys = append(keys, m.st.titleStyle.Render(k))
|
||||
} else {
|
||||
keys = append(keys, label.Render(k))
|
||||
}
|
||||
}
|
||||
lines = append(lines, " "+strings.Join(keys, " "))
|
||||
lines = append(lines, " "+label.Render("[Esc] Back"))
|
||||
case detailHistory:
|
||||
lines = append(lines, " "+label.Render("[Esc] Back"))
|
||||
default:
|
||||
lines = append(lines, " "+m.detailKeys())
|
||||
}
|
||||
|
||||
content := strings.Join(lines, "\n")
|
||||
return lipgloss.NewStyle().Width(width).MaxWidth(width).Render(content)
|
||||
}
|
||||
|
||||
func (m Model) fmtStatusWord(status string) string {
|
||||
switch status {
|
||||
case "DOWN", "SSL EXP":
|
||||
@@ -292,20 +317,6 @@ func (m Model) viewSLASidebar(width, _ int) string {
|
||||
}
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
|
||||
var keys []string
|
||||
for i, p := range slaPeriods {
|
||||
k := fmt.Sprintf("[%s] %s", p.key, p.label)
|
||||
if i == m.slaPeriodIdx {
|
||||
keys = append(keys, m.st.titleStyle.Render(k))
|
||||
} else {
|
||||
keys = append(keys, label.Render(k))
|
||||
}
|
||||
}
|
||||
b.WriteString(" " + strings.Join(keys, " ") + "\n")
|
||||
b.WriteString(" " + label.Render("[Esc] Back") + "\n")
|
||||
|
||||
return lipgloss.NewStyle().Width(width).MaxWidth(width).Render(b.String())
|
||||
}
|
||||
|
||||
@@ -368,7 +379,6 @@ func (m Model) viewHistorySidebar(width, _ int) string {
|
||||
statParts = append(statParts, "avg "+fmtDuration(avg))
|
||||
}
|
||||
b.WriteString(" " + label.Render(strings.Join(statParts, " │ ")) + "\n")
|
||||
b.WriteString(" " + label.Render("[Esc] Back") + "\n")
|
||||
|
||||
return lipgloss.NewStyle().Width(width).MaxWidth(width).Render(b.String())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user