From ca486ae4da83bdc6aae36fd7085de0d57378aed0 Mon Sep 17 00:00:00 2001 From: Kaede Akino Date: Sun, 20 Sep 2026 21:52:01 +0800 Subject: [PATCH] Refine Card --- TFM/Features/Hosts/HostsPageView.swift | 32 +++-------- TFM/Features/Snippets/SnippetsPageView.swift | 47 +++------------- TFM/UI/Managed/CardGrid.swift | 57 ++++++++++++++------ 3 files changed, 55 insertions(+), 81 deletions(-) diff --git a/TFM/Features/Hosts/HostsPageView.swift b/TFM/Features/Hosts/HostsPageView.swift index e0d8d4a3..bb7c132c 100644 --- a/TFM/Features/Hosts/HostsPageView.swift +++ b/TFM/Features/Hosts/HostsPageView.swift @@ -18,8 +18,6 @@ final class HostsPageState { } var query = "" - var selectedGroupID: HostGroup.ID? - var isUngroupedSelected = false /// Which tags the grid is narrowed to, ticked in the titlebar search /// field's magnifier menu. Several at once means "carries all of these". var selectedTags: Set = [] @@ -126,13 +124,12 @@ struct HostsPageView: ManagedCardPage { ) -> some View { if case .group(let groupID) = section.id, let group = app.hostLibrary.group(groupID) { let isExpanded = app.hostLibrary.isHostGroupExpanded(group.id) + let toggle = { app.hostLibrary.setExpanded(!isExpanded, for: group.id) } CardSectionHeader( title: section.title, count: section.items.count, - toggle: { app.hostLibrary.setExpanded(!isExpanded, for: group.id) }, + toggle: toggle, isCollapsed: !isExpanded, - select: { selectGroup(group.id) }, - isSelected: state.selectedGroupID == group.id, isDropTarget: isDropTarget ) .contextMenu { @@ -147,15 +144,13 @@ struct HostsPageView: ManagedCardPage { DestructiveMenuButton("Delete Group") { requestDelete(group) } } } else if case .ungrouped = section.id { + let isExpanded = section.isExpanded ?? true + let toggle = { app.hostLibrary.setUngroupedExpanded(!isExpanded) } CardSectionHeader( title: section.title, count: section.items.count, - toggle: section.isExpanded.map { expanded in - { app.hostLibrary.setUngroupedExpanded(!expanded) } - }, - isCollapsed: !(section.isExpanded ?? true), - select: selectUngrouped, - isSelected: state.isUngroupedSelected, + toggle: toggle, + isCollapsed: !isExpanded, isDropTarget: isDropTarget ) .contextMenu { @@ -269,7 +264,6 @@ struct HostsPageView: ManagedCardPage { state.confirmation = nil pruneHostSelection() - if state.selectedGroupID == group.id { state.selectedGroupID = nil } window.pruneVaultSelections(in: app.hostLibrary) } @@ -295,24 +289,10 @@ struct HostsPageView: ManagedCardPage { private func setHostSelection(_ ids: Set) { let valid = ids.intersection(Set(app.hostLibrary.hosts.map(\.id))) - state.selectedGroupID = nil - state.isUngroupedSelected = false let ordered = visibleHosts.filter { valid.contains($0.id) }.map { VaultSelectionID.host($0.id) } window.setVaultSelection(ordered, for: .hosts) } - private func selectGroup(_ id: HostGroup.ID) { - window.clearVaultSelection(for: .hosts) - state.isUngroupedSelected = false - state.selectedGroupID = id - } - - private func selectUngrouped() { - window.clearVaultSelection(for: .hosts) - state.selectedGroupID = nil - state.isUngroupedSelected = true - } - private func pruneHostSelection() { setHostSelection(hostSelection) } diff --git a/TFM/Features/Snippets/SnippetsPageView.swift b/TFM/Features/Snippets/SnippetsPageView.swift index 97b542e6..0e627b38 100644 --- a/TFM/Features/Snippets/SnippetsPageView.swift +++ b/TFM/Features/Snippets/SnippetsPageView.swift @@ -35,8 +35,6 @@ final class SnippetsPageState { } var query = "" - var selectedPackageID: SnippetPackage.ID? - var isUngroupedSelected = false var confirmation: ConfirmationRoute? } @@ -109,16 +107,13 @@ struct SnippetsPageView: ManagedCardPage { case .package(let id): if let package = app.snippets.snippetPackage(id) { let isExpanded = app.snippets.isSnippetPackageExpanded(id) + let toggle = { + app.snippets.setSnippetPackageExpanded(!isExpanded, for: package.id) + } CardSectionHeader( title: section.title, count: section.items.count, - toggle: { - app.snippets.setSnippetPackageExpanded( - !isExpanded, for: package.id - ) - }, + toggle: toggle, isCollapsed: !isExpanded, - select: { selectPackage(package.id) }, - isSelected: state.selectedPackageID == package.id, isDropTarget: isDropTarget ) .contextMenu { @@ -138,16 +133,12 @@ struct SnippetsPageView: ManagedCardPage { } } case .ungrouped: + let isExpanded = app.snippets.isSnippetsUngroupedExpanded + let toggle = { app.snippets.setSnippetsUngroupedExpanded(!isExpanded) } CardSectionHeader( title: section.title, count: section.items.count, - toggle: { - app.snippets.setSnippetsUngroupedExpanded( - !app.snippets.isSnippetsUngroupedExpanded - ) - }, - isCollapsed: !app.snippets.isSnippetsUngroupedExpanded, - select: selectUngrouped, - isSelected: state.isUngroupedSelected, + toggle: toggle, + isCollapsed: !isExpanded, isDropTarget: isDropTarget ) .contextMenu { @@ -247,25 +238,11 @@ struct SnippetsPageView: ManagedCardPage { private func setSelection(_ ids: Set) { let valid = ids.intersection(Set(app.snippets.snippets.map(\.id))) - state.selectedPackageID = nil - state.isUngroupedSelected = false let ordered = visibleSnippets.filter { valid.contains($0.id) } .map { VaultSelectionID.snippet($0.id) } window.setVaultSelection(ordered, for: .snippets) } - private func selectPackage(_ id: SnippetPackage.ID) { - window.clearVaultSelection(for: .snippets) - state.isUngroupedSelected = false - state.selectedPackageID = id - } - - private func selectUngrouped() { - window.clearVaultSelection(for: .snippets) - state.selectedPackageID = nil - state.isUngroupedSelected = true - } - private func saveSnippet(_ snippet: Snippet) { app.snippets.upsert(snippet) window.selectVaultItem(.snippet(snippet.id), on: .snippets) @@ -276,16 +253,8 @@ struct SnippetsPageView: ManagedCardPage { } private func deletePackage(_ package: SnippetPackage, keepSnippets: Bool) { - let deletedPackageIDs = Set( - [package.id] + app.snippets.descendantSnippetPackages(of: package.id).map(\.id) - ) app.snippets.delete(snippetPackageID: package.id, keepSnippets: keepSnippets) state.confirmation = nil - if let selectedPackageID = state.selectedPackageID, - deletedPackageIDs.contains(selectedPackageID) - { - state.selectedPackageID = nil - } window.pruneVaultSelections(in: app.snippets) } diff --git a/TFM/UI/Managed/CardGrid.swift b/TFM/UI/Managed/CardGrid.swift index ab52f728..4100cf6b 100644 --- a/TFM/UI/Managed/CardGrid.swift +++ b/TFM/UI/Managed/CardGrid.swift @@ -386,17 +386,46 @@ struct CardSectionHeader: View { @State private var isHovering = false var body: some View { + Group { + if select == nil, let toggle { + Button(action: toggle) { paddedLabel } + .buttonStyle(.plain) + .accessibilityLabel(Text(title)) + .accessibilityHint(Text(isCollapsed ? "Expand" : "Collapse")) + } else { + paddedLabel.onTapGesture { select?() } + } + } + // Same fill as `SidebarRow`, and fixed for the same reason: derived from + // the text color rather than the accent, so it does not change with the + // accent or with which view happens to hold focus. + .background(fill, in: .rect(cornerRadius: Metrics.Radius.medium)) + .contentShape(.rect(cornerRadius: Metrics.Radius.medium)) + .cardInteractiveFrame() + .onHover { isHovering = $0 } + .accessibilityElement(children: .combine) + .accessibilityAddTraits(isSelected ? [.isSelected, .isButton] : []) + } + + private var label: some View { HStack(spacing: Metrics.Space.s) { if let toggle { - Button(action: toggle) { + if select == nil { Image(systemName: "chevron.right") .font(.caption.weight(.semibold)) .foregroundStyle(.secondary) .rotationEffect(.degrees(isCollapsed ? 0 : 90)) - .contentShape(.rect) + } else { + Button(action: toggle) { + Image(systemName: "chevron.right") + .font(.caption.weight(.semibold)) + .foregroundStyle(.secondary) + .rotationEffect(.degrees(isCollapsed ? 0 : 90)) + .contentShape(.rect) + } + .buttonStyle(.plain) + .accessibilityLabel(Text(isCollapsed ? "Expand" : "Collapse")) } - .buttonStyle(.plain) - .accessibilityLabel(Text(isCollapsed ? "Expand" : "Collapse")) } Text(title) @@ -411,20 +440,16 @@ struct CardSectionHeader: View { .background(.quaternary.opacity(0.6), in: .capsule) } } - .padding(.horizontal, select == nil ? 0 : Metrics.Space.s) - .padding(.vertical, select == nil ? 0 : Metrics.Space.xs) - // Same fill as `SidebarRow`, and fixed for the same reason: derived from - // the text color rather than the accent, so it does not change with the - // accent or with which view happens to hold focus. - .background(fill, in: .rect(cornerRadius: Metrics.Radius.medium)) - .contentShape(.rect(cornerRadius: Metrics.Radius.medium)) - .onTapGesture { select?() } - .cardInteractiveFrame() - .onHover { isHovering = $0 } - .accessibilityElement(children: .combine) - .accessibilityAddTraits(isSelected ? [.isSelected, .isButton] : []) } + private var paddedLabel: some View { + label + .padding(.horizontal, isInteractive ? Metrics.Space.s : 0) + .padding(.vertical, isInteractive ? Metrics.Space.xs : 0) + } + + private var isInteractive: Bool { select != nil || toggle != nil } + private var fill: Color { if isDropTarget { Color.accentColor.opacity(0.16)