feat(office): mail preview, infinite scroll, pane resize, and scrollbars
Release Please / Release Please (push) Skipped
Tests / Test (Go 1.22) (push) Skipped
Tests / Test (Go stable) (push) Skipped
Release / GoReleaser (push) Failing after 38s

Polish the office TUI with proportional pane layout, drag resize, live
filtering, HTML mail preview, paginated mail lists, overflow scrollbars,
flattened navigation, and project save/status fixes; add UpdateProjectStatus.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-24 22:23:25 +01:00
co-authored by Cursor
parent 92285e439d
commit 72844dfe24
45 changed files with 2611 additions and 206 deletions
+76
View File
@@ -0,0 +1,76 @@
package ui
import (
"strings"
"github.com/charmbracelet/lipgloss"
"github.com/mattn/go-runewidth"
)
// ScrollbarMetrics computes thumb position for a vertical scrollbar.
func ScrollbarMetrics(totalLines, viewportHeight, yOffset int) (show bool, thumbStart, thumbEnd int) {
if totalLines <= viewportHeight || viewportHeight < 1 {
return false, 0, 0
}
maxOff := totalLines - viewportHeight
if maxOff < 0 {
maxOff = 0
}
thumbSize := viewportHeight * viewportHeight / totalLines
if thumbSize < 1 {
thumbSize = 1
}
if thumbSize > viewportHeight {
thumbSize = viewportHeight
}
travel := viewportHeight - thumbSize
thumbStart = 0
if maxOff > 0 {
thumbStart = yOffset * travel / maxOff
}
return true, thumbStart, thumbStart + thumbSize
}
// ApplyVerticalScrollbar draws a 1-column scrollbar on the right when content overflows.
func ApplyVerticalScrollbar(view string, width, height, totalLines, yOffset int) string {
show, thumbStart, thumbEnd := ScrollbarMetrics(totalLines, height, yOffset)
if !show || width < 2 {
return view
}
contentWidth := width - 1
lines := strings.Split(view, "\n")
for len(lines) < height {
lines = append(lines, "")
}
if len(lines) > height {
lines = lines[:height]
}
track := lipgloss.NewStyle().Foreground(lipgloss.Color("238"))
thumb := lipgloss.NewStyle().Foreground(lipgloss.Color("245"))
out := make([]string, height)
for row := 0; row < height; row++ {
line := padDisplayWidth(lines[row], contentWidth)
ch := "│"
style := track
if row >= thumbStart && row < thumbEnd {
ch = "█"
style = thumb
}
out[row] = line + style.Render(ch)
}
return strings.Join(out, "\n")
}
func padDisplayWidth(line string, width int) string {
if width < 1 {
return ""
}
w := runewidth.StringWidth(line)
if w > width {
return runewidth.Truncate(line, width, "")
}
if w < width {
line += strings.Repeat(" ", width-w)
}
return line
}