Repository navigation
Replace the Home header with a floating search toolbar - #121
Merged
AndrewG828 merged 47 commits intoOct 8, 2026
Merged
AndrewG828 merged 47 commits into
AndrewG828 merged 47 commits into
Conversation
Replaces the raster category illustrations with single-scale SVG line icons and reworks the category taxonomy in `Constants.filters`, which is the one place categories are declared: - "Sports & Outdoors" becomes "Active", with a matching `Active` imageset. Category artwork is looked up as `Image(filter.title)`, so the imageset had to be renamed in lockstep. - "Household" and "Other" gain colors. A nil color is what hides a category from the Home row and the browse grid, so both are now browsable; Household needed a new imageset. - `FilterView` derives its chips from `Constants.filters` instead of keeping a parallel hardcoded list that had already drifted (it was still offering "Sports & Outdoors" and omitting "Household"). The tinted circle behind each category icon is gone — the new line icons carry their own shape — so `CircularFilterButton` renders the artwork directly at 72pt. Filter and condition chips now cross-fade on selection instead of swapping in an "x" glyph that shifted the chip's width mid-tap. Also drops `sfsfs.swift`, an empty file that was never in the build phase. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017R8mua9xepzF3mnRETtd6W
Groundwork shared by the Explore, Home and Product Details screens that follow. Nothing calls these yet. - `GlassToolbarModifier` (new): the Liquid Glass pill behind floating toolbar controls. It lives in Components rather than next to its first caller because Home, Explore and the category browser all need the same treatment. The near-transparent fill plus explicit `contentShape` are load-bearing — neither `glassEffect` nor `Material` claims the full pill for hit testing on its own, so without them taps near a control's edge fall through to the feed. - `View.enableSwipeBack()`: screens that hide the navigation bar to draw their own chrome also lose the interactive pop gesture, because the system gesture is owned by the back button they just hid. This re-enables it and gates it on having something to pop back to. - `UIImage.prefersLightToolbarIcons(displayedIn:)`: samples a 16x4 luminance grid from the strip of a photo that actually sits under the toolbar (after aspect-fill cropping and EXIF orientation flattening) so overlaid icons can pick black or white. It requires 78% of samples to be dark before going white, so mid-tone heroes stay on black instead of flickering between the two. - `BackButton` takes an optional `tint` for the same reason: over a photo hero the chevron can't be unconditionally black. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017R8mua9xepzF3mnRETtd6W
Feed cells were locked to a 4:3 box, so tall and square photos were cropped to whatever fit. `CachedImageView` now reports the decoded image's aspect ratio back to its caller, and `ProductGalleryCell` sizes the image from it — falling back to 4:3 only while loading. Cells are wider (46pt of chrome instead of 68) and the column gap tightened to match. The cell also gains the information the old one dropped: category and condition under the title, and a bookmark toggle that saves without opening the listing. `loadSavedState()` only asks the server about a single post while `HomeViewModel.savedItems` hasn't loaded yet. `savedItems` holds every saved post, so once it has arrived, absence is a real answer — the naive version fired one `/post/isSaved` request per visible cell on every scroll. That needed `hasLoadedSavedItems` on `HomeViewModel` to distinguish "not saved" from "not known yet". `CategoriesView` is extracted as its own component: the Shop By Category row is about to be used by Explore, and it is the same row Home already shows. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017R8mua9xepzF3mnRETtd6W
…y Viewed Three new endpoints and the two view models that drive the Explore tab. No UI consumes them yet apart from view recording. `NetworkManager` gains `getDailyPicks`, `getTrendingPosts(category:page:limit:)` and `recordPostView`. The last one needed a `post(url:)` overload because it is the first request in the app that sends no body and decodes no response — the server upserts one view per viewer/post/UTC-day and ignores your own listings, so the client can fire it unconditionally and ignore failures. `ExploreViewModel` is a shared singleton with a 3-minute freshness window, since Explore is a tab root that re-appears constantly and should not refetch on every visit. Trending is keyed by category so switching categories bypasses the cache. `RecentlyViewedViewModel` keeps its own ordered id list plus a listing cache in `AppStorage`, so the section renders from disk on launch and only fetches the posts it is missing, eight at a time. `ProductDetailsViewModel.setPost` is what feeds it — opening a listing caches it locally and reports the view to the server. `Post` picks up the save-count fields. The count arrives under three different key spellings depending on endpoint, and `getPostInfo` returns `savers` as a user array rather than a number, so `displaySaveCount` takes the largest of whatever decoded. Trending payloads often omit savers entirely, hence `ensuringMinimumSaves` — if the viewer has the post saved locally, the true count is at least one. That is a lower bound, not a global total; worth confirming against the backend before we lean on it further. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017R8mua9xepzF3mnRETtd6W
The three "see all" destinations behind the Explore rails, plus their routes. They are routable and complete from this commit; nothing links to them until Explore itself lands. Each is a plain gallery over one slice of the discovery data layer. Trending is keyed on `category.id` via `.task(id:)` so pushing a different category refetches rather than showing the previous one's results, and Recently Viewed loads its first page for an immediate render before filling in the tail. Empty states are gated on the matching loading flag so none of them flash "no results" during the first fetch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017R8mua9xepzF3mnRETtd6W
The horizontal rails that make up the Explore feed, and the collage card that fronts Saved / Recently Viewed. `ExploreCollageCard` shows a single image when a collection is thin and a 2x2 grid once it has at least four listings, so a one-item Saved list doesn't render three empty tiles. `ExploreTrendingSection` puts the category picker in the section header itself — the header is the control — and shows a skeleton on first load only, so switching categories doesn't tear the rail down and rebuild it. Two things worth calling out for reviewers: - A trending card pushed the category list instead of the listing it displays. Fixed; it now pushes `.productDetails` like the Daily Picks cards do. - The upcoming-events section was a standalone shimmer with no data source and no caller. Left it out rather than shipping a placeholder that never resolves; it can come back with the events endpoint. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017R8mua9xepzF3mnRETtd6W
Assembles the discovery sections into a tab root: category shortcuts, a Just For You row fronting Saved and Recently Viewed, then Daily Picks and Trending. It is not in the tab bar yet — that comes with the navigation rework. Explore owns a local `SearchViewModel` rather than the shared singleton. Both tab roots stay mounted under the new navigation, so sharing one would mean typing in Explore silently replaced Home's results. `SearchPanel` (new) is the expanded search card, extracted rather than written inline: Home and the category browser present the identical card, and this is the first of the three to land. Keeping it in one place also keeps the "opaque, not glass" decision in one place — materializing a glass layer that large in the same frame the keyboard animates made opening search feel like a hang. Scrolling is disabled only while the card covers the feed with no results yet. Gating it on "is the panel open" instead — which is what the first cut did — also freezes the search results the panel hands back, leaving you unable to scroll past the first four. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017R8mua9xepzF3mnRETtd6W
`NewListingViewModel` tracked a single `PhotosPickerItem`, so adding nine photos meant nine trips through the picker. It now holds a selection array and loads the batch in one pass, trimmed to whatever slots are left. Two bugs fall out of the rewrite: - The picker enforced a limit of 7 images while `PaginatedImageView` advertised 9, so the last two picks were silently dropped. Both now read `NewListingViewModel.maxImages`, and the system picker is given `maxSelectionCount: remainingImageSlots` so it stops you at the limit instead of discarding the overflow afterwards. - The old loader appended from a `DispatchQueue.main.async` block inside an already-`@MainActor` view model, which reordered appends against the `selectedItem = nil` reset. `removeImage(at:)` is added here for the Sell form, which lets you delete a photo from the carousel. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017R8mua9xepzF3mnRETtd6W
Replaces the three-step push flow (add button → images → details) with a single form: photos, title and price, category, condition, description, publish. Nothing routes to it yet — the tab bar rework wires it up. The photo well is a carousel rather than a strip, so each image is reviewed at the size it will be posted at, with delete and add controls on the image itself and arrows only where there is somewhere to go. Categories come from `Constants.filters` instead of a hand-written list. The first cut hardcoded seven, which already disagreed with the taxonomy — it was missing Household. Only the "Handmade" → "Homemade" display name is special-cased here. New listings default to "Gently Used" rather than "Never Used": the previous default made the strongest claim on the seller's behalf, which is the wrong way round for a form that people will submit without reading. Left out of the port: a clothing-only "Additional Details" block whose call site was commented out and whose fields did nothing. Worth flagging for a follow-up — `NewListingViewModel.createNewListing()` calls `clear()` on failure as well as success, so a network error silently empties the form. That was survivable when the form was a pushed screen you were about to leave; on a tab root that keeps you where you are, it reads as the app throwing your listing away. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017R8mua9xepzF3mnRETtd6W
Home / Explore / Sell / Messages / Profile, five tabs where there were three, each with an independent navigation history. The single app-wide `NavigationStack` meant pushing a listing from Home and then switching to Messages left the listing on screen — there was one path and every tab shared it. `Router` now holds one path per tab and exposes the active one through the same `path` property, so `push`/`pop`/`popToRoot` and all their call sites are unchanged. `Router.Tab` names the slots, because the indices appear in the tab bar, the stack container and two unrelated screens. The container is hand-rolled rather than a `TabView`. The system bar kept drawing underneath the custom one and showed through the glass as a second outline. Every stack stays mounted and is toggled by opacity so tab state survives switching; the inactive ones are hidden from accessibility and hit testing. Fixes that come with the tab-count change: - `ProductDetailsView` sent you to `selection = 2` when you tapped your own name on a listing. Slot 2 is now Sell, not Profile; both call sites use `Router.Tab` instead of a literal. - `showsTabBar` reacts to the route rather than to `MessagesView.onDisappear`, which fires after the pop animation and left the bar missing for a beat. - Bottom content inset is always in the tree, just zero-height when the bar is hidden. Adding and removing the `safeAreaInset` broke scroll insets after a push and often failed to restore them on pop. - The `.reviewTesting` destination pointed at a view deleted from main; the branch it came from still referenced it and did not compile. The conversation toolbar picks up a Lucide calendar icon, which is what adds `lucide-icons-swift` to the project (Profile and Availability use two more of its glyphs in later commits). Four glyphs is a thin reason for a dependency — worth revisiting if we don't reach for more of it. Its status dot is drawn only when there actually is a confirmed meeting; unconditionally is how it started, which made a badge that told you nothing and lost the hint explaining why the button refuses to open the picker. `TabViewIcon` is removed: it drew the old three-icon bar and has no other caller. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017R8mua9xepzF3mnRETtd6W
Home's static header (wordmark, search icon, bell) becomes a glass toolbar that floats over the feed: a search pill that expands into the full search card, a notification button, and the section's filter button once that scrolls out of view. Search now happens in place instead of pushing `SearchView`. Results render into the same scroll view as the feed, so there is no navigation transition between typing and reading results. The Shop By Category row moves to Explore, which is where browsing by category now lives, and the expandable add button goes with the Sell tab. What is left on Home is the recent-listings feed and its filter. Two things to look at: - Scrolling is disabled only while the search card covers the feed with no results yet. Tying it to "is the card open" — the obvious version — also freezes the results the card produces, so you can't scroll past the first row. - Toolbar filter visibility is driven by KVO on the enclosing `UIScrollView`'s content offset. SwiftUI's PreferenceKey and GeometryReader paths stall mid-scroll and leave the button stuck in whichever state it was in. `onScrollGeometryChange` would do the same job natively, but only from iOS 18 and the deployment target is 17.5, so this is one mechanism instead of two. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017R8mua9xepzF3mnRETtd6W
This was referenced Sep 3, 2026
- Add Constants.productCategories as the one place that drops the "Recent" pseudo-category, and read FilterView's chips from it. - Use the Constants.Fonts.title3 / Constants.Colors.black tokens for the chip labels instead of an inline Rubik-Medium font and Color.black. - Drop a stray blank line in RangeSlider. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…andrewgao/feature/UI-UX-Lucid-Refactor-02-chrome-primitives
CLAUDE.md asks for reusable modifiers to be applied through a View extension (like .loadingView(isLoading:)) rather than .modifier(...) at each call site, so add .glassToolbarBackground(cornerRadius:isOpaque:). Also take the near-clear hit-testing fill from Constants.Colors.white. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…destinations' into user/andrewgao/feature/UI-UX-Lucid-Refactor-06-explore-sections
…sections' into user/andrewgao/feature/UI-UX-Lucid-Refactor-07-explore-tab
The Daily Picks and Trending sections each had their own copy of the listing card, the loading skeleton, the horizontal rail and the card metrics. Pull those into private ExplorePostCard, ExploreRailSkeleton, ExploreRail, ExploreSectionHeader and ExploreRailLayout so each section's body is an outline of named pieces, per CLAUDE.md. Also make the router private, add doc comments to the section views, split Trending's menu label and empty state into named sub-views, give ExploreCollageCard its Properties / UI sections, and build its 2x2 grid with a loop instead of four copies of the cell. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Add SearchResultsView (spinner / "No results" / grid) and SearchPill
(the collapsed glass search control) to Views/Components. Home builds
the same two pieces in the next PR, and CLAUDE.md wants shared UI in
Components rather than inline in each feature view.
- ExploreView: load its content with .task { } instead of
.onAppear { Task { } }, pull the search-expansion handler and the
loader out of body into named functions, split the Just For You
collages into named sub-views, keep all stored properties under
"Properties", and make the router private.
- SearchPanel: add MARK sections and take the shadow/stroke colors
from Constants.Colors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tab' into user/andrewgao/feature/UI-UX-Lucid-Refactor-08-multi-image-picker
…age-picker' into user/andrewgao/feature/UI-UX-Lucid-Refactor-09-sell-tab
- Move maxImages and remainingImageSlots under Properties (they had landed in the middle of the @published list and under Functions) and document them. - Document updateListingImages(newItems:), and log photos that fail to load instead of dropping them silently with try?. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…' into user/andrewgao/feature/UI-UX-Lucid-Refactor-10-tab-navigation
- Add ImageCountBadge to Views/Components for the "N / M" capsule over a photo carousel. The Sell form, the listing hero and the full-screen photo viewer each built their own copy; the next PRs switch the other two over. - Split the carousel's trash / add controls into named sub-views and rename actionButtons to publishButton, which is what it is. - Read categories from Constants.productCategories. - Drop dead code in listingField: the prefix and width parameters were never passed, and Field.price was never focused (price opens a sheet). - Take the chevron shadow color from Constants.Colors, and add a doc comment to the view. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…gation' into user/andrewgao/feature/UI-UX-Lucid-Refactor-11-home-search
- Router.activeTab / pathBinding(for:) now take Router.Tab instead of a raw Int, and per-tab paths live in a [Tab: [Route]] dictionary, which removes the index bounds checks. MainTabView converts its Int selection binding once through a selectedTab property. - MainTabView: move the tab button out of the ForEach into tabBarButton(for:) / unreadBadge(count:isTabSelected:), reuse isMessagesRouteActive in the path onChange instead of re-deriving it, and document the two private modifiers. - Add Constants.Fonts.tabBarLabel and .badge for the tab label (Rubik 13) and unread badge (Roboto 10) instead of inline Font.custom, and take the tab bar's colors from Constants.Colors. - Fix hasProfilePicture: `user.photoUrl != nil` is always true (the URL isn't optional, and the compiler warns about it), so the tab showed the placeholder image whenever the photo failed to load. Set it once the photo actually decodes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Use SearchPill and SearchResultsView (added with Explore) instead of
a second inline copy of each, and .glassToolbarBackground() instead of
.modifier(GlassToolbarModifier()).
- Load saved posts with .task { } rather than a Task inside onAppear,
and have pull-to-refresh await the filtered refresh directly instead
of wrapping it in an unawaited Task with try?; failures are logged.
- Move the search-expansion handler out of body, extract the inline
filter button, and add the Properties / UI / Private Methods sections.
- HomeScrollOffsetReader: replace DispatchQueue.main.async with
main-actor Tasks (CLAUDE.md: no raw DispatchQueue in feature code) and
mark the coordinator @mainactor.
- Use Constants.Spacing.horizontalPadding for the header inset.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configuration
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AndrewG828
added this pull request to stack #127
October 8, 2026 22:04
…02-chrome-primitives
…rimitives' into user/andrewgao/feature/UI-UX-Lucid-Refactor-03-listing-cards # Conflicts: # Resell.xcodeproj/project.pbxproj
Addresses review on #113: the cell still decided its own saved state and handled save failures, and CachedImageView carried an unneeded comment. - Add HomeViewModel.knownSavedState(for:), the "savedItems answers once it has loaded" rule, and have isPostSaved(_:) and the cell both use it. The cell now re-syncs only when that answer changes, not on every savedItems update. - setSaved(_:for:) now logs failures itself and returns whether the change went through. The cell keeps only its optimistic flip and the rollback, since the bookmark state is per card. - hasLoadedSavedItems is private now that only the view model reads it. - Drop the doc comment on CachedImageView.aspectRatio. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cards' into user/andrewgao/feature/UI-UX-Lucid-Refactor-04-discovery-data-layer
…y-data-layer' into user/andrewgao/feature/UI-UX-Lucid-Refactor-05-explore-destinations
…destinations' into user/andrewgao/feature/UI-UX-Lucid-Refactor-06-explore-sections
…sections' into user/andrewgao/feature/UI-UX-Lucid-Refactor-07-explore-tab
…tab' into user/andrewgao/feature/UI-UX-Lucid-Refactor-08-multi-image-picker
…age-picker' into user/andrewgao/feature/UI-UX-Lucid-Refactor-09-sell-tab
…' into user/andrewgao/feature/UI-UX-Lucid-Refactor-10-tab-navigation
…gation' into user/andrewgao/feature/UI-UX-Lucid-Refactor-11-home-search
Base automatically changed from
user/andrewgao/feature/UI-UX-Lucid-Refactor-10-tab-navigation
to
main
October 8, 2026 22:37
AndrewG828
deleted the
user/andrewgao/feature/UI-UX-Lucid-Refactor-11-home-search
branch
October 8, 2026 22:37
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.
Home's static header (wordmark, search icon, bell) becomes a glass toolbar that
floats over the feed: a search pill that expands into the full search card, a
notification button, and the section's filter button once that scrolls out of
view.
Search now happens in place instead of pushing
SearchView. Results render intothe same scroll view as the feed, so there is no navigation transition between
typing and reading results.
The Shop By Category row moves to Explore, which is where browsing by category
now lives, and the expandable add button goes with the Sell tab. What is left
on Home is the recent-listings feed and its filter.
Two things to look at:
results yet. Tying it to "is the card open" — the obvious version — also
freezes the results the card produces, so you can't scroll past the first row.
UIScrollView'scontent offset. SwiftUI's PreferenceKey and GeometryReader paths stall
mid-scroll and leave the button stuck in whichever state it was in.
onScrollGeometryChangewould do the same job natively, but only from iOS 18and the deployment target is 17.5, so this is one mechanism instead of two.
Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_017R8mua9xepzF3mnRETtd6W