@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,391 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import { ShoppingCart, Zap, Clock, CreditCard, Wallet, Minus, Plus, X } from 'lucide-react'
5
+ import type { LucideIcon } from 'lucide-react'
6
+ import { Card, CardContent, CardHeader, CardTitle } from '../../../ui/card'
7
+ import { Button } from '../../../ui/button'
8
+ import { Separator } from '../../../ui/separator'
9
+ import { Progress } from '../../../ui/progress'
10
+ import { ShoppingCart4SpinningCounter } from './shopping-cart-4-spinning-counter'
11
+ import { cn } from '../../../lib/utils'
12
+
13
+ export interface ShoppingCart4Item {
14
+ id: string // serialization-friendly (seed used number)
15
+ name: string
16
+ price: number
17
+ quantity: number
18
+ image: string
19
+ variant?: string
20
+ }
21
+
22
+ export interface ShoppingCart4Promotion {
23
+ threshold: number
24
+ reward: string
25
+ icon: LucideIcon
26
+ }
27
+
28
+ export interface ShoppingCart4RecentlyViewed {
29
+ id: string
30
+ name: string
31
+ price: number
32
+ image: string
33
+ }
34
+
35
+ export interface ShoppingCart4Props {
36
+ items: ShoppingCart4Item[]
37
+ promotions: ShoppingCart4Promotion[]
38
+ recentlyViewed?: ShoppingCart4RecentlyViewed[]
39
+ // business rules externalized:
40
+ taxRate?: number // default 0.08
41
+ // labels (Spanish defaults):
42
+ titleLabel?: string // 'Carrito de compras'
43
+ subtotalLabel?: string // 'Subtotal'
44
+ taxLabel?: string // 'Impuestos'
45
+ shippingLabel?: string // 'Envío'
46
+ shippingFreeLabel?: string // 'Gratis'
47
+ totalLabel?: string // 'Total'
48
+ awayFromLabel?: string // 'Te faltan ${amount} para'
49
+ activatedLabel?: string // '¡{reward} activada!'
50
+ itemsLabel?: string // 'artículos'
51
+ expressCheckoutLabel?: string // 'Compra rápida'
52
+ expressLabel?: string // 'Rápido'
53
+ standardLabel?: string // 'Estándar'
54
+ estimatedDeliveryLabel?: string // 'Entrega estimada: 2-3 días hábiles'
55
+ secureCheckoutLabel?: string // 'Pago seguro • SSL cifrado'
56
+ recentlyViewedLabel?: string // 'Vistos recientemente'
57
+ collapseLabel?: string // 'Contraer carrito'
58
+ expandLabel?: string // 'Expandir carrito'
59
+ checkoutLabel?: string // 'Ver carrito y finalizar'
60
+ quickCheckoutLabel?: string // 'Compra rápida'
61
+ checkoutNowLabel?: string // 'Finalizar ahora'
62
+ // callbacks:
63
+ onQuantityChange?: (id: string, quantity: number) => void
64
+ onRemove?: (id: string) => void
65
+ onCheckout?: () => void
66
+ onQuickCheckout?: () => void // Express mode checkout button
67
+ onPayWithApple?: () => void
68
+ onPayWithGoogle?: () => void
69
+ onRecentlyViewedClick?: (id: string) => void
70
+ }
71
+
72
+ const interpolate = (template: string, values: Record<string, string>) =>
73
+ template.replace(/\$\{(\w+)\}|\{(\w+)\}/g, (match, dollarKey, braceKey) => {
74
+ const key = dollarKey ?? braceKey
75
+ return values[key] ?? match
76
+ })
77
+
78
+ export function ShoppingCart4({
79
+ items,
80
+ promotions,
81
+ recentlyViewed,
82
+ taxRate = 0.08,
83
+ titleLabel = 'Carrito de compras',
84
+ subtotalLabel = 'Subtotal',
85
+ taxLabel = 'Impuestos',
86
+ shippingLabel = 'Envío',
87
+ shippingFreeLabel = 'Gratis',
88
+ totalLabel = 'Total',
89
+ awayFromLabel = 'Te faltan ${amount} para',
90
+ activatedLabel = '¡{reward} activada!',
91
+ itemsLabel = 'artículos',
92
+ expressCheckoutLabel = 'Compra rápida',
93
+ expressLabel = 'Rápido',
94
+ standardLabel = 'Estándar',
95
+ estimatedDeliveryLabel = 'Entrega estimada: 2-3 días hábiles',
96
+ secureCheckoutLabel = 'Pago seguro • SSL cifrado',
97
+ recentlyViewedLabel = 'Vistos recientemente',
98
+ collapseLabel = 'Contraer carrito',
99
+ expandLabel = 'Expandir carrito',
100
+ checkoutLabel = 'Ver carrito y finalizar',
101
+ quickCheckoutLabel = 'Compra rápida',
102
+ checkoutNowLabel = 'Finalizar ahora',
103
+ onQuantityChange,
104
+ onRemove,
105
+ onCheckout,
106
+ onQuickCheckout,
107
+ onPayWithApple,
108
+ onPayWithGoogle,
109
+ onRecentlyViewedClick,
110
+ }: ShoppingCart4Props) {
111
+ const [isExpanded, setIsExpanded] = useState(true)
112
+ const [quickCheckout, setQuickCheckout] = useState(false)
113
+
114
+ const itemCount = items.reduce((sum, item) => sum + item.quantity, 0)
115
+ const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0)
116
+ const tax = subtotal * taxRate
117
+ const total = subtotal + tax
118
+
119
+ const sortedPromotions = [...promotions].sort((a, b) => a.threshold - b.threshold)
120
+ const currentPromo = (() => {
121
+ for (let i = sortedPromotions.length - 1; i >= 0; i--) {
122
+ if (subtotal >= sortedPromotions[i].threshold) {
123
+ return { ...sortedPromotions[i], achieved: true }
124
+ }
125
+ }
126
+ const next = sortedPromotions.find(p => subtotal < p.threshold)
127
+ return next ? { ...next, achieved: false } : null
128
+ })()
129
+ const progressPercentage =
130
+ currentPromo && !currentPromo.achieved
131
+ ? Math.min(100, (subtotal / currentPromo.threshold) * 100)
132
+ : 100
133
+
134
+ return (
135
+ <section className='py-8'>
136
+ <div className='mx-auto max-w-md px-4'>
137
+ <Card className='shadow-lg'>
138
+ {/* Header */}
139
+ <CardHeader className='flex flex-row items-center justify-between space-y-0 pb-3'>
140
+ <CardTitle className='flex items-center gap-2'>
141
+ <ShoppingCart className='size-5' />
142
+ {titleLabel} ({itemCount})
143
+ </CardTitle>
144
+ <Button
145
+ variant='ghost'
146
+ className='h-8 cursor-pointer px-3 text-xs'
147
+ size='sm'
148
+ onClick={() => setIsExpanded(!isExpanded)}
149
+ aria-expanded={isExpanded}
150
+ aria-label={isExpanded ? collapseLabel : expandLabel}
151
+ >
152
+ {isExpanded ? <X /> : <Plus />}
153
+ </Button>
154
+ </CardHeader>
155
+
156
+ <CardContent className='flex flex-col gap-6'>
157
+ {items.length === 0 ? (
158
+ <div className='flex flex-col items-center justify-center gap-2 py-6 text-center'>
159
+ <ShoppingCart className='text-muted-foreground/50 size-10' />
160
+ <p className='text-muted-foreground text-sm'>Tu carrito está vacío</p>
161
+ </div>
162
+ ) : (
163
+ <>
164
+ {/* Promotion Progress */}
165
+ {currentPromo && !currentPromo.achieved ? (
166
+ <div className='flex flex-col gap-3'>
167
+ <div className='flex items-center justify-between text-sm'>
168
+ <span className='text-muted-foreground'>
169
+ {interpolate(awayFromLabel, {
170
+ amount: `$${(currentPromo.threshold - subtotal).toFixed(2)}`,
171
+ })}
172
+ </span>
173
+ <div className='flex items-center gap-1'>
174
+ <currentPromo.icon className='text-primary size-4' />
175
+ <span className='text-primary font-medium'>{currentPromo.reward}</span>
176
+ </div>
177
+ </div>
178
+ <Progress value={progressPercentage} className='h-2' />
179
+ </div>
180
+ ) : null}
181
+
182
+ {/* Active Promotion */}
183
+ {currentPromo?.achieved ? (
184
+ <div className='rounded-lg bg-success/10 p-3'>
185
+ <div className='flex items-center gap-2 text-success'>
186
+ <currentPromo.icon className='size-4' />
187
+ <span className='text-sm font-medium'>
188
+ {interpolate(activatedLabel, { reward: currentPromo.reward })}
189
+ </span>
190
+ </div>
191
+ </div>
192
+ ) : null}
193
+
194
+ {isExpanded && (
195
+ <>
196
+ {/* Cart Items */}
197
+ <div className='scrollbar-thin scrollbar-thumb-gray-300 dark:scrollbar-thumb-gray-600 scrollbar-track-transparent flex max-h-64 flex-col gap-4 overflow-y-auto'>
198
+ {items.map(item => (
199
+ <div key={item.id} className='flex items-start gap-3'>
200
+ <img src={item.image || undefined} alt={item.name} className='h-12 w-12 rounded-md object-cover' />
201
+
202
+ <div className='flex flex-1 flex-col gap-1'>
203
+ <h3 className='line-clamp-2 text-sm font-medium'>{item.name}</h3>
204
+ {item.variant ? <p className='text-muted-foreground text-xs'>{item.variant}</p> : null}
205
+
206
+ <div className='flex items-center justify-between'>
207
+ <span className='text-sm font-semibold'>${item.price.toFixed(2)}</span>
208
+
209
+ <div className='flex items-center'>
210
+ <Button
211
+ variant='ghost'
212
+ size='sm'
213
+ className='size-6 h-8 cursor-pointer p-0 px-3 text-xs'
214
+ onClick={
215
+ onQuantityChange
216
+ ? () => onQuantityChange(item.id, item.quantity - 1)
217
+ : undefined
218
+ }
219
+ disabled={item.quantity <= 1}
220
+ aria-label={`Disminuir cantidad de ${item.name}`}
221
+ >
222
+ <Minus />
223
+ </Button>
224
+ <span className='mx-2 w-6 text-center text-sm'>{item.quantity}</span>
225
+ <Button
226
+ variant='ghost'
227
+ size='sm'
228
+ className='size-6 h-8 cursor-pointer p-0 px-3 text-xs'
229
+ onClick={
230
+ onQuantityChange
231
+ ? () => onQuantityChange(item.id, item.quantity + 1)
232
+ : undefined
233
+ }
234
+ aria-label={`Aumentar cantidad de ${item.name}`}
235
+ >
236
+ <Plus />
237
+ </Button>
238
+ <Button
239
+ variant='ghost'
240
+ size='sm'
241
+ className='ms-2 size-6 h-8 cursor-pointer p-0 px-3 text-xs text-destructive hover:text-destructive'
242
+ onClick={onRemove ? () => onRemove(item.id) : undefined}
243
+ aria-label={`Quitar ${item.name}`}
244
+ >
245
+ <X />
246
+ </Button>
247
+ </div>
248
+ </div>
249
+ </div>
250
+ </div>
251
+ ))}
252
+ </div>
253
+
254
+ <Separator />
255
+
256
+ {/* Order Summary */}
257
+ <div className='flex flex-col gap-2'>
258
+ <div className='flex justify-between text-sm'>
259
+ <span>{subtotalLabel}</span>
260
+ <span>${subtotal.toFixed(2)}</span>
261
+ </div>
262
+ <div className='flex justify-between text-sm'>
263
+ <span>{taxLabel}</span>
264
+ <span>${tax.toFixed(2)}</span>
265
+ </div>
266
+ <div className='flex justify-between text-sm text-success'>
267
+ <span>{shippingLabel}</span>
268
+ <span>{shippingFreeLabel}</span>
269
+ </div>
270
+ <Separator />
271
+ <div className='flex justify-between font-semibold'>
272
+ <span>{totalLabel}</span>
273
+ <span>
274
+ <ShoppingCart4SpinningCounter value={total} decimals={2} prefix='$' />
275
+ </span>
276
+ </div>
277
+ </div>
278
+
279
+ {/* Quick Actions */}
280
+ <div className='flex flex-col gap-3'>
281
+ {/* Express Checkout Toggle */}
282
+ <div className='flex items-center justify-between text-sm'>
283
+ <span>{expressCheckoutLabel}</span>
284
+ <Button
285
+ variant='outline'
286
+ size='sm'
287
+ onClick={() => setQuickCheckout(!quickCheckout)}
288
+ className='h-8 cursor-pointer'
289
+ aria-pressed={quickCheckout}
290
+ >
291
+ <Zap />
292
+ {quickCheckout ? standardLabel : expressLabel}
293
+ </Button>
294
+ </div>
295
+
296
+ {quickCheckout && (
297
+ <div className='grid grid-cols-2 gap-2'>
298
+ <Button size='sm' variant='outline' className='h-8 cursor-pointer px-3 text-xs' onClick={onPayWithApple}>
299
+ <Wallet />
300
+ Apple Pay
301
+ </Button>
302
+ <Button size='sm' variant='outline' className='h-8 cursor-pointer px-3 text-xs' onClick={onPayWithGoogle}>
303
+ <CreditCard />
304
+ Google Pay
305
+ </Button>
306
+ </div>
307
+ )}
308
+
309
+ {/* Checkout Button */}
310
+ <Button
311
+ size='lg'
312
+ className='h-10 w-full cursor-pointer px-8'
313
+ onClick={quickCheckout ? onQuickCheckout : onCheckout}
314
+ >
315
+ {quickCheckout ? quickCheckoutLabel : checkoutLabel}
316
+ </Button>
317
+
318
+ {/* Estimated Delivery */}
319
+ <div className='text-muted-foreground flex items-center justify-center gap-2 text-xs'>
320
+ <Clock className='size-3' />
321
+ <span>{estimatedDeliveryLabel}</span>
322
+ </div>
323
+ </div>
324
+
325
+ {/* Security Badge */}
326
+ <div className='bg-muted/50 rounded-lg p-3 text-center'>
327
+ <div className='text-muted-foreground flex items-center justify-center gap-2 text-xs'>
328
+ <div className='size-2 rounded-full bg-success' aria-hidden='true' />
329
+ <span>{secureCheckoutLabel}</span>
330
+ </div>
331
+ </div>
332
+ </>
333
+ )}
334
+
335
+ {/* Collapsed Summary */}
336
+ {!isExpanded && (
337
+ <div className='flex flex-col gap-3'>
338
+ <div className='flex items-center justify-between'>
339
+ <span className='text-muted-foreground text-sm'>
340
+ {itemCount} {itemsLabel}
341
+ </span>
342
+ <span className='font-semibold'>
343
+ <ShoppingCart4SpinningCounter value={total} decimals={2} prefix='$' />
344
+ </span>
345
+ </div>
346
+
347
+ <Button size='sm' className='h-8 w-full cursor-pointer px-3 text-xs' onClick={onCheckout}>
348
+ {checkoutNowLabel}
349
+ </Button>
350
+ </div>
351
+ )}
352
+
353
+ {/* Recently Viewed */}
354
+ {isExpanded && recentlyViewed && recentlyViewed.length > 0 && (
355
+ <div className='flex flex-col gap-3'>
356
+ <h4 className='text-sm font-medium'>{recentlyViewedLabel}</h4>
357
+ <div className='grid grid-cols-3 gap-2'>
358
+ {recentlyViewed.map(item => (
359
+ <button
360
+ key={item.id}
361
+ type='button'
362
+ className={cn(
363
+ 'group text-start',
364
+ onRecentlyViewedClick && 'cursor-pointer',
365
+ )}
366
+ onClick={onRecentlyViewedClick ? () => onRecentlyViewedClick(item.id) : undefined}
367
+ >
368
+ <div className='mb-1 aspect-square overflow-hidden'>
369
+ <img
370
+ src={item.image || undefined}
371
+ alt={item.name}
372
+ className='size-full rounded-md object-cover'
373
+ />
374
+ </div>
375
+ <p className='text-muted-foreground truncate text-xs'>{item.name}</p>
376
+ <p className='text-xs font-medium'>${item.price.toFixed(2)}</p>
377
+ </button>
378
+ ))}
379
+ </div>
380
+ </div>
381
+ )}
382
+ </>
383
+ )}
384
+ </CardContent>
385
+ </Card>
386
+ </div>
387
+ </section>
388
+ )
389
+ }
390
+
391
+ export default ShoppingCart4
@@ -0,0 +1,29 @@
1
+ import type { ReactNode } from 'react'
2
+ import { cn } from '../../../lib/utils'
3
+
4
+ export interface StoreContainer1Props {
5
+ size?: 'default' | 'narrow' | 'wide' | 'full'
6
+ className?: string
7
+ children?: ReactNode
8
+ }
9
+
10
+ const sizeClasses: Record<NonNullable<StoreContainer1Props['size']>, string> = {
11
+ default: 'mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8',
12
+ narrow: 'mx-auto w-full max-w-3xl px-4 sm:px-6 lg:px-8',
13
+ wide: 'mx-auto w-full max-w-screen-2xl px-4 sm:px-6 lg:px-8',
14
+ full: 'w-full',
15
+ }
16
+
17
+ /**
18
+ * Trivial presentational layout container used as the root of spec-driven
19
+ * storefront pages. Constrains content width; children render as-is.
20
+ */
21
+ export function StoreContainer1({
22
+ size = 'default',
23
+ className,
24
+ children,
25
+ }: StoreContainer1Props) {
26
+ return <div className={cn(sizeClasses[size], className)}>{children}</div>
27
+ }
28
+
29
+ export default StoreContainer1
@@ -0,0 +1,244 @@
1
+ 'use client'
2
+
3
+ import { motion, useReducedMotion } from 'motion/react'
4
+
5
+ import { Badge } from '../../../ui/badge'
6
+ import { Button } from '../../../ui/button'
7
+ import { ShoppingBag, Star, ArrowRight } from 'lucide-react'
8
+ import type { ComponentProps, ReactNode } from 'react'
9
+ import type { CTA } from '../../../lib/types'
10
+
11
+ export interface FeaturedProduct {
12
+ id: string
13
+ name: string
14
+ brand: string
15
+ price: number
16
+ originalPrice: number
17
+ rating: number
18
+ reviewCount: number
19
+ inStock: boolean
20
+ isNew: boolean
21
+ isOnSale: boolean
22
+ image: string
23
+ colors: string[]
24
+ }
25
+
26
+ export interface StorefrontHero1Props {
27
+ title: string
28
+ description: string
29
+ products: FeaturedProduct[]
30
+ titleAccent?: string
31
+ badge?: string
32
+ primaryCta?: CTA
33
+ secondaryCta?: CTA
34
+ onAddToCart?: (product: FeaturedProduct) => void
35
+ onBuyNow?: (product: FeaturedProduct) => void
36
+ onSelectColor?: (product: FeaturedProduct, index: number, color: string) => void
37
+ addToCartLabel?: string
38
+ buyNowLabel?: string
39
+ newLabel?: string
40
+ outOfStockLabel?: string
41
+ colorsLabel?: string
42
+ }
43
+
44
+ function CtaButton({
45
+ cta,
46
+ variant,
47
+ children,
48
+ }: {
49
+ cta: CTA
50
+ variant?: ComponentProps<typeof Button>['variant']
51
+ children: ReactNode
52
+ }) {
53
+ if (cta.href) {
54
+ return (
55
+ <Button
56
+ variant={variant}
57
+ size='lg'
58
+ className='group relative cursor-pointer overflow-hidden px-8 py-6 text-base font-medium'
59
+ asChild
60
+ >
61
+ <a href={cta.href}>{children}</a>
62
+ </Button>
63
+ )
64
+ }
65
+
66
+ return (
67
+ <Button
68
+ variant={variant}
69
+ size='lg'
70
+ className='group relative cursor-pointer overflow-hidden px-8 py-6 text-base font-medium'
71
+ >
72
+ {children}
73
+ </Button>
74
+ )
75
+ }
76
+
77
+ function StorefrontHero1({
78
+ title,
79
+ description,
80
+ products,
81
+ titleAccent,
82
+ badge,
83
+ primaryCta,
84
+ secondaryCta,
85
+ onAddToCart,
86
+ onBuyNow,
87
+ onSelectColor,
88
+ addToCartLabel = 'Agregar al carrito',
89
+ buyNowLabel = 'Comprar ahora',
90
+ newLabel = 'Nuevo',
91
+ outOfStockLabel = 'Agotado',
92
+ colorsLabel = 'Colores:',
93
+ }: StorefrontHero1Props) {
94
+ const reduceMotion = useReducedMotion()
95
+
96
+ return (
97
+ <div className='from-primary/5 to-background relative overflow-hidden bg-gradient-to-br'>
98
+ <div className='relative z-10 mx-auto w-full max-w-7xl flex flex-col items-center justify-between gap-12 px-4 sm:px-6 lg:px-8 py-16 lg:py-20 xl:flex-row xl:items-start'>
99
+ <motion.div
100
+ initial={reduceMotion ? false : { opacity: 0, y: 24 }}
101
+ animate={{ opacity: 1, y: 0 }}
102
+ transition={{ duration: 0.5, ease: 'easeOut' }}
103
+ className='mx-auto max-w-xl text-center lg:mx-0 xl:text-left'
104
+ >
105
+ {badge && (
106
+ <div className='border-primary/20 bg-primary/5 text-primary inline-flex items-center rounded-full border px-4 py-1.5 text-sm font-medium backdrop-blur-sm'>
107
+ {badge}
108
+ </div>
109
+ )}
110
+ <h1 className='text-foreground mt-8 text-4xl font-bold tracking-tight sm:text-5xl md:text-7xl'>
111
+ {title}{' '}
112
+ {titleAccent && (
113
+ <span className='from-primary to-primary/70 bg-gradient-to-r bg-clip-text text-transparent'>
114
+ {titleAccent}
115
+ </span>
116
+ )}
117
+ </h1>
118
+ <p className='text-muted-foreground mt-8 text-lg'>{description}</p>
119
+ {(primaryCta || secondaryCta) && (
120
+ <div className='mt-10 flex flex-col gap-4 sm:flex-row sm:justify-center xl:justify-start'>
121
+ {primaryCta && (
122
+ <CtaButton cta={primaryCta}>
123
+ <span className='relative z-10 flex items-center gap-2'>
124
+ {primaryCta.label}
125
+ <ArrowRight className='transition-transform group-hover:translate-x-1' />
126
+ </span>
127
+ <span className='bg-primary/5 absolute inset-0 -z-0 opacity-0 transition-opacity group-hover:opacity-100'></span>
128
+ </CtaButton>
129
+ )}
130
+ {secondaryCta && (
131
+ <CtaButton cta={secondaryCta} variant='outline'>
132
+ <span className='relative z-10 flex items-center gap-2'>
133
+ <ShoppingBag />
134
+ {secondaryCta.label}
135
+ </span>
136
+ </CtaButton>
137
+ )}
138
+ </div>
139
+ )}
140
+ </motion.div>
141
+
142
+ <div className='w-full max-w-2xl lg:mt-0'>
143
+ <div className='grid gap-6 sm:grid-cols-2'>
144
+ {products.map((product, index) => (
145
+ <motion.div
146
+ key={product.id}
147
+ initial={reduceMotion ? false : { opacity: 0, y: 24 }}
148
+ whileInView={{ opacity: 1, y: 0 }}
149
+ viewport={{ once: true, margin: '-40px' }}
150
+ transition={{
151
+ duration: 0.45,
152
+ delay: reduceMotion ? 0 : index * 0.1,
153
+ ease: 'easeOut',
154
+ }}
155
+ className='bg-card relative h-full overflow-hidden rounded-lg border shadow-sm transition-shadow hover:shadow-md'
156
+ >
157
+ <div className='absolute top-4 left-4 z-10 flex flex-col gap-2'>
158
+ {product.isNew && (
159
+ <Badge variant='secondary' className='rounded-full'>{newLabel}</Badge>
160
+ )}
161
+ {product.isOnSale && (
162
+ <Badge variant='destructive' className='rounded-full'>
163
+ {Math.round(((product.originalPrice - product.price) / product.originalPrice) * 100)}% OFF
164
+ </Badge>
165
+ )}
166
+ </div>
167
+
168
+ <div className='relative aspect-square overflow-hidden'>
169
+ <img
170
+ src={product.image || undefined}
171
+ alt={`${product.brand} ${product.name}`}
172
+ className='size-full object-cover'
173
+ loading='lazy'
174
+ />
175
+ {!product.inStock && (
176
+ <div className='absolute inset-0 flex items-center justify-center bg-black/50'>
177
+ <span className='rounded bg-white/90 px-3 py-1 text-sm font-medium'>{outOfStockLabel}</span>
178
+ </div>
179
+ )}
180
+ </div>
181
+
182
+ <div className='p-4'>
183
+ <div className='text-muted-foreground mb-1 text-sm font-medium'>{product.brand}</div>
184
+ <h3 className='mb-2 font-medium'>{product.name}</h3>
185
+
186
+ <div className='mb-3'>
187
+ <span className='text-lg font-bold'>${product.price.toFixed(2)}</span>
188
+ {product.isOnSale && (
189
+ <span className='text-muted-foreground ms-2 text-sm line-through'>
190
+ ${product.originalPrice.toFixed(2)}
191
+ </span>
192
+ )}
193
+ </div>
194
+
195
+ <div className='mb-4 flex items-center'>
196
+ <span className='text-muted-foreground me-2 text-sm'>{colorsLabel}</span>
197
+ <div className='flex gap-1'>
198
+ {product.colors.map((color, i) => (
199
+ <button
200
+ key={i}
201
+ className='size-5 rounded-full border'
202
+ style={{ backgroundColor: color }}
203
+ aria-label={`Opción de color ${i + 1}`}
204
+ onClick={onSelectColor ? () => onSelectColor(product, i, product.colors[i]) : undefined}
205
+ />
206
+ ))}
207
+ </div>
208
+ </div>
209
+
210
+ <div className='mb-4 flex items-center'>
211
+ <div className='flex'>
212
+ {[...Array(5)].map((_, i) => (
213
+ <Star
214
+ key={i}
215
+ className={`size-4 ${i < Math.floor(product.rating) ? 'fill-foreground text-foreground' : 'text-muted-foreground'}`}
216
+ />
217
+ ))}
218
+ </div>
219
+ <span className='text-muted-foreground ms-2 text-sm'>({product.reviewCount.toLocaleString()})</span>
220
+ </div>
221
+
222
+ <div className='flex flex-col gap-2'>
223
+ <Button variant='outline' className="h-9 px-4 py-2 cursor-pointer" disabled={!product.inStock} onClick={onAddToCart ? () => onAddToCart(product) : undefined}>
224
+ <ShoppingBag />
225
+ {addToCartLabel}
226
+ </Button>
227
+ <Button className="h-9 px-4 py-2 cursor-pointer" disabled={!product.inStock} onClick={onBuyNow ? () => onBuyNow(product) : undefined}>
228
+ {buyNowLabel}
229
+ </Button>
230
+ </div>
231
+ </div>
232
+ </motion.div>
233
+ ))}
234
+ </div>
235
+ </div>
236
+ </div>
237
+
238
+ <div className='bg-primary/10 absolute -top-32 -right-32 size-96 rounded-full blur-3xl' />
239
+ <div className='bg-secondary/30 absolute -bottom-32 -left-32 size-96 rounded-full blur-3xl' />
240
+ </div>
241
+ )
242
+ }
243
+
244
+ export default StorefrontHero1