@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.
Files changed (184) hide show
  1. package/.turbo/turbo-build.log +24 -0
  2. package/.turbo/turbo-test.log +10 -0
  3. package/.turbo/turbo-type-check.log +0 -0
  4. package/AGENTS.md +1 -0
  5. package/README.md +161 -0
  6. package/components.json +33 -0
  7. package/dist/.tsbuildinfo +1 -0
  8. package/dist/contracts.d.mts +563 -0
  9. package/dist/contracts.d.ts +563 -0
  10. package/dist/contracts.js +207 -0
  11. package/dist/contracts.js.map +1 -0
  12. package/dist/contracts.mjs +168 -0
  13. package/dist/contracts.mjs.map +1 -0
  14. package/dist/index.d.mts +2408 -0
  15. package/dist/index.d.ts +2408 -0
  16. package/dist/index.js +16422 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/index.mjs +16352 -0
  19. package/dist/index.mjs.map +1 -0
  20. package/docs/standards/data-contracts.md +70 -0
  21. package/docs/standards/theming.md +85 -0
  22. package/package.json +62 -0
  23. package/src/blocks/application/app-shells/app-shell-1-sidebar.tsx +198 -0
  24. package/src/blocks/application/app-shells/app-shell-1.tsx +196 -0
  25. package/src/blocks/application/login-forms/login-page-1.tsx +246 -0
  26. package/src/blocks/application/login-forms/login-page-2.tsx +207 -0
  27. package/src/blocks/application/password-recovery/forgot-password-page-1-brand-mark.tsx +38 -0
  28. package/src/blocks/application/password-recovery/forgot-password-page-1.tsx +320 -0
  29. package/src/blocks/application/signup-forms/signup-page-1.tsx +231 -0
  30. package/src/blocks/application/verification/verify-1.tsx +307 -0
  31. package/src/blocks/application/verification/verify-confetti.tsx +192 -0
  32. package/src/blocks/e-commerce/category-filters/category-filter-1.tsx +260 -0
  33. package/src/blocks/e-commerce/category-filters/category-filter-2-filter-panel.tsx +179 -0
  34. package/src/blocks/e-commerce/category-filters/category-filter-2.tsx +341 -0
  35. package/src/blocks/e-commerce/category-filters/category-filter-4-chip-rail.tsx +57 -0
  36. package/src/blocks/e-commerce/category-filters/category-filter-4-drawer.tsx +124 -0
  37. package/src/blocks/e-commerce/category-filters/category-filter-4-fields.tsx +170 -0
  38. package/src/blocks/e-commerce/category-filters/category-filter-4-sidebar.tsx +52 -0
  39. package/src/blocks/e-commerce/category-filters/category-filter-4-sort-sheet.tsx +57 -0
  40. package/src/blocks/e-commerce/category-filters/category-filter-4-state.ts +68 -0
  41. package/src/blocks/e-commerce/category-filters/category-filter-4.tsx +298 -0
  42. package/src/blocks/e-commerce/checkout-forms/card-formatting.test.ts +47 -0
  43. package/src/blocks/e-commerce/checkout-forms/card-formatting.ts +9 -0
  44. package/src/blocks/e-commerce/checkout-forms/checkbox-check.tsx +74 -0
  45. package/src/blocks/e-commerce/checkout-forms/checkout-confirmation-fulfilment-progress.tsx +189 -0
  46. package/src/blocks/e-commerce/checkout-forms/checkout-confirmation.tsx +283 -0
  47. package/src/blocks/e-commerce/checkout-forms/checkout-field-feedback.tsx +29 -0
  48. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-contact-step.tsx +96 -0
  49. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-country-field.tsx +51 -0
  50. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-order-summary.tsx +139 -0
  51. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-payment-step.tsx +184 -0
  52. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-progress.tsx +36 -0
  53. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-shipping-step.tsx +107 -0
  54. package/src/blocks/e-commerce/checkout-forms/checkout-form-1.tsx +235 -0
  55. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-order-summary.tsx +130 -0
  56. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-payment-card.tsx +175 -0
  57. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-shipping-card.tsx +190 -0
  58. package/src/blocks/e-commerce/checkout-forms/checkout-form-2.tsx +312 -0
  59. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-information-step.tsx +91 -0
  60. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-order-summary.tsx +110 -0
  61. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-payment-step.tsx +137 -0
  62. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-progress.tsx +68 -0
  63. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-review-step.tsx +79 -0
  64. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-shipping-step.tsx +172 -0
  65. package/src/blocks/e-commerce/checkout-forms/checkout-form-3.tsx +392 -0
  66. package/src/blocks/e-commerce/checkout-forms/checkout-form-types.ts +7 -0
  67. package/src/blocks/e-commerce/navigation/category-icon.test.tsx +54 -0
  68. package/src/blocks/e-commerce/navigation/category-icon.tsx +94 -0
  69. package/src/blocks/e-commerce/navigation/mega-menu.test.tsx +139 -0
  70. package/src/blocks/e-commerce/navigation/mega-menu.tsx +271 -0
  71. package/src/blocks/e-commerce/navigation/mobile-category-drawer.test.tsx +152 -0
  72. package/src/blocks/e-commerce/navigation/mobile-category-drawer.tsx +276 -0
  73. package/src/blocks/e-commerce/order-history/order-history-1.tsx +159 -0
  74. package/src/blocks/e-commerce/order-history/order-history-2.tsx +222 -0
  75. package/src/blocks/e-commerce/product-categories/avatar-group.tsx +86 -0
  76. package/src/blocks/e-commerce/product-categories/product-category-1.tsx +126 -0
  77. package/src/blocks/e-commerce/product-categories/product-category-2.tsx +100 -0
  78. package/src/blocks/e-commerce/product-categories/product-category-4.tsx +129 -0
  79. package/src/blocks/e-commerce/product-categories/product-category-5.tsx +218 -0
  80. package/src/blocks/e-commerce/product-list/product-card-1.tsx +131 -0
  81. package/src/blocks/e-commerce/product-list/product-card-2.tsx +71 -0
  82. package/src/blocks/e-commerce/product-list/product-card-3-reaction-toggle.tsx +166 -0
  83. package/src/blocks/e-commerce/product-list/product-card-3.tsx +215 -0
  84. package/src/blocks/e-commerce/product-list/product-card-5-checkbox-check.tsx +98 -0
  85. package/src/blocks/e-commerce/product-list/product-card-5-reaction-toggle.tsx +171 -0
  86. package/src/blocks/e-commerce/product-list/product-card-5.tsx +367 -0
  87. package/src/blocks/e-commerce/product-overview/product-overview-1.tsx +201 -0
  88. package/src/blocks/e-commerce/product-overview/product-overview-2.tsx +272 -0
  89. package/src/blocks/e-commerce/product-overview/product-overview-5-gallery.tsx +59 -0
  90. package/src/blocks/e-commerce/product-overview/product-overview-5-lightbox-tab.tsx +104 -0
  91. package/src/blocks/e-commerce/product-overview/product-overview-5-reviews-tab.tsx +82 -0
  92. package/src/blocks/e-commerce/product-overview/product-overview-5-tabs.tsx +69 -0
  93. package/src/blocks/e-commerce/product-overview/product-overview-5.tsx +274 -0
  94. package/src/blocks/e-commerce/product-overview/product-quick-view-card.tsx +135 -0
  95. package/src/blocks/e-commerce/product-overview/product-quick-view-dialog.tsx +393 -0
  96. package/src/blocks/e-commerce/product-overview/product-quick-view.tsx +136 -0
  97. package/src/blocks/e-commerce/reviews/review-rating-1-review-form.tsx +133 -0
  98. package/src/blocks/e-commerce/reviews/review-rating-1.tsx +178 -0
  99. package/src/blocks/e-commerce/reviews/review-rating-2.tsx +246 -0
  100. package/src/blocks/e-commerce/shopping-carts/shopping-cart-1-spinning-counter.tsx +142 -0
  101. package/src/blocks/e-commerce/shopping-carts/shopping-cart-1.tsx +291 -0
  102. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-item-row.tsx +110 -0
  103. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-recommendations.tsx +51 -0
  104. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-spinning-counter.tsx +146 -0
  105. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2.tsx +295 -0
  106. package/src/blocks/e-commerce/shopping-carts/shopping-cart-3-spinning-counter.tsx +142 -0
  107. package/src/blocks/e-commerce/shopping-carts/shopping-cart-3.tsx +342 -0
  108. package/src/blocks/e-commerce/shopping-carts/shopping-cart-4-spinning-counter.tsx +146 -0
  109. package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.tsx +391 -0
  110. package/src/blocks/e-commerce/store-containers/store-container-1.tsx +29 -0
  111. package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.tsx +244 -0
  112. package/src/blocks/e-commerce/storefront-hero/storefront-hero-2.tsx +283 -0
  113. package/src/blocks/e-commerce/storefront-hero/storefront-hero-5.tsx +318 -0
  114. package/src/blocks/e-commerce/storefront-hero/storefront-hero-6.tsx +205 -0
  115. package/src/blocks/marketing/banners/avatar-group.tsx +69 -0
  116. package/src/blocks/marketing/banners/banner-1.tsx +188 -0
  117. package/src/blocks/marketing/cta/cta-section-1.tsx +112 -0
  118. package/src/blocks/marketing/faq/faq-block-1.tsx +45 -0
  119. package/src/blocks/marketing/features/feature-1.tsx +97 -0
  120. package/src/blocks/marketing/features/feature-2.tsx +82 -0
  121. package/src/blocks/marketing/footers/footer-section-1.tsx +141 -0
  122. package/src/blocks/marketing/footers/footer-section-3.tsx +192 -0
  123. package/src/blocks/marketing/footers/footer-section-4.tsx +355 -0
  124. package/src/blocks/marketing/hero-sections/avatar-group.tsx +86 -0
  125. package/src/blocks/marketing/hero-sections/hero-section-1.tsx +135 -0
  126. package/src/blocks/marketing/logo-clouds/logo-cloud-1-logo-icon.tsx +52 -0
  127. package/src/blocks/marketing/logo-clouds/logo-cloud-1.tsx +56 -0
  128. package/src/blocks/marketing/navbars/navbar-1.tsx +267 -0
  129. package/src/blocks/marketing/navbars/navbar-2.tsx +351 -0
  130. package/src/blocks/marketing/navbars/navbar-3.tsx +303 -0
  131. package/src/blocks/marketing/navbars/navbar-4-account-menu.tsx +118 -0
  132. package/src/blocks/marketing/navbars/navbar-4-cart-sheet.tsx +248 -0
  133. package/src/blocks/marketing/navbars/navbar-4-like-button.tsx +177 -0
  134. package/src/blocks/marketing/navbars/navbar-4-mobile-nav.tsx +104 -0
  135. package/src/blocks/marketing/navbars/navbar-4-product-grid.tsx +219 -0
  136. package/src/blocks/marketing/navbars/navbar-4-search-bar.tsx +52 -0
  137. package/src/blocks/marketing/navbars/navbar-4.tsx +411 -0
  138. package/src/blocks/marketing/pricing/pricing-section-1.tsx +94 -0
  139. package/src/blocks/marketing/statistics/statistic-1.tsx +93 -0
  140. package/src/blocks/marketing/testimonials/testimonial-1.tsx +141 -0
  141. package/src/blocks/marketing/testimonials/testimonial-2.tsx +108 -0
  142. package/src/blocks/shared/reaction-toggle.tsx +145 -0
  143. package/src/hooks/use-mobile.ts +19 -0
  144. package/src/index.ts +200 -0
  145. package/src/lib/contracts.test.ts +32 -0
  146. package/src/lib/contracts.ts +235 -0
  147. package/src/lib/types/menu.ts +43 -0
  148. package/src/lib/types.ts +4 -0
  149. package/src/lib/utils.ts +10 -0
  150. package/src/ui/accordion.tsx +65 -0
  151. package/src/ui/avatar.tsx +108 -0
  152. package/src/ui/badge.tsx +47 -0
  153. package/src/ui/breadcrumb.tsx +108 -0
  154. package/src/ui/button-group.tsx +83 -0
  155. package/src/ui/button.tsx +63 -0
  156. package/src/ui/card.tsx +91 -0
  157. package/src/ui/carousel.tsx +241 -0
  158. package/src/ui/dialog.tsx +158 -0
  159. package/src/ui/drawer.tsx +134 -0
  160. package/src/ui/dropdown-menu.tsx +256 -0
  161. package/src/ui/empty.tsx +103 -0
  162. package/src/ui/field.tsx +248 -0
  163. package/src/ui/input-group.tsx +170 -0
  164. package/src/ui/input-otp.tsx +76 -0
  165. package/src/ui/input.tsx +20 -0
  166. package/src/ui/label.tsx +23 -0
  167. package/src/ui/navigation-menu.tsx +167 -0
  168. package/src/ui/pagination.tsx +127 -0
  169. package/src/ui/progress.tsx +30 -0
  170. package/src/ui/radio-group.tsx +44 -0
  171. package/src/ui/select.tsx +189 -0
  172. package/src/ui/separator.tsx +27 -0
  173. package/src/ui/sheet.tsx +142 -0
  174. package/src/ui/sidebar.tsx +726 -0
  175. package/src/ui/skeleton.tsx +13 -0
  176. package/src/ui/table.tsx +115 -0
  177. package/src/ui/tabs.tsx +90 -0
  178. package/src/ui/textarea.tsx +17 -0
  179. package/src/ui/toggle-group.tsx +83 -0
  180. package/src/ui/toggle.tsx +46 -0
  181. package/src/ui/tooltip.tsx +56 -0
  182. package/tsconfig.json +20 -0
  183. package/tsup.config.ts +16 -0
  184. 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