From 2e9e2aae5f691dda6c50c76133927de30a71d7da Mon Sep 17 00:00:00 2001 From: Joe Julian Date: Sun, 29 Mar 2026 16:39:27 -0700 Subject: [PATCH] Add collapsible group tools section --- main.go | 11 ++++++++++- main_test.go | 21 +++++++++++++++++++++ ui_forms.go | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index 1ea95bd..15979ff 100644 --- a/main.go +++ b/main.go @@ -152,6 +152,7 @@ type ui struct { confirmDeleteGroup widget.Clickable cancelDeleteGroup widget.Clickable addCustomField widget.Clickable + toggleGroupControls widget.Clickable togglePasswordInline widget.Clickable showEntries widget.Clickable showTemplates widget.Clickable @@ -180,6 +181,8 @@ type ui struct { eyeIcon *widget.Icon eyeOffIcon *widget.Icon copyIcon *widget.Icon + expandMoreIcon *widget.Icon + expandLessIcon *widget.Icon clipboardWriter clipboard.Writer loadingMessage string lifecycleMode string @@ -187,6 +190,7 @@ type ui struct { defaultSaveAsPath string recentVaultsPath string editingEntry bool + groupControlsHidden bool recentVaults []string recentVaultGroups map[string][]string deleteGroupPath []string @@ -280,6 +284,8 @@ func newUIWithState(mode string, sess appstate.CurrentSession, paths statePaths) u.eyeIcon, _ = widget.NewIcon(icons.ActionVisibility) u.eyeOffIcon, _ = widget.NewIcon(icons.ActionVisibilityOff) u.copyIcon, _ = widget.NewIcon(icons.ContentContentCopy) + u.expandMoreIcon, _ = widget.NewIcon(icons.NavigationExpandMore) + u.expandLessIcon, _ = widget.NewIcon(icons.NavigationExpandLess) u.passwordProfile.SetText("strong") u.keyboardFocus = focusSearch u.setCustomFieldRows(nil) @@ -1173,6 +1179,9 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions { u.clearDeleteGroupConfirmation() u.runAction("create group", u.createGroupAction) } + for u.toggleGroupControls.Clicked(gtx) { + u.groupControlsHidden = !u.groupControlsHidden + } for u.renameGroup.Clicked(gtx) { u.clearDeleteGroupConfirmation() u.runAction("rename group", u.renameGroupAction) @@ -1358,7 +1367,7 @@ func (u *ui) listPanel(gtx layout.Context) layout.Dimensions { if u.isVaultLocked() { return layout.Dimensions{} } - return u.groupControls(gtx) + return u.groupControlsSection(gtx) }), layout.Rigid(layout.Spacer{Height: spacing}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { diff --git a/main_test.go b/main_test.go index 1c46ea6..66453e6 100644 --- a/main_test.go +++ b/main_test.go @@ -787,6 +787,27 @@ func TestUIGroupManagementAndPathNavigationAreControllerDriven(t *testing.T) { } } +func TestUIGroupControlsCanBeCollapsed(t *testing.T) { + t.Parallel() + + u := newUIWithModel("desktop", vault.Model{}) + u.showEntriesSection() + + if u.groupControlsHidden { + t.Fatal("groupControlsHidden = true, want false by default") + } + + u.groupControlsHidden = true + if !u.groupControlsHidden { + t.Fatal("groupControlsHidden = false, want true after collapsing") + } + + u.groupControlsHidden = false + if u.groupControlsHidden { + t.Fatal("groupControlsHidden = true, want false after expanding") + } +} + func TestUISavingEntryWithDifferentPathMovesItBetweenGroups(t *testing.T) { t.Parallel() diff --git a/ui_forms.go b/ui_forms.go index 876b179..23723df 100644 --- a/ui_forms.go +++ b/ui_forms.go @@ -256,6 +256,54 @@ func (u *ui) groupControls(gtx layout.Context) layout.Dimensions { ) } +func (u *ui) groupControlsSection(gtx layout.Context) layout.Dimensions { + if u.state.Section != appstate.SectionEntries { + return layout.Dimensions{} + } + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return u.groupControlsDisclosure(gtx) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if u.groupControlsHidden { + return layout.Dimensions{} + } + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), + layout.Rigid(u.groupControls), + ) + }), + ) +} + +func (u *ui) groupControlsDisclosure(gtx layout.Context) layout.Dimensions { + return u.toggleGroupControls.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return layout.Flex{Alignment: layout.Middle}.Layout(gtx, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + icon := u.expandLessIcon + if u.groupControlsHidden { + icon = u.expandMoreIcon + } + if icon == nil { + lbl := material.Label(u.theme, unit.Sp(16), ">") + if !u.groupControlsHidden { + lbl.Text = "v" + } + lbl.Color = accentColor + return lbl.Layout(gtx) + } + return icon.Layout(gtx, accentColor) + }), + layout.Rigid(layout.Spacer{Width: unit.Dp(4)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + lbl := material.Label(u.theme, unit.Sp(13), "Group Tools") + lbl.Color = accentColor + return lbl.Layout(gtx) + }), + ) + }) +} + func (u *ui) entryEditorPanel(gtx layout.Context) layout.Dimensions { return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(labeledEditorWithFocus(u.theme, "Title", &u.entryTitle, false, u.isFocused(detailFocusID(detailFieldTitle)))),