diff --git a/app/globals.css b/app/globals.css index 58ca848..20e10e0 100644 --- a/app/globals.css +++ b/app/globals.css @@ -4,6 +4,8 @@ @layer base { :root { + --app-topbar-height: 3.5rem; + --app-nav-offset: var(--bottom-nav-height); --bottom-nav-height: 6rem; --background: 252 38% 10%; --foreground: 52 100% 88%; @@ -168,7 +170,11 @@ @layer utilities { .pb-nav { - padding-bottom: var(--bottom-nav-height); + padding-bottom: var(--app-nav-offset); + } + + .pt-topbar { + padding-top: calc(var(--app-topbar-height) + env(safe-area-inset-top, 0px)); } .image-pixelated { diff --git a/components/avatar/avatar-customize-layout.tsx b/components/avatar/avatar-customize-layout.tsx index 1c067cc..c7bbf3a 100644 --- a/components/avatar/avatar-customize-layout.tsx +++ b/components/avatar/avatar-customize-layout.tsx @@ -1,4 +1,5 @@ import { AppPageHeader } from "@/components/layout/app-page-header"; +import { AppTopBar } from "@/components/layout/app-top-bar"; type AvatarCustomizeLayoutProps = { eyebrow?: string; @@ -14,14 +15,17 @@ export function AvatarCustomizeLayout({ children, }: AvatarCustomizeLayoutProps) { return ( -
- +
+ -
{children}
+
+ + {children} +
); } diff --git a/components/layout/app-nav.tsx b/components/layout/app-nav.tsx index e651378..a1ab4f0 100644 --- a/components/layout/app-nav.tsx +++ b/components/layout/app-nav.tsx @@ -2,6 +2,7 @@ import Link from "next/link"; import { usePathname } from "next/navigation"; +import { forwardRef } from "react"; import { Home, ListPlus, @@ -36,7 +37,10 @@ type AppNavProps = { items?: readonly NavItem[]; }; -export function AppNav({ items = appNavItems }: AppNavProps) { +export const AppNav = forwardRef(function AppNav( + { items = appNavItems }, + ref, +) { const pathname = usePathname(); const columnCount = Math.min( items.length, @@ -45,8 +49,8 @@ export function AppNav({ items = appNavItems }: AppNavProps) { return ( ); -} +}); diff --git a/components/layout/app-page-header.tsx b/components/layout/app-page-header.tsx index 6243919..2dac165 100644 --- a/components/layout/app-page-header.tsx +++ b/components/layout/app-page-header.tsx @@ -1,42 +1,27 @@ -import { HomeLogoLink } from "@/components/layout/home-logo-link"; -import { cn } from "@/lib/utils"; - type AppPageHeaderProps = { title: string; description?: string; eyebrow?: string; - wide?: boolean; }; export function AppPageHeader({ title, description, eyebrow, - wide = false, }: AppPageHeaderProps) { return ( -
-
-
- {eyebrow ? ( -

- {eyebrow} -

- ) : null} -

{title}

- {description ? ( -

- {description} -

- ) : null} -
- -
+
+ {eyebrow ? ( +

+ {eyebrow} +

+ ) : null} +

{title}

+ {description ? ( +

+ {description} +

+ ) : null}
); } diff --git a/components/layout/app-shell.tsx b/components/layout/app-shell.tsx index 110e5c1..9a39cff 100644 --- a/components/layout/app-shell.tsx +++ b/components/layout/app-shell.tsx @@ -1,4 +1,5 @@ import { AppPageHeader } from "@/components/layout/app-page-header"; +import { AppTopBar } from "@/components/layout/app-top-bar"; import { cn } from "@/lib/utils"; type AppShellProps = { @@ -8,6 +9,11 @@ type AppShellProps = { wide?: boolean; }; +const contentWidthClass = { + default: "max-w-lg md:max-w-2xl lg:max-w-4xl", + wide: "max-w-lg md:max-w-3xl lg:max-w-6xl", +} as const; + export function AppShell({ children, title, @@ -15,16 +21,17 @@ export function AppShell({ wide = false, }: AppShellProps) { return ( -
- {title ? ( - - ) : null} +
+
+ {title ? ( + + ) : null} {children}
diff --git a/components/layout/app-top-bar.tsx b/components/layout/app-top-bar.tsx new file mode 100644 index 0000000..edf0bce --- /dev/null +++ b/components/layout/app-top-bar.tsx @@ -0,0 +1,36 @@ +"use client"; + +import { usePathname } from "next/navigation"; + +import { HomeLogoLink } from "@/components/layout/home-logo-link"; +import { routes } from "@/lib/routes"; +import { cn } from "@/lib/utils"; + +type AppTopBarProps = { + className?: string; +}; + +export function AppTopBar({ className }: AppTopBarProps) { + const pathname = usePathname(); + const isHome = pathname === routes.avatar; + + return ( +
+
+ +
+
+ ); +} diff --git a/components/layout/global-bottom-nav.tsx b/components/layout/global-bottom-nav.tsx index 2b90b15..f62cb80 100644 --- a/components/layout/global-bottom-nav.tsx +++ b/components/layout/global-bottom-nav.tsx @@ -3,10 +3,20 @@ import { useEffect, useState } from "react"; import { AppNav } from "@/components/layout/app-nav"; +import { + NavOffsetProvider, + useNavOffsetRef, +} from "@/components/layout/nav-offset-provider"; import { HABIT_PET_DATA_UPDATED_EVENT } from "@/lib/app-events"; import { getOnboardingStatusClient } from "@/lib/onboarding-status"; import { createClient } from "@/lib/supabase/client"; +function MeasuredAppNav() { + const navRef = useNavOffsetRef(); + + return ; +} + export function GlobalBottomNav() { const [showNav, setShowNav] = useState(false); @@ -43,9 +53,9 @@ export function GlobalBottomNav() { }; }, []); - if (!showNav) { - return null; - } - - return ; + return ( + + {showNav ? : null} + + ); } diff --git a/components/layout/home-logo-link.tsx b/components/layout/home-logo-link.tsx index 38ddf28..30720ea 100644 --- a/components/layout/home-logo-link.tsx +++ b/components/layout/home-logo-link.tsx @@ -5,13 +5,18 @@ import { cn } from "@/lib/utils"; type HomeLogoLinkProps = { className?: string; + "aria-current"?: "page" | undefined; }; -export function HomeLogoLink({ className }: HomeLogoLinkProps) { +export function HomeLogoLink({ + className, + "aria-current": ariaCurrent, +}: HomeLogoLinkProps) { return ( void; +}; + +const NavOffsetContext = createContext(null); + +export function useNavOffsetRef() { + const context = useContext(NavOffsetContext); + + if (!context) { + throw new Error("useNavOffsetRef must be used within NavOffsetProvider."); + } + + const setNavElement = context.setNavElement; + + return useCallback( + (node: T | null) => { + setNavElement(node); + }, + [setNavElement], + ) as Ref; +} + +type NavOffsetProviderProps = { + children: ReactNode; + enabled?: boolean; +}; + +export function NavOffsetProvider({ + children, + enabled = true, +}: NavOffsetProviderProps) { + const navElementRef = useRef(null); + const observerRef = useRef(null); + + const syncOffset = useCallback(() => { + const root = document.documentElement; + + if (!enabled || !navElementRef.current) { + root.style.setProperty("--app-nav-offset", "0px"); + return; + } + + root.style.setProperty( + "--app-nav-offset", + `${navElementRef.current.offsetHeight}px`, + ); + }, [enabled]); + + const setNavElement = useCallback( + (element: HTMLElement | null) => { + if (observerRef.current) { + observerRef.current.disconnect(); + observerRef.current = null; + } + + navElementRef.current = element; + + if (!element || !enabled) { + syncOffset(); + return; + } + + const observer = new ResizeObserver(() => { + syncOffset(); + }); + + observer.observe(element); + observerRef.current = observer; + syncOffset(); + }, + [enabled, syncOffset], + ); + + useEffect(() => { + syncOffset(); + window.addEventListener("resize", syncOffset); + + return () => { + window.removeEventListener("resize", syncOffset); + if (observerRef.current) { + observerRef.current.disconnect(); + } + document.documentElement.style.setProperty("--app-nav-offset", "0px"); + }; + }, [syncOffset]); + + useEffect(() => { + if (!enabled) { + setNavElement(null); + } + }, [enabled, setNavElement]); + + return ( + + {children} + + ); +} diff --git a/components/reminders/in-app-reminder-banner.tsx b/components/reminders/in-app-reminder-banner.tsx index 08cbbfa..969626e 100644 --- a/components/reminders/in-app-reminder-banner.tsx +++ b/components/reminders/in-app-reminder-banner.tsx @@ -36,7 +36,7 @@ export function InAppReminderBanner() { role="status" aria-live="polite" className="fixed inset-x-0 z-[60] px-4 pb-[env(safe-area-inset-bottom,0px)]" - style={{ bottom: "calc(var(--bottom-nav-height) + 0.5rem)" }} + style={{ bottom: "calc(var(--app-nav-offset, var(--bottom-nav-height)) + 0.5rem)" }} >
diff --git a/components/shop/shop-content.tsx b/components/shop/shop-content.tsx index 0f8c039..bf7f33e 100644 --- a/components/shop/shop-content.tsx +++ b/components/shop/shop-content.tsx @@ -202,7 +202,7 @@ export function ShopContent() { const roomItems = items.filter((item) => item.type === "room"); const itemGridClassName = - "grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3"; + "grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3"; return ( <> diff --git a/components/ui/toast-provider.tsx b/components/ui/toast-provider.tsx index c88e297..e65b2cb 100644 --- a/components/ui/toast-provider.tsx +++ b/components/ui/toast-provider.tsx @@ -45,8 +45,8 @@ export function ToastProvider({ children }: { children: ReactNode }) { {children}
{messages.map((message) => (