From f2e4e707cf3409b0f20993ab2555f3fdb42588e9 Mon Sep 17 00:00:00 2001 From: Joe Julian Date: Thu, 2 Apr 2026 21:38:51 -0700 Subject: [PATCH] Make phone entry pane fully scrollable --- main.go | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++++ main_test.go | 32 ++++++++++++++++++++ 2 files changed, 117 insertions(+) diff --git a/main.go b/main.go index 723fb74..c915865 100644 --- a/main.go +++ b/main.go @@ -245,6 +245,7 @@ type ui struct { detailList widget.List apiPolicyList widget.List lifecycleList widget.List + phonePanelList widget.List recentVaultListState widget.List recentRemoteListState widget.List copyUser widget.Clickable @@ -556,6 +557,9 @@ func newUIWithState(mode string, sess appstate.CurrentSession, paths statePaths) lifecycleList: widget.List{ List: layout.List{Axis: layout.Vertical}, }, + phonePanelList: widget.List{ + List: layout.List{Axis: layout.Vertical}, + }, recentVaultListState: widget.List{ List: layout.List{Axis: layout.Vertical}, }, @@ -4026,6 +4030,87 @@ func (u *ui) listPanel(gtx layout.Context) layout.Dimensions { panel = compactCard } u.ensureNavClickables() + if u.mode == "phone" { + return panel(gtx, func(gtx layout.Context) layout.Dimensions { + rows := make([]layout.Widget, 0, 16+len(u.visible)) + if !u.isVaultLocked() { + rows = append(rows, u.navigationHeader) + rows = append(rows, func(gtx layout.Context) layout.Dimensions { + return layout.Spacer{Height: spacing}.Layout(gtx) + }) + } + if !u.isVaultLocked() && u.state.Section == appstate.SectionRecycleBin { + rows = append(rows, u.recycleBinSectionNotice) + rows = append(rows, func(gtx layout.Context) layout.Dimensions { + return layout.Spacer{Height: spacing}.Layout(gtx) + }) + } + if !u.isVaultLocked() && (u.state.Section == appstate.SectionEntries || u.state.Section == appstate.SectionRecycleBin) { + rows = append(rows, u.pathBar) + rows = append(rows, func(gtx layout.Context) layout.Dimensions { + return layout.Spacer{Height: spacing}.Layout(gtx) + }) + } + if !u.isVaultLocked() && u.state.Section == appstate.SectionEntries { + rows = append(rows, u.groupBar) + rows = append(rows, func(gtx layout.Context) layout.Dimensions { + return layout.Spacer{Height: spacing}.Layout(gtx) + }) + rows = append(rows, u.groupControlsSection) + rows = append(rows, func(gtx layout.Context) layout.Dimensions { + return layout.Spacer{Height: spacing}.Layout(gtx) + }) + } + rows = append(rows, func(gtx layout.Context) layout.Dimensions { + gtx.Constraints.Min.X = gtx.Constraints.Max.X + return u.outlinedFieldState(gtx, u.isFocused(focusSearch), func(gtx layout.Context) layout.Dimensions { + editor := material.Editor(u.theme, &u.search, "Search vault") + editor.Color = u.theme.Palette.Fg + editor.HintColor = mutedColor + return layout.UniformInset(unit.Dp(10)).Layout(gtx, editor.Layout) + }) + }) + rows = append(rows, func(gtx layout.Context) layout.Dimensions { + return layout.Spacer{Height: spacing}.Layout(gtx) + }) + if !u.isVaultLocked() { + rows = append(rows, func(gtx layout.Context) layout.Dimensions { + switch u.state.Section { + case appstate.SectionEntries: + btn := material.Button(u.theme, &u.addEntry, "+ Add Entry") + return btn.Layout(gtx) + case appstate.SectionAPITokens: + return tonedButton(gtx, u.theme, &u.issueAPIToken, "Issue API Token") + default: + return layout.Dimensions{} + } + }) + rows = append(rows, func(gtx layout.Context) layout.Dimensions { + return layout.Spacer{Height: spacing}.Layout(gtx) + }) + } + switch { + case u.state.Section == appstate.SectionAPITokens: + rows = append(rows, u.apiTokenListPanel) + case u.state.Section == appstate.SectionAPIAudit: + rows = append(rows, u.apiAuditListPanel) + case len(u.visible) == 0: + rows = append(rows, func(gtx layout.Context) layout.Dimensions { + return emptyStatePanel(gtx, u.theme, u.listEmptyState()) + }) + default: + for i := range u.visible { + idx := i + rows = append(rows, func(gtx layout.Context) layout.Dimensions { + return u.entryRow(gtx, &u.entryClicks[idx], idx, u.visible[idx]) + }) + } + } + return material.List(u.theme, &u.phonePanelList).Layout(gtx, len(rows), func(gtx layout.Context, i int) layout.Dimensions { + return rows[i](gtx) + }) + }) + } return panel(gtx, func(gtx layout.Context) layout.Dimensions { return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(func(gtx layout.Context) layout.Dimensions { diff --git a/main_test.go b/main_test.go index 42a56a2..7e08abe 100644 --- a/main_test.go +++ b/main_test.go @@ -774,6 +774,38 @@ func TestUIPhoneGroupBrowserToggleDoesNotChangeCurrentGroupToolsState(t *testing } } +func TestUIPhoneListPanelWithExpandedGroupControlsAndEntriesDoesNotPanic(t *testing.T) { + t.Parallel() + + u := newUIWithModel("phone", vault.Model{ + Groups: [][]string{ + {"Crew"}, + {"Crew", "Internet"}, + {"Crew", "eMail"}, + }, + Entries: []vault.Entry{ + {ID: "amazon", Title: "Amazon", Username: "joe", Path: []string{"Crew", "Internet"}}, + {ID: "mail", Title: "Mail", Username: "joe", Path: []string{"Crew", "eMail"}}, + }, + }) + u.groupControlsHidden = false + u.setCurrentPath([]string{"Crew"}) + u.filter() + + ops := new(op.Ops) + gtx := layout.Context{ + Ops: ops, + Constraints: layout.Exact(image.Pt(1080, 900)), + } + defer func() { + if r := recover(); r != nil { + t.Fatalf("listPanel() panicked on phone with groups, controls, and entries: %v", r) + } + }() + + _ = u.listPanel(gtx) +} + func TestUIAPIAuditSectionShowsRecordedEvents(t *testing.T) { t.Parallel()