Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ infra/cloudflare/node_modules/
gaff-dev.log
gaff-dev.pid

# Per-feature git worktrees live here (created via `git worktree add ./worktrees/<slug>`)
worktrees/

# Claude Code local project memory
.claude/projects/
.fallow/
8 changes: 6 additions & 2 deletions src/components/layout/admin-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,14 @@ function useHouseLinks(): NavLink[] {
}

/**
* Optional `mode="desktop-only"` hides the entire shell below `md` so
* Optional `mode="desktop-only"` hides the entire shell below `lg` so
* the existing mobile flow can render alongside it (the same pattern
* the desktop layouts used pre-migration). Default `mode="responsive"`
* keeps the shell visible everywhere.
*
* The boundary lives at `lg` (1024 px) — not `md` — because the
* three-column desktop layouts assume at least a laptop's width. iPad
* portrait (768–1023 px) gets the mobile layout instead.
*/
type Props = {
children?: ReactNode;
Expand All @@ -134,7 +138,7 @@ export function AdminSidebar({ children, mode = "responsive" }: Props) {
const desktopOnly = mode === "desktop-only";
const houseLinks = useHouseLinks();
return (
<div className={desktopOnly ? "hidden md:contents" : "contents"}>
<div className={desktopOnly ? "hidden lg:contents" : "contents"}>
{/* Pin the whole shell to the viewport so the inset becomes a
* scrollable region of fixed height rather than a page that
* grows past the fold. Children that need to scroll handle it
Expand Down
7 changes: 5 additions & 2 deletions src/components/layout/bottom-nav.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Mobile bottom nav. Hidden on `md+` where the AdminSidebar takes
* Mobile bottom nav. Hidden on `lg+` where the AdminSidebar takes
* over. Always shows the three primary tabs (Review · Shortlist ·
* Searches) so the layout stays consistent across household sizes —
* solo users land on the Shortlist screen's empty state rather than
Expand Down Expand Up @@ -76,7 +76,10 @@ export function BottomNav() {
return (
<nav
aria-label="Primary"
className="fixed inset-x-0 bottom-0 z-30 grid grid-cols-3 border-border border-t bg-card md:hidden"
// `pb-[env(safe-area-inset-bottom)]` keeps the tab labels above the home
// indicator on notched iPhones; on devices without an inset the value
// resolves to 0 and the nav renders flush against the viewport edge.
className="fixed inset-x-0 bottom-0 z-30 grid grid-cols-3 border-border border-t bg-card pb-[env(safe-area-inset-bottom)] lg:hidden"
>
{TABS.map((tab) => {
const active = tab.match(pathname);
Expand Down
6 changes: 3 additions & 3 deletions src/components/listing-detail/desktop-listing-detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export function DesktopListingDetail({
.map((s) => s.name)}
pendingAction={pendingAction}
/>
<div className="flex min-w-0 flex-1 gap-6 px-10 pt-6 pb-8">
<div className="flex min-w-0 flex-1 gap-6 px-6 pt-6 pb-8 lg:px-10">
<MediaColumn data={data} />
<InfoColumn data={data} onEditAddress={onEditAddress} />
</div>
Expand Down Expand Up @@ -120,7 +120,7 @@ function TopBar({
const title = shortAddressTitle(headline.addressRaw);
const origin = resolveFromOrigin(from);
return (
<header className="sticky top-0 z-20 flex items-center justify-between gap-4 border-bone border-b bg-ground/85 px-10 py-4 backdrop-blur">
<header className="sticky top-0 z-20 flex items-center justify-between gap-4 border-bone border-b bg-ground/85 px-6 py-4 backdrop-blur lg:px-10">
<div className="flex min-w-0 items-center gap-3.5">
<button
aria-label="Back"
Expand Down Expand Up @@ -286,7 +286,7 @@ function MediaColumn({ data }: { data: ListingDetailPayload }) {
const scrollNext = useCallback(() => emblaApi?.scrollNext(), [emblaApi]);

return (
<section className="flex w-[720px] shrink-0 flex-col gap-3.5">
<section className="flex min-w-0 flex-[2] flex-col gap-3.5">
<HeroPhoto
activeIndex={activeIndex}
alt={headline.addressRaw}
Expand Down
8 changes: 7 additions & 1 deletion src/components/listing-detail/detail-cta.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,13 @@ export function DetailCta({
const iKept = isKept(mySwipe);

return (
<div className="fixed right-0 bottom-0 left-0 z-30 mx-auto flex max-w-md items-center gap-2.5 border-border border-t bg-background/95 px-5 pt-3.5 pb-7 backdrop-blur">
<div
// `pb` uses `max(...)` so devices without a home-indicator (Android,
// older iPhones, desktop browsers) keep the original 28-px gap,
// while notched iPhones receive `safe-area-inset-bottom + 8 px` so
// the CTA never sits under the home indicator.
className="fixed right-0 bottom-0 left-0 z-30 mx-auto flex max-w-md items-center gap-2.5 border-border border-t bg-background/95 px-5 pt-3.5 pb-[max(1.75rem,calc(env(safe-area-inset-bottom)+0.5rem))] backdrop-blur"
>
<Button
aria-busy={pendingAction === "skip" || undefined}
aria-label="Skip"
Expand Down
8 changes: 4 additions & 4 deletions src/components/review/desktop-review.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ export function DesktopReview({
return (
<AdminSidebar mode="desktop-only">
<DesktopReviewHeader data={data} onSelectSearch={onSelectSearch} />
<div className="flex min-h-0 flex-1 gap-5 px-10 pb-8">
<div className="flex min-h-0 flex-1 gap-5 px-6 pb-8 lg:px-10">
<QueueRail
items={data.queue.items}
onSelectCluster={onSelectCluster}
Expand Down Expand Up @@ -203,7 +203,7 @@ function DesktopReviewHeader({
: null;
const pillLabel = selectedSearch ? selectedSearch.name : "All searches";
return (
<header className="flex items-end justify-between px-10 pt-9 pb-6">
<header className="flex items-end justify-between gap-4 px-6 pt-9 pb-6 lg:px-10">
<div className="flex flex-col gap-2.5">
<div className="flex flex-col gap-1">
<Eyebrow>Your queue</Eyebrow>
Expand Down Expand Up @@ -312,7 +312,7 @@ function QueueRail({
}, [selectedClusterId]);

return (
<aside className="flex min-h-0 w-[260px] shrink-0 flex-col">
<aside className="flex min-h-0 w-56 shrink-0 flex-col xl:w-[260px]">
<div className="flex min-h-0 flex-1 flex-col overflow-hidden rounded-2xl border border-border bg-card">
<div className="flex shrink-0 items-baseline justify-between border-bone border-b px-4 py-3">
<Eyebrow>Up next</Eyebrow>
Expand Down Expand Up @@ -451,7 +451,7 @@ function HeroColumn({
pendingAction?: DesktopReviewPendingAction;
}) {
return (
<section className="flex min-h-0 w-[540px] flex-1 shrink-0 flex-col gap-3.5">
<section className="flex min-h-0 min-w-0 flex-1 flex-col gap-3.5">
<article className="flex min-h-0 flex-1 flex-col overflow-hidden rounded-2xl border border-border bg-card">
<HeroPhoto
onLightboxOpenChange={onLightboxOpenChange}
Expand Down
2 changes: 1 addition & 1 deletion src/components/search-form/cost-estimate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ type Props = {

export function CostEstimate({ ctaLabel, disabled, pending, onSubmit }: Props) {
return (
<div className="sticky right-0 bottom-0 left-0 z-10 mt-8 flex items-center justify-end border-border border-t bg-card px-10 py-3">
<div className="sticky right-0 bottom-0 left-0 z-10 mt-8 flex items-center justify-end border-border border-t bg-card px-6 py-3 lg:px-10">
<Button
className="rounded-full px-6"
disabled={disabled}
Expand Down
2 changes: 1 addition & 1 deletion src/components/search-form/desktop-search-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ function Breadcrumb({
pauseAction?: ActionState;
}) {
return (
<header className="sticky top-0 z-30 flex items-center justify-between border-bone border-b bg-ground/85 px-10 py-5 backdrop-blur">
<header className="sticky top-0 z-30 flex items-center justify-between gap-4 border-bone border-b bg-ground/85 px-6 py-5 backdrop-blur lg:px-10">
<div className="flex items-center gap-3.5">
<button
aria-label="Close"
Expand Down
2 changes: 1 addition & 1 deletion src/components/search-form/search-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,7 @@ export function SearchForm({
form.handleSubmit();
}}
>
<div className="flex-1 space-y-10 px-10 pt-7 pb-8">
<div className="flex-1 space-y-10 px-6 pt-7 pb-8 lg:px-10">
{/* Headline + Reset, full width */}
<div className="flex items-start justify-between gap-6">
<div className="flex-1">{headlineSection}</div>
Expand Down
4 changes: 2 additions & 2 deletions src/components/searches/desktop-searches.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export function DesktopSearches({
<AdminSidebar mode="desktop-only">
<PageHeader />
<MetricStrip totals={portfolio.totals} />
<div className="flex min-w-0 flex-1 flex-wrap content-start gap-4 px-10 py-6">
<div className="flex min-w-0 flex-1 flex-wrap content-start gap-4 px-6 py-6 lg:px-10">
{searches.length === 0 ? (
<EmptyState />
) : (
Expand All @@ -75,7 +75,7 @@ export function DesktopSearches({

function PageHeader() {
return (
<header className="flex items-end justify-between px-10 pt-9 pb-4">
<header className="flex items-end justify-between gap-4 px-6 pt-9 pb-4 lg:px-10">
<div className="flex flex-col gap-1">
<Eyebrow>Your watch list</Eyebrow>
<h1 className="font-serif text-[40px] text-foreground leading-[44px] tracking-tight">
Expand Down
6 changes: 3 additions & 3 deletions src/components/shortlist/desktop-shortlist.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,15 +84,15 @@ export function DesktopShortlist({
sortKey={sortKey}
/>
{tabs.length > 0 ? (
<div className="border-bone border-b px-10 pb-4">
<div className="border-bone border-b px-6 pb-4 lg:px-10">
<ShortlistTabs
activeId={activeTab}
onChange={onTabChange}
tabs={tabs}
/>
</div>
) : null}
<div className="flex min-w-0 flex-1 flex-col gap-5 px-10 py-7">
<div className="flex min-w-0 flex-1 flex-col gap-5 px-6 py-7 lg:px-10">
{bodySlot ? (
bodySlot
) : (
Expand Down Expand Up @@ -133,7 +133,7 @@ function Header({
showSort?: boolean;
}) {
return (
<header className="flex items-end justify-between px-10 pt-9 pb-4">
<header className="flex items-end justify-between gap-4 px-6 pt-9 pb-4 lg:px-10">
<div className="flex flex-col gap-1">
{partnerLabel ? (
<Eyebrow>You & {partnerLabel}</Eyebrow>
Expand Down
8 changes: 7 additions & 1 deletion src/hooks/use-mobile.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import * as React from "react";

const MOBILE_BREAKPOINT = 768;
// Tracks the same breakpoint as the desktop/mobile shell boundary in the
// route shells (`lg:hidden` for mobile content, `hidden lg:contents` for the
// `AdminSidebar` desktop chrome). Below this width we render the
// `max-w-md` mobile layout — including on iPad portrait (768–1023 px) —
// because the desktop three-column layouts assume ≥1024 px and break
// otherwise. Bump in lockstep with the Tailwind `lg:` switches.
const MOBILE_BREAKPOINT = 1024;

export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
Expand Down
2 changes: 1 addition & 1 deletion src/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -455,7 +455,7 @@ function ReviewPage() {
navigate,
})}

<div className="mx-auto min-h-screen max-w-md bg-background pb-24 md:hidden">
<div className="mx-auto min-h-screen max-w-md bg-background pb-24 lg:hidden">
<ReviewHeader
leftToday={card?.leftToday ?? 0}
searchPill={card?.searchPill}
Expand Down
4 changes: 2 additions & 2 deletions src/routes/listings/$clusterId.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ function ListingDetailPage() {
pendingAction={pendingAction}
/>

<div className="mx-auto min-h-screen max-w-md bg-background pb-32 md:hidden">
<div className="mx-auto min-h-screen max-w-md bg-background pb-32 lg:hidden">
{/* Top bar */}
<header className="flex items-center justify-between px-4 pt-2 pb-3.5">
<button
Expand Down Expand Up @@ -509,7 +509,7 @@ function ListingDetailSkeleton() {
</section>
</div>
</AdminSidebar>
<div className="mx-auto min-h-screen max-w-md bg-background pb-32 md:hidden">
<div className="mx-auto min-h-screen max-w-md bg-background pb-32 lg:hidden">
<header className="flex items-center justify-between px-4 pt-2 pb-3.5">
<Skeleton className="size-9 rounded-full" />
<div className="flex gap-2.5">
Expand Down
2 changes: 1 addition & 1 deletion src/routes/searches/$id.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ function EditSearchPage() {
pauseAction={pauseAction}
pending={pending}
/>
<div className="md:hidden">
<div className="lg:hidden">
<SearchForm
initial={initial}
mode="edit"
Expand Down
14 changes: 8 additions & 6 deletions src/routes/searches/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ function SearchesIndexPage() {
searches={data}
/>

<div className="mx-auto min-h-screen max-w-md bg-background pb-28 md:hidden">
<div className="mx-auto min-h-screen max-w-md bg-background pb-28 lg:hidden">
<header className="flex flex-col gap-1 px-6 pt-6 pb-5">
<h1 className="font-medium font-serif text-[32px] text-foreground leading-[110%] tracking-[-0.03em]">
Searches
Expand Down Expand Up @@ -164,14 +164,16 @@ function SearchList({
params={{ id: s.id }}
to="/searches/$id"
>
<div className="flex items-baseline justify-between">
<h2 className="font-serif text-foreground text-xl">{s.name}</h2>
<div className="flex flex-wrap items-baseline justify-between gap-x-3 gap-y-1">
<h2 className="min-w-0 flex-1 truncate font-serif text-foreground text-xl">
{s.name}
</h2>
{s.active ? (
<span className="rounded-full bg-primary/15 px-2 py-0.5 text-[10px] text-primary uppercase tracking-wide">
<span className="shrink-0 whitespace-nowrap rounded-full bg-primary/15 px-2 py-0.5 text-[10px] text-primary uppercase tracking-wide">
{cadence ? `Active · ${cadence}` : "Active"}
</span>
) : (
<span className="rounded-full bg-muted-foreground/15 px-2 py-0.5 text-[10px] text-muted-foreground uppercase tracking-wide">
<span className="shrink-0 whitespace-nowrap rounded-full bg-muted-foreground/15 px-2 py-0.5 text-[10px] text-muted-foreground uppercase tracking-wide">
Paused
</span>
)}
Expand All @@ -185,7 +187,7 @@ function SearchList({
{s.portals.join(", ")}
</p>
{stats ? (
<div className="mt-3 flex items-center gap-4 border-border border-t pt-3 text-[11px] text-muted-foreground">
<div className="mt-3 flex flex-wrap items-center gap-x-4 gap-y-1 border-border border-t pt-3 text-[11px] text-muted-foreground">
<span>
<span className="font-semibold text-foreground">
{stats.listingsThisWeek}
Expand Down
2 changes: 1 addition & 1 deletion src/routes/searches/new.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ function NewSearchPage() {
onSubmit={(v) => create.mutate(v)}
pending={create.isPending}
/>
<div className="md:hidden">
<div className="lg:hidden">
<SearchForm
mode="create"
onCancel={() => navigate({ to: "/searches" })}
Expand Down
2 changes: 1 addition & 1 deletion src/routes/settings/household.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ function HouseholdSettingsPage() {
/>
</AdminSidebar>

<div className="mx-auto min-h-screen max-w-md bg-background pb-28 md:hidden">
<div className="mx-auto min-h-screen max-w-md bg-background pb-28 lg:hidden">
<header className="flex flex-col gap-1 px-6 pt-6 pb-5">
<h1 className="font-medium font-serif text-[32px] text-foreground leading-[110%] tracking-[-0.03em]">
Household
Expand Down
2 changes: 1 addition & 1 deletion src/routes/shortlist.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ function ShortlistPage() {
tabs={tabs}
/>

<div className="mx-auto min-h-screen max-w-md bg-background pb-24 md:hidden">
<div className="mx-auto min-h-screen max-w-md bg-background pb-24 lg:hidden">
{toast ? (
<div
aria-live="polite"
Expand Down
Loading