feat: responsive TUI layout — mobile/tablet/desktop support

- NEW responsive.go: wrapFooter, adaptiveSidePad, clampWidth, truncateStr
- Footer auto-wraps to multi-line on narrow terminals (full labels preserved)
- Box width clamped to terminal width across all tabs
- Host/key/snippet rows truncated with ellipsis; compact format <35 cols
- SFTP panes stack vertically when terminal < 50 cols
- Tab bar truncates names on overflow
- Form contentW minimum lowered 50->30 for mobile
- Fixed: key_list_tab & snippet_list_tab dropped last data row (off-by-one bug)
This commit is contained in:
swanadiva
2026-06-23 19:08:44 +07:00
parent d359688b75
commit d75621e9b4
11 changed files with 302 additions and 113 deletions
+27 -1
View File
@@ -4,6 +4,7 @@ import (
"strings"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
// Tab represents a single tab in the TUI
@@ -182,15 +183,40 @@ func (tm *TabManager) View() string {
return b.String()
}
// renderTabBar renders the top tab bar
// renderTabBar renders the top tab bar (responsive: truncates names on overflow)
func renderTabBar(tm *TabManager) string {
if len(tm.tabs) == 0 {
return ""
}
const cellPad = 4 // Padding(0,2) per tab = 2 left + 2 right
availW := tm.width - 2
if availW < 1 {
availW = 1
}
// Measure total width and decide if truncation is needed
totalW := 0
for _, tab := range tm.tabs {
totalW += lipgloss.Width(tab.Name()) + cellPad
}
maxNameW := 0
if totalW > availW {
perTab := availW / len(tm.tabs)
maxNameW = perTab - cellPad
if maxNameW < 1 {
maxNameW = 1
}
}
var cells []string
for i, tab := range tm.tabs {
name := tab.Name()
if maxNameW > 0 && lipgloss.Width(name) > maxNameW {
name = truncateStr(name, maxNameW)
}
if i == tm.active {
cells = append(cells, TabActiveStyle.Render(name))
} else {