Rebuild the feed bottom bar on standard SwiftUI controls - #555
Merged
Merged
Conversation
bradleymackey
force-pushed
the
native-standardization-buttons
branch
from
September 14, 2026 16:44
f43c598 to
83537dd
Compare
Replaces the hand-built chrome at the bottom of the vault feed with
standard controls.
## The problem
`VaultItemFeedView.unifiedInfoSection` was a hand-assembled bar:
`.background(Color.primary.opacity(0.05))` + `RoundedRectangle(12)`
wrapping two buttons that were styled entirely by hand — `.padding(8/16)`
+ `.background(...)` + `.clipShape(Capsule())` + a forced
`.foregroundStyle(.white)`. Neither had a pressed state.
The Clear button used `.background(Color.secondary)`. `Color.secondary`
is a *label* colour, not a fill; in dark mode that is a light grey behind
white text.
The tag filters were `TagPillView` + `.onTapGesture`, which produces no
`.isButton` trait, no VoiceOver activation, no keyboard or Switch Control
focus, and no press feedback. At `.footnote` with 8pt vertical padding
the tap target was roughly 29pt tall.
## What changed
| Element | Before | After |
| --- | --- | --- |
| Tag filters | `TagPillView` + `.onTapGesture` | `Toggle` + `.toggleStyle(.button)` + `.buttonStyle(.bordered)` + `.buttonBorderShape(.capsule)` + `.tint(tag colour)` |
| Clear | hand-built capsule | `.buttonStyle(.bordered)` + `.buttonBorderShape(.capsule)` |
| Edit/Done | hand-built capsule | `.buttonStyle(.borderedProminent)` + `.buttonBorderShape(.capsule)` |
| Bar container | `Color.primary.opacity(0.05)` + `RoundedRectangle(12)` | removed; the controls sit on the content the way a system bottom bar does |
| Animation | four stacked `.spring(response: 0.3, dampingFraction: 1.0)` | `.snappy` |
| Status label | `.foregroundColor(.secondary)` repeated on seven sibling views | one `.foregroundStyle(.secondary)` on the container |
Selected and unselected tag states now come from the toggle style rather
than the brightness-derived fill and stroke colours, so the pills pick up
the standard tinted/untinted treatment. `TagPillView` itself is unchanged
and still used by the three read-only sites that display an item's tags.
`VaultListView`'s add-item menu label becomes
`Label("Add Item", systemImage: "plus")` instead of a bare `Image`.
Reordering still uses `.draggable`/`.dropDestination` and
`VaultItemFeedReorderer`. The `LazyVGrid` is deliberate, and `List.onMove`
would force a single-column layout.
## Why this is not a `.toolbar`
The obvious native move is to put this in `.toolbar` — item count in
`ToolbarItem(placement: .status)`, `EditButton()` and Clear in
`.bottomBar` — and let iOS 26 render the bar on Liquid Glass.
That was tried and reverted. Toolbar content does not rasterise reliably
in the snapshot harness: the bar drew twice (once over the navigation
title, once at the bottom), the conditional Clear button never appeared
even with two tags active, and `EditButton()` still read "Edit" in a
snapshot where the cards had already rendered their editing state — so
the `\.editMode` binding had not been applied at capture time.
This is the same class of problem as the Liquid Glass finding in the
previous commit: UIKit-hosted chrome does not resolve when the hierarchy
is rendered off-screen. Moving this chrome into a toolbar would therefore
have silently gutted the six `unifiedBar_*` tests that exist to cover it.
The bar stays in `.safeAreaInset`, which rasterises correctly, and the
standardization is achieved through the control styles instead.
## Verification
Local, iPhone 18 Pro Max / iOS 27.0:
- `xcodebuild build-for-testing` — `TEST BUILD SUCCEEDED`
- Full suite, `-parallel-testing-enabled NO` — `TEST EXECUTE SUCCEEDED`,
2834 passed, 0 failures across 24 bundles, matching the pre-change count
- 15 snapshots re-recorded and visually reviewed: 13 feed, plus
`VaultMainNavigationView` and `VaultAutofillCodeSelectorView`, which
both embed the feed
- `make format` + `make lint` — clean
⚠️ Automatic CI is still disabled (#548), so this is local verification only.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
bradleymackey
force-pushed
the
native-standardization-feed-toolbar
branch
from
September 14, 2026 16:49
4b4f990 to
b581376
Compare
bradleymackey
added a commit
that referenced
this pull request
Sep 18, 2026
The bordered button style fills the capsule in both states and signals selection only through tint, which reads as near-identical either way. TagPillView fills a selected pill and leaves an unselected one outlined, which is unambiguous. Keep the Toggle and .toggleStyle(.button) so the pills retain the button trait and selected state that #555 added; only the visual style reverts, via .buttonStyle(.plain) letting TagPillView draw its own capsule. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
bradleymackey
added a commit
that referenced
this pull request
Sep 18, 2026
## Summary The feed's bottom bar (tag pills + item count + Clear/Edit) lost its container and its descriptive filter text. These were **two separate changes**, months apart — and neither was #551, which only touched the backup screens: - **The container** — `510bfeaa` (#555) deleted `.background(Color.primary.opacity(0.05))` + `.clipShape(RoundedRectangle(cornerRadius: 12))` from the status row, on the stated grounds that "the controls sit on the content the way a system bottom bar does". The controls were left with no surface at all, so the `LazyVGrid` scrolled directly under the status label. - **The detail** — `315486f5` (#499, January) replaced the localized prose *"2 tag filters"* with a bare `• 🏷 2` badge. For the record: there is **no sort control in the app and never has been** (`git log --all -i -S "sort" -- Sources/VaultiOS` is empty). Order is fixed to `.relativeOrder` at the store layer and rearranged by drag in edit mode. ## What changed - **The container is back exactly where it was**: `Color.primary.opacity(0.05)` + `RoundedRectangle(cornerRadius: 12)` around the **status row only**, inset by 12pt. The tag pills stay **above** it, sitting directly on the content — that is the original arrangement and it is the one restored here. - **A single active filter is named** (`• 🏷 work`). The pill row scrolls horizontally, so an active tag can sit off-screen; the name is information the bare count never carried. With more than one filter there is no room for names inside the container, so it shows the bare count exactly as before (`• 🏷 2`). - **Item count is localized**: `statusLabel` hardcoded English `count == 1 ? "item" : "items"`. It now uses the existing `feedViewModel.searching.title.%lld` plural via a new `VaultDataModel.itemsCountDescription`, mirroring `filteringByTagsDescription`. (That plural lives in VaultFeed's bundle, so it cannot be read from VaultiOS's `localized()`, which resolves against the "Feed" table.) The button styles introduced by #555 (`.bordered` / `.borderedProminent` capsules, `Toggle`-based tag pills) are left alone — those were an accessibility fix, not part of the regression. ### A material was tried and rejected The first attempt replaced the container with a full-width `.bar` material across the whole `safeAreaInset`. Two problems, both visible in the history of this branch: 1. It pulled the tag pills **inside** the band, which is not how the bar is meant to read. 2. `.background(.bar)` applied directly puts the label in a **vibrancy context**. That is not cosmetic — it rendered the `key.horizontal` and `tag.fill` icons completely invisible and forced the secondary text to black. (`.background { Rectangle().fill(.bar) }` avoids the vibrancy, if a material is ever wanted here.) ## Manifesto Reviewed against `MANIFESTO.md`. **C5 / C2** are the live ones and drove an explicit **non-goal**: no "matched of total" count. The feed query filters on `visibility == always` and unions search-passphrase matches only when the query matches (`PersistedLocalVaultStore.swift:42-49, 102-106`), so `items.count` deliberately excludes passphrase-hidden items. Surfacing an unfiltered total would disclose that hidden items exist and let a coercer diff the two numbers. The status label still counts only what the feed already displays, and `itemsCountDescription` carries a doc comment saying so. Nothing else is touched: no bulk operation (C1), no telemetry (C3), no auth change (C4), no undo or audit surface (C6), no default flipped (C7), no step removed from a sensitive flow (C8), no duress feature advertised (C9), no payload change (C10). ## Testing - Full `iOSAllTests` plan passes locally on iPhone 18 Pro Max / iOS 27.0. - Feed snapshots re-recorded and visually compared against `git show 510bfea^:…/unifiedBar_multipleTagsFiltered.1.png` to confirm the layout matches the original. Two added — `unifiedBar_singleFilterIsNamed` and `unifiedBar_multipleFiltersFallBackToCount` — pinning both sides of the naming threshold. - The navigation and autofill suites that embed the feed were re-recorded too.⚠️ Automatic CI is still disabled (#548), so this is local verification only. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Replaces the hand-built chrome at the bottom of the vault feed with
standard controls.
The problem
VaultItemFeedView.unifiedInfoSectionwas a hand-assembled bar:.background(Color.primary.opacity(0.05))+RoundedRectangle(12)wrapping two buttons that were styled entirely by hand —
.padding(8/16).background(...)+.clipShape(Capsule())+ a forced.foregroundStyle(.white). Neither had a pressed state.The Clear button used
.background(Color.secondary).Color.secondaryis a label colour, not a fill; in dark mode that is a light grey behind
white text.
The tag filters were
TagPillView+.onTapGesture, which produces no.isButtontrait, no VoiceOver activation, no keyboard or Switch Controlfocus, and no press feedback. At
.footnotewith 8pt vertical paddingthe tap target was roughly 29pt tall.
What changed
TagPillView+.onTapGestureToggle+.toggleStyle(.button)+.buttonStyle(.bordered)+.buttonBorderShape(.capsule)+.tint(tag colour).buttonStyle(.bordered)+.buttonBorderShape(.capsule).buttonStyle(.borderedProminent)+.buttonBorderShape(.capsule)Color.primary.opacity(0.05)+RoundedRectangle(12).spring(response: 0.3, dampingFraction: 1.0).snappy.foregroundColor(.secondary)repeated on seven sibling views.foregroundStyle(.secondary)on the containerSelected and unselected tag states now come from the toggle style rather
than the brightness-derived fill and stroke colours, so the pills pick up
the standard tinted/untinted treatment.
TagPillViewitself is unchangedand still used by the three read-only sites that display an item's tags.
VaultListView's add-item menu label becomesLabel("Add Item", systemImage: "plus")instead of a bareImage.Reordering still uses
.draggable/.dropDestinationandVaultItemFeedReorderer. TheLazyVGridis deliberate, andList.onMovewould force a single-column layout.
Why this is not a
.toolbarThe obvious native move is to put this in
.toolbar— item count inToolbarItem(placement: .status),EditButton()and Clear in.bottomBar— and let iOS 26 render the bar on Liquid Glass.That was tried and reverted. Toolbar content does not rasterise reliably
in the snapshot harness: the bar drew twice (once over the navigation
title, once at the bottom), the conditional Clear button never appeared
even with two tags active, and
EditButton()still read "Edit" in asnapshot where the cards had already rendered their editing state — so
the
\.editModebinding had not been applied at capture time.This is the same class of problem as the Liquid Glass finding in the
previous commit: UIKit-hosted chrome does not resolve when the hierarchy
is rendered off-screen. Moving this chrome into a toolbar would therefore
have silently gutted the six
unifiedBar_*tests that exist to cover it.The bar stays in
.safeAreaInset, which rasterises correctly, and thestandardization is achieved through the control styles instead.
Verification
Local, iPhone 18 Pro Max / iOS 27.0:
xcodebuild build-for-testing—TEST BUILD SUCCEEDED-parallel-testing-enabled NO—TEST EXECUTE SUCCEEDED,2834 passed, 0 failures across 24 bundles, matching the pre-change count
VaultMainNavigationViewandVaultAutofillCodeSelectorView, whichboth embed the feed
make format+make lint— clean🤖 Generated with Claude Code