@thorprovider/blocks 0.1.3 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +155 -0
- package/README.md +228 -68
- package/docs/standards/data-contracts.md +13 -2
- package/package.json +26 -10
- package/scripts/fix-esm-extensions.mjs +40 -0
- package/src/blocks/application/login-forms/login-page.test.tsx +190 -0
- package/src/blocks/application/login-forms/login-page.tsx +240 -0
- package/src/blocks/application/password-recovery/forgot-password-page-1.tsx +6 -8
- package/src/blocks/application/signup-forms/signup-page-1.tsx +127 -129
- package/src/blocks/application/verification/verify-1.tsx +2 -2
- package/src/blocks/application/viewport-ownership.test.tsx +41 -0
- package/src/blocks/e-commerce/category-filters/category-listing-filter-fields.tsx +218 -0
- package/src/blocks/e-commerce/category-filters/{category-filter-2.test.tsx → category-listing.test.tsx} +287 -199
- package/src/blocks/e-commerce/category-filters/category-listing.tsx +934 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-contact-step.tsx +149 -0
- package/src/blocks/e-commerce/checkout-forms/{checkout-form-1-country-field.tsx → checkout-country-field.tsx} +10 -8
- package/src/blocks/e-commerce/checkout-forms/checkout-order-summary.tsx +230 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-payment-step.tsx +100 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-progress.tsx +62 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-review-step.tsx +59 -0
- package/src/blocks/e-commerce/checkout-forms/{checkout-form-1-shipping-step.tsx → checkout-shipping-step.tsx} +65 -41
- package/src/blocks/e-commerce/checkout-forms/checkout.test.tsx +478 -0
- package/src/blocks/e-commerce/checkout-forms/checkout.tsx +550 -0
- package/src/blocks/e-commerce/navigation/storefront-navbar-account-menu.test.tsx +56 -0
- package/src/blocks/{marketing/navbars/navbar-4-account-menu.tsx → e-commerce/navigation/storefront-navbar-account-menu.tsx} +3 -3
- package/src/blocks/{marketing/navbars/navbar-4-cart-sheet.test.tsx → e-commerce/navigation/storefront-navbar-cart-sheet.test.tsx} +22 -22
- package/src/blocks/{marketing/navbars/navbar-4-cart-sheet.tsx → e-commerce/navigation/storefront-navbar-cart-sheet.tsx} +3 -3
- package/src/blocks/{marketing/navbars/navbar-4-mobile-nav.tsx → e-commerce/navigation/storefront-navbar-mobile-nav.tsx} +4 -12
- package/src/blocks/{marketing/navbars/navbar-4-search-bar.tsx → e-commerce/navigation/storefront-navbar-search-bar.tsx} +3 -3
- package/src/blocks/{marketing/navbars/navbar-4.test.tsx → e-commerce/navigation/storefront-navbar.test.tsx} +67 -36
- package/src/blocks/{marketing/navbars/navbar-4.tsx → e-commerce/navigation/storefront-navbar.tsx} +43 -25
- package/src/blocks/e-commerce/product-categories/category-card-grid.test.tsx +122 -0
- package/src/blocks/e-commerce/product-categories/category-card.test.tsx +68 -0
- package/src/blocks/e-commerce/product-categories/category-card.tsx +172 -0
- package/src/blocks/e-commerce/product-categories/category-grid.test.tsx +67 -0
- package/src/blocks/e-commerce/product-categories/category-shared.tsx +109 -0
- package/src/blocks/e-commerce/product-categories/product-category-grid.tsx +170 -0
- package/src/blocks/e-commerce/product-categories/product-category.test.tsx +266 -0
- package/src/blocks/e-commerce/product-categories/product-category.tsx +462 -0
- package/src/blocks/e-commerce/product-list/product-card.test.tsx +263 -0
- package/src/blocks/e-commerce/product-list/product-card.tsx +447 -0
- package/src/blocks/e-commerce/product-list/product-grid.test.tsx +162 -0
- package/src/blocks/e-commerce/product-list/product-grid.tsx +114 -0
- package/src/blocks/e-commerce/product-overview/product-overview-gallery.tsx +71 -0
- package/src/blocks/e-commerce/product-overview/{product-overview-5-lightbox-tab.tsx → product-overview-lightbox-tab.tsx} +35 -23
- package/src/blocks/e-commerce/product-overview/{product-overview-5-reviews-tab.tsx → product-overview-reviews-tab.tsx} +40 -17
- package/src/blocks/e-commerce/product-overview/{product-overview-5-tabs.tsx → product-overview-tabs.tsx} +8 -7
- package/src/blocks/e-commerce/product-overview/product-overview.test.tsx +283 -0
- package/src/blocks/e-commerce/product-overview/product-overview.tsx +1186 -0
- package/src/blocks/e-commerce/reviews/review-rating-2.tsx +5 -6
- package/src/blocks/e-commerce/shopping-carts/shopping-cart.test.tsx +399 -0
- package/src/blocks/e-commerce/shopping-carts/{shopping-cart-4.tsx → shopping-cart.tsx} +119 -85
- package/src/blocks/e-commerce/storefront-hero/storefront-hero.test.tsx +412 -0
- package/src/blocks/e-commerce/storefront-hero/storefront-hero.tsx +1283 -0
- package/src/blocks/marketing/features/feature.test.tsx +153 -0
- package/src/blocks/marketing/features/feature.tsx +195 -0
- package/src/blocks/marketing/footers/footer-section-accessibility.test.tsx +118 -0
- package/src/blocks/marketing/footers/footer-section-actions.test.tsx +131 -0
- package/src/blocks/marketing/footers/footer-section-data.test.tsx +127 -0
- package/src/blocks/marketing/footers/footer-section-native-links.test.tsx +100 -0
- package/src/blocks/marketing/footers/footer-section-public-api.test.tsx +11 -0
- package/src/blocks/marketing/footers/footer-section.test.tsx +46 -0
- package/src/blocks/marketing/footers/footer-section.tsx +532 -0
- package/src/blocks/marketing/navbars/navbar-public-api.test.tsx +57 -0
- package/src/blocks/marketing/navbars/navbar.mega.test.tsx +149 -0
- package/src/blocks/marketing/navbars/navbar.test.tsx +155 -0
- package/src/blocks/marketing/navbars/navbar.tsx +735 -0
- package/src/blocks/marketing/testimonials/testimonial-section.test.tsx +171 -0
- package/src/blocks/marketing/testimonials/testimonial-section.tsx +237 -0
- package/src/index.ts +156 -128
- package/src/lib/contracts-curated.test.ts +47 -29
- package/src/lib/contracts.test.ts +198 -2
- package/src/lib/contracts.ts +410 -16
- package/src/ui/button-group.tsx +1 -1
- package/src/ui/carousel.tsx +1 -1
- package/src/ui/dialog.tsx +1 -1
- package/src/ui/field.tsx +2 -2
- package/src/ui/input-group.tsx +3 -3
- package/src/ui/pagination.tsx +1 -1
- package/src/ui/sidebar.tsx +7 -7
- package/src/ui/toggle-group.tsx +1 -1
- package/tsconfig.build.json +12 -0
- package/tsup.config.ts +11 -7
- package/.turbo/turbo-build.log +0 -24
- package/.turbo/turbo-test.log +0 -47
- package/.turbo/turbo-type-check.log +0 -0
- package/dist/.tsbuildinfo +0 -1
- package/dist/contracts.d.mts +0 -1732
- package/dist/contracts.d.ts +0 -1732
- package/dist/contracts.js +0 -423
- package/dist/contracts.js.map +0 -1
- package/dist/contracts.mjs +0 -365
- package/dist/contracts.mjs.map +0 -1
- package/dist/index.d.mts +0 -3068
- package/dist/index.d.ts +0 -3068
- package/dist/index.js +0 -17834
- package/dist/index.js.map +0 -1
- package/dist/index.mjs +0 -17740
- package/dist/index.mjs.map +0 -1
- package/src/blocks/application/login-forms/login-page-1.tsx +0 -246
- package/src/blocks/application/login-forms/login-page-2.tsx +0 -207
- package/src/blocks/e-commerce/category-filters/category-filter-1.tsx +0 -260
- package/src/blocks/e-commerce/category-filters/category-filter-2-filter-panel.tsx +0 -222
- package/src/blocks/e-commerce/category-filters/category-filter-2.tsx +0 -921
- package/src/blocks/e-commerce/category-filters/category-filter-4-chip-rail.tsx +0 -57
- package/src/blocks/e-commerce/category-filters/category-filter-4-drawer.tsx +0 -124
- package/src/blocks/e-commerce/category-filters/category-filter-4-fields.tsx +0 -170
- package/src/blocks/e-commerce/category-filters/category-filter-4-sidebar.tsx +0 -52
- package/src/blocks/e-commerce/category-filters/category-filter-4-sort-sheet.tsx +0 -57
- package/src/blocks/e-commerce/category-filters/category-filter-4-state.ts +0 -68
- package/src/blocks/e-commerce/category-filters/category-filter-4.tsx +0 -298
- package/src/blocks/e-commerce/checkout-forms/card-formatting.test.ts +0 -47
- package/src/blocks/e-commerce/checkout-forms/card-formatting.ts +0 -9
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-contact-step.tsx +0 -96
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-order-summary.tsx +0 -252
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-payment-step.tsx +0 -188
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-progress.tsx +0 -36
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1.test.tsx +0 -216
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1.tsx +0 -338
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2-order-summary.tsx +0 -130
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2-payment-card.tsx +0 -175
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2-shipping-card.tsx +0 -190
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2.tsx +0 -312
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-information-step.tsx +0 -91
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-order-summary.tsx +0 -110
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-payment-step.tsx +0 -137
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-progress.tsx +0 -68
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-review-step.tsx +0 -79
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-shipping-step.tsx +0 -172
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3.tsx +0 -392
- package/src/blocks/e-commerce/product-categories/avatar-group.tsx +0 -86
- package/src/blocks/e-commerce/product-categories/product-category-1.tsx +0 -126
- package/src/blocks/e-commerce/product-categories/product-category-2.test.tsx +0 -51
- package/src/blocks/e-commerce/product-categories/product-category-2.tsx +0 -120
- package/src/blocks/e-commerce/product-categories/product-category-4.tsx +0 -129
- package/src/blocks/e-commerce/product-categories/product-category-5.tsx +0 -218
- package/src/blocks/e-commerce/product-list/product-card-1.test.tsx +0 -88
- package/src/blocks/e-commerce/product-list/product-card-1.tsx +0 -192
- package/src/blocks/e-commerce/product-list/product-card-2.tsx +0 -71
- package/src/blocks/e-commerce/product-list/product-card-3-reaction-toggle.tsx +0 -166
- package/src/blocks/e-commerce/product-list/product-card-3.tsx +0 -215
- package/src/blocks/e-commerce/product-list/product-card-5-checkbox-check.tsx +0 -98
- package/src/blocks/e-commerce/product-list/product-card-5-reaction-toggle.tsx +0 -171
- package/src/blocks/e-commerce/product-list/product-card-5.tsx +0 -367
- package/src/blocks/e-commerce/product-overview/product-overview-1.test.tsx +0 -182
- package/src/blocks/e-commerce/product-overview/product-overview-1.tsx +0 -370
- package/src/blocks/e-commerce/product-overview/product-overview-2.tsx +0 -272
- package/src/blocks/e-commerce/product-overview/product-overview-5-gallery.tsx +0 -59
- package/src/blocks/e-commerce/product-overview/product-overview-5.tsx +0 -274
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-1-spinning-counter.tsx +0 -142
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-1.tsx +0 -291
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-item-row.tsx +0 -110
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-recommendations.tsx +0 -51
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-spinning-counter.tsx +0 -146
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2.tsx +0 -295
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-3-spinning-counter.tsx +0 -142
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-3.tsx +0 -342
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-4-spinning-counter.tsx +0 -146
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.test.tsx +0 -229
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.test.tsx +0 -72
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.tsx +0 -263
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-2.tsx +0 -283
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-5.tsx +0 -318
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-6.tsx +0 -205
- package/src/blocks/marketing/features/feature-1.tsx +0 -97
- package/src/blocks/marketing/features/feature-2.tsx +0 -82
- package/src/blocks/marketing/footers/footer-section-1.test.tsx +0 -117
- package/src/blocks/marketing/footers/footer-section-1.tsx +0 -178
- package/src/blocks/marketing/footers/footer-section-3.tsx +0 -192
- package/src/blocks/marketing/footers/footer-section-4.tsx +0 -355
- package/src/blocks/marketing/navbars/navbar-1.tsx +0 -267
- package/src/blocks/marketing/navbars/navbar-2.tsx +0 -351
- package/src/blocks/marketing/navbars/navbar-3.tsx +0 -303
- package/src/blocks/marketing/navbars/navbar-4-account-menu.test.tsx +0 -38
- package/src/blocks/marketing/navbars/navbar-4-like-button.tsx +0 -177
- package/src/blocks/marketing/navbars/navbar-4-product-grid.tsx +0 -219
- package/src/blocks/marketing/testimonials/testimonial-1.tsx +0 -141
- package/src/blocks/marketing/testimonials/testimonial-2.tsx +0 -108
|
@@ -0,0 +1,735 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState, type ComponentProps, type ComponentType } from 'react'
|
|
4
|
+
import { ArrowRight, Menu } from 'lucide-react'
|
|
5
|
+
|
|
6
|
+
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '../../../ui/accordion'
|
|
7
|
+
import { Button } from '../../../ui/button'
|
|
8
|
+
import {
|
|
9
|
+
NavigationMenu,
|
|
10
|
+
NavigationMenuContent,
|
|
11
|
+
NavigationMenuItem,
|
|
12
|
+
NavigationMenuLink,
|
|
13
|
+
NavigationMenuList,
|
|
14
|
+
NavigationMenuTrigger,
|
|
15
|
+
navigationMenuTriggerStyle,
|
|
16
|
+
} from '../../../ui/navigation-menu'
|
|
17
|
+
import { Separator } from '../../../ui/separator'
|
|
18
|
+
import {
|
|
19
|
+
Sheet,
|
|
20
|
+
SheetClose,
|
|
21
|
+
SheetContent,
|
|
22
|
+
SheetHeader,
|
|
23
|
+
SheetTitle,
|
|
24
|
+
SheetTrigger,
|
|
25
|
+
} from '../../../ui/sheet'
|
|
26
|
+
import { cn } from '../../../lib/utils'
|
|
27
|
+
import type { CTA } from '../../../lib/types'
|
|
28
|
+
|
|
29
|
+
/** Structural presentations of the page-level header. */
|
|
30
|
+
export type NavbarPresentation = 'bar' | 'mega' | 'floating'
|
|
31
|
+
|
|
32
|
+
/** Icon component bound by the host at render time. */
|
|
33
|
+
export type NavbarIcon = ComponentType<{ className?: string }>
|
|
34
|
+
|
|
35
|
+
/** Maps a serializable icon key to a host-bound icon component. */
|
|
36
|
+
export type NavbarIconMap = Record<string, NavbarIcon>
|
|
37
|
+
|
|
38
|
+
export interface NavbarItem {
|
|
39
|
+
label: string
|
|
40
|
+
href: string
|
|
41
|
+
description?: string
|
|
42
|
+
/** Key resolved through the consumer's `iconMap`. Omit for entries without an icon. */
|
|
43
|
+
icon?: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface NavbarGroup {
|
|
47
|
+
/** Optional column heading. */
|
|
48
|
+
title?: string
|
|
49
|
+
items: NavbarItem[]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface NavbarLink {
|
|
53
|
+
label: string
|
|
54
|
+
/**
|
|
55
|
+
* Destination for a plain link. Ignored when `groups` is present, because a grouped
|
|
56
|
+
* entry opens a flyout instead of navigating.
|
|
57
|
+
*/
|
|
58
|
+
href?: string
|
|
59
|
+
/** Grouped entries rendered as a multi-column flyout (and an accordion on mobile). */
|
|
60
|
+
groups?: NavbarGroup[]
|
|
61
|
+
/** Renders the link with extra prominence in the flat presentations. */
|
|
62
|
+
emphasis?: boolean
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface NavbarFeatured {
|
|
66
|
+
eyebrow: string
|
|
67
|
+
title: string
|
|
68
|
+
description?: string
|
|
69
|
+
href: string
|
|
70
|
+
linkLabel?: string
|
|
71
|
+
image?: string
|
|
72
|
+
imageAlt?: string
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface NavbarProps {
|
|
76
|
+
/** Structural presentation. Omit for the static `bar`. */
|
|
77
|
+
presentation?: NavbarPresentation
|
|
78
|
+
/** Brand text shown next to the logo mark. */
|
|
79
|
+
logoText: string
|
|
80
|
+
logoHref?: string
|
|
81
|
+
/** Current location, used to indicate the active entry. */
|
|
82
|
+
currentPath?: string
|
|
83
|
+
links: NavbarLink[]
|
|
84
|
+
/** Promo card shown beside grouped flyout entries. */
|
|
85
|
+
featured?: NavbarFeatured
|
|
86
|
+
/** Sign-in action. Rendered as a link when `href` is set, otherwise as a button. */
|
|
87
|
+
signIn?: CTA
|
|
88
|
+
/** Primary call to action. Rendered as a link when `href` is set, otherwise as a button. */
|
|
89
|
+
cta?: CTA
|
|
90
|
+
/** Host-bound icons for grouped entries that declare an `icon` key. */
|
|
91
|
+
iconMap?: NavbarIconMap
|
|
92
|
+
/** Fired when the sign-in action has no `href`. */
|
|
93
|
+
onSignIn?: () => void
|
|
94
|
+
/** Fired when the primary call to action has no `href`. */
|
|
95
|
+
onCtaClick?: () => void
|
|
96
|
+
/** Classes merged onto the header surface, so a brand can restyle it. */
|
|
97
|
+
className?: string
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const FOCUS_RING =
|
|
101
|
+
'focus-visible:ring-ring focus-visible:ring-2 focus-visible:outline-none'
|
|
102
|
+
|
|
103
|
+
function Logo({ text, href }: { text: string; href?: string }) {
|
|
104
|
+
const className = cn(
|
|
105
|
+
FOCUS_RING,
|
|
106
|
+
'flex items-center gap-2 rounded-sm focus-visible:ring-offset-2'
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
const content = (
|
|
110
|
+
<>
|
|
111
|
+
<svg viewBox='0 0 32 32' aria-hidden='true' className='text-primary size-7' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
|
112
|
+
<path
|
|
113
|
+
d='M26 24.75C26.4142 24.75 26.75 24.4142 26.75 24C26.75 23.5858 26.4142 23.25 26 23.25V24.75ZM26 23.25H11V24.75H26V23.25ZM8.75 21V15H7.25V21H8.75ZM11 23.25C9.75736 23.25 8.75 22.2426 8.75 21H7.25C7.25 23.0711 8.92893 24.75 11 24.75V23.25Z'
|
|
114
|
+
fill='currentColor'
|
|
115
|
+
/>
|
|
116
|
+
<path
|
|
117
|
+
d='M1.5 3.25C1.08579 3.25 0.75 3.58579 0.75 4C0.75 4.41421 1.08579 4.75 1.5 4.75V3.25ZM1.5 4.75H6V3.25H1.5V4.75ZM7.25 6V21H8.75V6H7.25ZM6 4.75C6.69036 4.75 7.25 5.30964 7.25 6H8.75C8.75 4.48122 7.51878 3.25 6 3.25V4.75Z'
|
|
118
|
+
fill='currentColor'
|
|
119
|
+
/>
|
|
120
|
+
<path
|
|
121
|
+
d='M22 21.75C22.4142 21.75 22.75 21.4142 22.75 21C22.75 20.5858 22.4142 20.25 22 20.25V21.75ZM22 20.25H11V21.75H22V20.25ZM8.75 18V12H7.25V18H8.75ZM11 20.25C9.75736 20.25 8.75 19.2426 8.75 18H7.25C7.25 20.0711 8.92893 21.75 11 21.75V20.25Z'
|
|
122
|
+
fill='currentColor'
|
|
123
|
+
/>
|
|
124
|
+
<path
|
|
125
|
+
d='M27.2057 19.754C27.0654 20.1438 26.6357 20.346 26.246 20.2057C25.8562 20.0654 25.654 19.6357 25.7943 19.246L27.2057 19.754ZM30.0361 9.67744L29.3305 9.4234L29.3305 9.4234L30.0361 9.67744ZM25.7943 19.246L29.3305 9.4234L30.7418 9.93148L27.2057 19.754L25.7943 19.246ZM28.1543 7.75L8 7.75V6.25L28.1543 6.25V7.75ZM29.3305 9.4234C29.6237 8.60882 29.0201 7.75 28.1543 7.75V6.25C30.059 6.25 31.3869 8.13941 30.7418 9.93148L29.3305 9.4234Z'
|
|
126
|
+
fill='currentColor'
|
|
127
|
+
/>
|
|
128
|
+
<path
|
|
129
|
+
d='M13.5 21.75C13.0858 21.75 12.75 21.4142 12.75 21C12.75 20.5858 13.0858 20.25 13.5 20.25V21.75ZM26.7111 19.009L27.4174 19.2613L27.4174 19.2613L26.7111 19.009ZM13.5 20.25H23.8858V21.75H13.5V20.25ZM26.0048 18.7568L27.7937 13.7477L29.2063 14.2523L27.4174 19.2613L26.0048 18.7568ZM23.8858 20.25C24.8367 20.25 25.6849 19.6522 26.0048 18.7568L27.4174 19.2613C26.8843 20.7537 25.4706 21.75 23.8858 21.75V20.25Z'
|
|
130
|
+
fill='currentColor'
|
|
131
|
+
/>
|
|
132
|
+
<path d='M21.1694 10.5806L14.5651 17.1849' stroke='currentColor' strokeWidth='1.5' strokeLinecap='round' />
|
|
133
|
+
<path d='M22.1694 14.5806L18.5632 18.1868' stroke='currentColor' strokeWidth='1.5' strokeLinecap='round' />
|
|
134
|
+
<circle cx='13.1' cy='26.1' r='1.7' stroke='currentColor' strokeWidth='1.2' />
|
|
135
|
+
<circle cx='22.1' cy='26.1' r='1.7' stroke='currentColor' strokeWidth='1.2' />
|
|
136
|
+
</svg>
|
|
137
|
+
<span className='text-base font-semibold tracking-tight'>{text}</span>
|
|
138
|
+
</>
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
if (!href) {
|
|
142
|
+
return <span className={className}>{content}</span>
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<a href={href} className={className}>
|
|
147
|
+
{content}
|
|
148
|
+
</a>
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function isCurrent(href: string | undefined, currentPath: string | undefined) {
|
|
153
|
+
return href !== undefined && href === currentPath
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Non-color cue so the active entry does not rely on color alone. */
|
|
157
|
+
const CURRENT_CUE = 'font-semibold underline decoration-2 underline-offset-8'
|
|
158
|
+
|
|
159
|
+
function ActionButton({
|
|
160
|
+
action,
|
|
161
|
+
onClick,
|
|
162
|
+
variant,
|
|
163
|
+
className,
|
|
164
|
+
rounded,
|
|
165
|
+
}: {
|
|
166
|
+
action: CTA
|
|
167
|
+
onClick?: () => void
|
|
168
|
+
variant?: ComponentProps<typeof Button>['variant']
|
|
169
|
+
className?: string
|
|
170
|
+
rounded?: boolean
|
|
171
|
+
}) {
|
|
172
|
+
const classes = cn(rounded && 'rounded-full', className)
|
|
173
|
+
|
|
174
|
+
if (action.href && !onClick) {
|
|
175
|
+
return (
|
|
176
|
+
<Button variant={variant} size='sm' className={classes} asChild>
|
|
177
|
+
<a href={action.href}>{action.label}</a>
|
|
178
|
+
</Button>
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<Button variant={variant} size='sm' className={classes} onClick={onClick}>
|
|
184
|
+
{action.label}
|
|
185
|
+
</Button>
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function EntryIcon({ name, iconMap }: { name?: string; iconMap?: NavbarIconMap }) {
|
|
190
|
+
if (!name) {
|
|
191
|
+
return null
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const Icon = iconMap?.[name]
|
|
195
|
+
if (!Icon) {
|
|
196
|
+
return null
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return (
|
|
200
|
+
<span
|
|
201
|
+
aria-hidden='true'
|
|
202
|
+
className='bg-muted text-foreground flex size-8 shrink-0 items-center justify-center rounded-md'
|
|
203
|
+
>
|
|
204
|
+
<Icon className='size-4' />
|
|
205
|
+
</span>
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function FeaturedCard({ featured }: { featured: NavbarFeatured }) {
|
|
210
|
+
return (
|
|
211
|
+
<a
|
|
212
|
+
href={featured.href}
|
|
213
|
+
className={cn(
|
|
214
|
+
'border-border group relative flex h-full min-h-56 flex-col justify-end overflow-hidden rounded-md border',
|
|
215
|
+
FOCUS_RING
|
|
216
|
+
)}
|
|
217
|
+
>
|
|
218
|
+
{featured.image ? (
|
|
219
|
+
<img
|
|
220
|
+
src={featured.image}
|
|
221
|
+
alt={featured.imageAlt ?? ''}
|
|
222
|
+
className='absolute inset-0 size-full object-cover transition-transform duration-500 ease-out group-hover:scale-105'
|
|
223
|
+
/>
|
|
224
|
+
) : null}
|
|
225
|
+
{/* Token-driven scrim: follows the consumer theme instead of a hardcoded dark overlay. */}
|
|
226
|
+
<div
|
|
227
|
+
aria-hidden='true'
|
|
228
|
+
className='from-background via-background/70 absolute inset-0 bg-gradient-to-t to-transparent'
|
|
229
|
+
/>
|
|
230
|
+
<div className='relative z-10 flex flex-col p-4'>
|
|
231
|
+
<p className='text-muted-foreground text-xs font-semibold tracking-wide uppercase'>
|
|
232
|
+
{featured.eyebrow}
|
|
233
|
+
</p>
|
|
234
|
+
<p className='text-foreground mt-1 text-sm font-medium'>{featured.title}</p>
|
|
235
|
+
{featured.description ? (
|
|
236
|
+
<p className='text-muted-foreground mt-1 text-sm'>{featured.description}</p>
|
|
237
|
+
) : null}
|
|
238
|
+
{featured.linkLabel ? (
|
|
239
|
+
<span className='text-foreground mt-3 inline-flex items-center gap-1 text-sm font-medium'>
|
|
240
|
+
{featured.linkLabel}
|
|
241
|
+
<ArrowRight className='size-3.5 transition-transform duration-200 ease-out group-hover:translate-x-0.5' />
|
|
242
|
+
</span>
|
|
243
|
+
) : null}
|
|
244
|
+
</div>
|
|
245
|
+
</a>
|
|
246
|
+
)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function MegaEntry({
|
|
250
|
+
link,
|
|
251
|
+
featured,
|
|
252
|
+
currentPath,
|
|
253
|
+
iconMap,
|
|
254
|
+
}: {
|
|
255
|
+
link: NavbarLink
|
|
256
|
+
featured?: NavbarFeatured
|
|
257
|
+
currentPath?: string
|
|
258
|
+
iconMap?: NavbarIconMap
|
|
259
|
+
}) {
|
|
260
|
+
const groups = link.groups ?? []
|
|
261
|
+
const hasCurrentChild = groups.some(group =>
|
|
262
|
+
group.items.some(item => isCurrent(item.href, currentPath))
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
return (
|
|
266
|
+
<NavigationMenuItem>
|
|
267
|
+
<NavigationMenuTrigger className={cn(hasCurrentChild && CURRENT_CUE)}>
|
|
268
|
+
{link.label}
|
|
269
|
+
</NavigationMenuTrigger>
|
|
270
|
+
<NavigationMenuContent>
|
|
271
|
+
<div
|
|
272
|
+
className={cn(
|
|
273
|
+
'grid gap-3 p-2',
|
|
274
|
+
featured
|
|
275
|
+
? 'w-[760px] grid-cols-[1fr_248px]'
|
|
276
|
+
: groups.length > 1
|
|
277
|
+
? 'w-[760px] grid-cols-2'
|
|
278
|
+
: 'w-[360px]'
|
|
279
|
+
)}
|
|
280
|
+
>
|
|
281
|
+
<div className={cn('grid gap-3', groups.length > 1 && 'grid-cols-2')}>
|
|
282
|
+
{groups.map((group, groupIndex) => (
|
|
283
|
+
<div key={group.title ?? `group-${groupIndex}`}>
|
|
284
|
+
{group.title ? (
|
|
285
|
+
<p className='text-muted-foreground px-3 pt-1 pb-1 text-xs font-semibold tracking-wide uppercase'>
|
|
286
|
+
{group.title}
|
|
287
|
+
</p>
|
|
288
|
+
) : null}
|
|
289
|
+
<ul className='grid gap-1'>
|
|
290
|
+
{group.items.map(item => {
|
|
291
|
+
const active = isCurrent(item.href, currentPath)
|
|
292
|
+
|
|
293
|
+
return (
|
|
294
|
+
<li key={item.href}>
|
|
295
|
+
<NavigationMenuLink
|
|
296
|
+
href={item.href}
|
|
297
|
+
aria-current={active ? 'page' : undefined}
|
|
298
|
+
className={cn('items-start gap-3 p-3', active && CURRENT_CUE)}
|
|
299
|
+
>
|
|
300
|
+
<EntryIcon name={item.icon} iconMap={iconMap} />
|
|
301
|
+
<span className='flex flex-col gap-0.5'>
|
|
302
|
+
<span className='text-sm font-medium'>{item.label}</span>
|
|
303
|
+
{item.description ? (
|
|
304
|
+
<span className='text-muted-foreground line-clamp-2 text-sm leading-5'>
|
|
305
|
+
{item.description}
|
|
306
|
+
</span>
|
|
307
|
+
) : null}
|
|
308
|
+
</span>
|
|
309
|
+
</NavigationMenuLink>
|
|
310
|
+
</li>
|
|
311
|
+
)
|
|
312
|
+
})}
|
|
313
|
+
</ul>
|
|
314
|
+
</div>
|
|
315
|
+
))}
|
|
316
|
+
</div>
|
|
317
|
+
{featured ? <FeaturedCard featured={featured} /> : null}
|
|
318
|
+
</div>
|
|
319
|
+
</NavigationMenuContent>
|
|
320
|
+
</NavigationMenuItem>
|
|
321
|
+
)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function MobileMenu({
|
|
325
|
+
links,
|
|
326
|
+
currentPath,
|
|
327
|
+
signIn,
|
|
328
|
+
cta,
|
|
329
|
+
onSignIn,
|
|
330
|
+
onCtaClick,
|
|
331
|
+
}: {
|
|
332
|
+
links: NavbarLink[]
|
|
333
|
+
currentPath?: string
|
|
334
|
+
signIn?: CTA
|
|
335
|
+
cta?: CTA
|
|
336
|
+
onSignIn?: () => void
|
|
337
|
+
onCtaClick?: () => void
|
|
338
|
+
}) {
|
|
339
|
+
const [open, setOpen] = useState(false)
|
|
340
|
+
const groupedLinks = links.filter(link => (link.groups?.length ?? 0) > 0)
|
|
341
|
+
const plainLinks = links.filter(link => (link.groups?.length ?? 0) === 0)
|
|
342
|
+
|
|
343
|
+
// Nothing to navigate to and no action to offer: no drawer, no dead trigger.
|
|
344
|
+
if (links.length === 0 && !signIn && !cta) {
|
|
345
|
+
return null
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return (
|
|
349
|
+
<Sheet open={open} onOpenChange={setOpen}>
|
|
350
|
+
<SheetTrigger asChild>
|
|
351
|
+
<Button variant='ghost' size='icon' aria-label='Open menu' className='lg:hidden'>
|
|
352
|
+
<Menu />
|
|
353
|
+
</Button>
|
|
354
|
+
</SheetTrigger>
|
|
355
|
+
<SheetContent
|
|
356
|
+
side='right'
|
|
357
|
+
className={cn(
|
|
358
|
+
'w-full max-w-xs p-0',
|
|
359
|
+
groupedLinks.length > 0 && 'max-w-sm gap-0 overflow-y-auto'
|
|
360
|
+
)}
|
|
361
|
+
>
|
|
362
|
+
<SheetHeader className='border-border border-b'>
|
|
363
|
+
<SheetTitle className='text-left text-base'>Menu</SheetTitle>
|
|
364
|
+
</SheetHeader>
|
|
365
|
+
|
|
366
|
+
<nav aria-label='Mobile' className='px-2 py-2'>
|
|
367
|
+
{groupedLinks.length > 0 ? (
|
|
368
|
+
<Accordion type='multiple'>
|
|
369
|
+
{groupedLinks.map(link => (
|
|
370
|
+
<AccordionItem
|
|
371
|
+
key={`${link.label}:${link.href ?? 'group'}`}
|
|
372
|
+
value={link.label}
|
|
373
|
+
className='border-none'
|
|
374
|
+
>
|
|
375
|
+
<AccordionTrigger className='px-3 py-3 text-base font-medium hover:no-underline'>
|
|
376
|
+
{link.label}
|
|
377
|
+
</AccordionTrigger>
|
|
378
|
+
<AccordionContent className='pb-1'>
|
|
379
|
+
{(link.groups ?? []).map((group, groupIndex) => (
|
|
380
|
+
<div key={group.title ?? `group-${groupIndex}`}>
|
|
381
|
+
{group.title ? (
|
|
382
|
+
<p className='text-muted-foreground px-3 pt-1 pb-1 text-xs font-semibold tracking-wide uppercase'>
|
|
383
|
+
{group.title}
|
|
384
|
+
</p>
|
|
385
|
+
) : null}
|
|
386
|
+
<ul className='flex flex-col'>
|
|
387
|
+
{group.items.map(item => {
|
|
388
|
+
const active = isCurrent(item.href, currentPath)
|
|
389
|
+
|
|
390
|
+
return (
|
|
391
|
+
<li key={item.href}>
|
|
392
|
+
<SheetClose asChild>
|
|
393
|
+
<a
|
|
394
|
+
href={item.href}
|
|
395
|
+
aria-current={active ? 'page' : undefined}
|
|
396
|
+
className={cn(
|
|
397
|
+
'hover:bg-accent hover:text-accent-foreground flex items-start gap-3 rounded-md px-3 py-3 transition-colors duration-150 ease-out',
|
|
398
|
+
FOCUS_RING,
|
|
399
|
+
active ? 'text-foreground' : 'text-muted-foreground',
|
|
400
|
+
active && CURRENT_CUE
|
|
401
|
+
)}
|
|
402
|
+
>
|
|
403
|
+
<EntryIcon name={item.icon} />
|
|
404
|
+
<span className='flex flex-col gap-0.5'>
|
|
405
|
+
<span className='text-sm font-medium'>{item.label}</span>
|
|
406
|
+
{item.description ? (
|
|
407
|
+
<span className='text-muted-foreground text-sm'>
|
|
408
|
+
{item.description}
|
|
409
|
+
</span>
|
|
410
|
+
) : null}
|
|
411
|
+
</span>
|
|
412
|
+
</a>
|
|
413
|
+
</SheetClose>
|
|
414
|
+
</li>
|
|
415
|
+
)
|
|
416
|
+
})}
|
|
417
|
+
</ul>
|
|
418
|
+
</div>
|
|
419
|
+
))}
|
|
420
|
+
</AccordionContent>
|
|
421
|
+
</AccordionItem>
|
|
422
|
+
))}
|
|
423
|
+
</Accordion>
|
|
424
|
+
) : null}
|
|
425
|
+
|
|
426
|
+
{plainLinks.length > 0 ? (
|
|
427
|
+
<div className={cn('flex flex-col', groupedLinks.length > 0 && 'mt-1')}>
|
|
428
|
+
{plainLinks.map(link => {
|
|
429
|
+
const active = isCurrent(link.href, currentPath)
|
|
430
|
+
const className = cn(
|
|
431
|
+
'hover:bg-accent hover:text-accent-foreground rounded-md px-3 py-3 text-base font-medium transition-colors duration-150 ease-out',
|
|
432
|
+
FOCUS_RING,
|
|
433
|
+
active ? 'text-foreground' : 'text-muted-foreground',
|
|
434
|
+
active && CURRENT_CUE
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
return (
|
|
438
|
+
<SheetClose key={`${link.label}:${link.href ?? 'plain'}`} asChild>
|
|
439
|
+
{link.href ? (
|
|
440
|
+
<a href={link.href} aria-current={active ? 'page' : undefined} className={className}>
|
|
441
|
+
{link.label}
|
|
442
|
+
</a>
|
|
443
|
+
) : (
|
|
444
|
+
<button type='button' className={className}>
|
|
445
|
+
{link.label}
|
|
446
|
+
</button>
|
|
447
|
+
)}
|
|
448
|
+
</SheetClose>
|
|
449
|
+
)
|
|
450
|
+
})}
|
|
451
|
+
</div>
|
|
452
|
+
) : null}
|
|
453
|
+
</nav>
|
|
454
|
+
|
|
455
|
+
{signIn || cta ? (
|
|
456
|
+
<div className='mt-auto flex flex-col gap-4 p-4'>
|
|
457
|
+
<Separator />
|
|
458
|
+
<div className='flex flex-col gap-2'>
|
|
459
|
+
{signIn ? (
|
|
460
|
+
<SheetClose asChild>
|
|
461
|
+
<ActionButton
|
|
462
|
+
action={signIn}
|
|
463
|
+
onClick={onSignIn}
|
|
464
|
+
variant='outline'
|
|
465
|
+
className='w-full'
|
|
466
|
+
/>
|
|
467
|
+
</SheetClose>
|
|
468
|
+
) : null}
|
|
469
|
+
{cta ? (
|
|
470
|
+
<SheetClose asChild>
|
|
471
|
+
<ActionButton action={cta} onClick={onCtaClick} className='w-full' />
|
|
472
|
+
</SheetClose>
|
|
473
|
+
) : null}
|
|
474
|
+
</div>
|
|
475
|
+
</div>
|
|
476
|
+
) : null}
|
|
477
|
+
</SheetContent>
|
|
478
|
+
</Sheet>
|
|
479
|
+
)
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function FlatLinks({
|
|
483
|
+
links,
|
|
484
|
+
currentPath,
|
|
485
|
+
rounded,
|
|
486
|
+
}: {
|
|
487
|
+
links: NavbarLink[]
|
|
488
|
+
currentPath?: string
|
|
489
|
+
rounded?: boolean
|
|
490
|
+
}) {
|
|
491
|
+
if (links.length === 0) {
|
|
492
|
+
return null
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return (
|
|
496
|
+
<nav aria-label='Main' className='hidden lg:block'>
|
|
497
|
+
<ul className='flex items-center gap-1'>
|
|
498
|
+
{links.map(link => {
|
|
499
|
+
const active = isCurrent(link.href, currentPath)
|
|
500
|
+
const className = cn(
|
|
501
|
+
'hover:text-foreground text-sm font-medium transition-colors duration-150 ease-out',
|
|
502
|
+
FOCUS_RING,
|
|
503
|
+
rounded ? 'rounded-full px-3 py-2' : 'rounded-md px-3 py-2',
|
|
504
|
+
active ? 'text-foreground' : 'text-muted-foreground',
|
|
505
|
+
active && CURRENT_CUE,
|
|
506
|
+
!active && link.emphasis && 'text-foreground'
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
return (
|
|
510
|
+
<li key={`${link.label}:${link.href ?? 'plain'}`}>
|
|
511
|
+
{link.href ? (
|
|
512
|
+
<a href={link.href} aria-current={active ? 'page' : undefined} className={className}>
|
|
513
|
+
{link.label}
|
|
514
|
+
</a>
|
|
515
|
+
) : (
|
|
516
|
+
<span className={className}>{link.label}</span>
|
|
517
|
+
)}
|
|
518
|
+
</li>
|
|
519
|
+
)
|
|
520
|
+
})}
|
|
521
|
+
</ul>
|
|
522
|
+
</nav>
|
|
523
|
+
)
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function MegaLinks({
|
|
527
|
+
links,
|
|
528
|
+
featured,
|
|
529
|
+
currentPath,
|
|
530
|
+
iconMap,
|
|
531
|
+
}: {
|
|
532
|
+
links: NavbarLink[]
|
|
533
|
+
featured?: NavbarFeatured
|
|
534
|
+
currentPath?: string
|
|
535
|
+
iconMap?: NavbarIconMap
|
|
536
|
+
}) {
|
|
537
|
+
if (links.length === 0) {
|
|
538
|
+
return null
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return (
|
|
542
|
+
<NavigationMenu className='hidden lg:flex' aria-label='Main'>
|
|
543
|
+
<NavigationMenuList>
|
|
544
|
+
{links.map(link => {
|
|
545
|
+
const grouped = (link.groups?.length ?? 0) > 0
|
|
546
|
+
|
|
547
|
+
if (grouped) {
|
|
548
|
+
return (
|
|
549
|
+
<MegaEntry
|
|
550
|
+
key={`${link.label}:group`}
|
|
551
|
+
link={link}
|
|
552
|
+
featured={featured}
|
|
553
|
+
currentPath={currentPath}
|
|
554
|
+
iconMap={iconMap}
|
|
555
|
+
/>
|
|
556
|
+
)
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const active = isCurrent(link.href, currentPath)
|
|
560
|
+
|
|
561
|
+
return (
|
|
562
|
+
<NavigationMenuItem key={`${link.label}:${link.href ?? 'plain'}`}>
|
|
563
|
+
<NavigationMenuLink
|
|
564
|
+
href={link.href}
|
|
565
|
+
aria-current={active ? 'page' : undefined}
|
|
566
|
+
className={cn(
|
|
567
|
+
navigationMenuTriggerStyle(),
|
|
568
|
+
active && CURRENT_CUE,
|
|
569
|
+
!active && link.emphasis && 'text-foreground font-semibold'
|
|
570
|
+
)}
|
|
571
|
+
>
|
|
572
|
+
{link.label}
|
|
573
|
+
</NavigationMenuLink>
|
|
574
|
+
</NavigationMenuItem>
|
|
575
|
+
)
|
|
576
|
+
})}
|
|
577
|
+
</NavigationMenuList>
|
|
578
|
+
</NavigationMenu>
|
|
579
|
+
)
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Page-level navigation header.
|
|
584
|
+
*
|
|
585
|
+
* Responsibilities end at navigation: the header renders no cart, account, or search
|
|
586
|
+
* affordance — those belong to the commerce shell (`StorefrontNavbar`).
|
|
587
|
+
*
|
|
588
|
+
* `floating` keeps the header in normal document flow (`sticky`), so it reserves its own
|
|
589
|
+
* space and the consumer adds no compensating padding or scroll offset. A `sticky` header
|
|
590
|
+
* is defeated by an `overflow` ancestor that scrolls; use `bar` there.
|
|
591
|
+
*/
|
|
592
|
+
export function Navbar({
|
|
593
|
+
presentation = 'bar',
|
|
594
|
+
logoText,
|
|
595
|
+
logoHref,
|
|
596
|
+
currentPath,
|
|
597
|
+
links,
|
|
598
|
+
featured,
|
|
599
|
+
signIn,
|
|
600
|
+
cta,
|
|
601
|
+
iconMap,
|
|
602
|
+
onSignIn,
|
|
603
|
+
onCtaClick,
|
|
604
|
+
className,
|
|
605
|
+
}: NavbarProps) {
|
|
606
|
+
const sentinelRef = useRef<HTMLDivElement>(null)
|
|
607
|
+
const [scrolled, setScrolled] = useState(false)
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Scroll detection:
|
|
611
|
+
* - Inside a preview with an overflow ancestor the scroller is not the viewport, so
|
|
612
|
+
* `scrollTop` on the nearest scrollable ancestor is the reliable signal.
|
|
613
|
+
* - On a normal page, an IntersectionObserver on the 8px sentinel gives hysteresis
|
|
614
|
+
* without flickering at a hard `scrollY > 8` threshold.
|
|
615
|
+
*/
|
|
616
|
+
useEffect(() => {
|
|
617
|
+
if (presentation !== 'floating') {
|
|
618
|
+
return
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const sentinel = sentinelRef.current
|
|
622
|
+
if (!sentinel) {
|
|
623
|
+
return
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
let ancestor: HTMLElement | null = sentinel.parentElement
|
|
627
|
+
while (ancestor) {
|
|
628
|
+
const { overflowY } = getComputedStyle(ancestor)
|
|
629
|
+
if (
|
|
630
|
+
(overflowY === 'auto' || overflowY === 'scroll' || overflowY === 'overlay') &&
|
|
631
|
+
ancestor.scrollHeight > ancestor.clientHeight + 1
|
|
632
|
+
) {
|
|
633
|
+
const scroller = ancestor
|
|
634
|
+
const update = () => setScrolled(scroller.scrollTop > 8)
|
|
635
|
+
update()
|
|
636
|
+
scroller.addEventListener('scroll', update, { passive: true })
|
|
637
|
+
return () => scroller.removeEventListener('scroll', update)
|
|
638
|
+
}
|
|
639
|
+
ancestor = ancestor.parentElement
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const observer = new IntersectionObserver(([entry]) => setScrolled(!entry.isIntersecting), {
|
|
643
|
+
threshold: 0,
|
|
644
|
+
})
|
|
645
|
+
|
|
646
|
+
observer.observe(sentinel)
|
|
647
|
+
return () => observer.disconnect()
|
|
648
|
+
}, [presentation])
|
|
649
|
+
|
|
650
|
+
if (presentation === 'floating') {
|
|
651
|
+
return (
|
|
652
|
+
<>
|
|
653
|
+
{/*
|
|
654
|
+
Out of flow: gives the observer an 8px hysteresis target without painting a
|
|
655
|
+
blank strip under the transparent unscrolled header.
|
|
656
|
+
*/}
|
|
657
|
+
<div
|
|
658
|
+
ref={sentinelRef}
|
|
659
|
+
aria-hidden='true'
|
|
660
|
+
className='pointer-events-none absolute inset-x-0 top-0 h-2'
|
|
661
|
+
/>
|
|
662
|
+
|
|
663
|
+
<header className={cn('sticky top-0 z-50 w-full', className)}>
|
|
664
|
+
<div
|
|
665
|
+
className={cn(
|
|
666
|
+
'mx-auto flex h-14 items-center justify-between gap-4 transition-all duration-300 ease-out motion-reduce:transition-none',
|
|
667
|
+
scrolled
|
|
668
|
+
? 'border-border/60 bg-background/80 max-w-5xl rounded-full border px-4 shadow-sm backdrop-blur-md sm:px-5'
|
|
669
|
+
: 'max-w-7xl border border-transparent px-4 sm:px-6 lg:px-8'
|
|
670
|
+
)}
|
|
671
|
+
>
|
|
672
|
+
<Logo text={logoText} href={logoHref} />
|
|
673
|
+
|
|
674
|
+
<FlatLinks links={links} currentPath={currentPath} rounded />
|
|
675
|
+
|
|
676
|
+
<div className='hidden items-center gap-2 lg:flex'>
|
|
677
|
+
{signIn ? (
|
|
678
|
+
<ActionButton action={signIn} onClick={onSignIn} variant='ghost' rounded />
|
|
679
|
+
) : null}
|
|
680
|
+
{cta ? <ActionButton action={cta} onClick={onCtaClick} rounded /> : null}
|
|
681
|
+
</div>
|
|
682
|
+
|
|
683
|
+
<MobileMenu
|
|
684
|
+
links={links}
|
|
685
|
+
currentPath={currentPath}
|
|
686
|
+
signIn={signIn}
|
|
687
|
+
cta={cta}
|
|
688
|
+
onSignIn={onSignIn}
|
|
689
|
+
onCtaClick={onCtaClick}
|
|
690
|
+
/>
|
|
691
|
+
</div>
|
|
692
|
+
</header>
|
|
693
|
+
</>
|
|
694
|
+
)
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
return (
|
|
698
|
+
<header className={cn('border-border bg-background w-full border-b', className)}>
|
|
699
|
+
<div className='mx-auto flex h-16 w-full max-w-7xl items-center justify-between gap-4 px-4 sm:px-6 lg:px-8'>
|
|
700
|
+
{presentation === 'mega' ? (
|
|
701
|
+
<div className='flex items-center gap-6'>
|
|
702
|
+
<Logo text={logoText} href={logoHref} />
|
|
703
|
+
<MegaLinks
|
|
704
|
+
links={links}
|
|
705
|
+
featured={featured}
|
|
706
|
+
currentPath={currentPath}
|
|
707
|
+
iconMap={iconMap}
|
|
708
|
+
/>
|
|
709
|
+
</div>
|
|
710
|
+
) : (
|
|
711
|
+
<>
|
|
712
|
+
<Logo text={logoText} href={logoHref} />
|
|
713
|
+
<FlatLinks links={links} currentPath={currentPath} />
|
|
714
|
+
</>
|
|
715
|
+
)}
|
|
716
|
+
|
|
717
|
+
<div className='hidden items-center gap-2 lg:flex'>
|
|
718
|
+
{signIn ? <ActionButton action={signIn} onClick={onSignIn} variant='ghost' /> : null}
|
|
719
|
+
{cta ? <ActionButton action={cta} onClick={onCtaClick} /> : null}
|
|
720
|
+
</div>
|
|
721
|
+
|
|
722
|
+
<MobileMenu
|
|
723
|
+
links={links}
|
|
724
|
+
currentPath={currentPath}
|
|
725
|
+
signIn={signIn}
|
|
726
|
+
cta={cta}
|
|
727
|
+
onSignIn={onSignIn}
|
|
728
|
+
onCtaClick={onCtaClick}
|
|
729
|
+
/>
|
|
730
|
+
</div>
|
|
731
|
+
</header>
|
|
732
|
+
)
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
export default Navbar
|