diff --git a/main.go b/main.go index b2743ed..6ec4f08 100644 --- a/main.go +++ b/main.go @@ -76,6 +76,17 @@ const ( autofillNoticeSuppressed autofillNoticeMode = "suppressed" ) +type listPanelTopSection string + +const ( + listPanelTopSearch listPanelTopSection = "search" + listPanelTopNavigation listPanelTopSection = "navigation" + listPanelTopPath listPanelTopSection = "path" + listPanelTopGroup listPanelTopSection = "group" + listPanelTopGroupTools listPanelTopSection = "group_tools" + listPanelTopPrimary listPanelTopSection = "primary" +) + type bannerKind string const ( @@ -5917,6 +5928,63 @@ func (u *ui) entryRowMetrics() (unit.Dp, unit.Sp, unit.Sp, unit.Sp, unit.Sp, uni return inset, titleSize, metaSize, urlSize, pathSize, dividerGap } +func (u *ui) listPanelTopSections() []listPanelTopSection { + sections := make([]listPanelTopSection, 0, 6) + if u.state.Section != appstate.SectionAbout { + sections = append(sections, listPanelTopSearch) + } + if !u.isVaultLocked() { + sections = append(sections, listPanelTopNavigation) + } + if !u.isVaultLocked() && (u.state.Section == appstate.SectionEntries || u.state.Section == appstate.SectionRecycleBin) { + sections = append(sections, listPanelTopPath) + } + if !u.isVaultLocked() && u.state.Section == appstate.SectionEntries { + sections = append(sections, listPanelTopGroup, listPanelTopGroupTools) + } + if !u.isVaultLocked() { + sections = append(sections, listPanelTopPrimary) + } + return sections +} + +func (u *ui) listPanelSearchRow(gtx layout.Context) layout.Dimensions { + if u.state.Section == appstate.SectionAbout { + return layout.Dimensions{} + } + if u.mode == "phone" { + 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, u.searchPlaceholder()) + editor.Color = u.theme.Palette.Fg + editor.HintColor = mutedColor + return layout.UniformInset(unit.Dp(10)).Layout(gtx, editor.Layout) + }) +} + +func (u *ui) listPanelPrimaryActionRow(gtx layout.Context) layout.Dimensions { + if u.state.Section == appstate.SectionAbout { + return layout.Dimensions{} + } + if u.isVaultLocked() { + return layout.Dimensions{} + } + switch u.state.Section { + case appstate.SectionEntries: + label := "Add Entry" + if u.mode == "phone" { + label = "+ " + label + } + btn := material.Button(u.theme, &u.addEntry, label) + return btn.Layout(gtx) + case appstate.SectionAPITokens: + return tonedButton(gtx, u.theme, &u.issueAPIToken, "Issue API Token") + default: + return layout.Dimensions{} + } +} + func (u *ui) listPanel(gtx layout.Context) layout.Dimensions { panel := card spacing := u.sectionSpacing() @@ -5928,58 +5996,21 @@ func (u *ui) listPanel(gtx layout.Context) layout.Dimensions { return panel(gtx, func(gtx layout.Context) layout.Dimensions { visibleEntries, entryClicks := u.visibleEntrySnapshot() rows := make([]layout.Widget, 0, 16+len(visibleEntries)) - if u.state.Section != appstate.SectionAbout { - 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, u.searchPlaceholder()) - 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, u.navigationHeader) - if u.state.Section == appstate.SectionEntries || u.state.Section == appstate.SectionAbout { - rows = append(rows, func(gtx layout.Context) layout.Dimensions { - return layout.Spacer{Height: spacing}.Layout(gtx) - }) + for _, section := range u.listPanelTopSections() { + switch section { + case listPanelTopSearch: + rows = append(rows, u.listPanelSearchRow) + case listPanelTopNavigation: + rows = append(rows, u.navigationHeader) + case listPanelTopPath: + rows = append(rows, u.pathBar) + case listPanelTopGroup: + rows = append(rows, u.groupBar) + case listPanelTopGroupTools: + rows = append(rows, u.groupControlsSection) + case listPanelTopPrimary: + rows = append(rows, u.listPanelPrimaryActionRow) } - } - 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) - }) - } - 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") - case appstate.SectionAbout: - return emptyStatePanel(gtx, u.theme, u.listEmptyState()) - default: - return layout.Dimensions{} - } - }) rows = append(rows, func(gtx layout.Context) layout.Dimensions { return layout.Spacer{Height: spacing}.Layout(gtx) }) @@ -6008,84 +6039,45 @@ func (u *ui) listPanel(gtx layout.Context) layout.Dimensions { }) } 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 { - if u.isVaultLocked() { - return layout.Dimensions{} - } - return u.navigationHeader(gtx) - }), - layout.Rigid(layout.Spacer{Height: spacing}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if u.state.Section == appstate.SectionAbout { - return emptyStatePanel(gtx, u.theme, u.listEmptyState()) - } - return layout.Dimensions{} - }), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if u.state.Section == appstate.SectionAbout { - return layout.Dimensions{} - } - return layout.Spacer{Height: spacing}.Layout(gtx) - }), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if u.isVaultLocked() || (u.state.Section != appstate.SectionEntries && u.state.Section != appstate.SectionRecycleBin) { - return layout.Dimensions{} - } - return u.pathBar(gtx) - }), - layout.Rigid(layout.Spacer{Height: spacing}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if u.isVaultLocked() || u.state.Section != appstate.SectionEntries { - return layout.Dimensions{} - } - return u.groupBar(gtx) - }), - layout.Rigid(layout.Spacer{Height: spacing}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if u.isVaultLocked() || u.state.Section != appstate.SectionEntries { - return layout.Dimensions{} - } - return u.groupControlsSection(gtx) - }), - layout.Rigid(layout.Spacer{Height: spacing}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if u.state.Section == appstate.SectionAbout { - return layout.Dimensions{} - } - if u.mode == "phone" { - 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, u.searchPlaceholder()) - editor.Color = u.theme.Palette.Fg - editor.HintColor = mutedColor - return layout.UniformInset(unit.Dp(10)).Layout(gtx, editor.Layout) - }) - }), - layout.Rigid(layout.Spacer{Height: spacing}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if u.state.Section == appstate.SectionAbout { - return layout.Dimensions{} - } - if u.isVaultLocked() { - return layout.Dimensions{} - } - switch u.state.Section { - case appstate.SectionEntries: - label := "Add Entry" - if u.mode == "phone" { - label = "+ " + label + children := make([]layout.FlexChild, 0, 16) + for _, section := range u.listPanelTopSections() { + switch section { + case listPanelTopSearch: + children = append(children, layout.Rigid(u.listPanelSearchRow)) + case listPanelTopNavigation: + children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if u.isVaultLocked() { + return layout.Dimensions{} } - btn := material.Button(u.theme, &u.addEntry, label) - return btn.Layout(gtx) - case appstate.SectionAPITokens: - return tonedButton(gtx, u.theme, &u.issueAPIToken, "Issue API Token") - default: - return layout.Dimensions{} - } - }), - layout.Rigid(layout.Spacer{Height: spacing}.Layout), + return u.navigationHeader(gtx) + })) + case listPanelTopPath: + children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if u.isVaultLocked() || (u.state.Section != appstate.SectionEntries && u.state.Section != appstate.SectionRecycleBin) { + return layout.Dimensions{} + } + return u.pathBar(gtx) + })) + case listPanelTopGroup: + children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if u.isVaultLocked() || u.state.Section != appstate.SectionEntries { + return layout.Dimensions{} + } + return u.groupBar(gtx) + })) + case listPanelTopGroupTools: + children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if u.isVaultLocked() || u.state.Section != appstate.SectionEntries { + return layout.Dimensions{} + } + return u.groupControlsSection(gtx) + })) + case listPanelTopPrimary: + children = append(children, layout.Rigid(u.listPanelPrimaryActionRow)) + } + children = append(children, layout.Rigid(layout.Spacer{Height: spacing}.Layout)) + } + children = append(children, layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { if u.state.Section == appstate.SectionAPITokens { return u.apiTokenListPanel(gtx) @@ -6094,7 +6086,7 @@ func (u *ui) listPanel(gtx layout.Context) layout.Dimensions { return u.apiAuditListPanel(gtx) } if u.state.Section == appstate.SectionAbout { - return layout.Dimensions{} + return emptyStatePanel(gtx, u.theme, u.listEmptyState()) } if len(u.visible) == 0 { return emptyStatePanel(gtx, u.theme, u.listEmptyState()) @@ -6106,6 +6098,7 @@ func (u *ui) listPanel(gtx layout.Context) layout.Dimensions { }) }), ) + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, children...) }) } diff --git a/main_test.go b/main_test.go index 1e58c8c..b0ea704 100644 --- a/main_test.go +++ b/main_test.go @@ -251,6 +251,30 @@ func TestUISearchBehaviorIsConsistentAcrossDesktopAndPhoneLayouts(t *testing.T) } } +func TestUIListPanelTopSectionsMatchAcrossDesktopAndPhoneForEntries(t *testing.T) { + t.Parallel() + + desktop := newUIWithModel("desktop", vault.Model{}) + desktop.state.Section = appstate.SectionEntries + phone := newUIWithModel("phone", vault.Model{}) + phone.state.Section = appstate.SectionEntries + + want := []listPanelTopSection{ + listPanelTopSearch, + listPanelTopNavigation, + listPanelTopPath, + listPanelTopGroup, + listPanelTopGroupTools, + listPanelTopPrimary, + } + if got := desktop.listPanelTopSections(); !slices.Equal(got, want) { + t.Fatalf("desktop.listPanelTopSections() = %v, want %v", got, want) + } + if got := phone.listPanelTopSections(); !slices.Equal(got, want) { + t.Fatalf("phone.listPanelTopSections() = %v, want %v", got, want) + } +} + func TestUICurrentVaultSummary(t *testing.T) { t.Parallel()