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
+112 -4
View File
@@ -15,6 +15,7 @@ type entityField int
const (
entityFieldPrimary entityField = iota
entityFieldSecondary
entityFieldStatus
)
// EntityForm is the top section of the detail pane for editable entities.
@@ -25,6 +26,9 @@ type EntityForm struct {
primaryLabel string
secondaryLabel string
readOnly bool
hasStatus bool
status model.ProjectLifecycle
responsibleID string
primary textinput.Model
secondary textarea.Model
field entityField
@@ -65,6 +69,27 @@ func newEntityForm() EntityForm {
}
}
func (f *EntityForm) FieldCount() int {
n := 2
if f.hasStatus {
n = 3
}
return n
}
func (f *EntityForm) FormFields() model.FormFields {
return model.FormFields{
PrimaryLabel: f.primaryLabel,
SecondaryLabel: f.secondaryLabel,
Primary: f.primary.Value(),
Secondary: f.secondary.Value(),
ReadOnly: f.readOnly,
HasStatus: f.hasStatus,
Status: f.status,
ResponsibleID: f.responsibleID,
}
}
func (f *EntityForm) Active() bool { return f.active }
func (f *EntityForm) ItemID() string { return f.itemID }
@@ -82,10 +107,16 @@ func (f *EntityForm) Load(kind model.Kind, itemID string, fields model.FormField
f.primaryLabel = fields.PrimaryLabel
f.secondaryLabel = fields.SecondaryLabel
f.readOnly = fields.ReadOnly
f.hasStatus = fields.HasStatus
f.status = fields.Status
f.responsibleID = fields.ResponsibleID
f.primary.SetValue(fields.Primary)
f.secondary.SetValue(fields.Secondary)
f.dirty = false
f.field = entityFieldPrimary
if f.hasStatus && f.readOnly {
f.field = entityFieldStatus
}
f.applyFocus()
f.layoutFields()
}
@@ -93,6 +124,9 @@ func (f *EntityForm) Load(kind model.Kind, itemID string, fields model.FormField
func (f *EntityForm) Clear() {
f.active = false
f.itemID = ""
f.hasStatus = false
f.status = model.ProjectLifecycleOpen
f.responsibleID = ""
f.primary.SetValue("")
f.secondary.SetValue("")
f.dirty = false
@@ -140,19 +174,52 @@ func (f *EntityForm) layoutFields() {
}
func (f *EntityForm) FocusNext() {
if f.field < entityFieldSecondary {
last := entityFieldSecondary
if f.hasStatus {
last = entityFieldStatus
}
if f.field < last {
f.field++
}
f.applyFocus()
}
func (f *EntityForm) FocusPrev() {
if f.field > entityFieldPrimary {
first := entityFieldPrimary
if f.field > first {
f.field--
}
f.applyFocus()
}
func (f *EntityForm) SetFieldIndex(i int) {
switch i {
case 0:
f.field = entityFieldPrimary
case 1:
f.field = entityFieldSecondary
case 2:
if f.hasStatus {
f.field = entityFieldStatus
}
default:
return
}
f.applyFocus()
}
func (f *EntityForm) cycleStatus(delta int) {
if !f.hasStatus || f.readOnly {
return
}
if delta > 0 {
f.status = f.status.Next()
} else {
f.status = f.status.Prev()
}
f.dirty = true
}
func (f *EntityForm) applyFocus() {
if f.readOnly {
f.primary.Blur()
@@ -163,9 +230,12 @@ func (f *EntityForm) applyFocus() {
case entityFieldPrimary:
f.primary.Focus()
f.secondary.Blur()
default:
case entityFieldSecondary:
f.primary.Blur()
f.secondary.Focus()
default:
f.primary.Blur()
f.secondary.Blur()
}
}
@@ -173,12 +243,27 @@ func (f *EntityForm) Update(msg tea.Msg) tea.Cmd {
if !f.active || !f.focused || f.readOnly {
return nil
}
if key, ok := msg.(tea.KeyMsg); ok && f.field == entityFieldStatus {
switch key.String() {
case "left", "h":
f.cycleStatus(-1)
return nil
case "right", "l":
f.cycleStatus(1)
return nil
case " ":
f.cycleStatus(1)
return nil
}
}
var cmd tea.Cmd
switch f.field {
case entityFieldPrimary:
f.primary, cmd = f.primary.Update(msg)
default:
case entityFieldSecondary:
f.secondary, cmd = f.secondary.Update(msg)
default:
return nil
}
if _, ok := msg.(tea.KeyMsg); ok {
f.dirty = true
@@ -199,6 +284,28 @@ func (f EntityForm) View() string {
if f.focused && f.field == entityFieldSecondary && !f.readOnly {
sLabel = f.styles.labelAct.Render(f.secondaryLabel)
}
statusBlock := ""
if f.hasStatus {
stLabel := f.styles.label.Render("Status")
if f.focused && f.field == entityFieldStatus && !f.readOnly {
stLabel = f.styles.labelAct.Render("Status")
}
valStyle := f.styles.label
if f.status == model.ProjectLifecycleClosed {
valStyle = valStyle.Foreground(lipgloss.Color("245"))
} else {
valStyle = valStyle.Foreground(lipgloss.Color("42"))
}
hint := ""
if f.focused && f.field == entityFieldStatus && !f.readOnly {
hint = f.styles.label.Render(" ←/→ toggle")
}
statusBlock = strings.Join([]string{
"",
stLabel,
valStyle.Render(f.status.Label()) + hint,
}, "\n")
}
ro := ""
if f.readOnly {
ro = lipgloss.NewStyle().Foreground(lipgloss.Color("241")).Render(" (read-only)") + "\n"
@@ -210,6 +317,7 @@ func (f EntityForm) View() string {
"",
sLabel,
f.secondary.View(),
statusBlock,
}, "\n")
}