@thorprovider/blocks 0.1.3 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/CHANGELOG.md +155 -0
  2. package/README.md +228 -68
  3. package/docs/standards/data-contracts.md +13 -2
  4. package/package.json +26 -10
  5. package/scripts/fix-esm-extensions.mjs +40 -0
  6. package/src/blocks/application/login-forms/login-page.test.tsx +190 -0
  7. package/src/blocks/application/login-forms/login-page.tsx +240 -0
  8. package/src/blocks/application/password-recovery/forgot-password-page-1.tsx +6 -8
  9. package/src/blocks/application/signup-forms/signup-page-1.tsx +127 -129
  10. package/src/blocks/application/verification/verify-1.tsx +2 -2
  11. package/src/blocks/application/viewport-ownership.test.tsx +41 -0
  12. package/src/blocks/e-commerce/category-filters/category-listing-filter-fields.tsx +218 -0
  13. package/src/blocks/e-commerce/category-filters/{category-filter-2.test.tsx → category-listing.test.tsx} +287 -199
  14. package/src/blocks/e-commerce/category-filters/category-listing.tsx +934 -0
  15. package/src/blocks/e-commerce/checkout-forms/checkout-contact-step.tsx +149 -0
  16. package/src/blocks/e-commerce/checkout-forms/{checkout-form-1-country-field.tsx → checkout-country-field.tsx} +10 -8
  17. package/src/blocks/e-commerce/checkout-forms/checkout-order-summary.tsx +230 -0
  18. package/src/blocks/e-commerce/checkout-forms/checkout-payment-step.tsx +100 -0
  19. package/src/blocks/e-commerce/checkout-forms/checkout-progress.tsx +62 -0
  20. package/src/blocks/e-commerce/checkout-forms/checkout-review-step.tsx +59 -0
  21. package/src/blocks/e-commerce/checkout-forms/{checkout-form-1-shipping-step.tsx → checkout-shipping-step.tsx} +65 -41
  22. package/src/blocks/e-commerce/checkout-forms/checkout.test.tsx +478 -0
  23. package/src/blocks/e-commerce/checkout-forms/checkout.tsx +550 -0
  24. package/src/blocks/e-commerce/navigation/storefront-navbar-account-menu.test.tsx +56 -0
  25. package/src/blocks/{marketing/navbars/navbar-4-account-menu.tsx → e-commerce/navigation/storefront-navbar-account-menu.tsx} +3 -3
  26. package/src/blocks/{marketing/navbars/navbar-4-cart-sheet.test.tsx → e-commerce/navigation/storefront-navbar-cart-sheet.test.tsx} +22 -22
  27. package/src/blocks/{marketing/navbars/navbar-4-cart-sheet.tsx → e-commerce/navigation/storefront-navbar-cart-sheet.tsx} +3 -3
  28. package/src/blocks/{marketing/navbars/navbar-4-mobile-nav.tsx → e-commerce/navigation/storefront-navbar-mobile-nav.tsx} +4 -12
  29. package/src/blocks/{marketing/navbars/navbar-4-search-bar.tsx → e-commerce/navigation/storefront-navbar-search-bar.tsx} +3 -3
  30. package/src/blocks/{marketing/navbars/navbar-4.test.tsx → e-commerce/navigation/storefront-navbar.test.tsx} +67 -36
  31. package/src/blocks/{marketing/navbars/navbar-4.tsx → e-commerce/navigation/storefront-navbar.tsx} +43 -25
  32. package/src/blocks/e-commerce/product-categories/category-card-grid.test.tsx +122 -0
  33. package/src/blocks/e-commerce/product-categories/category-card.test.tsx +68 -0
  34. package/src/blocks/e-commerce/product-categories/category-card.tsx +172 -0
  35. package/src/blocks/e-commerce/product-categories/category-grid.test.tsx +67 -0
  36. package/src/blocks/e-commerce/product-categories/category-shared.tsx +109 -0
  37. package/src/blocks/e-commerce/product-categories/product-category-grid.tsx +170 -0
  38. package/src/blocks/e-commerce/product-categories/product-category.test.tsx +266 -0
  39. package/src/blocks/e-commerce/product-categories/product-category.tsx +462 -0
  40. package/src/blocks/e-commerce/product-list/product-card.test.tsx +263 -0
  41. package/src/blocks/e-commerce/product-list/product-card.tsx +447 -0
  42. package/src/blocks/e-commerce/product-list/product-grid.test.tsx +162 -0
  43. package/src/blocks/e-commerce/product-list/product-grid.tsx +114 -0
  44. package/src/blocks/e-commerce/product-overview/product-overview-gallery.tsx +71 -0
  45. package/src/blocks/e-commerce/product-overview/{product-overview-5-lightbox-tab.tsx → product-overview-lightbox-tab.tsx} +35 -23
  46. package/src/blocks/e-commerce/product-overview/{product-overview-5-reviews-tab.tsx → product-overview-reviews-tab.tsx} +40 -17
  47. package/src/blocks/e-commerce/product-overview/{product-overview-5-tabs.tsx → product-overview-tabs.tsx} +8 -7
  48. package/src/blocks/e-commerce/product-overview/product-overview.test.tsx +283 -0
  49. package/src/blocks/e-commerce/product-overview/product-overview.tsx +1186 -0
  50. package/src/blocks/e-commerce/reviews/review-rating-2.tsx +5 -6
  51. package/src/blocks/e-commerce/shopping-carts/shopping-cart.test.tsx +399 -0
  52. package/src/blocks/e-commerce/shopping-carts/{shopping-cart-4.tsx → shopping-cart.tsx} +119 -85
  53. package/src/blocks/e-commerce/storefront-hero/storefront-hero.test.tsx +412 -0
  54. package/src/blocks/e-commerce/storefront-hero/storefront-hero.tsx +1283 -0
  55. package/src/blocks/marketing/features/feature.test.tsx +153 -0
  56. package/src/blocks/marketing/features/feature.tsx +195 -0
  57. package/src/blocks/marketing/footers/footer-section-accessibility.test.tsx +118 -0
  58. package/src/blocks/marketing/footers/footer-section-actions.test.tsx +131 -0
  59. package/src/blocks/marketing/footers/footer-section-data.test.tsx +127 -0
  60. package/src/blocks/marketing/footers/footer-section-native-links.test.tsx +100 -0
  61. package/src/blocks/marketing/footers/footer-section-public-api.test.tsx +11 -0
  62. package/src/blocks/marketing/footers/footer-section.test.tsx +46 -0
  63. package/src/blocks/marketing/footers/footer-section.tsx +532 -0
  64. package/src/blocks/marketing/navbars/navbar-public-api.test.tsx +57 -0
  65. package/src/blocks/marketing/navbars/navbar.mega.test.tsx +149 -0
  66. package/src/blocks/marketing/navbars/navbar.test.tsx +155 -0
  67. package/src/blocks/marketing/navbars/navbar.tsx +735 -0
  68. package/src/blocks/marketing/testimonials/testimonial-section.test.tsx +171 -0
  69. package/src/blocks/marketing/testimonials/testimonial-section.tsx +237 -0
  70. package/src/index.ts +156 -128
  71. package/src/lib/contracts-curated.test.ts +47 -29
  72. package/src/lib/contracts.test.ts +198 -2
  73. package/src/lib/contracts.ts +410 -16
  74. package/src/ui/button-group.tsx +1 -1
  75. package/src/ui/carousel.tsx +1 -1
  76. package/src/ui/dialog.tsx +1 -1
  77. package/src/ui/field.tsx +2 -2
  78. package/src/ui/input-group.tsx +3 -3
  79. package/src/ui/pagination.tsx +1 -1
  80. package/src/ui/sidebar.tsx +7 -7
  81. package/src/ui/toggle-group.tsx +1 -1
  82. package/tsconfig.build.json +12 -0
  83. package/tsup.config.ts +11 -7
  84. package/.turbo/turbo-build.log +0 -24
  85. package/.turbo/turbo-test.log +0 -47
  86. package/.turbo/turbo-type-check.log +0 -0
  87. package/dist/.tsbuildinfo +0 -1
  88. package/dist/contracts.d.mts +0 -1732
  89. package/dist/contracts.d.ts +0 -1732
  90. package/dist/contracts.js +0 -423
  91. package/dist/contracts.js.map +0 -1
  92. package/dist/contracts.mjs +0 -365
  93. package/dist/contracts.mjs.map +0 -1
  94. package/dist/index.d.mts +0 -3068
  95. package/dist/index.d.ts +0 -3068
  96. package/dist/index.js +0 -17834
  97. package/dist/index.js.map +0 -1
  98. package/dist/index.mjs +0 -17740
  99. package/dist/index.mjs.map +0 -1
  100. package/src/blocks/application/login-forms/login-page-1.tsx +0 -246
  101. package/src/blocks/application/login-forms/login-page-2.tsx +0 -207
  102. package/src/blocks/e-commerce/category-filters/category-filter-1.tsx +0 -260
  103. package/src/blocks/e-commerce/category-filters/category-filter-2-filter-panel.tsx +0 -222
  104. package/src/blocks/e-commerce/category-filters/category-filter-2.tsx +0 -921
  105. package/src/blocks/e-commerce/category-filters/category-filter-4-chip-rail.tsx +0 -57
  106. package/src/blocks/e-commerce/category-filters/category-filter-4-drawer.tsx +0 -124
  107. package/src/blocks/e-commerce/category-filters/category-filter-4-fields.tsx +0 -170
  108. package/src/blocks/e-commerce/category-filters/category-filter-4-sidebar.tsx +0 -52
  109. package/src/blocks/e-commerce/category-filters/category-filter-4-sort-sheet.tsx +0 -57
  110. package/src/blocks/e-commerce/category-filters/category-filter-4-state.ts +0 -68
  111. package/src/blocks/e-commerce/category-filters/category-filter-4.tsx +0 -298
  112. package/src/blocks/e-commerce/checkout-forms/card-formatting.test.ts +0 -47
  113. package/src/blocks/e-commerce/checkout-forms/card-formatting.ts +0 -9
  114. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-contact-step.tsx +0 -96
  115. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-order-summary.tsx +0 -252
  116. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-payment-step.tsx +0 -188
  117. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-progress.tsx +0 -36
  118. package/src/blocks/e-commerce/checkout-forms/checkout-form-1.test.tsx +0 -216
  119. package/src/blocks/e-commerce/checkout-forms/checkout-form-1.tsx +0 -338
  120. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-order-summary.tsx +0 -130
  121. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-payment-card.tsx +0 -175
  122. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-shipping-card.tsx +0 -190
  123. package/src/blocks/e-commerce/checkout-forms/checkout-form-2.tsx +0 -312
  124. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-information-step.tsx +0 -91
  125. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-order-summary.tsx +0 -110
  126. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-payment-step.tsx +0 -137
  127. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-progress.tsx +0 -68
  128. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-review-step.tsx +0 -79
  129. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-shipping-step.tsx +0 -172
  130. package/src/blocks/e-commerce/checkout-forms/checkout-form-3.tsx +0 -392
  131. package/src/blocks/e-commerce/product-categories/avatar-group.tsx +0 -86
  132. package/src/blocks/e-commerce/product-categories/product-category-1.tsx +0 -126
  133. package/src/blocks/e-commerce/product-categories/product-category-2.test.tsx +0 -51
  134. package/src/blocks/e-commerce/product-categories/product-category-2.tsx +0 -120
  135. package/src/blocks/e-commerce/product-categories/product-category-4.tsx +0 -129
  136. package/src/blocks/e-commerce/product-categories/product-category-5.tsx +0 -218
  137. package/src/blocks/e-commerce/product-list/product-card-1.test.tsx +0 -88
  138. package/src/blocks/e-commerce/product-list/product-card-1.tsx +0 -192
  139. package/src/blocks/e-commerce/product-list/product-card-2.tsx +0 -71
  140. package/src/blocks/e-commerce/product-list/product-card-3-reaction-toggle.tsx +0 -166
  141. package/src/blocks/e-commerce/product-list/product-card-3.tsx +0 -215
  142. package/src/blocks/e-commerce/product-list/product-card-5-checkbox-check.tsx +0 -98
  143. package/src/blocks/e-commerce/product-list/product-card-5-reaction-toggle.tsx +0 -171
  144. package/src/blocks/e-commerce/product-list/product-card-5.tsx +0 -367
  145. package/src/blocks/e-commerce/product-overview/product-overview-1.test.tsx +0 -182
  146. package/src/blocks/e-commerce/product-overview/product-overview-1.tsx +0 -370
  147. package/src/blocks/e-commerce/product-overview/product-overview-2.tsx +0 -272
  148. package/src/blocks/e-commerce/product-overview/product-overview-5-gallery.tsx +0 -59
  149. package/src/blocks/e-commerce/product-overview/product-overview-5.tsx +0 -274
  150. package/src/blocks/e-commerce/shopping-carts/shopping-cart-1-spinning-counter.tsx +0 -142
  151. package/src/blocks/e-commerce/shopping-carts/shopping-cart-1.tsx +0 -291
  152. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-item-row.tsx +0 -110
  153. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-recommendations.tsx +0 -51
  154. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-spinning-counter.tsx +0 -146
  155. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2.tsx +0 -295
  156. package/src/blocks/e-commerce/shopping-carts/shopping-cart-3-spinning-counter.tsx +0 -142
  157. package/src/blocks/e-commerce/shopping-carts/shopping-cart-3.tsx +0 -342
  158. package/src/blocks/e-commerce/shopping-carts/shopping-cart-4-spinning-counter.tsx +0 -146
  159. package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.test.tsx +0 -229
  160. package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.test.tsx +0 -72
  161. package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.tsx +0 -263
  162. package/src/blocks/e-commerce/storefront-hero/storefront-hero-2.tsx +0 -283
  163. package/src/blocks/e-commerce/storefront-hero/storefront-hero-5.tsx +0 -318
  164. package/src/blocks/e-commerce/storefront-hero/storefront-hero-6.tsx +0 -205
  165. package/src/blocks/marketing/features/feature-1.tsx +0 -97
  166. package/src/blocks/marketing/features/feature-2.tsx +0 -82
  167. package/src/blocks/marketing/footers/footer-section-1.test.tsx +0 -117
  168. package/src/blocks/marketing/footers/footer-section-1.tsx +0 -178
  169. package/src/blocks/marketing/footers/footer-section-3.tsx +0 -192
  170. package/src/blocks/marketing/footers/footer-section-4.tsx +0 -355
  171. package/src/blocks/marketing/navbars/navbar-1.tsx +0 -267
  172. package/src/blocks/marketing/navbars/navbar-2.tsx +0 -351
  173. package/src/blocks/marketing/navbars/navbar-3.tsx +0 -303
  174. package/src/blocks/marketing/navbars/navbar-4-account-menu.test.tsx +0 -38
  175. package/src/blocks/marketing/navbars/navbar-4-like-button.tsx +0 -177
  176. package/src/blocks/marketing/navbars/navbar-4-product-grid.tsx +0 -219
  177. package/src/blocks/marketing/testimonials/testimonial-1.tsx +0 -141
  178. package/src/blocks/marketing/testimonials/testimonial-2.tsx +0 -108
@@ -1,205 +0,0 @@
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
@@ -1,97 +0,0 @@
1
- import { Badge } from '../../../ui/badge'
2
- import { Button } from '../../../ui/button'
3
- import { Card, CardContent } from '../../../ui/card'
4
- import { ArrowRight } from 'lucide-react'
5
- import type { LucideIcon } from 'lucide-react'
6
-
7
- export interface Feature1Item {
8
- id: string
9
- title: string
10
- description: string
11
- icon: LucideIcon
12
- }
13
-
14
- export interface Feature1Props {
15
- items: Feature1Item[]
16
- badge?: string
17
- title?: string
18
- description?: string
19
- learnMoreLabel?: string
20
- ctaLabel?: string
21
- onCtaClick?: () => void
22
- onLearnMore?: (item: Feature1Item) => void
23
- }
24
-
25
- export function Feature1({
26
- items,
27
- badge,
28
- title,
29
- description,
30
- learnMoreLabel,
31
- ctaLabel,
32
- onCtaClick,
33
- onLearnMore,
34
- }: Feature1Props) {
35
- return (
36
- <section className='mx-auto w-full max-w-7xl px-4 py-12 sm:px-6 md:py-16 lg:px-8'>
37
- {(badge || title || description) && (
38
- <header className='mx-auto mb-12 max-w-2xl text-center'>
39
- {badge && (
40
- <Badge variant='secondary' className='h-auto mb-4 rounded-full px-4 py-1 font-medium'>
41
- {badge}
42
- </Badge>
43
- )}
44
- {title && (
45
- <h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl'>{title}</h2>
46
- )}
47
- {description && (
48
- <p className='text-muted-foreground text-pretty'>{description}</p>
49
- )}
50
- </header>
51
- )}
52
-
53
- <div className='grid gap-4 sm:grid-cols-2 sm:gap-6 md:grid-cols-3'>
54
- {items.map(feature => (
55
- <article key={feature.id} className='group'>
56
- <Card className='relative h-full overflow-hidden transition-all hover:shadow-md'>
57
- <CardContent className='px-6'>
58
- <div className='bg-secondary text-secondary-foreground mb-4 inline-flex size-12 items-center justify-center rounded-xl'>
59
- <feature.icon className='size-5' aria-hidden='true' />
60
- </div>
61
- <h3 className='mb-2 text-lg font-semibold'>{feature.title}</h3>
62
- <p className='text-muted-foreground mb-4 text-sm'>{feature.description}</p>
63
-
64
- {learnMoreLabel && (
65
- <Button
66
- variant='link'
67
- size='sm'
68
- className='h-8 px-3 text-xs text-muted-foreground hover:text-foreground h-auto cursor-pointer !p-0 text-sm'
69
- onClick={onLearnMore ? () => onLearnMore(feature) : undefined}
70
- >
71
- {learnMoreLabel}
72
- <ArrowRight />
73
- </Button>
74
- )}
75
- </CardContent>
76
- </Card>
77
- </article>
78
- ))}
79
- </div>
80
-
81
- {ctaLabel && (
82
- <div className='mt-12 text-center'>
83
- <Button
84
- size='lg'
85
- className='h-10 cursor-pointer rounded-lg !px-8'
86
- onClick={onCtaClick}
87
- >
88
- {ctaLabel}
89
- <ArrowRight />
90
- </Button>
91
- </div>
92
- )}
93
- </section>
94
- )
95
- }
96
-
97
- export default Feature1
@@ -1,82 +0,0 @@
1
- import { cn } from '../../../lib/utils'
2
- import type { LucideIcon } from 'lucide-react'
3
-
4
- export interface Feature2Item {
5
- id: string
6
- title: string
7
- description: string
8
- icon: LucideIcon
9
- }
10
-
11
- export interface Feature2Props {
12
- items: Feature2Item[]
13
- title?: string
14
- description?: string
15
- image?: { src: string; alt: string }
16
- className?: string
17
- }
18
-
19
- export function Feature2({
20
- items,
21
- title,
22
- description,
23
- image,
24
- className,
25
- }: Feature2Props) {
26
- return (
27
- <section
28
- className={cn('mx-auto w-full max-w-7xl px-4 py-12 sm:px-6 md:py-16 lg:px-8', className)}
29
- >
30
- <div className='grid gap-12 lg:grid-cols-2 lg:gap-16'>
31
- <div className='flex flex-col gap-10'>
32
- {(title || description) && (
33
- <header className='flex flex-col gap-4'>
34
- {title && (
35
- <h2 className='text-3xl font-bold tracking-tight text-balance sm:text-4xl lg:text-5xl'>
36
- {title}
37
- </h2>
38
- )}
39
- {description && (
40
- <p className='text-muted-foreground text-base text-pretty md:text-lg'>
41
- {description}
42
- </p>
43
- )}
44
- </header>
45
- )}
46
-
47
- <div className='grid gap-6 sm:grid-cols-2 sm:gap-8'>
48
- {items.map(item => (
49
- <div key={item.id} className='group flex flex-col items-start gap-3 rounded-lg'>
50
- <div className='bg-secondary text-secondary-foreground inline-flex size-10 items-center justify-center rounded-full'>
51
- <item.icon className='size-5' aria-hidden='true' />
52
- </div>
53
- <div className='flex flex-col gap-1.5'>
54
- <h3 className='text-foreground text-base font-semibold md:text-lg'>{item.title}</h3>
55
- <p className='text-muted-foreground text-sm text-balance'>{item.description}</p>
56
- </div>
57
- </div>
58
- ))}
59
- </div>
60
- </div>
61
-
62
- {image && (
63
- <div className='relative'>
64
- <div className='bg-muted relative z-10 overflow-hidden rounded-lg shadow-sm transition-all duration-300 hover:shadow-md'>
65
- <div className='aspect-[4/3] w-full'>
66
- <img
67
- src={image.src || undefined}
68
- alt={image.alt}
69
- className='size-full object-cover grayscale'
70
- loading='lazy'
71
- decoding='async'
72
- />
73
- </div>
74
- </div>
75
- </div>
76
- )}
77
- </div>
78
- </section>
79
- )
80
- }
81
-
82
- export default Feature2
@@ -1,117 +0,0 @@
1
- import type { ReactElement, ReactNode } from 'react'
2
- import { renderToStaticMarkup } from 'react-dom/server'
3
- import { describe, expect, it, vi } from 'vitest'
4
- import { FooterSection1 } from './footer-section-1'
5
-
6
- const base = {
7
- brandName: 'ElectroCuba',
8
- linkColumns: [],
9
- }
10
-
11
- type SubmitHandler = (event: { preventDefault: () => void }) => void
12
-
13
- function findForm(node: ReactNode): ReactElement<{ onSubmit?: SubmitHandler }> | undefined {
14
- if (node === null || node === undefined || typeof node !== 'object') return undefined
15
- if (Array.isArray(node)) {
16
- for (const child of node) {
17
- const found = findForm(child)
18
- if (found) return found
19
- }
20
- return undefined
21
- }
22
- const element = node as ReactElement<{ children?: ReactNode }>
23
- if (element.type === 'form') return element as ReactElement<{ onSubmit?: SubmitHandler }>
24
- return findForm(element.props?.children)
25
- }
26
-
27
- describe('FooterSection1 — optional brand accent', () => {
28
- it('renders the brand name unchanged when no accent is supplied', () => {
29
- const html = renderToStaticMarkup(<FooterSection1 {...base} />)
30
- expect(html).toContain('>ElectroCuba</span>')
31
- expect(html).not.toContain('text-brand-promo')
32
- })
33
-
34
- it('accents a matched trailing segment without duplicating the brand name', () => {
35
- const html = renderToStaticMarkup(
36
- <FooterSection1 {...base} brandAccent={{ text: 'Cuba', className: 'text-brand-promo' }} />,
37
- )
38
- expect(html).toContain('Electro<span class="text-brand-promo">Cuba</span>')
39
- expect(html.match(/Electro/g)).toHaveLength(1)
40
- expect(html.match(/Cuba/g)).toHaveLength(1)
41
- })
42
-
43
- it('accents the full brand name when the accent covers all of it', () => {
44
- const html = renderToStaticMarkup(
45
- <FooterSection1 {...base} brandAccent={{ text: 'ElectroCuba', className: 'text-brand-promo' }} />,
46
- )
47
- expect(html).toContain('<span class="text-brand-promo">ElectroCuba</span>')
48
- })
49
-
50
- it('does not accent when the segment is not a trailing match', () => {
51
- const html = renderToStaticMarkup(<FooterSection1 {...base} brandAccent={{ text: 'Electro' }} />)
52
- expect(html).toContain('>ElectroCuba</span>')
53
- })
54
- })
55
-
56
- describe('FooterSection1 — newsletter requires a submit callback', () => {
57
- it('renders no form, input, or button when the newsletter has no onSubmit', () => {
58
- const html = renderToStaticMarkup(
59
- <FooterSection1 {...base} newsletter={{ buttonLabel: 'Suscribirse', placeholder: 'tu@correo.com' }} />,
60
- )
61
- expect(html).not.toContain('<form')
62
- expect(html).not.toContain('<input')
63
- expect(html).not.toContain('<button')
64
- })
65
-
66
- it('renders the interactive form when a submit handler is supplied', () => {
67
- const html = renderToStaticMarkup(
68
- <FooterSection1 {...base} newsletter={{ buttonLabel: 'Suscribirse', onSubmit: () => {} }} />,
69
- )
70
- expect(html).toContain('<form')
71
- expect(html).toContain('<input')
72
- expect(html).toContain('<button')
73
- expect(html).toContain('Suscribirse')
74
- })
75
-
76
- it('retains the supplied submit handler and forwards the email', () => {
77
- const onSubmit = vi.fn()
78
- const tree = FooterSection1({
79
- ...base,
80
- newsletter: { buttonLabel: 'Suscribirse', onSubmit },
81
- })
82
- const form = findForm(tree)
83
- expect(form).toBeDefined()
84
-
85
- vi.stubGlobal(
86
- 'FormData',
87
- class {
88
- get() {
89
- return 'shopper@example.com'
90
- }
91
- },
92
- )
93
- try {
94
- form?.props.onSubmit?.({ preventDefault: () => {} })
95
- } finally {
96
- vi.unstubAllGlobals()
97
- }
98
-
99
- expect(onSubmit).toHaveBeenCalledWith('shopper@example.com')
100
- })
101
- })
102
-
103
- describe('FooterSection1 — optional outer surface className', () => {
104
- it('keeps the default surface when className is omitted', () => {
105
- const html = renderToStaticMarkup(<FooterSection1 {...base} />)
106
- expect(html).toContain('bg-secondary')
107
- })
108
-
109
- it('merges a branded surface onto the footer and drops the default background', () => {
110
- const html = renderToStaticMarkup(
111
- <FooterSection1 {...base} className='bg-brand-nav text-brand-nav-foreground' />,
112
- )
113
- expect(html).toContain('bg-brand-nav')
114
- expect(html).toContain('text-brand-nav-foreground')
115
- expect(html).not.toContain('bg-secondary')
116
- })
117
- })
@@ -1,178 +0,0 @@
1
- 'use client'
2
-
3
- import { Input } from '../../../ui/input'
4
- import { Button } from '../../../ui/button'
5
- import { ArrowRight } from 'lucide-react'
6
- import { cn } from '../../../lib/utils'
7
-
8
- export interface FooterLink {
9
- label: string
10
- href?: string
11
- }
12
-
13
- export interface FooterLinkColumn {
14
- title: string
15
- links: FooterLink[]
16
- }
17
-
18
- export interface FooterNewsletter {
19
- placeholder?: string
20
- buttonLabel: string
21
- /**
22
- * Submit handler. The interactive form (input + submit button) renders only
23
- * when this is supplied. A newsletter without `onSubmit` renders no form,
24
- * input, or button — never a fake form.
25
- */
26
- onSubmit?: (email: string) => void
27
- }
28
-
29
- export interface BrandAccent {
30
- /** Trailing segment of `brandName` to render with the accent styling. */
31
- text: string
32
- /** Consumer-supplied classes for the accent span (e.g. a brand color token). */
33
- className?: string
34
- }
35
-
36
- export interface FooterSection1Props {
37
- brandName: string
38
- /**
39
- * Optional accent for a trailing segment of `brandName` (e.g. the "Cuba" in
40
- * "ElectroCuba"). When omitted, or when `text` is not a trailing segment of
41
- * `brandName`, the brand name renders unchanged.
42
- */
43
- brandAccent?: BrandAccent
44
- brandDescription?: string
45
- linkColumns: FooterLinkColumn[]
46
- newsletter?: FooterNewsletter
47
- /**
48
- * Consumer-supplied classes merged onto the outer `<footer>` as the final
49
- * argument, so a semantic surface such as `bg-brand-nav
50
- * text-brand-nav-foreground` overrides the default `bg-secondary`. Purely
51
- * presentational: no layout, structure, or behavior changes.
52
- */
53
- className?: string
54
- }
55
-
56
- export function FooterSection1({
57
- brandName,
58
- brandAccent,
59
- brandDescription,
60
- linkColumns,
61
- newsletter,
62
- className,
63
- }: FooterSection1Props) {
64
- const accentText = brandAccent?.text ?? ''
65
- const hasAccent = accentText.length > 0 && brandName.endsWith(accentText)
66
- const baseName = hasAccent ? brandName.slice(0, brandName.length - accentText.length) : brandName
67
- const newsletterSubmit = newsletter?.onSubmit
68
-
69
- return (
70
- <footer
71
- className={cn('bg-secondary w-full py-16', className)}
72
- aria-label='Pie de página'
73
- >
74
- <div className='mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8'>
75
- <div className='grid grid-cols-1 gap-8 lg:grid-cols-12'>
76
- {/* Left Section */}
77
- <div className='me-8 md:col-span-5'>
78
- <div className='mb-6 flex items-center gap-2'>
79
- <svg width='32' height='32' viewBox='0 0 32 32' fill='none' xmlns='http://www.w3.org/2000/svg'>
80
- <path
81
- 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'
82
- fill='currentColor'
83
- />
84
- <path
85
- 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'
86
- fill='currentColor'
87
- />
88
- <path
89
- 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'
90
- fill='currentColor'
91
- />
92
- <path
93
- 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'
94
- fill='currentColor'
95
- />
96
- <path
97
- 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'
98
- fill='currentColor'
99
- />
100
- <path d='M21.1694 10.5806L14.5651 17.1849' stroke='currentColor' />
101
- <path d='M22.1694 14.5806L18.5632 18.1868' stroke='currentColor' />
102
- <circle cx='13.1' cy='26.1' r='1.7' stroke='currentColor' />
103
- <circle cx='22.1' cy='26.1' r='1.7' stroke='currentColor' />
104
- </svg>
105
- <span className='text-xl font-semibold'>
106
- {baseName}
107
- {hasAccent ? <span className={brandAccent?.className}>{accentText}</span> : null}
108
- </span>
109
- </div>
110
- {brandDescription && (
111
- <p className='text-muted-foreground mb-8 max-w-sm text-sm leading-relaxed text-balance'>
112
- {brandDescription}
113
- </p>
114
- )}
115
- {newsletter && newsletterSubmit ? (
116
- <form
117
- className='relative'
118
- onSubmit={e => {
119
- e.preventDefault()
120
- const email = new FormData(e.currentTarget).get('email')
121
- if (typeof email === 'string') {
122
- newsletterSubmit(email)
123
- }
124
- }}
125
- >
126
- <Input
127
- type='email'
128
- name='email'
129
- placeholder={newsletter.placeholder}
130
- className='border-muted-foreground/20 bg-background/80 h-12 pe-32'
131
- aria-label='Correo electrónico'
132
- />
133
- <Button
134
- type='submit'
135
- variant='default'
136
- className='absolute end-1 top-1 h-10 cursor-pointer px-4'
137
- aria-label='Suscribirse a la newsletter'
138
- >
139
- {newsletter.buttonLabel}
140
- <ArrowRight />
141
- </Button>
142
- </form>
143
- ) : null}
144
- </div>
145
-
146
- {/* Right Section */}
147
- <div className='md:col-span-7'>
148
- <div className='grid grid-cols-1 gap-8 sm:grid-cols-2 md:grid-cols-3'>
149
- {linkColumns.map(column => (
150
- <div key={column.title}>
151
- <h3 className='mb-4 text-lg font-medium'>{column.title}</h3>
152
- <ul className='flex flex-col gap-3'>
153
- {column.links.map(link => (
154
- <li key={link.label}>
155
- {link.href ? (
156
- <a
157
- href={link.href}
158
- className='text-muted-foreground hover:text-primary text-sm transition-colors'
159
- >
160
- {link.label}
161
- </a>
162
- ) : (
163
- <span className='text-muted-foreground text-sm'>{link.label}</span>
164
- )}
165
- </li>
166
- ))}
167
- </ul>
168
- </div>
169
- ))}
170
- </div>
171
- </div>
172
- </div>
173
- </div>
174
- </footer>
175
- )
176
- }
177
-
178
- export default FooterSection1