@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,205 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
import { ArrowRight, Sparkles } from 'lucide-react'
|
|
5
|
+
|
|
6
|
+
import { Badge } from '../../../ui/badge'
|
|
7
|
+
import { Button } from '../../../ui/button'
|
|
8
|
+
import { Card, CardContent } from '../../../ui/card'
|
|
9
|
+
|
|
10
|
+
export interface HeroDeal {
|
|
11
|
+
/** Unique identifier for the deal. */
|
|
12
|
+
id: string
|
|
13
|
+
/** Deal name shown in the badge, e.g. "Summer Sale". */
|
|
14
|
+
title: string
|
|
15
|
+
/** Discount label, e.g. "50% OFF". */
|
|
16
|
+
discount: string
|
|
17
|
+
/** Countdown text, e.g. "2d : 15h : 30m". */
|
|
18
|
+
endTime: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface HeroCategory {
|
|
22
|
+
/** Unique identifier for the category. */
|
|
23
|
+
id: string
|
|
24
|
+
/** Category name. */
|
|
25
|
+
name: string
|
|
26
|
+
/** Product count label, e.g. "2.5k+". */
|
|
27
|
+
count: string
|
|
28
|
+
/** Image URL for the category card. */
|
|
29
|
+
image: string
|
|
30
|
+
/** Accessible image description (falls back to `name`). */
|
|
31
|
+
imageAlt?: string
|
|
32
|
+
/** Optional link destination; renders the card arrow as a link when present. */
|
|
33
|
+
href?: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface StorefrontHero6Props {
|
|
37
|
+
/** Small pill shown above the headline. */
|
|
38
|
+
badge?: string
|
|
39
|
+
/** Main headline. */
|
|
40
|
+
title: string
|
|
41
|
+
/** Accent line rendered on a new line after the headline. */
|
|
42
|
+
subtitle?: string
|
|
43
|
+
/** Supporting paragraph under the headline. */
|
|
44
|
+
description: string
|
|
45
|
+
/** Deals rendered in the sticky auto-rotating top bar. */
|
|
46
|
+
deals: HeroDeal[]
|
|
47
|
+
/** Product categories rendered as a card grid. */
|
|
48
|
+
categories: HeroCategory[]
|
|
49
|
+
/** Milliseconds between auto-rotating deals (default 3000). */
|
|
50
|
+
rotationInterval?: number
|
|
51
|
+
/** Label shown before the deal countdown, e.g. "Ends in". */
|
|
52
|
+
endsInLabel?: string
|
|
53
|
+
/** Label of the shop-now button in the deals bar. */
|
|
54
|
+
shopNowLabel?: string
|
|
55
|
+
/** Unit appended to each category product count. */
|
|
56
|
+
productsLabel?: string
|
|
57
|
+
/** Prefix for the category arrow accessible label. */
|
|
58
|
+
viewLabel?: string
|
|
59
|
+
/** Called when the shop-now button in the deals bar is clicked. */
|
|
60
|
+
onShopNow?: (deal: HeroDeal) => void
|
|
61
|
+
/** Called when a category arrow is clicked (categories without `href`). */
|
|
62
|
+
onCategoryClick?: (category: HeroCategory) => void
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function StorefrontHero6({
|
|
66
|
+
badge,
|
|
67
|
+
title,
|
|
68
|
+
subtitle,
|
|
69
|
+
description,
|
|
70
|
+
deals,
|
|
71
|
+
categories,
|
|
72
|
+
rotationInterval = 3000,
|
|
73
|
+
endsInLabel = 'Termina en',
|
|
74
|
+
shopNowLabel = 'Comprar ahora',
|
|
75
|
+
productsLabel = 'productos',
|
|
76
|
+
viewLabel = 'Ver',
|
|
77
|
+
onShopNow,
|
|
78
|
+
onCategoryClick,
|
|
79
|
+
}: StorefrontHero6Props) {
|
|
80
|
+
const [currentDeal, setCurrentDeal] = useState(0)
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (deals.length <= 1) return
|
|
84
|
+
|
|
85
|
+
const interval = setInterval(() => {
|
|
86
|
+
setCurrentDeal(prev => (prev + 1) % deals.length)
|
|
87
|
+
}, rotationInterval)
|
|
88
|
+
|
|
89
|
+
return () => clearInterval(interval)
|
|
90
|
+
}, [deals.length, rotationInterval])
|
|
91
|
+
|
|
92
|
+
const deal = deals[currentDeal]
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<div className='relative'>
|
|
96
|
+
{deal && (
|
|
97
|
+
<header className='bg-primary text-primary-foreground sticky top-0 z-50 shadow-md'>
|
|
98
|
+
<div className='mx-auto w-full max-w-7xl px-4 py-2 sm:px-6 lg:px-8'>
|
|
99
|
+
<div className='flex items-center justify-center'>
|
|
100
|
+
<div className='flex w-full max-w-3xl items-center justify-center gap-4'>
|
|
101
|
+
<div className='flex items-center gap-2'>
|
|
102
|
+
<Badge
|
|
103
|
+
variant='secondary'
|
|
104
|
+
className='bg-primary-foreground text-primary px-2.5 py-0.5 font-semibold'
|
|
105
|
+
>
|
|
106
|
+
{deal.title}
|
|
107
|
+
</Badge>
|
|
108
|
+
<span className='font-semibold'>{deal.discount}</span>
|
|
109
|
+
</div>
|
|
110
|
+
<span className='hidden sm:inline'>
|
|
111
|
+
{endsInLabel} {deal.endTime}
|
|
112
|
+
</span>
|
|
113
|
+
<Button
|
|
114
|
+
variant='link'
|
|
115
|
+
size='sm'
|
|
116
|
+
className='text-primary-foreground hover:text-primary-foreground/80 ml-auto h-8 cursor-pointer px-3 text-xs'
|
|
117
|
+
onClick={onShopNow ? () => onShopNow(deal) : undefined}
|
|
118
|
+
>
|
|
119
|
+
{shopNowLabel} <ArrowRight />
|
|
120
|
+
</Button>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
</header>
|
|
125
|
+
)}
|
|
126
|
+
|
|
127
|
+
<main className='mx-auto w-full max-w-7xl px-4 py-8 sm:px-6 lg:px-8'>
|
|
128
|
+
<div className='mb-12 text-center'>
|
|
129
|
+
{badge && (
|
|
130
|
+
<div className='relative inline-block'>
|
|
131
|
+
<Badge
|
|
132
|
+
variant='outline'
|
|
133
|
+
className='mb-4 flex h-auto items-center gap-2 rounded-full px-3.5 py-1.5 text-sm font-normal'
|
|
134
|
+
>
|
|
135
|
+
<Sparkles className='size-4' aria-hidden='true' />
|
|
136
|
+
{badge}
|
|
137
|
+
</Badge>
|
|
138
|
+
</div>
|
|
139
|
+
)}
|
|
140
|
+
<h1 className='text-4xl font-bold tracking-tight text-balance sm:text-5xl md:text-6xl'>
|
|
141
|
+
{title}
|
|
142
|
+
{subtitle && (
|
|
143
|
+
<>
|
|
144
|
+
<br />
|
|
145
|
+
{subtitle}
|
|
146
|
+
</>
|
|
147
|
+
)}
|
|
148
|
+
</h1>
|
|
149
|
+
<p className='text-muted-foreground mx-auto mt-4 max-w-2xl text-lg'>{description}</p>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<div className='grid gap-4 sm:grid-cols-2 lg:grid-cols-4'>
|
|
153
|
+
{categories.map(category => (
|
|
154
|
+
<Card key={category.id} className='group relative overflow-hidden py-0 transition-all duration-500'>
|
|
155
|
+
<img
|
|
156
|
+
src={category.image || undefined}
|
|
157
|
+
alt={category.imageAlt ?? category.name}
|
|
158
|
+
className='aspect-4/5 size-full object-cover grayscale transition-all duration-500 hover:grayscale-0'
|
|
159
|
+
width={400}
|
|
160
|
+
height={500}
|
|
161
|
+
loading='lazy'
|
|
162
|
+
/>
|
|
163
|
+
<CardContent className='absolute inset-x-0 bottom-0 p-4'>
|
|
164
|
+
<div className='mt-1 flex items-center justify-between'>
|
|
165
|
+
<div>
|
|
166
|
+
<h2 className='text-background dark:text-foreground text-lg font-medium'>{category.name}</h2>
|
|
167
|
+
<span className='text-background/80 dark:text-foreground/80 text-sm'>
|
|
168
|
+
{category.count} {productsLabel}
|
|
169
|
+
</span>
|
|
170
|
+
</div>
|
|
171
|
+
{category.href ? (
|
|
172
|
+
<Button
|
|
173
|
+
variant='secondary'
|
|
174
|
+
size='icon'
|
|
175
|
+
className='h-9 w-9 cursor-pointer rounded-full backdrop-blur-xs'
|
|
176
|
+
aria-label={`${viewLabel} ${category.name}`}
|
|
177
|
+
asChild
|
|
178
|
+
>
|
|
179
|
+
<a href={category.href}>
|
|
180
|
+
<ArrowRight />
|
|
181
|
+
</a>
|
|
182
|
+
</Button>
|
|
183
|
+
) : (
|
|
184
|
+
<Button
|
|
185
|
+
variant='secondary'
|
|
186
|
+
size='icon'
|
|
187
|
+
className='h-9 w-9 cursor-pointer rounded-full backdrop-blur-xs'
|
|
188
|
+
aria-label={`${viewLabel} ${category.name}`}
|
|
189
|
+
onClick={onCategoryClick ? () => onCategoryClick(category) : undefined}
|
|
190
|
+
>
|
|
191
|
+
<ArrowRight />
|
|
192
|
+
</Button>
|
|
193
|
+
)}
|
|
194
|
+
</div>
|
|
195
|
+
</CardContent>
|
|
196
|
+
</Card>
|
|
197
|
+
))}
|
|
198
|
+
</div>
|
|
199
|
+
</main>
|
|
200
|
+
</div>
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export { StorefrontHero6 }
|
|
205
|
+
export default StorefrontHero6
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { useEffect, useState } from 'react'
|
|
5
|
+
|
|
6
|
+
import { cn } from '../../../lib/utils'
|
|
7
|
+
|
|
8
|
+
const LIFT = -4
|
|
9
|
+
const FALLOFF = 0.45
|
|
10
|
+
const ACTIVE_SCALE = 1.05
|
|
11
|
+
const DURATION = 320
|
|
12
|
+
const EASE_IN = 'cubic-bezier(0.22, 1, 0.36, 1)'
|
|
13
|
+
const EASE_OUT = 'cubic-bezier(0.34, 3.85, 0.64, 1)'
|
|
14
|
+
|
|
15
|
+
export function AvatarGroup({
|
|
16
|
+
children,
|
|
17
|
+
className,
|
|
18
|
+
overlap = '-ml-2',
|
|
19
|
+
}: {
|
|
20
|
+
children: React.ReactNode
|
|
21
|
+
className?: string
|
|
22
|
+
overlap?: string
|
|
23
|
+
}) {
|
|
24
|
+
const items = React.Children.toArray(children)
|
|
25
|
+
const [activeIndex, setActiveIndex] = useState<number | null>(null)
|
|
26
|
+
const [reducedMotion, setReducedMotion] = useState(false)
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const query = window.matchMedia('(prefers-reduced-motion: reduce)')
|
|
30
|
+
const sync = () => setReducedMotion(query.matches)
|
|
31
|
+
|
|
32
|
+
sync()
|
|
33
|
+
query.addEventListener('change', sync)
|
|
34
|
+
return () => query.removeEventListener('change', sync)
|
|
35
|
+
}, [])
|
|
36
|
+
|
|
37
|
+
const itemStyle = (index: number): React.CSSProperties | undefined => {
|
|
38
|
+
if (reducedMotion) {
|
|
39
|
+
return undefined
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const isActive = index === activeIndex
|
|
43
|
+
const shift = activeIndex === null ? 0 : LIFT * FALLOFF ** Math.abs(index - activeIndex)
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
transform: `translateY(${shift.toFixed(3)}px) scale(${isActive ? ACTIVE_SCALE : 1})`,
|
|
47
|
+
transition: `transform ${DURATION}ms ${activeIndex === null ? EASE_OUT : EASE_IN}`,
|
|
48
|
+
willChange: 'transform',
|
|
49
|
+
zIndex: isActive ? 1 : undefined,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div className={cn('flex', className)} onMouseLeave={() => setActiveIndex(null)}>
|
|
55
|
+
{items.map((child, index) => (
|
|
56
|
+
<div
|
|
57
|
+
key={index}
|
|
58
|
+
className={cn('relative', index > 0 && overlap)}
|
|
59
|
+
style={itemStyle(index)}
|
|
60
|
+
onMouseEnter={() => setActiveIndex(index)}
|
|
61
|
+
>
|
|
62
|
+
{child}
|
|
63
|
+
</div>
|
|
64
|
+
))}
|
|
65
|
+
</div>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export default AvatarGroup
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
import { motion, useReducedMotion } from 'motion/react'
|
|
5
|
+
import { ArrowRight, X } from 'lucide-react'
|
|
6
|
+
import { Avatar, AvatarFallback, AvatarImage } from '../../../ui/avatar'
|
|
7
|
+
import { Badge } from '../../../ui/badge'
|
|
8
|
+
import { Button } from '../../../ui/button'
|
|
9
|
+
import { AvatarGroup } from './avatar-group'
|
|
10
|
+
|
|
11
|
+
export type Banner1Shape = 'bar' | 'pill'
|
|
12
|
+
|
|
13
|
+
export interface Banner1Props {
|
|
14
|
+
/**
|
|
15
|
+
* 'bar' — full-width strip above the header (the default).
|
|
16
|
+
* 'pill' — floating rounded card pinned to the bottom of the viewport, with an avatar stack.
|
|
17
|
+
*/
|
|
18
|
+
shape?: Banner1Shape
|
|
19
|
+
badge?: string
|
|
20
|
+
message: string
|
|
21
|
+
href: string
|
|
22
|
+
linkLabel: string
|
|
23
|
+
/** Used by the 'pill' shape only; decorative avatars of the announcement's authors. */
|
|
24
|
+
avatars?: string[]
|
|
25
|
+
/**
|
|
26
|
+
* 'bar' only. When true, the inner content spans the full width of its parent
|
|
27
|
+
* (skips the centered `max-w-7xl` container) so badge/message/CTA reach the edges.
|
|
28
|
+
*/
|
|
29
|
+
fullWidth?: boolean
|
|
30
|
+
/** Fired when the user clicks the banner CTA. When provided, the CTA renders as a button instead of a link. */
|
|
31
|
+
onCtaClick?: () => void
|
|
32
|
+
/** Fired when the user dismisses the banner. */
|
|
33
|
+
onDismiss?: () => void
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type Visibility = 'visible' | 'closing' | 'closed'
|
|
37
|
+
|
|
38
|
+
export function Banner1({
|
|
39
|
+
shape = 'bar',
|
|
40
|
+
badge,
|
|
41
|
+
message,
|
|
42
|
+
href,
|
|
43
|
+
linkLabel,
|
|
44
|
+
avatars,
|
|
45
|
+
fullWidth,
|
|
46
|
+
onCtaClick,
|
|
47
|
+
onDismiss,
|
|
48
|
+
}: Banner1Props) {
|
|
49
|
+
const [visibility, setVisibility] = useState<Visibility>('visible')
|
|
50
|
+
const reduceMotion = useReducedMotion()
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (visibility !== 'closing') {
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const timer = window.setTimeout(() => setVisibility('closed'), 200)
|
|
58
|
+
return () => window.clearTimeout(timer)
|
|
59
|
+
}, [visibility])
|
|
60
|
+
|
|
61
|
+
const dismiss = () => {
|
|
62
|
+
onDismiss?.()
|
|
63
|
+
setVisibility(reduceMotion ? 'closed' : 'closing')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (visibility === 'closed') {
|
|
67
|
+
return null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const closing = visibility === 'closing'
|
|
71
|
+
|
|
72
|
+
if (shape === 'pill') {
|
|
73
|
+
return (
|
|
74
|
+
<div
|
|
75
|
+
className={`fixed inset-x-4 bottom-4 z-50 transition-all duration-200 ease-out motion-reduce:transition-none sm:inset-x-auto sm:left-1/2 sm:-translate-x-1/2 ${
|
|
76
|
+
closing ? 'translate-y-2 opacity-0' : 'opacity-100'
|
|
77
|
+
}`}
|
|
78
|
+
>
|
|
79
|
+
<div className='bg-background/95 border-border flex items-center gap-3 rounded-full border py-2 pr-2 pl-3 shadow-lg backdrop-blur sm:pl-4'>
|
|
80
|
+
{avatars && avatars.length > 0 && (
|
|
81
|
+
<AvatarGroup aria-hidden='true'>
|
|
82
|
+
{avatars.map(src => (
|
|
83
|
+
<Avatar key={src} className='ring-background size-6 ring-2'>
|
|
84
|
+
<AvatarImage src={src || undefined} alt='' />
|
|
85
|
+
<AvatarFallback />
|
|
86
|
+
</Avatar>
|
|
87
|
+
))}
|
|
88
|
+
</AvatarGroup>
|
|
89
|
+
)}
|
|
90
|
+
<p className='truncate text-sm'>{message}</p>
|
|
91
|
+
<a
|
|
92
|
+
href={href}
|
|
93
|
+
className='hover:text-foreground text-muted-foreground focus-visible:ring-ring hidden shrink-0 items-center gap-1 rounded-sm text-sm font-medium transition-colors duration-150 focus-visible:ring-2 focus-visible:outline-none sm:inline-flex'
|
|
94
|
+
>
|
|
95
|
+
{linkLabel}
|
|
96
|
+
<ArrowRight className='size-3.5' />
|
|
97
|
+
</a>
|
|
98
|
+
<Button
|
|
99
|
+
variant='ghost'
|
|
100
|
+
size='icon'
|
|
101
|
+
className='size-8 shrink-0 rounded-full'
|
|
102
|
+
aria-label='Dismiss announcement'
|
|
103
|
+
onClick={dismiss}
|
|
104
|
+
>
|
|
105
|
+
<X className='size-4' />
|
|
106
|
+
</Button>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<div
|
|
114
|
+
className={`grid transition-all duration-200 ease-out motion-reduce:transition-none ${
|
|
115
|
+
closing ? 'grid-rows-[0fr] opacity-0' : 'grid-rows-[1fr] opacity-100'
|
|
116
|
+
}`}
|
|
117
|
+
>
|
|
118
|
+
<div className='overflow-hidden'>
|
|
119
|
+
<div className='bg-primary text-primary-foreground'>
|
|
120
|
+
<div
|
|
121
|
+
className={`flex w-full items-center gap-3 px-4 py-2.5 sm:px-6 lg:px-8 ${
|
|
122
|
+
fullWidth ? '' : 'mx-auto max-w-7xl'
|
|
123
|
+
}`}
|
|
124
|
+
>
|
|
125
|
+
{badge ? (
|
|
126
|
+
<Badge
|
|
127
|
+
variant='secondary'
|
|
128
|
+
className='hidden shrink-0 rounded-full px-2 py-0 text-xs font-semibold sm:inline-flex'
|
|
129
|
+
>
|
|
130
|
+
{badge}
|
|
131
|
+
</Badge>
|
|
132
|
+
) : null}
|
|
133
|
+
|
|
134
|
+
<p className='min-w-0 flex-1 truncate text-sm'>{message}</p>
|
|
135
|
+
|
|
136
|
+
{onCtaClick ? (
|
|
137
|
+
<span className='relative inline-flex'>
|
|
138
|
+
<motion.span
|
|
139
|
+
aria-hidden='true'
|
|
140
|
+
className='pointer-events-none absolute inset-0 rounded-md ring-2 ring-background/70'
|
|
141
|
+
initial={{ scale: 1, opacity: 0.7 }}
|
|
142
|
+
animate={
|
|
143
|
+
reduceMotion
|
|
144
|
+
? { scale: 1, opacity: 0 }
|
|
145
|
+
: { scale: [1, 1.35], opacity: [0.7, 0] }
|
|
146
|
+
}
|
|
147
|
+
transition={{ duration: 1.8, repeat: Infinity, ease: 'easeOut', repeatDelay: 0.4 }}
|
|
148
|
+
/>
|
|
149
|
+
<Button
|
|
150
|
+
type='button'
|
|
151
|
+
variant='secondary'
|
|
152
|
+
size='sm'
|
|
153
|
+
onClick={onCtaClick}
|
|
154
|
+
className='shrink-0'
|
|
155
|
+
>
|
|
156
|
+
<span className='hidden sm:inline'>{linkLabel}</span>
|
|
157
|
+
<span className='sr-only sm:hidden'>{linkLabel}</span>
|
|
158
|
+
<ArrowRight className='size-3.5' aria-hidden='true' />
|
|
159
|
+
</Button>
|
|
160
|
+
</span>
|
|
161
|
+
) : (
|
|
162
|
+
<a
|
|
163
|
+
href={href}
|
|
164
|
+
className='focus-visible:ring-ring inline-flex shrink-0 items-center gap-1 rounded-sm text-sm font-medium underline-offset-4 transition-opacity duration-150 hover:opacity-80 hover:underline focus-visible:ring-2 focus-visible:outline-none'
|
|
165
|
+
>
|
|
166
|
+
<span className='hidden sm:inline'>{linkLabel}</span>
|
|
167
|
+
<span className='sr-only sm:hidden'>{linkLabel}</span>
|
|
168
|
+
<ArrowRight className='size-3.5' aria-hidden='true' />
|
|
169
|
+
</a>
|
|
170
|
+
)}
|
|
171
|
+
|
|
172
|
+
<Button
|
|
173
|
+
variant='ghost'
|
|
174
|
+
size='icon'
|
|
175
|
+
className='hover:bg-primary-foreground/10 hover:text-primary-foreground size-8 shrink-0'
|
|
176
|
+
aria-label='Dismiss announcement'
|
|
177
|
+
onClick={dismiss}
|
|
178
|
+
>
|
|
179
|
+
<X className='size-4' />
|
|
180
|
+
</Button>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export default Banner1
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { Badge } from '../../../ui/badge'
|
|
2
|
+
import { Button } from '../../../ui/button'
|
|
3
|
+
import { ArrowRight, Phone, Radio } from 'lucide-react'
|
|
4
|
+
import type { CTA } from '../../../lib/types'
|
|
5
|
+
|
|
6
|
+
export interface CtaSection1Props {
|
|
7
|
+
badgeText?: string
|
|
8
|
+
title?: string
|
|
9
|
+
description?: string
|
|
10
|
+
primaryCta?: CTA
|
|
11
|
+
secondaryCta?: CTA
|
|
12
|
+
onPrimaryClick?: () => void
|
|
13
|
+
onSecondaryClick?: () => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const HEADING_ID = 'cta-section-1-heading'
|
|
17
|
+
|
|
18
|
+
export function CtaSection1({
|
|
19
|
+
badgeText,
|
|
20
|
+
title,
|
|
21
|
+
description,
|
|
22
|
+
primaryCta,
|
|
23
|
+
secondaryCta,
|
|
24
|
+
onPrimaryClick,
|
|
25
|
+
onSecondaryClick,
|
|
26
|
+
}: CtaSection1Props) {
|
|
27
|
+
return (
|
|
28
|
+
<section
|
|
29
|
+
className='bg-background relative w-full overflow-hidden py-24'
|
|
30
|
+
aria-labelledby={title ? HEADING_ID : undefined}
|
|
31
|
+
>
|
|
32
|
+
<div className='mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8'>
|
|
33
|
+
<div className='relative mx-auto max-w-4xl text-center'>
|
|
34
|
+
{badgeText && (
|
|
35
|
+
<Badge
|
|
36
|
+
variant='secondary'
|
|
37
|
+
className='mb-6 h-auto rounded-md px-2.5 py-0.5 text-sm font-medium'
|
|
38
|
+
>
|
|
39
|
+
<Radio className='!size-5' aria-hidden='true' />
|
|
40
|
+
{badgeText}
|
|
41
|
+
</Badge>
|
|
42
|
+
)}
|
|
43
|
+
|
|
44
|
+
{title && (
|
|
45
|
+
<h1
|
|
46
|
+
id={HEADING_ID}
|
|
47
|
+
className='mb-6 text-4xl font-bold tracking-tight text-balance sm:text-5xl lg:text-6xl'
|
|
48
|
+
>
|
|
49
|
+
{title}
|
|
50
|
+
</h1>
|
|
51
|
+
)}
|
|
52
|
+
|
|
53
|
+
{description && (
|
|
54
|
+
<p className='text-muted-foreground mx-auto mb-10 max-w-2xl text-lg text-balance'>
|
|
55
|
+
{description}
|
|
56
|
+
</p>
|
|
57
|
+
)}
|
|
58
|
+
|
|
59
|
+
{(primaryCta || secondaryCta) && (
|
|
60
|
+
<div className='flex flex-wrap items-center justify-center gap-4'>
|
|
61
|
+
{primaryCta &&
|
|
62
|
+
(primaryCta.href ? (
|
|
63
|
+
<Button asChild size='lg' className='h-10 cursor-pointer px-6 w-fit'>
|
|
64
|
+
<a href={primaryCta.href}>
|
|
65
|
+
{primaryCta.label}
|
|
66
|
+
<ArrowRight />
|
|
67
|
+
</a>
|
|
68
|
+
</Button>
|
|
69
|
+
) : (
|
|
70
|
+
<Button
|
|
71
|
+
size='lg'
|
|
72
|
+
className='h-10 cursor-pointer px-6 w-fit'
|
|
73
|
+
onClick={onPrimaryClick}
|
|
74
|
+
>
|
|
75
|
+
{primaryCta.label}
|
|
76
|
+
<ArrowRight />
|
|
77
|
+
</Button>
|
|
78
|
+
))}
|
|
79
|
+
|
|
80
|
+
{secondaryCta &&
|
|
81
|
+
(secondaryCta.href ? (
|
|
82
|
+
<Button
|
|
83
|
+
asChild
|
|
84
|
+
variant='outline'
|
|
85
|
+
size='lg'
|
|
86
|
+
className='h-10 cursor-pointer px-6 w-fit'
|
|
87
|
+
>
|
|
88
|
+
<a href={secondaryCta.href}>
|
|
89
|
+
<Phone />
|
|
90
|
+
{secondaryCta.label}
|
|
91
|
+
</a>
|
|
92
|
+
</Button>
|
|
93
|
+
) : (
|
|
94
|
+
<Button
|
|
95
|
+
variant='outline'
|
|
96
|
+
size='lg'
|
|
97
|
+
className='h-10 cursor-pointer px-6 w-fit'
|
|
98
|
+
onClick={onSecondaryClick}
|
|
99
|
+
>
|
|
100
|
+
<Phone />
|
|
101
|
+
{secondaryCta.label}
|
|
102
|
+
</Button>
|
|
103
|
+
))}
|
|
104
|
+
</div>
|
|
105
|
+
)}
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
</section>
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export default CtaSection1
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '../../../ui/accordion'
|
|
2
|
+
|
|
3
|
+
export interface FaqItem {
|
|
4
|
+
value: string
|
|
5
|
+
question: string
|
|
6
|
+
answer: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface FaqBlock1Props {
|
|
10
|
+
/** Preguntas y respuestas del bloque. Cada item necesita un `value` único. */
|
|
11
|
+
items: FaqItem[]
|
|
12
|
+
title?: string
|
|
13
|
+
description?: string
|
|
14
|
+
/** Permite abrir varios items a la vez (`'multiple'`) o solo uno (`'single'`). */
|
|
15
|
+
accordionType?: 'single' | 'multiple'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function FaqBlock1({
|
|
19
|
+
items,
|
|
20
|
+
title = 'Preguntas frecuentes',
|
|
21
|
+
description,
|
|
22
|
+
accordionType = 'multiple',
|
|
23
|
+
}: FaqBlock1Props) {
|
|
24
|
+
return (
|
|
25
|
+
<div className='w-full py-16'>
|
|
26
|
+
<div className='mx-auto w-full max-w-4xl px-4 sm:px-6 lg:px-8'>
|
|
27
|
+
<div className='mb-12 text-center'>
|
|
28
|
+
<h2 className='mb-4 text-4xl font-bold'>{title}</h2>
|
|
29
|
+
{description && <p className='text-muted-foreground'>{description}</p>}
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<Accordion type={accordionType} className='flex flex-col gap-4'>
|
|
33
|
+
{items.map(item => (
|
|
34
|
+
<AccordionItem key={item.value} value={item.value} className='rounded-md !border'>
|
|
35
|
+
<AccordionTrigger className='cursor-pointer px-4 py-4'>{item.question}</AccordionTrigger>
|
|
36
|
+
<AccordionContent className='text-muted-foreground px-4'>{item.answer}</AccordionContent>
|
|
37
|
+
</AccordionItem>
|
|
38
|
+
))}
|
|
39
|
+
</Accordion>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default FaqBlock1
|