feat(tui): lazygit-style titled panel borders
All panels wrapped in titled rounded borders (╭─ Title ──╮). Focused panel gets accent-colored border, unfocused panels get muted border. - Monitors panel: titled "Monitors", focused when detail is closed - Logs panel: titled "Logs", always unfocused (passive display) - Detail panel: titled with monitor name, focused when open Table's own RoundedBorder replaced with HiddenBorder — the titled panel border provides the visual frame, table uses space-separated columns internally. Consistent chrome across all panels.
This commit is contained in:
@@ -0,0 +1,52 @@
|
|||||||
|
package tui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m Model) titledPanel(title, content string, width int, focused bool) string {
|
||||||
|
borderColor := m.theme.Border
|
||||||
|
titleColor := m.theme.Muted
|
||||||
|
if focused {
|
||||||
|
borderColor = m.theme.Accent
|
||||||
|
titleColor = m.theme.Accent
|
||||||
|
}
|
||||||
|
|
||||||
|
bc := lipgloss.NewStyle().Foreground(borderColor)
|
||||||
|
tc := lipgloss.NewStyle().Foreground(titleColor).Bold(true)
|
||||||
|
|
||||||
|
innerW := width - 2
|
||||||
|
if innerW < 10 {
|
||||||
|
innerW = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
titleRendered := tc.Render(" " + title + " ")
|
||||||
|
titleLen := len([]rune(title)) + 2
|
||||||
|
fillLen := innerW - titleLen - 1
|
||||||
|
if fillLen < 0 {
|
||||||
|
fillLen = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
top := bc.Render("╭─") + titleRendered + bc.Render(strings.Repeat("─", fillLen)+"╮")
|
||||||
|
|
||||||
|
contentStyle := lipgloss.NewStyle().Width(innerW).MaxWidth(innerW)
|
||||||
|
inner := contentStyle.Render(content)
|
||||||
|
|
||||||
|
var lines []string
|
||||||
|
lines = append(lines, top)
|
||||||
|
for _, line := range strings.Split(inner, "\n") {
|
||||||
|
lines = append(lines, bc.Render("│")+line+strings.Repeat(" ", max(0, innerW-lipgloss.Width(line)))+bc.Render("│"))
|
||||||
|
}
|
||||||
|
lines = append(lines, bc.Render("╰"+strings.Repeat("─", innerW)+"╯"))
|
||||||
|
|
||||||
|
return strings.Join(lines, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func max(a, b int) int {
|
||||||
|
if a > b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
@@ -52,8 +52,7 @@ func (m Model) renderTable(headers []string, items int, buildRows func(start, en
|
|||||||
}
|
}
|
||||||
|
|
||||||
t := table.New().
|
t := table.New().
|
||||||
Border(lipgloss.RoundedBorder()).
|
Border(lipgloss.HiddenBorder()).
|
||||||
BorderStyle(m.st.tableBorderStyle).
|
|
||||||
Width(tableWidth).
|
Width(tableWidth).
|
||||||
Headers(headers...).
|
Headers(headers...).
|
||||||
Rows(rows...).
|
Rows(rows...).
|
||||||
@@ -94,5 +93,5 @@ func (m Model) renderTable(headers []string, items int, buildRows func(start, en
|
|||||||
return base
|
return base
|
||||||
})
|
})
|
||||||
|
|
||||||
return "\n" + t.Render()
|
return t.Render()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -157,35 +157,38 @@ func (m Model) viewDashboard() string {
|
|||||||
availW := m.termWidth - chromePadH
|
availW := m.termWidth - chromePadH
|
||||||
leftW := availW * 70 / 100
|
leftW := availW * 70 / 100
|
||||||
rightW := availW - leftW
|
rightW := availW - leftW
|
||||||
m.contentWidth = leftW
|
m.contentWidth = leftW - 2
|
||||||
monitors := m.viewSitesTab()
|
monitors := m.viewSitesTab()
|
||||||
left := lipgloss.NewStyle().Width(leftW).Render(monitors)
|
monPanel := m.titledPanel("Monitors", monitors, leftW, !m.detailOpen)
|
||||||
sidebarBorder := lipgloss.NewStyle().
|
sidebarContent := m.viewLogsSidebar(rightW-2, m.maxTableRows)
|
||||||
Border(lipgloss.RoundedBorder()).
|
logPanel := m.titledPanel("Logs", sidebarContent, rightW, false)
|
||||||
BorderForeground(m.theme.Border).
|
top := lipgloss.JoinHorizontal(lipgloss.Top, monPanel, logPanel)
|
||||||
BorderLeft(false).
|
|
||||||
BorderBottom(false)
|
|
||||||
innerW := rightW - 2
|
|
||||||
if innerW < 10 {
|
|
||||||
innerW = 10
|
|
||||||
}
|
|
||||||
sidebar := m.viewLogsSidebar(innerW, m.maxTableRows)
|
|
||||||
right := sidebarBorder.Render(sidebar)
|
|
||||||
top := lipgloss.JoinHorizontal(lipgloss.Top, left, right)
|
|
||||||
if m.detailOpen {
|
if m.detailOpen {
|
||||||
detail := m.viewDetailInline(availW)
|
site := ""
|
||||||
content = top + "\n" + detail
|
if m.cursor < len(m.sites) {
|
||||||
|
site = m.sites[m.cursor].Name
|
||||||
|
}
|
||||||
|
detail := m.viewDetailInline(availW - 2)
|
||||||
|
detailPanel := m.titledPanel(site, detail, availW, true)
|
||||||
|
content = top + "\n" + detailPanel
|
||||||
} else {
|
} else {
|
||||||
content = top
|
content = top
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
m.contentWidth = m.termWidth
|
m.contentWidth = m.termWidth - 2
|
||||||
monitors := m.viewSitesTab()
|
monitors := m.viewSitesTab()
|
||||||
|
availW := m.termWidth - chromePadH
|
||||||
|
monPanel := m.titledPanel("Monitors", monitors, availW, !m.detailOpen)
|
||||||
if m.detailOpen {
|
if m.detailOpen {
|
||||||
detail := m.viewDetailInline(m.termWidth - chromePadH)
|
site := ""
|
||||||
content = monitors + "\n" + detail
|
if m.cursor < len(m.sites) {
|
||||||
|
site = m.sites[m.cursor].Name
|
||||||
|
}
|
||||||
|
detail := m.viewDetailInline(availW - 2)
|
||||||
|
detailPanel := m.titledPanel(site, detail, availW, true)
|
||||||
|
content = monPanel + "\n" + detailPanel
|
||||||
} else {
|
} else {
|
||||||
content = monitors
|
content = monPanel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case tabMaint:
|
case tabMaint:
|
||||||
|
|||||||
@@ -18,15 +18,6 @@ func (m Model) viewDetailInline(width int) string {
|
|||||||
|
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
|
|
||||||
title := m.st.titleStyle.Render(site.Name)
|
|
||||||
b.WriteString(" " + title + "\n")
|
|
||||||
|
|
||||||
divW := width - 4
|
|
||||||
if divW < 20 {
|
|
||||||
divW = 20
|
|
||||||
}
|
|
||||||
b.WriteString(" " + m.st.subtleStyle.Render(strings.Repeat("─", divW)) + "\n")
|
|
||||||
|
|
||||||
status := m.fmtStatus(site.Status, site.Paused, m.isMonitorInMaintenance(site.ID))
|
status := m.fmtStatus(site.Status, site.Paused, m.isMonitorInMaintenance(site.ID))
|
||||||
latency := m.fmtLatency(site.Latency)
|
latency := m.fmtLatency(site.Latency)
|
||||||
uptime := m.fmtUptime(hist.Statuses)
|
uptime := m.fmtUptime(hist.Statuses)
|
||||||
|
|||||||
Reference in New Issue
Block a user