@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,411 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
4
|
+
import { AnimatePresence, motion, useReducedMotion } from 'motion/react'
|
|
5
|
+
import { Menu, Search, ShoppingBag, X } from 'lucide-react'
|
|
6
|
+
|
|
7
|
+
import { Button } from '../../../ui/button'
|
|
8
|
+
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '../../../ui/sheet'
|
|
9
|
+
import { cn } from '../../../lib/utils'
|
|
10
|
+
|
|
11
|
+
import { Navbar4AccountMenu, type AccountMenuItem, type StoreUser } from './navbar-4-account-menu'
|
|
12
|
+
import { Navbar4CartSheet, type CartItem } from './navbar-4-cart-sheet'
|
|
13
|
+
import type { UtilityLink } from './navbar-4-mobile-nav'
|
|
14
|
+
import { Navbar4SearchBar } from './navbar-4-search-bar'
|
|
15
|
+
import { MegaMenu } from '../../e-commerce/navigation/mega-menu'
|
|
16
|
+
import { MobileCategoryDrawer } from '../../e-commerce/navigation/mobile-category-drawer'
|
|
17
|
+
import type { MenuItem } from '../../../lib/types/menu'
|
|
18
|
+
import type { StoreCategory } from '../../../lib/contracts'
|
|
19
|
+
|
|
20
|
+
function toMenuItem(category: StoreCategory): MenuItem {
|
|
21
|
+
return {
|
|
22
|
+
id: category.href,
|
|
23
|
+
label: category.label,
|
|
24
|
+
href: category.href,
|
|
25
|
+
children: category.children?.map(toMenuItem),
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Drawer entries come from three sources (categories, utility links, account
|
|
31
|
+
* link). Hosts may reuse the same href across sources (e.g. a placeholder
|
|
32
|
+
* href), which would produce duplicate React keys if passed through as-is.
|
|
33
|
+
* Dedupe by href so every rendered item gets a unique id.
|
|
34
|
+
*/
|
|
35
|
+
function uniqueById<T extends { id: string }>(items: T[]): T[] {
|
|
36
|
+
const seen = new Set<string>()
|
|
37
|
+
return items.filter((item) => {
|
|
38
|
+
if (seen.has(item.id)) return false
|
|
39
|
+
seen.add(item.id)
|
|
40
|
+
return true
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function Logo({ text, href }: { text: string; href?: string }) {
|
|
45
|
+
const className =
|
|
46
|
+
'focus-visible:ring-ring flex items-center gap-2 rounded-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none'
|
|
47
|
+
|
|
48
|
+
const content = (
|
|
49
|
+
<>
|
|
50
|
+
<svg viewBox='0 0 32 32' aria-hidden='true' className='text-primary size-7' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
|
51
|
+
<path
|
|
52
|
+
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'
|
|
53
|
+
fill='currentColor'
|
|
54
|
+
/>
|
|
55
|
+
<path
|
|
56
|
+
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'
|
|
57
|
+
fill='currentColor'
|
|
58
|
+
/>
|
|
59
|
+
<path
|
|
60
|
+
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'
|
|
61
|
+
fill='currentColor'
|
|
62
|
+
/>
|
|
63
|
+
<path
|
|
64
|
+
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'
|
|
65
|
+
fill='currentColor'
|
|
66
|
+
/>
|
|
67
|
+
<path
|
|
68
|
+
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'
|
|
69
|
+
fill='currentColor'
|
|
70
|
+
/>
|
|
71
|
+
<path d='M21.1694 10.5806L14.5651 17.1849' stroke='currentColor' strokeWidth='1.5' strokeLinecap='round' />
|
|
72
|
+
<path d='M22.1694 14.5806L18.5632 18.1868' stroke='currentColor' strokeWidth='1.5' strokeLinecap='round' />
|
|
73
|
+
<circle cx='13.1' cy='26.1' r='1.7' stroke='currentColor' strokeWidth='1.2' />
|
|
74
|
+
<circle cx='22.1' cy='26.1' r='1.7' stroke='currentColor' strokeWidth='1.2' />
|
|
75
|
+
</svg>
|
|
76
|
+
<span className='text-base font-semibold tracking-tight'>{text}</span>
|
|
77
|
+
</>
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if (!href) {
|
|
81
|
+
return <span className={className}>{content}</span>
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<a href={href} className={className}>
|
|
86
|
+
{content}
|
|
87
|
+
</a>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
type CartBadgeProps = {
|
|
92
|
+
count: number
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Transitions.dev `notification-badge`: diagonal slide + spring pop when the
|
|
97
|
+
* count appears or changes. Positioned outside the bag so it never covers the icon.
|
|
98
|
+
*/
|
|
99
|
+
function CartBadge({ count }: CartBadgeProps) {
|
|
100
|
+
const reduceMotion = useReducedMotion()
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<AnimatePresence>
|
|
104
|
+
{count > 0 ? (
|
|
105
|
+
<motion.span
|
|
106
|
+
key='badge-shell'
|
|
107
|
+
className='bg-foreground text-background pointer-events-none absolute -top-2.5 -end-3.5 flex size-[1.125rem] items-center justify-center overflow-hidden rounded-full text-[10px] font-medium tabular-nums shadow-sm ring-2 ring-background'
|
|
108
|
+
initial={
|
|
109
|
+
reduceMotion
|
|
110
|
+
? false
|
|
111
|
+
: { scale: 0, x: -8.2, y: 12.4, opacity: 0, filter: 'blur(2px)' }
|
|
112
|
+
}
|
|
113
|
+
animate={{ scale: 1, x: 0, y: 0, opacity: 1, filter: 'blur(0px)' }}
|
|
114
|
+
exit={
|
|
115
|
+
reduceMotion
|
|
116
|
+
? { opacity: 0 }
|
|
117
|
+
: {
|
|
118
|
+
scale: 0,
|
|
119
|
+
opacity: 0,
|
|
120
|
+
filter: 'blur(2px)',
|
|
121
|
+
transition: { duration: 0.18, ease: [0.4, 0, 0.2, 1] },
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
transition={
|
|
125
|
+
reduceMotion
|
|
126
|
+
? { duration: 0 }
|
|
127
|
+
: { type: 'spring', stiffness: 520, damping: 18, mass: 0.6 }
|
|
128
|
+
}
|
|
129
|
+
style={{ willChange: 'transform, opacity, filter' }}
|
|
130
|
+
>
|
|
131
|
+
<AnimatePresence mode='popLayout' initial={false}>
|
|
132
|
+
<motion.span
|
|
133
|
+
key={count}
|
|
134
|
+
className='block'
|
|
135
|
+
initial={reduceMotion ? false : { y: 8, opacity: 0, filter: 'blur(2px)' }}
|
|
136
|
+
animate={{ y: 0, opacity: 1, filter: 'blur(0px)' }}
|
|
137
|
+
exit={reduceMotion ? undefined : { y: -8, opacity: 0, filter: 'blur(2px)' }}
|
|
138
|
+
transition={{ duration: 0.22, ease: [0.22, 1, 0.36, 1] }}
|
|
139
|
+
>
|
|
140
|
+
{count > 99 ? '99+' : count}
|
|
141
|
+
</motion.span>
|
|
142
|
+
</AnimatePresence>
|
|
143
|
+
</motion.span>
|
|
144
|
+
) : null}
|
|
145
|
+
</AnimatePresence>
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface Navbar4Props {
|
|
150
|
+
storeCategories: StoreCategory[]
|
|
151
|
+
shippingNote: string
|
|
152
|
+
utilityLinks: UtilityLink[]
|
|
153
|
+
logoText: string
|
|
154
|
+
logoHref?: string
|
|
155
|
+
/** When provided, a mobile-only "Account" link is appended to the utility links. */
|
|
156
|
+
accountHref?: string
|
|
157
|
+
searchQuery?: string
|
|
158
|
+
onSearchChange?: (value: string) => void
|
|
159
|
+
items?: CartItem[]
|
|
160
|
+
onQtyChange?: (id: string, delta: number) => void
|
|
161
|
+
onRemove?: (id: string) => void
|
|
162
|
+
user?: StoreUser
|
|
163
|
+
accountMenuItems?: AccountMenuItem[]
|
|
164
|
+
onHelp?: () => void
|
|
165
|
+
onSignOut?: () => void
|
|
166
|
+
onCheckout?: () => void
|
|
167
|
+
onViewCart?: () => void
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function Navbar4({
|
|
171
|
+
storeCategories,
|
|
172
|
+
shippingNote,
|
|
173
|
+
utilityLinks,
|
|
174
|
+
logoText,
|
|
175
|
+
logoHref,
|
|
176
|
+
accountHref,
|
|
177
|
+
searchQuery: searchQueryProp,
|
|
178
|
+
onSearchChange,
|
|
179
|
+
items: itemsProp,
|
|
180
|
+
onQtyChange: onQtyChangeProp,
|
|
181
|
+
onRemove: onRemoveProp,
|
|
182
|
+
user,
|
|
183
|
+
accountMenuItems,
|
|
184
|
+
onHelp,
|
|
185
|
+
onSignOut,
|
|
186
|
+
onCheckout,
|
|
187
|
+
onViewCart,
|
|
188
|
+
}: Navbar4Props) {
|
|
189
|
+
/**
|
|
190
|
+
* Integration seam
|
|
191
|
+
* ----------------
|
|
192
|
+
* Pass cart + search from your storefront, or leave uncontrolled for a
|
|
193
|
+
* self-contained demo. Controlled props win when provided.
|
|
194
|
+
*/
|
|
195
|
+
const [internalItems, setInternalItems] = useState<CartItem[]>([])
|
|
196
|
+
const [internalSearch, setInternalSearch] = useState('')
|
|
197
|
+
const [searchOpen, setSearchOpen] = useState(false)
|
|
198
|
+
const [menuOpen, setMenuOpen] = useState(false)
|
|
199
|
+
const searchTriggerRef = useRef<HTMLButtonElement>(null)
|
|
200
|
+
|
|
201
|
+
const items = itemsProp ?? internalItems
|
|
202
|
+
const searchQuery = searchQueryProp ?? internalSearch
|
|
203
|
+
|
|
204
|
+
const setSearchQuery = (value: string) => {
|
|
205
|
+
onSearchChange?.(value)
|
|
206
|
+
if (searchQueryProp === undefined) {
|
|
207
|
+
setInternalSearch(value)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const changeQty =
|
|
212
|
+
onQtyChangeProp ??
|
|
213
|
+
((id: string, delta: number) =>
|
|
214
|
+
setInternalItems(current =>
|
|
215
|
+
current.flatMap(item => {
|
|
216
|
+
if (item.id !== id) {
|
|
217
|
+
return item
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const qty = item.qty + delta
|
|
221
|
+
return qty <= 0 ? [] : [{ ...item, qty }]
|
|
222
|
+
}),
|
|
223
|
+
))
|
|
224
|
+
|
|
225
|
+
const removeItem =
|
|
226
|
+
onRemoveProp ?? ((id: string) => setInternalItems(current => current.filter(item => item.id !== id)))
|
|
227
|
+
|
|
228
|
+
const count = useMemo(() => items.reduce((total, item) => total + item.qty, 0), [items])
|
|
229
|
+
|
|
230
|
+
useEffect(() => {
|
|
231
|
+
if (!searchOpen) {
|
|
232
|
+
return
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
document.getElementById('store-search-mobile')?.focus()
|
|
236
|
+
}, [searchOpen])
|
|
237
|
+
|
|
238
|
+
const dismissSearch = () => {
|
|
239
|
+
setSearchOpen(false)
|
|
240
|
+
searchTriggerRef.current?.focus()
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return (
|
|
244
|
+
<header className='border-border bg-background/95 sticky top-0 z-40 w-full border-b backdrop-blur-md supports-backdrop-filter:bg-background/80'>
|
|
245
|
+
<div className='bg-foreground text-background hidden lg:block'>
|
|
246
|
+
<div className='mx-auto flex h-9 w-full max-w-7xl items-center justify-between px-4 text-xs tracking-wide sm:px-6 lg:px-8'>
|
|
247
|
+
<p className='text-background/80'>{shippingNote}</p>
|
|
248
|
+
<ul className='flex items-center gap-5'>
|
|
249
|
+
{utilityLinks.map(link => (
|
|
250
|
+
<li key={`${link.href}:${link.label}`}>
|
|
251
|
+
<a
|
|
252
|
+
href={link.href}
|
|
253
|
+
className='text-background/80 hover:text-background transition-colors duration-150'
|
|
254
|
+
>
|
|
255
|
+
{link.label}
|
|
256
|
+
</a>
|
|
257
|
+
</li>
|
|
258
|
+
))}
|
|
259
|
+
</ul>
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
262
|
+
|
|
263
|
+
<div className='mx-auto flex h-16 w-full max-w-7xl items-center gap-3 px-4 sm:px-6 lg:px-8'>
|
|
264
|
+
<Button
|
|
265
|
+
variant='ghost'
|
|
266
|
+
size='icon'
|
|
267
|
+
className='lg:hidden'
|
|
268
|
+
aria-label='Open menu'
|
|
269
|
+
aria-expanded={menuOpen}
|
|
270
|
+
onClick={() => setMenuOpen(open => !open)}
|
|
271
|
+
>
|
|
272
|
+
<Menu />
|
|
273
|
+
</Button>
|
|
274
|
+
|
|
275
|
+
<MobileCategoryDrawer
|
|
276
|
+
categories={uniqueById([
|
|
277
|
+
...storeCategories.map(toMenuItem),
|
|
278
|
+
...utilityLinks.map(link => ({ id: link.href, label: link.label, href: link.href })),
|
|
279
|
+
...(accountHref ? [{ id: accountHref, label: 'Account', href: accountHref }] : []),
|
|
280
|
+
])}
|
|
281
|
+
collections={[]}
|
|
282
|
+
open={menuOpen}
|
|
283
|
+
onClose={() => setMenuOpen(false)}
|
|
284
|
+
viewAllHref=''
|
|
285
|
+
/>
|
|
286
|
+
|
|
287
|
+
<div className='flex-1 lg:flex-none'>
|
|
288
|
+
<Logo text={logoText} href={logoHref} />
|
|
289
|
+
</div>
|
|
290
|
+
|
|
291
|
+
<nav aria-label='Store' className='hidden flex-1 lg:block'>
|
|
292
|
+
<ul className='flex items-center gap-0.5'>
|
|
293
|
+
{storeCategories.map(category =>
|
|
294
|
+
category.children && category.children.length > 0 ? (
|
|
295
|
+
<li key={category.href} className='group relative'>
|
|
296
|
+
<a
|
|
297
|
+
href={category.href}
|
|
298
|
+
className={cn(
|
|
299
|
+
'hover:bg-muted focus-visible:ring-ring rounded-md px-3 py-2 text-sm font-medium transition-colors duration-150 focus-visible:ring-2 focus-visible:outline-none',
|
|
300
|
+
category.emphasis ? 'text-destructive' : 'text-foreground',
|
|
301
|
+
)}
|
|
302
|
+
>
|
|
303
|
+
{category.label}
|
|
304
|
+
</a>
|
|
305
|
+
<div className='invisible absolute left-0 top-full z-50 pt-2 opacity-0 transition-opacity duration-150 group-hover:visible group-hover:opacity-100'>
|
|
306
|
+
<MegaMenu
|
|
307
|
+
categories={[toMenuItem(category)]}
|
|
308
|
+
collections={[]}
|
|
309
|
+
collectionsHref=''
|
|
310
|
+
categoryColumnTitle={category.label}
|
|
311
|
+
/>
|
|
312
|
+
</div>
|
|
313
|
+
</li>
|
|
314
|
+
) : (
|
|
315
|
+
<li key={category.href}>
|
|
316
|
+
<a
|
|
317
|
+
href={category.href}
|
|
318
|
+
className={cn(
|
|
319
|
+
'hover:bg-muted focus-visible:ring-ring rounded-md px-3 py-2 text-sm font-medium transition-colors duration-150 focus-visible:ring-2 focus-visible:outline-none',
|
|
320
|
+
category.emphasis ? 'text-destructive' : 'text-foreground',
|
|
321
|
+
)}
|
|
322
|
+
>
|
|
323
|
+
{category.label}
|
|
324
|
+
</a>
|
|
325
|
+
</li>
|
|
326
|
+
),
|
|
327
|
+
)}
|
|
328
|
+
</ul>
|
|
329
|
+
</nav>
|
|
330
|
+
|
|
331
|
+
<div className='flex items-center gap-1.5'>
|
|
332
|
+
<div className='hidden lg:block lg:w-64'>
|
|
333
|
+
<Navbar4SearchBar
|
|
334
|
+
id='store-search'
|
|
335
|
+
value={searchQuery}
|
|
336
|
+
onValueChange={setSearchQuery}
|
|
337
|
+
/>
|
|
338
|
+
</div>
|
|
339
|
+
|
|
340
|
+
<Button
|
|
341
|
+
ref={searchTriggerRef}
|
|
342
|
+
variant='ghost'
|
|
343
|
+
size='icon'
|
|
344
|
+
className='lg:hidden'
|
|
345
|
+
aria-label='Search products'
|
|
346
|
+
aria-expanded={searchOpen}
|
|
347
|
+
onClick={() => setSearchOpen(open => !open)}
|
|
348
|
+
>
|
|
349
|
+
{searchOpen ? <X /> : <Search />}
|
|
350
|
+
</Button>
|
|
351
|
+
|
|
352
|
+
{user ? (
|
|
353
|
+
<div className='hidden lg:block'>
|
|
354
|
+
<Navbar4AccountMenu
|
|
355
|
+
user={user}
|
|
356
|
+
items={accountMenuItems}
|
|
357
|
+
onHelp={onHelp}
|
|
358
|
+
onSignOut={onSignOut}
|
|
359
|
+
/>
|
|
360
|
+
</div>
|
|
361
|
+
) : null}
|
|
362
|
+
|
|
363
|
+
<Sheet>
|
|
364
|
+
<SheetTrigger asChild>
|
|
365
|
+
<Button
|
|
366
|
+
variant='ghost'
|
|
367
|
+
size='icon'
|
|
368
|
+
className='relative overflow-visible'
|
|
369
|
+
aria-label={`Cart, ${count} ${count === 1 ? 'item' : 'items'}`}
|
|
370
|
+
>
|
|
371
|
+
<ShoppingBag className='size-5' />
|
|
372
|
+
<CartBadge count={count} />
|
|
373
|
+
</Button>
|
|
374
|
+
</SheetTrigger>
|
|
375
|
+
<SheetContent side='right' className='flex w-full max-w-sm flex-col gap-0 p-0'>
|
|
376
|
+
<SheetHeader className='border-border border-b'>
|
|
377
|
+
<SheetTitle className='text-left text-base'>
|
|
378
|
+
Cart{count > 0 ? ` (${count})` : ''}
|
|
379
|
+
</SheetTitle>
|
|
380
|
+
</SheetHeader>
|
|
381
|
+
<Navbar4CartSheet
|
|
382
|
+
items={items}
|
|
383
|
+
onQtyChange={changeQty}
|
|
384
|
+
onRemove={removeItem}
|
|
385
|
+
onCheckout={onCheckout}
|
|
386
|
+
onViewCart={onViewCart}
|
|
387
|
+
/>
|
|
388
|
+
</SheetContent>
|
|
389
|
+
</Sheet>
|
|
390
|
+
</div>
|
|
391
|
+
</div>
|
|
392
|
+
|
|
393
|
+
<p aria-live='polite' className='sr-only'>
|
|
394
|
+
{`Cart contains ${count} ${count === 1 ? 'item' : 'items'}`}
|
|
395
|
+
</p>
|
|
396
|
+
|
|
397
|
+
{searchOpen ? (
|
|
398
|
+
<div className='border-border flex flex-col gap-0 border-t p-4 lg:hidden'>
|
|
399
|
+
<Navbar4SearchBar
|
|
400
|
+
id='store-search-mobile'
|
|
401
|
+
value={searchQuery}
|
|
402
|
+
onValueChange={setSearchQuery}
|
|
403
|
+
onDismiss={dismissSearch}
|
|
404
|
+
/>
|
|
405
|
+
</div>
|
|
406
|
+
) : null}
|
|
407
|
+
</header>
|
|
408
|
+
)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export default Navbar4
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { ArrowRight, Check } from 'lucide-react'
|
|
2
|
+
import { Button } from '../../../ui/button'
|
|
3
|
+
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '../../../ui/card'
|
|
4
|
+
import { cn } from '../../../lib/utils'
|
|
5
|
+
import { Badge } from '../../../ui/badge'
|
|
6
|
+
|
|
7
|
+
export interface PricingTier {
|
|
8
|
+
id: string
|
|
9
|
+
name: string
|
|
10
|
+
description: string
|
|
11
|
+
/** Precio sin símbolo de moneda; el símbolo se pasa aparte en `currencySymbol`. */
|
|
12
|
+
price: string
|
|
13
|
+
/** Frecuencia de facturación, p. ej. '/mes'. */
|
|
14
|
+
frequency: string
|
|
15
|
+
features: string[]
|
|
16
|
+
popular?: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface PricingSection1Props {
|
|
20
|
+
tiers: PricingTier[]
|
|
21
|
+
currencySymbol?: string
|
|
22
|
+
sectionTitle?: string
|
|
23
|
+
sectionLabel?: string
|
|
24
|
+
popularBadgeLabel?: string
|
|
25
|
+
getStartedLabel?: string
|
|
26
|
+
onSelectTier?: (tier: PricingTier) => void
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function PricingSection1({
|
|
30
|
+
tiers,
|
|
31
|
+
currencySymbol = '$',
|
|
32
|
+
sectionTitle = 'El plan perfecto para tu negocio',
|
|
33
|
+
sectionLabel = 'Nuestros precios',
|
|
34
|
+
popularBadgeLabel = 'Popular',
|
|
35
|
+
getStartedLabel = 'Empezar',
|
|
36
|
+
onSelectTier,
|
|
37
|
+
}: PricingSection1Props) {
|
|
38
|
+
return (
|
|
39
|
+
<section className='py-16' id='pricing'>
|
|
40
|
+
<div className='mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8'>
|
|
41
|
+
<header className='mb-12 text-center'>
|
|
42
|
+
<p className='text-muted-foreground mb-2 text-sm font-medium'>{sectionLabel}</p>
|
|
43
|
+
<h2 className='text-3xl font-bold text-balance lg:text-5xl'>{sectionTitle}</h2>
|
|
44
|
+
</header>
|
|
45
|
+
|
|
46
|
+
<div className='grid gap-6 lg:grid-cols-3 lg:gap-8'>
|
|
47
|
+
{tiers.map(tier => (
|
|
48
|
+
<Card key={tier.id} className={cn('overflow-hidden py-6', { 'shadow-lg': tier.popular })}>
|
|
49
|
+
<CardHeader className='px-6'>
|
|
50
|
+
<div className='flex items-center justify-between gap-4'>
|
|
51
|
+
<CardTitle className='text-xl font-bold lg:text-2xl'>{tier.name}</CardTitle>
|
|
52
|
+
{tier.popular && (
|
|
53
|
+
<Badge className='px-2.5 py-0.5 font-semibold rounded-full'>{popularBadgeLabel}</Badge>
|
|
54
|
+
)}
|
|
55
|
+
</div>
|
|
56
|
+
<p className='text-muted-foreground text-sm'>{tier.description}</p>
|
|
57
|
+
</CardHeader>
|
|
58
|
+
<CardContent className='flex flex-col flex-1 px-6'>
|
|
59
|
+
<div className='mb-6 flex items-baseline gap-2'>
|
|
60
|
+
<span className='text-3xl font-bold lg:text-5xl'>
|
|
61
|
+
{currencySymbol}
|
|
62
|
+
{tier.price}
|
|
63
|
+
</span>
|
|
64
|
+
<span className='text-muted-foreground text-sm'>{tier.frequency}</span>
|
|
65
|
+
</div>
|
|
66
|
+
<ul className='flex flex-col gap-3'>
|
|
67
|
+
{tier.features.map(feature => (
|
|
68
|
+
<li key={feature} className='flex items-center gap-2'>
|
|
69
|
+
<Check className='size-4 shrink-0' />
|
|
70
|
+
<span className='text-muted-foreground text-sm'>{feature}</span>
|
|
71
|
+
</li>
|
|
72
|
+
))}
|
|
73
|
+
</ul>
|
|
74
|
+
</CardContent>
|
|
75
|
+
<CardFooter className='border-0 bg-transparent px-6 pb-6'>
|
|
76
|
+
<Button
|
|
77
|
+
className='h-10 px-8 w-full cursor-pointer gap-2'
|
|
78
|
+
size='lg'
|
|
79
|
+
variant={tier.popular ? 'default' : 'outline'}
|
|
80
|
+
aria-label={`${getStartedLabel} con ${tier.name}`}
|
|
81
|
+
onClick={onSelectTier ? () => onSelectTier(tier) : undefined}
|
|
82
|
+
>
|
|
83
|
+
{getStartedLabel} <ArrowRight />
|
|
84
|
+
</Button>
|
|
85
|
+
</CardFooter>
|
|
86
|
+
</Card>
|
|
87
|
+
))}
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
</section>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export default PricingSection1
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Card, CardContent, CardHeader, CardTitle } from '../../../ui/card'
|
|
2
|
+
import { Badge } from '../../../ui/badge'
|
|
3
|
+
import { Progress } from '../../../ui/progress'
|
|
4
|
+
import { BarChart3 } from 'lucide-react'
|
|
5
|
+
import type { LucideIcon } from 'lucide-react'
|
|
6
|
+
|
|
7
|
+
export interface StatisticItem {
|
|
8
|
+
id: string
|
|
9
|
+
title: string
|
|
10
|
+
value: string
|
|
11
|
+
description: string
|
|
12
|
+
progress: number
|
|
13
|
+
progressLabel: string
|
|
14
|
+
icon: LucideIcon
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface Statistic1Props {
|
|
18
|
+
items: StatisticItem[]
|
|
19
|
+
badge?: string
|
|
20
|
+
title?: string
|
|
21
|
+
description?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function Statistic1({ items, badge, title, description }: Statistic1Props) {
|
|
25
|
+
return (
|
|
26
|
+
<section className='py-16 md:py-20'>
|
|
27
|
+
<div className='mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8'>
|
|
28
|
+
<div className='flex flex-col gap-16'>
|
|
29
|
+
{(badge || title || description) && (
|
|
30
|
+
<div className='flex flex-col gap-6 text-center'>
|
|
31
|
+
{badge && (
|
|
32
|
+
<Badge
|
|
33
|
+
variant='outline'
|
|
34
|
+
className='h-auto [&>svg]:size-4! rounded-md inline-flex items-center gap-2 px-4 py-2 text-sm font-medium self-center'
|
|
35
|
+
>
|
|
36
|
+
<BarChart3 />
|
|
37
|
+
{badge}
|
|
38
|
+
</Badge>
|
|
39
|
+
)}
|
|
40
|
+
{title && (
|
|
41
|
+
<h2 className='text-4xl font-bold tracking-tight text-balance lg:text-5xl'>
|
|
42
|
+
{title}
|
|
43
|
+
</h2>
|
|
44
|
+
)}
|
|
45
|
+
{description && (
|
|
46
|
+
<p className='text-muted-foreground mx-auto max-w-2xl text-lg leading-relaxed'>
|
|
47
|
+
{description}
|
|
48
|
+
</p>
|
|
49
|
+
)}
|
|
50
|
+
</div>
|
|
51
|
+
)}
|
|
52
|
+
|
|
53
|
+
<div className='grid gap-6 sm:grid-cols-2 md:grid-cols-3'>
|
|
54
|
+
{items.map(item => (
|
|
55
|
+
<Card
|
|
56
|
+
key={item.id}
|
|
57
|
+
className='group relative gap-2 overflow-hidden py-6 transition-all duration-300 hover:shadow-lg'
|
|
58
|
+
>
|
|
59
|
+
<CardHeader className='px-6'>
|
|
60
|
+
<div className='flex items-start justify-between'>
|
|
61
|
+
<div className='flex-1'>
|
|
62
|
+
<CardTitle className='text-muted-foreground text-sm font-medium text-balance'>
|
|
63
|
+
{item.title}
|
|
64
|
+
</CardTitle>
|
|
65
|
+
<div className='mt-2'>
|
|
66
|
+
<div className='text-2xl font-bold'>{item.value}</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
<div className='bg-muted rounded-xl p-3'>
|
|
70
|
+
<item.icon className='text-muted-foreground size-6' />
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
<p className='text-muted-foreground mt-1 text-xs'>{item.description}</p>
|
|
74
|
+
</CardHeader>
|
|
75
|
+
<CardContent className='pt-0 px-6'>
|
|
76
|
+
<div className='flex flex-col gap-2'>
|
|
77
|
+
<div className='flex items-center justify-between text-xs'>
|
|
78
|
+
<span className='text-muted-foreground'>{item.progressLabel}</span>
|
|
79
|
+
<span className='font-medium'>{item.progress}%</span>
|
|
80
|
+
</div>
|
|
81
|
+
<Progress value={item.progress} className='h-2' />
|
|
82
|
+
</div>
|
|
83
|
+
</CardContent>
|
|
84
|
+
</Card>
|
|
85
|
+
))}
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</section>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export default Statistic1
|