@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,274 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import { Camera, Heart, Info, MessageSquare, Package, Share2, ShoppingCart, Star } from 'lucide-react'
5
+ import { Card, CardContent } from '../../../ui/card'
6
+ import { Button } from '../../../ui/button'
7
+ import { Separator } from '../../../ui/separator'
8
+ import { ReactionToggle } from '../../shared/reaction-toggle'
9
+ import { ProductOverview5Gallery } from './product-overview-5-gallery'
10
+ import { ProductOverview5LightboxTab } from './product-overview-5-lightbox-tab'
11
+ import { ProductOverview5ReviewsTab } from './product-overview-5-reviews-tab'
12
+ import {
13
+ ProductOverview5Tabs,
14
+ ProductOverview5TabsContentFade,
15
+ ProductOverview5TabsIndicator,
16
+ ProductOverview5TabsList,
17
+ ProductOverview5TabsTrigger,
18
+ } from './product-overview-5-tabs'
19
+
20
+ export interface ProductOverview5Review {
21
+ id: number
22
+ author: string
23
+ rating: number
24
+ comment: string
25
+ date: string
26
+ helpful: number
27
+ }
28
+
29
+ export interface ProductOverview5Product {
30
+ name: string
31
+ price: number
32
+ originalPrice: null | number
33
+ rating: number
34
+ reviewCount: number
35
+ sku: string
36
+ mainImages: string[]
37
+ galleryImages: string[]
38
+ description: string
39
+ features: string[]
40
+ specifications: Record<string, string>
41
+ userReviews: ProductOverview5Review[]
42
+ }
43
+
44
+ export interface ProductOverview5Props {
45
+ product: ProductOverview5Product
46
+ badgeLabel?: string
47
+ availabilityLabel?: string
48
+ currencySymbol?: string
49
+ skuLabel?: string
50
+ overviewLabel?: string
51
+ specsLabel?: string
52
+ featuresLabel?: string
53
+ specificationsLabel?: string
54
+ reviewsLabel?: string
55
+ galleryLabel?: string
56
+ saveLabel?: string
57
+ savedLabel?: string
58
+ shareLabel?: string
59
+ addToCartLabel?: string
60
+ basedOnReviewsLabel?: string
61
+ foundHelpfulLabel?: string
62
+ ofCounterLabel?: string
63
+ photosLabel?: string
64
+ onAddToCart?: (product: ProductOverview5Product) => void
65
+ onToggleWishlist?: (product: ProductOverview5Product, isWishlisted: boolean) => void
66
+ onShare?: (product: ProductOverview5Product) => void
67
+ }
68
+
69
+ function renderStars(rating: number) {
70
+ return Array.from({ length: 5 }, (_, i) => (
71
+ <Star key={i} className={`size-4 ${i < Math.floor(rating) ? 'fill-foreground text-foreground' : i < rating ? 'text-foreground' : ''}`} />
72
+ ))
73
+ }
74
+
75
+ export default function ProductOverview5({
76
+ product,
77
+ badgeLabel = 'Destacado',
78
+ availabilityLabel = 'Disponible',
79
+ currencySymbol = '$',
80
+ skuLabel = 'SKU',
81
+ overviewLabel = 'Resumen',
82
+ specsLabel = 'Especificaciones',
83
+ featuresLabel = 'Características clave',
84
+ specificationsLabel = 'Especificaciones técnicas',
85
+ reviewsLabel = 'Reseñas',
86
+ galleryLabel = 'Galería',
87
+ saveLabel = 'Guardar',
88
+ savedLabel = 'Guardado',
89
+ shareLabel = 'Compartir',
90
+ addToCartLabel = 'Agregar al carrito',
91
+ basedOnReviewsLabel = 'Basado en {count} reseñas',
92
+ foundHelpfulLabel = '{count} personas encontraron esto útil',
93
+ ofCounterLabel = '{current} de {total}',
94
+ photosLabel = 'fotos',
95
+ onAddToCart,
96
+ onToggleWishlist,
97
+ onShare,
98
+ }: ProductOverview5Props) {
99
+ const [selectedImage, setSelectedImage] = useState(0)
100
+ const [isWishlisted, setIsWishlisted] = useState(false)
101
+
102
+ return (
103
+ <section className='py-12'>
104
+ <div className='mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8'>
105
+ <div className='grid gap-12 lg:grid-cols-2'>
106
+ <ProductOverview5Gallery
107
+ product={product}
108
+ badgeLabel={badgeLabel}
109
+ selectedImage={selectedImage}
110
+ onSelectImage={setSelectedImage}
111
+ photosLabel={photosLabel}
112
+ />
113
+
114
+ <div className='flex flex-col gap-8'>
115
+ <div>
116
+ <h1 className='text-3xl leading-tight font-bold text-balance'>{product.name}</h1>
117
+ <div className='mt-4 flex flex-wrap items-center gap-4'>
118
+ <div className='flex items-center gap-2'>
119
+ <div className='flex'>{renderStars(product.rating)}</div>
120
+ <span className='text-sm font-medium'>{product.rating}</span>
121
+ </div>
122
+ <span className='text-muted-foreground text-sm'>
123
+ {product.userReviews.length} {reviewsLabel.toLowerCase()}
124
+ </span>
125
+ <Separator orientation='vertical' className='h-4' />
126
+ <span className='text-muted-foreground text-sm'>
127
+ {skuLabel}: {product.sku}
128
+ </span>
129
+ </div>
130
+ </div>
131
+
132
+ <div className='flex items-baseline gap-3'>
133
+ <span className='text-4xl font-bold'>
134
+ {currencySymbol}
135
+ {product.price}
136
+ </span>
137
+ {product.originalPrice && (
138
+ <span className='text-muted-foreground text-xl line-through'>
139
+ {currencySymbol}
140
+ {product.originalPrice}
141
+ </span>
142
+ )}
143
+ </div>
144
+
145
+ <div className='inline-flex items-center w-fit gap-2 rounded-full bg-success/10 px-3 py-1.5'>
146
+ <div className='size-2 rounded-full bg-success' />
147
+ <span className='text-sm font-medium text-success'>{availabilityLabel}</span>
148
+ </div>
149
+
150
+ <p className='text-muted-foreground leading-relaxed'>{product.description}</p>
151
+
152
+ <div className='flex flex-col gap-4'>
153
+ <Button
154
+ size='lg'
155
+ className='h-12 w-full cursor-pointer gap-2'
156
+ onClick={onAddToCart ? () => onAddToCart(product) : undefined}
157
+ >
158
+ <ShoppingCart />
159
+ {addToCartLabel}
160
+ </Button>
161
+ <div className='flex gap-3'>
162
+ <Button
163
+ variant='outline'
164
+ size='lg'
165
+ className='h-12 flex-1 cursor-pointer gap-2'
166
+ aria-pressed={isWishlisted}
167
+ onClick={() => {
168
+ const next = !isWishlisted
169
+ setIsWishlisted(next)
170
+ onToggleWishlist?.(product, next)
171
+ }}
172
+ >
173
+ <ReactionToggle active={isWishlisted} icon={Heart} activeColorClassName='text-destructive' />
174
+ {isWishlisted ? savedLabel : saveLabel}
175
+ </Button>
176
+ <Button
177
+ variant='outline'
178
+ size='lg'
179
+ className='h-12 flex-1 cursor-pointer gap-2'
180
+ onClick={onShare ? () => onShare(product) : undefined}
181
+ >
182
+ <Share2 />
183
+ {shareLabel}
184
+ </Button>
185
+ </div>
186
+ </div>
187
+ </div>
188
+ </div>
189
+
190
+ <div className='mt-16'>
191
+ <ProductOverview5Tabs defaultValue='overview' className='w-full'>
192
+ <ProductOverview5TabsList className='bg-muted/50 relative grid !h-12 w-full grid-cols-4 rounded-xl p-1'>
193
+ <ProductOverview5TabsIndicator />
194
+ <ProductOverview5TabsTrigger
195
+ value='overview'
196
+ className='z-10 h-10 gap-2 text-sm font-medium data-active:bg-transparent data-active:shadow-none dark:data-active:bg-transparent'
197
+ >
198
+ <Info className='size-4 shrink-0' />
199
+ <span className='hidden sm:inline'>{overviewLabel}</span>
200
+ </ProductOverview5TabsTrigger>
201
+ <ProductOverview5TabsTrigger
202
+ value='specs'
203
+ className='z-10 h-10 gap-2 text-sm font-medium data-active:bg-transparent data-active:shadow-none dark:data-active:bg-transparent'
204
+ >
205
+ <Package className='size-4 shrink-0' />
206
+ <span className='hidden sm:inline'>{specsLabel}</span>
207
+ </ProductOverview5TabsTrigger>
208
+ <ProductOverview5TabsTrigger
209
+ value='reviews'
210
+ className='z-10 h-10 gap-2 text-sm font-medium data-active:bg-transparent data-active:shadow-none dark:data-active:bg-transparent'
211
+ >
212
+ <MessageSquare className='size-4 shrink-0' />
213
+ <span className='hidden sm:inline'>
214
+ {reviewsLabel} ({product.userReviews.length})
215
+ </span>
216
+ </ProductOverview5TabsTrigger>
217
+ <ProductOverview5TabsTrigger
218
+ value='gallery'
219
+ className='z-10 h-10 gap-2 text-sm font-medium data-active:bg-transparent data-active:shadow-none dark:data-active:bg-transparent'
220
+ >
221
+ <Camera className='size-4 shrink-0' />
222
+ <span className='hidden sm:inline'>{galleryLabel}</span>
223
+ </ProductOverview5TabsTrigger>
224
+ </ProductOverview5TabsList>
225
+ <ProductOverview5TabsContentFade value='overview' className='mt-8'>
226
+ <Card className='border shadow-none'>
227
+ <CardContent className='p-8 py-0'>
228
+ <h3 className='mb-6 text-xl font-semibold text-balance'>{featuresLabel}</h3>
229
+ <div className='grid gap-4 sm:grid-cols-2'>
230
+ {product.features.map(feature => (
231
+ <div key={feature} className='group flex items-center gap-3'>
232
+ <div className='bg-primary/20 size-2 rounded-full transition-colors' />
233
+ <span className='text-sm'>{feature}</span>
234
+ </div>
235
+ ))}
236
+ </div>
237
+ </CardContent>
238
+ </Card>
239
+ </ProductOverview5TabsContentFade>
240
+ <ProductOverview5TabsContentFade value='specs' className='mt-8'>
241
+ <Card className='border shadow-none'>
242
+ <CardContent className='p-8 py-0'>
243
+ <h3 className='mb-6 text-xl font-semibold text-balance'>{specificationsLabel}</h3>
244
+ <div className='flex flex-col gap-4'>
245
+ {Object.entries(product.specifications).map(([key, value]) => (
246
+ <div key={key} className='border-muted grid grid-cols-1 gap-2 border-b py-3 last:border-b-0 md:grid-cols-3 md:gap-4'>
247
+ <span className='text-muted-foreground text-sm font-medium'>{key}</span>
248
+ <span className='col-span-1 text-sm md:col-span-2'>{value}</span>
249
+ </div>
250
+ ))}
251
+ </div>
252
+ </CardContent>
253
+ </Card>
254
+ </ProductOverview5TabsContentFade>
255
+ <ProductOverview5TabsContentFade value='reviews' className='mt-8'>
256
+ <ProductOverview5ReviewsTab
257
+ product={product}
258
+ basedOnReviewsLabel={basedOnReviewsLabel}
259
+ foundHelpfulLabel={foundHelpfulLabel}
260
+ />
261
+ </ProductOverview5TabsContentFade>
262
+ <ProductOverview5TabsContentFade value='gallery' className='mt-8'>
263
+ <ProductOverview5LightboxTab
264
+ product={product}
265
+ galleryLabel={galleryLabel}
266
+ ofCounterLabel={ofCounterLabel}
267
+ />
268
+ </ProductOverview5TabsContentFade>
269
+ </ProductOverview5Tabs>
270
+ </div>
271
+ </div>
272
+ </section>
273
+ )
274
+ }
@@ -0,0 +1,135 @@
1
+ 'use client'
2
+
3
+ import { Eye, Star } from 'lucide-react'
4
+
5
+ import { Badge } from '../../../ui/badge'
6
+ import { Card, CardContent } from '../../../ui/card'
7
+ import { cn } from '../../../lib/utils'
8
+
9
+ import type { QuickViewProduct } from './product-quick-view'
10
+
11
+ const money = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 })
12
+
13
+ /**
14
+ * One product in the grid, and the thing a quick view is meant to keep you inside of.
15
+ *
16
+ * The whole card is the button. A card that carries a separate "Quick view" link has two targets
17
+ * for one intention, and on a touch screen the hover-revealed one is a target that never appears
18
+ * at all - so the control is drawn on hover and focus as an *affordance*, while the press area
19
+ * is the entire card at every input type.
20
+ */
21
+ export function ProductQuickViewCard({
22
+ product,
23
+ isActive,
24
+ onOpen,
25
+ }: {
26
+ product: QuickViewProduct
27
+ /** Whether this is the product currently open in the quick view. */
28
+ isActive: boolean
29
+ onOpen: () => void
30
+ }) {
31
+ const swatches = product.variants.find(variant => variant.options.some(option => option.swatch))?.options ?? []
32
+
33
+ return (
34
+ <Card
35
+ className={cn(
36
+ 'group/card relative gap-0 overflow-hidden py-0 transition-shadow',
37
+ // The mark that survives behind the dialog: when the reader closes it, the card they
38
+ // were looking at is the one still lit, so the grid tells them where they got to.
39
+ isActive ? 'ring-primary ring-2' : 'hover:shadow-lg'
40
+ )}
41
+ >
42
+ <div className='bg-muted relative aspect-4/5 w-full overflow-hidden'>
43
+ <img
44
+ src={product.image || undefined}
45
+ alt={product.imageAlt}
46
+ className='size-full object-cover transition-transform duration-500 ease-out group-hover/card:scale-[1.03]'
47
+ loading='lazy'
48
+ decoding='async'
49
+ />
50
+
51
+ {product.compareAt ? (
52
+ <Badge className='absolute top-3 left-3 rounded-full'>
53
+ {`${Math.round((1 - product.price / product.compareAt) * 100)}% de descuento`}
54
+ </Badge>
55
+ ) : null}
56
+
57
+ {/*
58
+ Drawn on hover and focus, and never the thing that has to be hit. `pointer-events-none`
59
+ keeps every press on the card's own trigger underneath, so there is exactly one target
60
+ however the reader is pointing at it.
61
+ */}
62
+ <div
63
+ aria-hidden='true'
64
+ className={cn(
65
+ 'pointer-events-none absolute inset-x-0 bottom-0 flex justify-center p-3',
66
+ 'translate-y-2 opacity-0 transition-[opacity,transform] duration-200 ease-out',
67
+ 'group-hover/card:translate-y-0 group-hover/card:opacity-100',
68
+ 'group-has-focus-visible/card:translate-y-0 group-has-focus-visible/card:opacity-100'
69
+ )}
70
+ >
71
+ <span className='bg-background/90 text-foreground ring-border flex items-center gap-2 rounded-full px-3 py-1.5 text-sm font-medium ring-1 backdrop-blur'>
72
+ <Eye className='size-4' />
73
+ Vista rápida
74
+ </span>
75
+ </div>
76
+ </div>
77
+
78
+ {/*
79
+ The card's one interactive element, and it covers the card.
80
+
81
+ A stretched link would leave only the title actually pressable: the picture sits over a
82
+ pseudo-element and swallows every click on the image and on the Quick view pill. A real
83
+ element in the DOM, on top, with an explicit z-index, is unambiguous at every input type.
84
+ It is empty and carries an accessible name, so the price and rating underneath are not
85
+ read as part of the button's label.
86
+ */}
87
+ <button
88
+ type='button'
89
+ onClick={onOpen}
90
+ aria-haspopup='dialog'
91
+ className='focus-visible:ring-ring/50 absolute inset-0 z-10 cursor-pointer rounded-xl outline-none focus-visible:ring-[3px]'
92
+ >
93
+ <span className='sr-only'>{`Vista rápida: ${product.name}`}</span>
94
+ </button>
95
+
96
+ <CardContent className='flex flex-col gap-1 p-4'>
97
+ <div className='flex items-start justify-between gap-3'>
98
+ <p className='text-base font-medium'>{product.name}</p>
99
+
100
+ <p className='shrink-0 text-base font-medium tabular-nums'>{money.format(product.price)}</p>
101
+ </div>
102
+
103
+ <p className='text-muted-foreground text-sm'>{product.tagline}</p>
104
+
105
+ <div className='mt-2 flex items-center justify-between gap-3'>
106
+ <p className='text-muted-foreground flex items-center gap-1.5 text-xs'>
107
+ {/* Foreground token per theming contract, never the only carrier of the rating. */}
108
+ <Star className='size-3.5 fill-foreground text-foreground' aria-hidden='true' />
109
+ <span className='text-foreground font-medium tabular-nums'>{product.rating}</span>
110
+ <span className='tabular-nums'>({product.reviewCount})</span>
111
+ </p>
112
+
113
+ {swatches.length > 0 ? (
114
+ <span className='flex items-center gap-1'>
115
+ {/*
116
+ Finish colours are product data, so they stay literal rather than tokenised, and
117
+ they are decoration here - the same information is in words inside the panel.
118
+ */}
119
+ {swatches.map(option => (
120
+ <span
121
+ key={option.value}
122
+ aria-hidden='true'
123
+ className='ring-border size-3 rounded-full ring-1'
124
+ style={{ backgroundColor: option.swatch }}
125
+ />
126
+ ))}
127
+ </span>
128
+ ) : null}
129
+ </div>
130
+ </CardContent>
131
+ </Card>
132
+ )
133
+ }
134
+
135
+ export default ProductQuickViewCard