@thorprovider/blocks 0.1.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/.turbo/turbo-build.log +24 -0
- package/.turbo/turbo-test.log +10 -0
- package/.turbo/turbo-type-check.log +0 -0
- package/AGENTS.md +1 -0
- package/README.md +161 -0
- package/components.json +33 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/contracts.d.mts +563 -0
- package/dist/contracts.d.ts +563 -0
- package/dist/contracts.js +207 -0
- package/dist/contracts.js.map +1 -0
- package/dist/contracts.mjs +168 -0
- package/dist/contracts.mjs.map +1 -0
- package/dist/index.d.mts +2408 -0
- package/dist/index.d.ts +2408 -0
- package/dist/index.js +16422 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +16352 -0
- package/dist/index.mjs.map +1 -0
- package/docs/standards/data-contracts.md +70 -0
- package/docs/standards/theming.md +85 -0
- package/package.json +62 -0
- package/src/blocks/application/app-shells/app-shell-1-sidebar.tsx +198 -0
- package/src/blocks/application/app-shells/app-shell-1.tsx +196 -0
- package/src/blocks/application/login-forms/login-page-1.tsx +246 -0
- package/src/blocks/application/login-forms/login-page-2.tsx +207 -0
- package/src/blocks/application/password-recovery/forgot-password-page-1-brand-mark.tsx +38 -0
- package/src/blocks/application/password-recovery/forgot-password-page-1.tsx +320 -0
- package/src/blocks/application/signup-forms/signup-page-1.tsx +231 -0
- package/src/blocks/application/verification/verify-1.tsx +307 -0
- package/src/blocks/application/verification/verify-confetti.tsx +192 -0
- package/src/blocks/e-commerce/category-filters/category-filter-1.tsx +260 -0
- package/src/blocks/e-commerce/category-filters/category-filter-2-filter-panel.tsx +179 -0
- package/src/blocks/e-commerce/category-filters/category-filter-2.tsx +341 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4-chip-rail.tsx +57 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4-drawer.tsx +124 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4-fields.tsx +170 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4-sidebar.tsx +52 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4-sort-sheet.tsx +57 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4-state.ts +68 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4.tsx +298 -0
- package/src/blocks/e-commerce/checkout-forms/card-formatting.test.ts +47 -0
- package/src/blocks/e-commerce/checkout-forms/card-formatting.ts +9 -0
- package/src/blocks/e-commerce/checkout-forms/checkbox-check.tsx +74 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-confirmation-fulfilment-progress.tsx +189 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-confirmation.tsx +283 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-field-feedback.tsx +29 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-contact-step.tsx +96 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-country-field.tsx +51 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-order-summary.tsx +139 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-payment-step.tsx +184 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-progress.tsx +36 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-shipping-step.tsx +107 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1.tsx +235 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2-order-summary.tsx +130 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2-payment-card.tsx +175 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2-shipping-card.tsx +190 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2.tsx +312 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-information-step.tsx +91 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-order-summary.tsx +110 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-payment-step.tsx +137 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-progress.tsx +68 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-review-step.tsx +79 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-shipping-step.tsx +172 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3.tsx +392 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-types.ts +7 -0
- package/src/blocks/e-commerce/navigation/category-icon.test.tsx +54 -0
- package/src/blocks/e-commerce/navigation/category-icon.tsx +94 -0
- package/src/blocks/e-commerce/navigation/mega-menu.test.tsx +139 -0
- package/src/blocks/e-commerce/navigation/mega-menu.tsx +271 -0
- package/src/blocks/e-commerce/navigation/mobile-category-drawer.test.tsx +152 -0
- package/src/blocks/e-commerce/navigation/mobile-category-drawer.tsx +276 -0
- package/src/blocks/e-commerce/order-history/order-history-1.tsx +159 -0
- package/src/blocks/e-commerce/order-history/order-history-2.tsx +222 -0
- package/src/blocks/e-commerce/product-categories/avatar-group.tsx +86 -0
- package/src/blocks/e-commerce/product-categories/product-category-1.tsx +126 -0
- package/src/blocks/e-commerce/product-categories/product-category-2.tsx +100 -0
- package/src/blocks/e-commerce/product-categories/product-category-4.tsx +129 -0
- package/src/blocks/e-commerce/product-categories/product-category-5.tsx +218 -0
- package/src/blocks/e-commerce/product-list/product-card-1.tsx +131 -0
- package/src/blocks/e-commerce/product-list/product-card-2.tsx +71 -0
- package/src/blocks/e-commerce/product-list/product-card-3-reaction-toggle.tsx +166 -0
- package/src/blocks/e-commerce/product-list/product-card-3.tsx +215 -0
- package/src/blocks/e-commerce/product-list/product-card-5-checkbox-check.tsx +98 -0
- package/src/blocks/e-commerce/product-list/product-card-5-reaction-toggle.tsx +171 -0
- package/src/blocks/e-commerce/product-list/product-card-5.tsx +367 -0
- package/src/blocks/e-commerce/product-overview/product-overview-1.tsx +201 -0
- package/src/blocks/e-commerce/product-overview/product-overview-2.tsx +272 -0
- package/src/blocks/e-commerce/product-overview/product-overview-5-gallery.tsx +59 -0
- package/src/blocks/e-commerce/product-overview/product-overview-5-lightbox-tab.tsx +104 -0
- package/src/blocks/e-commerce/product-overview/product-overview-5-reviews-tab.tsx +82 -0
- package/src/blocks/e-commerce/product-overview/product-overview-5-tabs.tsx +69 -0
- package/src/blocks/e-commerce/product-overview/product-overview-5.tsx +274 -0
- package/src/blocks/e-commerce/product-overview/product-quick-view-card.tsx +135 -0
- package/src/blocks/e-commerce/product-overview/product-quick-view-dialog.tsx +393 -0
- package/src/blocks/e-commerce/product-overview/product-quick-view.tsx +136 -0
- package/src/blocks/e-commerce/reviews/review-rating-1-review-form.tsx +133 -0
- package/src/blocks/e-commerce/reviews/review-rating-1.tsx +178 -0
- package/src/blocks/e-commerce/reviews/review-rating-2.tsx +246 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-1-spinning-counter.tsx +142 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-1.tsx +291 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-item-row.tsx +110 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-recommendations.tsx +51 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-spinning-counter.tsx +146 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2.tsx +295 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-3-spinning-counter.tsx +142 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-3.tsx +342 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-4-spinning-counter.tsx +146 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.tsx +391 -0
- package/src/blocks/e-commerce/store-containers/store-container-1.tsx +29 -0
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.tsx +244 -0
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-2.tsx +283 -0
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-5.tsx +318 -0
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-6.tsx +205 -0
- package/src/blocks/marketing/banners/avatar-group.tsx +69 -0
- package/src/blocks/marketing/banners/banner-1.tsx +188 -0
- package/src/blocks/marketing/cta/cta-section-1.tsx +112 -0
- package/src/blocks/marketing/faq/faq-block-1.tsx +45 -0
- package/src/blocks/marketing/features/feature-1.tsx +97 -0
- package/src/blocks/marketing/features/feature-2.tsx +82 -0
- package/src/blocks/marketing/footers/footer-section-1.tsx +141 -0
- package/src/blocks/marketing/footers/footer-section-3.tsx +192 -0
- package/src/blocks/marketing/footers/footer-section-4.tsx +355 -0
- package/src/blocks/marketing/hero-sections/avatar-group.tsx +86 -0
- package/src/blocks/marketing/hero-sections/hero-section-1.tsx +135 -0
- package/src/blocks/marketing/logo-clouds/logo-cloud-1-logo-icon.tsx +52 -0
- package/src/blocks/marketing/logo-clouds/logo-cloud-1.tsx +56 -0
- package/src/blocks/marketing/navbars/navbar-1.tsx +267 -0
- package/src/blocks/marketing/navbars/navbar-2.tsx +351 -0
- package/src/blocks/marketing/navbars/navbar-3.tsx +303 -0
- package/src/blocks/marketing/navbars/navbar-4-account-menu.tsx +118 -0
- package/src/blocks/marketing/navbars/navbar-4-cart-sheet.tsx +248 -0
- package/src/blocks/marketing/navbars/navbar-4-like-button.tsx +177 -0
- package/src/blocks/marketing/navbars/navbar-4-mobile-nav.tsx +104 -0
- package/src/blocks/marketing/navbars/navbar-4-product-grid.tsx +219 -0
- package/src/blocks/marketing/navbars/navbar-4-search-bar.tsx +52 -0
- package/src/blocks/marketing/navbars/navbar-4.tsx +411 -0
- package/src/blocks/marketing/pricing/pricing-section-1.tsx +94 -0
- package/src/blocks/marketing/statistics/statistic-1.tsx +93 -0
- package/src/blocks/marketing/testimonials/testimonial-1.tsx +141 -0
- package/src/blocks/marketing/testimonials/testimonial-2.tsx +108 -0
- package/src/blocks/shared/reaction-toggle.tsx +145 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/index.ts +200 -0
- package/src/lib/contracts.test.ts +32 -0
- package/src/lib/contracts.ts +235 -0
- package/src/lib/types/menu.ts +43 -0
- package/src/lib/types.ts +4 -0
- package/src/lib/utils.ts +10 -0
- package/src/ui/accordion.tsx +65 -0
- package/src/ui/avatar.tsx +108 -0
- package/src/ui/badge.tsx +47 -0
- package/src/ui/breadcrumb.tsx +108 -0
- package/src/ui/button-group.tsx +83 -0
- package/src/ui/button.tsx +63 -0
- package/src/ui/card.tsx +91 -0
- package/src/ui/carousel.tsx +241 -0
- package/src/ui/dialog.tsx +158 -0
- package/src/ui/drawer.tsx +134 -0
- package/src/ui/dropdown-menu.tsx +256 -0
- package/src/ui/empty.tsx +103 -0
- package/src/ui/field.tsx +248 -0
- package/src/ui/input-group.tsx +170 -0
- package/src/ui/input-otp.tsx +76 -0
- package/src/ui/input.tsx +20 -0
- package/src/ui/label.tsx +23 -0
- package/src/ui/navigation-menu.tsx +167 -0
- package/src/ui/pagination.tsx +127 -0
- package/src/ui/progress.tsx +30 -0
- package/src/ui/radio-group.tsx +44 -0
- package/src/ui/select.tsx +189 -0
- package/src/ui/separator.tsx +27 -0
- package/src/ui/sheet.tsx +142 -0
- package/src/ui/sidebar.tsx +726 -0
- package/src/ui/skeleton.tsx +13 -0
- package/src/ui/table.tsx +115 -0
- package/src/ui/tabs.tsx +90 -0
- package/src/ui/textarea.tsx +17 -0
- package/src/ui/toggle-group.tsx +83 -0
- package/src/ui/toggle.tsx +46 -0
- package/src/ui/tooltip.tsx +56 -0
- package/tsconfig.json +20 -0
- package/tsup.config.ts +16 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState, type ComponentProps, type Ref } from 'react'
|
|
4
|
+
import { Menu } from 'lucide-react'
|
|
5
|
+
|
|
6
|
+
import { Button } from '../../../ui/button'
|
|
7
|
+
import { Separator } from '../../../ui/separator'
|
|
8
|
+
import {
|
|
9
|
+
Sheet,
|
|
10
|
+
SheetClose,
|
|
11
|
+
SheetContent,
|
|
12
|
+
SheetHeader,
|
|
13
|
+
SheetTitle,
|
|
14
|
+
SheetTrigger,
|
|
15
|
+
} from '../../../ui/sheet'
|
|
16
|
+
import { cn } from '../../../lib/utils'
|
|
17
|
+
import type { CTA } from '../../../lib/types'
|
|
18
|
+
|
|
19
|
+
export interface NavLink {
|
|
20
|
+
label: string
|
|
21
|
+
href?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface Navbar3Props {
|
|
25
|
+
navLinks: NavLink[]
|
|
26
|
+
/** Brand text shown next to the logo mark. */
|
|
27
|
+
logoText: string
|
|
28
|
+
logoHref?: string
|
|
29
|
+
/** Current path used to highlight the active nav link. */
|
|
30
|
+
currentPath?: string
|
|
31
|
+
/** Sign-in action. Rendered as a link when `href` is set, otherwise as a button. */
|
|
32
|
+
signIn?: CTA
|
|
33
|
+
/** Primary call-to-action. Rendered as a link when `href` is set, otherwise as a button. */
|
|
34
|
+
cta?: CTA
|
|
35
|
+
/**
|
|
36
|
+
* When `true` (default), the header condenses into a floating rounded pill once scrolled.
|
|
37
|
+
* Set to `false` for a plain solid bar instead of the condensing pill.
|
|
38
|
+
*/
|
|
39
|
+
floatingPill?: boolean
|
|
40
|
+
/** Fired when the sign-in button (no `href`) is clicked. */
|
|
41
|
+
onSignIn?: () => void
|
|
42
|
+
/** Fired when the CTA button (no `href`) is clicked. */
|
|
43
|
+
onCtaClick?: () => void
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Composition contract
|
|
48
|
+
* --------------------
|
|
49
|
+
* The header is `fixed`, so the first section of the page must clear it with `pt-16`,
|
|
50
|
+
* and `html { scroll-padding-top: 4rem }` keeps in-page anchors from landing underneath it.
|
|
51
|
+
*
|
|
52
|
+
* Set `floatingPill` to false for a plain solid bar on scroll instead of the condensing pill.
|
|
53
|
+
*/
|
|
54
|
+
function Logo({ text, href }: { text: string; href?: string }) {
|
|
55
|
+
const className =
|
|
56
|
+
'focus-visible:ring-ring flex items-center gap-2 rounded-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none'
|
|
57
|
+
|
|
58
|
+
const content = (
|
|
59
|
+
<>
|
|
60
|
+
<svg viewBox='0 0 32 32' aria-hidden='true' className='text-primary size-7' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
|
61
|
+
<path
|
|
62
|
+
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'
|
|
63
|
+
fill='currentColor'
|
|
64
|
+
/>
|
|
65
|
+
<path
|
|
66
|
+
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'
|
|
67
|
+
fill='currentColor'
|
|
68
|
+
/>
|
|
69
|
+
<path
|
|
70
|
+
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'
|
|
71
|
+
fill='currentColor'
|
|
72
|
+
/>
|
|
73
|
+
<path
|
|
74
|
+
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'
|
|
75
|
+
fill='currentColor'
|
|
76
|
+
/>
|
|
77
|
+
<path
|
|
78
|
+
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'
|
|
79
|
+
fill='currentColor'
|
|
80
|
+
/>
|
|
81
|
+
<path d='M21.1694 10.5806L14.5651 17.1849' stroke='currentColor' strokeWidth='1.5' strokeLinecap='round' />
|
|
82
|
+
<path d='M22.1694 14.5806L18.5632 18.1868' stroke='currentColor' strokeWidth='1.5' strokeLinecap='round' />
|
|
83
|
+
<circle cx='13.1' cy='26.1' r='1.7' stroke='currentColor' strokeWidth='1.2' />
|
|
84
|
+
<circle cx='22.1' cy='26.1' r='1.7' stroke='currentColor' strokeWidth='1.2' />
|
|
85
|
+
</svg>
|
|
86
|
+
<span className='text-base font-semibold tracking-tight'>{text}</span>
|
|
87
|
+
</>
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
if (!href) {
|
|
91
|
+
return <span className={className}>{content}</span>
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<a href={href} className={className}>
|
|
96
|
+
{content}
|
|
97
|
+
</a>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function ActionButton({
|
|
102
|
+
cta,
|
|
103
|
+
onClick,
|
|
104
|
+
variant,
|
|
105
|
+
className,
|
|
106
|
+
rounded,
|
|
107
|
+
ref,
|
|
108
|
+
}: {
|
|
109
|
+
cta: CTA
|
|
110
|
+
onClick?: () => void
|
|
111
|
+
variant?: ComponentProps<typeof Button>['variant']
|
|
112
|
+
className?: string
|
|
113
|
+
rounded?: boolean
|
|
114
|
+
ref?: Ref<HTMLButtonElement>
|
|
115
|
+
}) {
|
|
116
|
+
const classes = cn(rounded && 'rounded-full', className)
|
|
117
|
+
|
|
118
|
+
if (cta.href && !onClick) {
|
|
119
|
+
return (
|
|
120
|
+
<Button variant={variant} size='sm' className={classes} asChild ref={ref}>
|
|
121
|
+
<a href={cta.href}>{cta.label}</a>
|
|
122
|
+
</Button>
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<Button variant={variant} size='sm' className={classes} onClick={onClick} ref={ref}>
|
|
128
|
+
{cta.label}
|
|
129
|
+
</Button>
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function Navbar3({
|
|
134
|
+
navLinks,
|
|
135
|
+
logoText,
|
|
136
|
+
logoHref,
|
|
137
|
+
currentPath,
|
|
138
|
+
signIn,
|
|
139
|
+
cta,
|
|
140
|
+
floatingPill = true,
|
|
141
|
+
onSignIn,
|
|
142
|
+
onCtaClick,
|
|
143
|
+
}: Navbar3Props) {
|
|
144
|
+
const sentinelRef = useRef<HTMLDivElement>(null)
|
|
145
|
+
const [scrolled, setScrolled] = useState(false)
|
|
146
|
+
const [open, setOpen] = useState(false)
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Scroll detection:
|
|
150
|
+
* - Inside the store preview the scroller is an overflow ancestor (not the viewport).
|
|
151
|
+
* IntersectionObserver + an out-of-flow sentinel is unreliable there, so use scrollTop.
|
|
152
|
+
* - On a normal page, an IntersectionObserver on the 8px sentinel gives hysteresis without
|
|
153
|
+
* flickering at a hard `scrollY > 8` threshold.
|
|
154
|
+
*/
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
const sentinel = sentinelRef.current
|
|
157
|
+
if (!sentinel) {
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let scrollParent: HTMLElement | null = null
|
|
162
|
+
let ancestor: HTMLElement | null = sentinel.parentElement
|
|
163
|
+
while (ancestor) {
|
|
164
|
+
const { overflowY } = getComputedStyle(ancestor)
|
|
165
|
+
if (
|
|
166
|
+
(overflowY === 'auto' || overflowY === 'scroll' || overflowY === 'overlay') &&
|
|
167
|
+
ancestor.scrollHeight > ancestor.clientHeight + 1
|
|
168
|
+
) {
|
|
169
|
+
scrollParent = ancestor
|
|
170
|
+
break
|
|
171
|
+
}
|
|
172
|
+
ancestor = ancestor.parentElement
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (scrollParent) {
|
|
176
|
+
const scroller = scrollParent
|
|
177
|
+
const update = () => setScrolled(scroller.scrollTop > 8)
|
|
178
|
+
update()
|
|
179
|
+
scroller.addEventListener('scroll', update, { passive: true })
|
|
180
|
+
return () => scroller.removeEventListener('scroll', update)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const observer = new IntersectionObserver(([entry]) => setScrolled(!entry.isIntersecting), {
|
|
184
|
+
threshold: 0,
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
observer.observe(sentinel)
|
|
188
|
+
return () => observer.disconnect()
|
|
189
|
+
}, [])
|
|
190
|
+
|
|
191
|
+
return (
|
|
192
|
+
<>
|
|
193
|
+
{/*
|
|
194
|
+
Absolute + out of flow: keeps an 8px hysteresis target for the observer
|
|
195
|
+
without painting a blank strip under the transparent (unscrolled) header.
|
|
196
|
+
*/}
|
|
197
|
+
<div
|
|
198
|
+
ref={sentinelRef}
|
|
199
|
+
aria-hidden='true'
|
|
200
|
+
className='pointer-events-none absolute inset-x-0 top-0 h-2'
|
|
201
|
+
/>
|
|
202
|
+
|
|
203
|
+
<header className='fixed inset-x-0 top-0 z-50'>
|
|
204
|
+
<div
|
|
205
|
+
className={cn(
|
|
206
|
+
'mx-auto flex h-14 items-center justify-between gap-4 transition-all duration-300 ease-out motion-reduce:transition-none',
|
|
207
|
+
scrolled
|
|
208
|
+
? floatingPill
|
|
209
|
+
? 'border-border/60 bg-background/80 mt-3 max-w-5xl rounded-full border px-4 shadow-sm backdrop-blur-md sm:px-5'
|
|
210
|
+
: 'border-border/60 bg-background/90 max-w-full border-b px-4 backdrop-blur-md sm:px-6 lg:px-8'
|
|
211
|
+
: 'max-w-7xl border border-transparent px-4 sm:px-6 lg:px-8'
|
|
212
|
+
)}
|
|
213
|
+
>
|
|
214
|
+
<Logo text={logoText} href={logoHref} />
|
|
215
|
+
|
|
216
|
+
<nav aria-label='Main' className='hidden lg:block'>
|
|
217
|
+
<ul className='flex items-center gap-1'>
|
|
218
|
+
{navLinks.map(link => {
|
|
219
|
+
const isActive = link.href !== undefined && link.href === currentPath
|
|
220
|
+
const className = cn(
|
|
221
|
+
'hover:text-foreground focus-visible:ring-ring rounded-full px-3 py-2 text-sm font-medium transition-colors duration-150 ease-out focus-visible:ring-2 focus-visible:outline-none',
|
|
222
|
+
isActive ? 'text-foreground' : 'text-muted-foreground'
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
return (
|
|
226
|
+
<li key={link.label}>
|
|
227
|
+
{link.href ? (
|
|
228
|
+
<a href={link.href} aria-current={isActive ? 'page' : undefined} className={className}>
|
|
229
|
+
{link.label}
|
|
230
|
+
</a>
|
|
231
|
+
) : (
|
|
232
|
+
<span className={className}>{link.label}</span>
|
|
233
|
+
)}
|
|
234
|
+
</li>
|
|
235
|
+
)
|
|
236
|
+
})}
|
|
237
|
+
</ul>
|
|
238
|
+
</nav>
|
|
239
|
+
|
|
240
|
+
<div className='hidden items-center gap-2 lg:flex'>
|
|
241
|
+
{signIn && <ActionButton cta={signIn} onClick={onSignIn} variant='ghost' />}
|
|
242
|
+
{cta && <ActionButton cta={cta} onClick={onCtaClick} rounded={floatingPill} />}
|
|
243
|
+
</div>
|
|
244
|
+
|
|
245
|
+
<Sheet open={open} onOpenChange={setOpen}>
|
|
246
|
+
<SheetTrigger asChild>
|
|
247
|
+
<Button variant='ghost' size='icon' aria-label='Open menu' className='lg:hidden'>
|
|
248
|
+
<Menu />
|
|
249
|
+
</Button>
|
|
250
|
+
</SheetTrigger>
|
|
251
|
+
<SheetContent side='right' className='w-full max-w-xs p-0'>
|
|
252
|
+
<SheetHeader className='border-border border-b'>
|
|
253
|
+
<SheetTitle className='text-left text-base'>Menu</SheetTitle>
|
|
254
|
+
</SheetHeader>
|
|
255
|
+
|
|
256
|
+
<nav aria-label='Mobile' className='flex flex-col px-2 py-2'>
|
|
257
|
+
{navLinks.map(link => {
|
|
258
|
+
const isActive = link.href !== undefined && link.href === currentPath
|
|
259
|
+
const className = cn(
|
|
260
|
+
'hover:bg-accent hover:text-accent-foreground focus-visible:ring-ring rounded-md px-3 py-3 text-base font-medium transition-colors duration-150 ease-out focus-visible:ring-2 focus-visible:outline-none',
|
|
261
|
+
isActive ? 'text-foreground' : 'text-muted-foreground'
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
return (
|
|
265
|
+
<SheetClose key={link.label} asChild>
|
|
266
|
+
{link.href ? (
|
|
267
|
+
<a href={link.href} aria-current={isActive ? 'page' : undefined} className={className}>
|
|
268
|
+
{link.label}
|
|
269
|
+
</a>
|
|
270
|
+
) : (
|
|
271
|
+
<button type='button' className={className}>
|
|
272
|
+
{link.label}
|
|
273
|
+
</button>
|
|
274
|
+
)}
|
|
275
|
+
</SheetClose>
|
|
276
|
+
)
|
|
277
|
+
})}
|
|
278
|
+
</nav>
|
|
279
|
+
|
|
280
|
+
<div className='mt-auto flex flex-col gap-4 p-4'>
|
|
281
|
+
<Separator />
|
|
282
|
+
<div className='flex flex-col gap-2'>
|
|
283
|
+
{signIn && (
|
|
284
|
+
<SheetClose asChild>
|
|
285
|
+
<ActionButton cta={signIn} onClick={onSignIn} variant='outline' className='w-full' />
|
|
286
|
+
</SheetClose>
|
|
287
|
+
)}
|
|
288
|
+
{cta && (
|
|
289
|
+
<SheetClose asChild>
|
|
290
|
+
<ActionButton cta={cta} onClick={onCtaClick} className='w-full' />
|
|
291
|
+
</SheetClose>
|
|
292
|
+
)}
|
|
293
|
+
</div>
|
|
294
|
+
</div>
|
|
295
|
+
</SheetContent>
|
|
296
|
+
</Sheet>
|
|
297
|
+
</div>
|
|
298
|
+
</header>
|
|
299
|
+
</>
|
|
300
|
+
)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export default Navbar3
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { Heart, HelpCircle, LogOut, MapPin, Package, Settings } from 'lucide-react'
|
|
2
|
+
|
|
3
|
+
import { Avatar, AvatarFallback, AvatarImage } from '../../../ui/avatar'
|
|
4
|
+
import { Button } from '../../../ui/button'
|
|
5
|
+
import {
|
|
6
|
+
DropdownMenu,
|
|
7
|
+
DropdownMenuContent,
|
|
8
|
+
DropdownMenuGroup,
|
|
9
|
+
DropdownMenuItem,
|
|
10
|
+
DropdownMenuLabel,
|
|
11
|
+
DropdownMenuSeparator,
|
|
12
|
+
DropdownMenuTrigger,
|
|
13
|
+
} from '../../../ui/dropdown-menu'
|
|
14
|
+
|
|
15
|
+
export interface StoreUser {
|
|
16
|
+
name: string
|
|
17
|
+
email: string
|
|
18
|
+
avatar: string
|
|
19
|
+
initials: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface AccountMenuItem {
|
|
23
|
+
label: string
|
|
24
|
+
href: string
|
|
25
|
+
description?: string
|
|
26
|
+
icon?: 'orders' | 'wishlist' | 'addresses' | 'settings'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface Navbar4AccountMenuProps {
|
|
30
|
+
/** Signed-in user. When omitted, the menu is not rendered. */
|
|
31
|
+
user?: StoreUser
|
|
32
|
+
/** Account links shown below the user header. Defaults to `[]`. */
|
|
33
|
+
items?: AccountMenuItem[]
|
|
34
|
+
onHelp?: () => void
|
|
35
|
+
onSignOut?: () => void
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const accountIcons = {
|
|
39
|
+
orders: Package,
|
|
40
|
+
wishlist: Heart,
|
|
41
|
+
addresses: MapPin,
|
|
42
|
+
settings: Settings,
|
|
43
|
+
} as const
|
|
44
|
+
|
|
45
|
+
export function Navbar4AccountMenu({
|
|
46
|
+
user,
|
|
47
|
+
items = [],
|
|
48
|
+
onHelp,
|
|
49
|
+
onSignOut,
|
|
50
|
+
}: Navbar4AccountMenuProps) {
|
|
51
|
+
if (!user) {
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<DropdownMenu>
|
|
57
|
+
<DropdownMenuTrigger asChild>
|
|
58
|
+
<Button
|
|
59
|
+
variant='ghost'
|
|
60
|
+
size='icon'
|
|
61
|
+
className='rounded-full'
|
|
62
|
+
aria-label={`Account menu for ${user.name}`}
|
|
63
|
+
>
|
|
64
|
+
<Avatar size='sm' className='pointer-events-none size-8'>
|
|
65
|
+
<AvatarImage src={user.avatar || undefined} alt='' />
|
|
66
|
+
<AvatarFallback>{user.initials}</AvatarFallback>
|
|
67
|
+
</Avatar>
|
|
68
|
+
</Button>
|
|
69
|
+
</DropdownMenuTrigger>
|
|
70
|
+
<DropdownMenuContent
|
|
71
|
+
align='end'
|
|
72
|
+
sideOffset={8}
|
|
73
|
+
className='w-64 origin-(--transform-origin) p-1.5 duration-[250ms] data-closed:duration-150'
|
|
74
|
+
>
|
|
75
|
+
<DropdownMenuGroup>
|
|
76
|
+
<DropdownMenuLabel className='px-2 py-2 font-normal'>
|
|
77
|
+
<div className='flex items-center gap-3'>
|
|
78
|
+
<Avatar size='sm' className='pointer-events-none'>
|
|
79
|
+
<AvatarImage src={user.avatar || undefined} alt='' />
|
|
80
|
+
<AvatarFallback>{user.initials}</AvatarFallback>
|
|
81
|
+
</Avatar>
|
|
82
|
+
<div className='min-w-0'>
|
|
83
|
+
<p className='text-foreground truncate text-sm font-medium'>{user.name}</p>
|
|
84
|
+
<p className='text-muted-foreground truncate text-xs'>{user.email}</p>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</DropdownMenuLabel>
|
|
88
|
+
<DropdownMenuSeparator />
|
|
89
|
+
{items.map(item => {
|
|
90
|
+
const Icon = item.icon ? accountIcons[item.icon] : Package
|
|
91
|
+
return (
|
|
92
|
+
<DropdownMenuItem key={item.href} asChild className='gap-2.5 px-2 py-2'>
|
|
93
|
+
<a href={item.href}>
|
|
94
|
+
<Icon className='text-muted-foreground size-4' />
|
|
95
|
+
<span className='flex min-w-0 flex-col'>
|
|
96
|
+
<span className='text-sm'>{item.label}</span>
|
|
97
|
+
{item.description ? (
|
|
98
|
+
<span className='text-muted-foreground text-xs font-normal'>{item.description}</span>
|
|
99
|
+
) : null}
|
|
100
|
+
</span>
|
|
101
|
+
</a>
|
|
102
|
+
</DropdownMenuItem>
|
|
103
|
+
)
|
|
104
|
+
})}
|
|
105
|
+
<DropdownMenuSeparator />
|
|
106
|
+
<DropdownMenuItem className='gap-2.5 px-2 py-2' onSelect={onHelp}>
|
|
107
|
+
<HelpCircle className='text-muted-foreground size-4' />
|
|
108
|
+
Help & support
|
|
109
|
+
</DropdownMenuItem>
|
|
110
|
+
<DropdownMenuItem variant='destructive' className='gap-2.5 px-2 py-2' onSelect={onSignOut}>
|
|
111
|
+
<LogOut className='size-4' />
|
|
112
|
+
Sign out
|
|
113
|
+
</DropdownMenuItem>
|
|
114
|
+
</DropdownMenuGroup>
|
|
115
|
+
</DropdownMenuContent>
|
|
116
|
+
</DropdownMenu>
|
|
117
|
+
)
|
|
118
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
import { AnimatePresence, motion, useReducedMotion } from 'motion/react'
|
|
5
|
+
import { Minus, Plus, ShoppingBag } from 'lucide-react'
|
|
6
|
+
|
|
7
|
+
import { Button } from '../../../ui/button'
|
|
8
|
+
import {
|
|
9
|
+
Empty,
|
|
10
|
+
EmptyContent,
|
|
11
|
+
EmptyDescription,
|
|
12
|
+
EmptyHeader,
|
|
13
|
+
EmptyMedia,
|
|
14
|
+
EmptyTitle,
|
|
15
|
+
} from '../../../ui/empty'
|
|
16
|
+
import { Separator } from '../../../ui/separator'
|
|
17
|
+
import { SheetClose } from '../../../ui/sheet'
|
|
18
|
+
|
|
19
|
+
export interface CartItem {
|
|
20
|
+
id: string
|
|
21
|
+
name: string
|
|
22
|
+
variant: string
|
|
23
|
+
price: number
|
|
24
|
+
qty: number
|
|
25
|
+
image: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface Navbar4CartSheetProps {
|
|
29
|
+
items: CartItem[]
|
|
30
|
+
onQtyChange: (id: string, delta: number) => void
|
|
31
|
+
onRemove: (id: string) => void
|
|
32
|
+
/** Override price formatting (locale, currency). Defaults to USD. */
|
|
33
|
+
formatPrice?: (value: number) => string
|
|
34
|
+
onCheckout?: () => void
|
|
35
|
+
onViewCart?: () => void
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const usdFormatter = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' })
|
|
39
|
+
|
|
40
|
+
/** Approximate Transitions.dev `delete-with-smoky-dissolve` for list rows (Pro effect). */
|
|
41
|
+
const smokyExit = {
|
|
42
|
+
opacity: 0,
|
|
43
|
+
y: 10,
|
|
44
|
+
scale: 0.96,
|
|
45
|
+
filter: 'blur(8px)',
|
|
46
|
+
transition: { duration: 0.35, ease: [0.4, 0, 0.2, 1] as const },
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Transitions.dev `texts-reveal`: lines rise from below with blur, staggered.
|
|
51
|
+
* Exit is a quiet in-place fade — no Y return, no reverse stagger.
|
|
52
|
+
*/
|
|
53
|
+
const revealEase = [0.22, 1, 0.36, 1] as const
|
|
54
|
+
const revealHidden = { opacity: 0, y: 12, filter: 'blur(3px)' }
|
|
55
|
+
const revealShown = { opacity: 1, y: 0, filter: 'blur(0px)' }
|
|
56
|
+
const revealExit = { opacity: 0, y: 0, filter: 'blur(0px)', transition: { duration: 0.2, ease: 'easeOut' as const } }
|
|
57
|
+
|
|
58
|
+
export function Navbar4CartSheet({
|
|
59
|
+
items,
|
|
60
|
+
onQtyChange,
|
|
61
|
+
onRemove,
|
|
62
|
+
formatPrice,
|
|
63
|
+
onCheckout,
|
|
64
|
+
onViewCart,
|
|
65
|
+
}: Navbar4CartSheetProps) {
|
|
66
|
+
const reduceMotion = useReducedMotion()
|
|
67
|
+
const format = formatPrice ?? (value => usdFormatter.format(value))
|
|
68
|
+
const subtotal = items.reduce((total, item) => total + item.price * item.qty, 0)
|
|
69
|
+
const isEmpty = items.length === 0
|
|
70
|
+
/** Wait for the last cart row's exit before revealing empty copy. */
|
|
71
|
+
const [showEmpty, setShowEmpty] = useState(isEmpty)
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (!isEmpty) {
|
|
75
|
+
setShowEmpty(false)
|
|
76
|
+
}
|
|
77
|
+
}, [isEmpty])
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<>
|
|
81
|
+
<div className='relative flex min-h-0 flex-1 flex-col overflow-hidden'>
|
|
82
|
+
<ul className='flex-1 overflow-y-auto px-4'>
|
|
83
|
+
<AnimatePresence
|
|
84
|
+
initial={false}
|
|
85
|
+
mode='popLayout'
|
|
86
|
+
onExitComplete={() => {
|
|
87
|
+
if (items.length === 0) {
|
|
88
|
+
setShowEmpty(true)
|
|
89
|
+
}
|
|
90
|
+
}}
|
|
91
|
+
>
|
|
92
|
+
{items.map(item => (
|
|
93
|
+
<motion.li
|
|
94
|
+
key={item.id}
|
|
95
|
+
layout={!reduceMotion}
|
|
96
|
+
initial={reduceMotion ? false : { opacity: 0, y: 8, filter: 'blur(4px)' }}
|
|
97
|
+
animate={{ opacity: 1, y: 0, filter: 'blur(0px)' }}
|
|
98
|
+
exit={reduceMotion ? { opacity: 0 } : smokyExit}
|
|
99
|
+
transition={{ duration: 0.28, ease: [0.22, 1, 0.36, 1] }}
|
|
100
|
+
className='border-border flex gap-4 border-b py-4 last:border-b-0'
|
|
101
|
+
style={{ willChange: 'transform, opacity, filter' }}
|
|
102
|
+
>
|
|
103
|
+
<img
|
|
104
|
+
src={item.image || undefined}
|
|
105
|
+
alt=''
|
|
106
|
+
width={80}
|
|
107
|
+
height={96}
|
|
108
|
+
className='bg-muted size-20 shrink-0 rounded-md object-cover'
|
|
109
|
+
/>
|
|
110
|
+
<div className='flex min-w-0 flex-1 flex-col'>
|
|
111
|
+
<div className='flex items-start justify-between gap-2'>
|
|
112
|
+
<div className='min-w-0'>
|
|
113
|
+
<p className='truncate text-sm font-medium'>{item.name}</p>
|
|
114
|
+
<p className='text-muted-foreground text-sm'>{item.variant}</p>
|
|
115
|
+
</div>
|
|
116
|
+
<p className='text-sm font-medium'>{format(item.price * item.qty)}</p>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<div className='mt-auto flex items-center justify-between pt-2'>
|
|
120
|
+
<div className='border-border flex items-center rounded-md border'>
|
|
121
|
+
<Button
|
|
122
|
+
variant='ghost'
|
|
123
|
+
size='icon'
|
|
124
|
+
className='size-8 rounded-r-none'
|
|
125
|
+
aria-label={`Decrease quantity, ${item.name}`}
|
|
126
|
+
onClick={() => onQtyChange(item.id, -1)}
|
|
127
|
+
>
|
|
128
|
+
<Minus />
|
|
129
|
+
</Button>
|
|
130
|
+
<span className='w-8 text-center text-sm tabular-nums'>{item.qty}</span>
|
|
131
|
+
<Button
|
|
132
|
+
variant='ghost'
|
|
133
|
+
size='icon'
|
|
134
|
+
className='size-8 rounded-l-none'
|
|
135
|
+
aria-label={`Increase quantity, ${item.name}`}
|
|
136
|
+
onClick={() => onQtyChange(item.id, 1)}
|
|
137
|
+
>
|
|
138
|
+
<Plus />
|
|
139
|
+
</Button>
|
|
140
|
+
</div>
|
|
141
|
+
<Button
|
|
142
|
+
variant='ghost'
|
|
143
|
+
size='sm'
|
|
144
|
+
className='text-muted-foreground hover:text-foreground'
|
|
145
|
+
aria-label={`Remove ${item.name} from cart`}
|
|
146
|
+
onClick={() => onRemove(item.id)}
|
|
147
|
+
>
|
|
148
|
+
Remove
|
|
149
|
+
</Button>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
</motion.li>
|
|
153
|
+
))}
|
|
154
|
+
</AnimatePresence>
|
|
155
|
+
</ul>
|
|
156
|
+
|
|
157
|
+
<AnimatePresence>
|
|
158
|
+
{showEmpty ? (
|
|
159
|
+
<motion.div
|
|
160
|
+
key='empty'
|
|
161
|
+
initial={false}
|
|
162
|
+
exit={reduceMotion ? { opacity: 0 } : revealExit}
|
|
163
|
+
className='absolute inset-0'
|
|
164
|
+
>
|
|
165
|
+
<Empty className='h-full border-0'>
|
|
166
|
+
<EmptyHeader>
|
|
167
|
+
<motion.div
|
|
168
|
+
initial={reduceMotion ? false : revealHidden}
|
|
169
|
+
animate={revealShown}
|
|
170
|
+
transition={
|
|
171
|
+
reduceMotion
|
|
172
|
+
? { duration: 0 }
|
|
173
|
+
: { duration: 0.5, ease: revealEase, delay: 0 }
|
|
174
|
+
}
|
|
175
|
+
style={{ willChange: 'transform, opacity, filter' }}
|
|
176
|
+
>
|
|
177
|
+
<EmptyMedia variant='icon'>
|
|
178
|
+
<ShoppingBag aria-hidden='true' />
|
|
179
|
+
</EmptyMedia>
|
|
180
|
+
</motion.div>
|
|
181
|
+
<motion.div
|
|
182
|
+
initial={reduceMotion ? false : revealHidden}
|
|
183
|
+
animate={revealShown}
|
|
184
|
+
transition={
|
|
185
|
+
reduceMotion
|
|
186
|
+
? { duration: 0 }
|
|
187
|
+
: { duration: 0.5, ease: revealEase, delay: 0.04 }
|
|
188
|
+
}
|
|
189
|
+
style={{ willChange: 'transform, opacity, filter' }}
|
|
190
|
+
>
|
|
191
|
+
<EmptyTitle>Your cart is empty</EmptyTitle>
|
|
192
|
+
</motion.div>
|
|
193
|
+
<motion.div
|
|
194
|
+
initial={reduceMotion ? false : revealHidden}
|
|
195
|
+
animate={revealShown}
|
|
196
|
+
transition={
|
|
197
|
+
reduceMotion
|
|
198
|
+
? { duration: 0 }
|
|
199
|
+
: { duration: 0.5, ease: revealEase, delay: 0.08 }
|
|
200
|
+
}
|
|
201
|
+
style={{ willChange: 'transform, opacity, filter' }}
|
|
202
|
+
>
|
|
203
|
+
<EmptyDescription>Once you add something it will show up here.</EmptyDescription>
|
|
204
|
+
</motion.div>
|
|
205
|
+
</EmptyHeader>
|
|
206
|
+
<motion.div
|
|
207
|
+
initial={reduceMotion ? false : revealHidden}
|
|
208
|
+
animate={revealShown}
|
|
209
|
+
transition={
|
|
210
|
+
reduceMotion
|
|
211
|
+
? { duration: 0 }
|
|
212
|
+
: { duration: 0.5, ease: revealEase, delay: 0.12 }
|
|
213
|
+
}
|
|
214
|
+
style={{ willChange: 'transform, opacity, filter' }}
|
|
215
|
+
>
|
|
216
|
+
<EmptyContent>
|
|
217
|
+
<SheetClose asChild>
|
|
218
|
+
<Button>Continue shopping</Button>
|
|
219
|
+
</SheetClose>
|
|
220
|
+
</EmptyContent>
|
|
221
|
+
</motion.div>
|
|
222
|
+
</Empty>
|
|
223
|
+
</motion.div>
|
|
224
|
+
) : null}
|
|
225
|
+
</AnimatePresence>
|
|
226
|
+
</div>
|
|
227
|
+
|
|
228
|
+
{!isEmpty ? (
|
|
229
|
+
<div className='flex flex-col gap-4 p-4'>
|
|
230
|
+
<Separator />
|
|
231
|
+
<div className='flex items-center justify-between text-sm'>
|
|
232
|
+
<span className='text-muted-foreground'>Subtotal</span>
|
|
233
|
+
<span className='font-medium tabular-nums'>{format(subtotal)}</span>
|
|
234
|
+
</div>
|
|
235
|
+
<p className='text-muted-foreground text-xs'>Shipping and taxes are calculated at checkout.</p>
|
|
236
|
+
<div className='flex flex-col gap-2'>
|
|
237
|
+
<Button className='w-full' onClick={onCheckout}>
|
|
238
|
+
Checkout
|
|
239
|
+
</Button>
|
|
240
|
+
<Button variant='ghost' className='w-full' onClick={onViewCart}>
|
|
241
|
+
View cart
|
|
242
|
+
</Button>
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
) : null}
|
|
246
|
+
</>
|
|
247
|
+
)
|
|
248
|
+
}
|