diff --git a/Vault/Sources/VaultiOS/Views/Feed/VaultItemFeedView.swift b/Vault/Sources/VaultiOS/Views/Feed/VaultItemFeedView.swift index 5bdb90b17..77bc71848 100644 --- a/Vault/Sources/VaultiOS/Views/Feed/VaultItemFeedView.swift +++ b/Vault/Sources/VaultiOS/Views/Feed/VaultItemFeedView.swift @@ -30,23 +30,21 @@ public struct VaultItemFeedView< } public var body: some View { - VStack { - listOfCodesView - } - .navigationTitle(Text(dataModel.feedTitle)) - .task { - await dataModel.reloadData() - } - .onChange(of: dataModel.itemsSearchQuery) { _, _ in - Task { - await dataModel.reloadItems() + listOfCodesView + .navigationTitle(Text(dataModel.feedTitle)) + .task { + await dataModel.reloadData() } - } - .onChange(of: dataModel.itemsFilteringByTags) { _, _ in - Task { - await dataModel.reloadItems() + .onChange(of: dataModel.itemsSearchQuery) { _, _ in + Task { + await dataModel.reloadItems() + } + } + .onChange(of: dataModel.itemsFilteringByTags) { _, _ in + Task { + await dataModel.reloadItems() + } } - } } private var currentBehaviour: VaultItemViewBehaviour { @@ -83,99 +81,68 @@ public struct VaultItemFeedView< .safeAreaInset(edge: .bottom, spacing: 0) { VStack(spacing: 8) { if dataModel.allTags.isNotEmpty { - ScrollView(.horizontal, showsIndicators: false) { - HStack { - ForEach(dataModel.allTags) { tag in - TagPillView(tag: tag, isSelected: dataModel.itemsFilteringByTags.contains(tag.id)) - .id(tag) - .onTapGesture { - dataModel.toggleFiltering(tag: tag.id) - } - } - } - .font(.footnote) - } - .scrollClipDisabled() - .padding(.horizontal) - .transition(.move(edge: .bottom).combined(with: .opacity)) + tagFilterBar } - unifiedInfoSection + bottomBar .padding(.horizontal) - .transition(.move(edge: .bottom).combined(with: .opacity)) } - .padding(.top, 8) - .padding(.bottom, 8) - .animation(.spring(response: 0.3, dampingFraction: 1.0), value: state.isEditing) - .animation(.spring(response: 0.3, dampingFraction: 1.0), value: dataModel.isSearching) - .animation(.spring(response: 0.3, dampingFraction: 1.0), value: dataModel.itemsFilteringByTags.count) - .animation(.spring(response: 0.3, dampingFraction: 1.0), value: dataModel.allTags.isEmpty) + .padding(.vertical, 8) + .animation(.snappy, value: state.isEditing) + .animation(.snappy, value: dataModel.isSearching) + .animation(.snappy, value: dataModel.itemsFilteringByTags) + .animation(.snappy, value: dataModel.allTags.isEmpty) } } - /// Unified bottom section with item count, filtering status, and action buttons - private var unifiedInfoSection: some View { - HStack { - // Left side: Item count or drag to reorder message - if state.isEditing { - Label { - Text(localized(key: "codeFeed.editMode.dragToReorder")) - .foregroundColor(.secondary) - .font(.subheadline) - .lineLimit(1) - } icon: { - Image(systemName: "arrow.up.arrow.down") - .foregroundColor(.secondary) - } - } else { - let count = dataModel.items.count - let itemText = count == 1 ? "item" : "items" - let filterCount = dataModel.itemsFilteringByTags.count - - HStack(spacing: 4) { - Image(systemName: "key.horizontal") - .foregroundColor(.secondary) - Text("\(count) \(itemText)") - .foregroundColor(.secondary) - - if filterCount > 0 { - Text("•") - .foregroundColor(.secondary) - Image(systemName: "tag.fill") - .foregroundColor(.secondary) - .font(.caption) - Text("\(filterCount)") - .foregroundColor(.secondary) + /// Horizontally scrolling row of tag filters, presented above the bottom bar. + private var tagFilterBar: some View { + ScrollView(.horizontal, showsIndicators: false) { + HStack { + ForEach(dataModel.allTags) { tag in + Toggle(isOn: filterBinding(for: tag)) { + Label { + Text(tag.name.isBlank ? "Tag" : tag.name) + } icon: { + TagIconView(iconName: tag.iconName) + } } + .id(tag) + .tint(tag.color.color) } - .font(.subheadline) - .lineLimit(1) } + .toggleStyle(.button) + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.small) + .font(.footnote) + .padding(.horizontal) + } + .scrollClipDisabled() + .transition(.move(edge: .bottom).combined(with: .opacity)) + } + + /// Item count and the feed-level actions. + private var bottomBar: some View { + HStack { + statusLabel + .font(.subheadline) + .foregroundStyle(.secondary) Spacer() - // Right side: Action buttons if dataModel.items.isNotEmpty { HStack(spacing: 8) { - // Clear button when filtering by tags if dataModel.itemsFilteringByTags.isNotEmpty, !state.isEditing { Button { dataModel.itemsFilteringByTags.removeAll() } label: { Label("Clear", systemImage: "tag.slash.fill") - .lineLimit(1) } - .fontWeight(.semibold) - .font(.footnote) - .foregroundStyle(.white) - .padding(.vertical, 8) - .padding(.horizontal, 16) - .background(Color.secondary) - .clipShape(Capsule()) - .fixedSize() + .buttonStyle(.bordered) + .tint(.secondary) } - // Edit/Done button Button { state.isEditing.toggle() } label: { @@ -183,24 +150,54 @@ public struct VaultItemFeedView< state.isEditing ? "Done" : "Edit", systemImage: state.isEditing ? "checkmark" : "pencil", ) - .lineLimit(1) } - .fontWeight(.semibold) - .font(.footnote) - .foregroundStyle(.white) - .padding(.vertical, 8) - .padding(.horizontal, 16) - .background(Color.accentColor) - .clipShape(Capsule()) - .fixedSize() + .buttonStyle(.borderedProminent) } + .buttonBorderShape(.capsule) + .controlSize(.small) + .font(.footnote) + .lineLimit(1) + .fixedSize() } } .frame(minHeight: 44) - .padding(.vertical, 8) - .padding(.horizontal, 12) - .background(Color.primary.opacity(0.05)) - .clipShape(RoundedRectangle(cornerRadius: 12)) + .transition(.move(edge: .bottom).combined(with: .opacity)) + } + + @ViewBuilder + private var statusLabel: some View { + if state.isEditing { + Label( + localized(key: "codeFeed.editMode.dragToReorder"), + systemImage: "arrow.up.arrow.down", + ) + .lineLimit(1) + } else { + let count = dataModel.items.count + let itemText = count == 1 ? "item" : "items" + let filterCount = dataModel.itemsFilteringByTags.count + + HStack(spacing: 4) { + Image(systemName: "key.horizontal") + Text("\(count) \(itemText)") + + if filterCount > 0 { + Text("•") + Image(systemName: "tag.fill") + .font(.caption) + Text("\(filterCount)") + } + } + .lineLimit(1) + } + } + + private func filterBinding(for tag: VaultItemTag) -> Binding { + Binding { + dataModel.itemsFilteringByTags.contains(tag.id) + } set: { _ in + dataModel.toggleFiltering(tag: tag.id) + } } @State private var targetedIds = Set>() diff --git a/Vault/Sources/VaultiOS/Views/VaultListView.swift b/Vault/Sources/VaultiOS/Views/VaultListView.swift index 85db2abe9..052a41f94 100644 --- a/Vault/Sources/VaultiOS/Views/VaultListView.swift +++ b/Vault/Sources/VaultiOS/Views/VaultListView.swift @@ -68,7 +68,7 @@ struct VaultListView< } } } label: { - Image(systemName: "plus") + Label("Add Item", systemImage: "plus") } } } diff --git a/Vault/Tests/VaultiOSAutofillTests/__Snapshots__/VaultAutofillCodeSelectorViewSnapshotTests/layout.1.png b/Vault/Tests/VaultiOSAutofillTests/__Snapshots__/VaultAutofillCodeSelectorViewSnapshotTests/layout.1.png index 4e8ccb54f..5de347746 100644 Binary files a/Vault/Tests/VaultiOSAutofillTests/__Snapshots__/VaultAutofillCodeSelectorViewSnapshotTests/layout.1.png and b/Vault/Tests/VaultiOSAutofillTests/__Snapshots__/VaultAutofillCodeSelectorViewSnapshotTests/layout.1.png differ diff --git a/Vault/Tests/VaultiOSTests/__Snapshots__/VaultItemFeedViewSnapshotTests/layout_multipleCodesAtMediumSize.1.png b/Vault/Tests/VaultiOSTests/__Snapshots__/VaultItemFeedViewSnapshotTests/layout_multipleCodesAtMediumSize.1.png index dc09c1586..3bafea35d 100644 Binary files a/Vault/Tests/VaultiOSTests/__Snapshots__/VaultItemFeedViewSnapshotTests/layout_multipleCodesAtMediumSize.1.png and b/Vault/Tests/VaultiOSTests/__Snapshots__/VaultItemFeedViewSnapshotTests/layout_multipleCodesAtMediumSize.1.png differ diff --git a/Vault/Tests/VaultiOSTests/__Snapshots__/VaultItemFeedViewSnapshotTests/layout_noCodes.1.png b/Vault/Tests/VaultiOSTests/__Snapshots__/VaultItemFeedViewSnapshotTests/layout_noCodes.1.png index 873301050..3b80c1b7c 100644 Binary files a/Vault/Tests/VaultiOSTests/__Snapshots__/VaultItemFeedViewSnapshotTests/layout_noCodes.1.png and b/Vault/Tests/VaultiOSTests/__Snapshots__/VaultItemFeedViewSnapshotTests/layout_noCodes.1.png differ diff --git a/Vault/Tests/VaultiOSTests/__Snapshots__/VaultItemFeedViewSnapshotTests/layout_singleCodeAtMediumSize.1.png b/Vault/Tests/VaultiOSTests/__Snapshots__/VaultItemFeedViewSnapshotTests/layout_singleCodeAtMediumSize.1.png index 5fa5e38d9..c9ac66beb 100644 Binary files a/Vault/Tests/VaultiOSTests/__Snapshots__/VaultItemFeedViewSnapshotTests/layout_singleCodeAtMediumSize.1.png and b/Vault/Tests/VaultiOSTests/__Snapshots__/VaultItemFeedViewSnapshotTests/layout_singleCodeAtMediumSize.1.png differ diff --git a/Vault/Tests/VaultiOSTests/__Snapshots__/VaultItemFeedViewSnapshotTests/searchBar_includesTagsIfTheyExistInTheVaultStore.1.png b/Vault/Tests/VaultiOSTests/__Snapshots__/VaultItemFeedViewSnapshotTests/searchBar_includesTagsIfTheyExistInTheVaultStore.1.png index b0e60a88c..5df3718e4 100644 Binary files a/Vault/Tests/VaultiOSTests/__Snapshots__/VaultItemFeedViewSnapshotTests/searchBar_includesTagsIfTheyExistInTheVaultStore.1.png and b/Vault/Tests/VaultiOSTests/__Snapshots__/VaultItemFeedViewSnapshotTests/searchBar_includesTagsIfTheyExistInTheVaultStore.1.png differ diff --git a/Vault/Tests/VaultiOSTests/__Snapshots__/VaultItemFeedViewSnapshotTests/searchBar_tagsBeingFiltered.1.png b/Vault/Tests/VaultiOSTests/__Snapshots__/VaultItemFeedViewSnapshotTests/searchBar_tagsBeingFiltered.1.png index 40db2a7fb..a37c4a13b 100644 Binary files a/Vault/Tests/VaultiOSTests/__Snapshots__/VaultItemFeedViewSnapshotTests/searchBar_tagsBeingFiltered.1.png and b/Vault/Tests/VaultiOSTests/__Snapshots__/VaultItemFeedViewSnapshotTests/searchBar_tagsBeingFiltered.1.png differ diff --git a/Vault/Tests/VaultiOSTests/__Snapshots__/VaultItemFeedViewSnapshotTests/unifiedBar_clearButtonVisible.1.png b/Vault/Tests/VaultiOSTests/__Snapshots__/VaultItemFeedViewSnapshotTests/unifiedBar_clearButtonVisible.1.png index 20b5135f9..64aa40b27 100644 Binary files a/Vault/Tests/VaultiOSTests/__Snapshots__/VaultItemFeedViewSnapshotTests/unifiedBar_clearButtonVisible.1.png and b/Vault/Tests/VaultiOSTests/__Snapshots__/VaultItemFeedViewSnapshotTests/unifiedBar_clearButtonVisible.1.png differ diff --git a/Vault/Tests/VaultiOSTests/__Snapshots__/VaultItemFeedViewSnapshotTests/unifiedBar_multipleTagsFiltered.1.png b/Vault/Tests/VaultiOSTests/__Snapshots__/VaultItemFeedViewSnapshotTests/unifiedBar_multipleTagsFiltered.1.png index 994a861ef..3dde4147e 100644 Binary files a/Vault/Tests/VaultiOSTests/__Snapshots__/VaultItemFeedViewSnapshotTests/unifiedBar_multipleTagsFiltered.1.png and b/Vault/Tests/VaultiOSTests/__Snapshots__/VaultItemFeedViewSnapshotTests/unifiedBar_multipleTagsFiltered.1.png differ diff --git a/Vault/Tests/VaultiOSTests/__Snapshots__/VaultItemFeedViewSnapshotTests/unifiedBar_narrowWidth_buttonsDoNotWrap.1.png b/Vault/Tests/VaultiOSTests/__Snapshots__/VaultItemFeedViewSnapshotTests/unifiedBar_narrowWidth_buttonsDoNotWrap.1.png index e83c5c20b..86f9c9c48 100644 Binary files a/Vault/Tests/VaultiOSTests/__Snapshots__/VaultItemFeedViewSnapshotTests/unifiedBar_narrowWidth_buttonsDoNotWrap.1.png and b/Vault/Tests/VaultiOSTests/__Snapshots__/VaultItemFeedViewSnapshotTests/unifiedBar_narrowWidth_buttonsDoNotWrap.1.png differ diff --git a/Vault/Tests/VaultiOSTests/__Snapshots__/VaultItemFeedViewSnapshotTests/unifiedBar_searchingWithNoResults.1.png b/Vault/Tests/VaultiOSTests/__Snapshots__/VaultItemFeedViewSnapshotTests/unifiedBar_searchingWithNoResults.1.png index 873301050..3b80c1b7c 100644 Binary files a/Vault/Tests/VaultiOSTests/__Snapshots__/VaultItemFeedViewSnapshotTests/unifiedBar_searchingWithNoResults.1.png and b/Vault/Tests/VaultiOSTests/__Snapshots__/VaultItemFeedViewSnapshotTests/unifiedBar_searchingWithNoResults.1.png differ diff --git a/Vault/Tests/VaultiOSTests/__Snapshots__/VaultItemFeedViewSnapshotTests/unifiedBar_searchingWithResults.1.png b/Vault/Tests/VaultiOSTests/__Snapshots__/VaultItemFeedViewSnapshotTests/unifiedBar_searchingWithResults.1.png index 924844b64..7ebd5cec3 100644 Binary files a/Vault/Tests/VaultiOSTests/__Snapshots__/VaultItemFeedViewSnapshotTests/unifiedBar_searchingWithResults.1.png and b/Vault/Tests/VaultiOSTests/__Snapshots__/VaultItemFeedViewSnapshotTests/unifiedBar_searchingWithResults.1.png differ diff --git a/Vault/Tests/VaultiOSTests/__Snapshots__/VaultItemFeedViewSnapshotTests/unifiedBar_tagFilteringInEditMode.1.png b/Vault/Tests/VaultiOSTests/__Snapshots__/VaultItemFeedViewSnapshotTests/unifiedBar_tagFilteringInEditMode.1.png index 6ed678b93..d1361db66 100644 Binary files a/Vault/Tests/VaultiOSTests/__Snapshots__/VaultItemFeedViewSnapshotTests/unifiedBar_tagFilteringInEditMode.1.png and b/Vault/Tests/VaultiOSTests/__Snapshots__/VaultItemFeedViewSnapshotTests/unifiedBar_tagFilteringInEditMode.1.png differ diff --git a/Vault/Tests/VaultiOSTests/__Snapshots__/VaultItemFeedViewSnapshotTests/viewState_toggleEditingMode.editing.png b/Vault/Tests/VaultiOSTests/__Snapshots__/VaultItemFeedViewSnapshotTests/viewState_toggleEditingMode.editing.png index 79f0c742f..b0d7010b5 100644 Binary files a/Vault/Tests/VaultiOSTests/__Snapshots__/VaultItemFeedViewSnapshotTests/viewState_toggleEditingMode.editing.png and b/Vault/Tests/VaultiOSTests/__Snapshots__/VaultItemFeedViewSnapshotTests/viewState_toggleEditingMode.editing.png differ diff --git a/Vault/Tests/VaultiOSTests/__Snapshots__/VaultItemFeedViewSnapshotTests/viewState_toggleEditingMode.notEditing.png b/Vault/Tests/VaultiOSTests/__Snapshots__/VaultItemFeedViewSnapshotTests/viewState_toggleEditingMode.notEditing.png index 5fa5e38d9..c9ac66beb 100644 Binary files a/Vault/Tests/VaultiOSTests/__Snapshots__/VaultItemFeedViewSnapshotTests/viewState_toggleEditingMode.notEditing.png and b/Vault/Tests/VaultiOSTests/__Snapshots__/VaultItemFeedViewSnapshotTests/viewState_toggleEditingMode.notEditing.png differ diff --git a/Vault/Tests/VaultiOSTests/__Snapshots__/VaultMainNavigationViewSnapshotTests/layout.1.png b/Vault/Tests/VaultiOSTests/__Snapshots__/VaultMainNavigationViewSnapshotTests/layout.1.png index 1977c9754..a57189696 100644 Binary files a/Vault/Tests/VaultiOSTests/__Snapshots__/VaultMainNavigationViewSnapshotTests/layout.1.png and b/Vault/Tests/VaultiOSTests/__Snapshots__/VaultMainNavigationViewSnapshotTests/layout.1.png differ