@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,229 +0,0 @@
1
- import { renderToStaticMarkup } from 'react-dom/server'
2
- import { describe, expect, it } from 'vitest'
3
- import { ShoppingCart4, type ShoppingCart4Item, type ShoppingCart4Totals } from './shopping-cart-4'
4
-
5
- const items: ShoppingCart4Item[] = [
6
- {
7
- id: 'line_1',
8
- name: 'Taladro',
9
- price: 45,
10
- quantity: 2,
11
- image: 'https://cdn.example/taladro.jpg',
12
- lineTotal: { amount: '90.00', currencyCode: 'USD' },
13
- },
14
- { id: 'line_2', name: 'Batería', price: 20, quantity: 1 },
15
- ]
16
-
17
- const totals: ShoppingCart4Totals = {
18
- subtotal: { amount: '110.00', currencyCode: 'USD' },
19
- discount: { amount: '10.00', currencyCode: 'USD' },
20
- tax: { amount: '8.00', currencyCode: 'USD' },
21
- shipping: { amount: '0', currencyCode: 'USD' },
22
- total: { amount: '108.00', currencyCode: 'USD' },
23
- }
24
-
25
- const formatMoney = (money: { amount: string; currencyCode: string }) =>
26
- `${money.currencyCode} ${money.amount}`
27
-
28
- describe('ShoppingCart4 — backend-authoritative totals', () => {
29
- it('renders supplied backend totals verbatim (no local arithmetic)', () => {
30
- const html = renderToStaticMarkup(
31
- <ShoppingCart4 items={items} promotions={[]} totals={totals} formatMoney={formatMoney} />,
32
- )
33
- expect(html).toContain('USD 110.00')
34
- expect(html).toContain('USD 10.00')
35
- expect(html).toContain('USD 8.00')
36
- expect(html).toContain('Gratis')
37
- expect(html).toContain('USD 108.00')
38
- })
39
-
40
- it('does not invent subtotal or total when none are supplied', () => {
41
- const html = renderToStaticMarkup(
42
- <ShoppingCart4 items={items} promotions={[]} formatMoney={formatMoney} />,
43
- )
44
- expect(html).not.toContain('Subtotal')
45
- expect(html).not.toContain('Total')
46
- })
47
-
48
- it('renders a controlled pending state', () => {
49
- const html = renderToStaticMarkup(
50
- <ShoppingCart4 items={items} promotions={[]} totalsState='pending' />,
51
- )
52
- expect(html).toContain('Actualizando totales')
53
- })
54
-
55
- it('renders a controlled error state with a recovery action', () => {
56
- const html = renderToStaticMarkup(
57
- <ShoppingCart4
58
- items={items}
59
- promotions={[]}
60
- totalsState='error'
61
- onRetryTotals={() => {}}
62
- />,
63
- )
64
- expect(html).toContain('No se pudieron actualizar los totales')
65
- expect(html).toContain('Reintentar')
66
- })
67
-
68
- it('omits an absent line image without emitting an empty src', () => {
69
- const html = renderToStaticMarkup(
70
- <ShoppingCart4 items={items} promotions={[]} totals={totals} formatMoney={formatMoney} />,
71
- )
72
- expect(html).not.toMatch(/src=""/)
73
- })
74
-
75
- it('does not fabricate a delivery estimate or express payment controls', () => {
76
- const html = renderToStaticMarkup(
77
- <ShoppingCart4 items={items} promotions={[]} totals={totals} formatMoney={formatMoney} />,
78
- )
79
- expect(html).not.toContain('Entrega estimada')
80
- expect(html).not.toContain('Apple Pay')
81
- expect(html).not.toContain('Google Pay')
82
- expect(html).not.toContain('Compra rápida')
83
- })
84
-
85
- it('never renders a fake zero for absent or malformed required totals', () => {
86
- const html = renderToStaticMarkup(
87
- <ShoppingCart4
88
- items={items}
89
- promotions={[]}
90
- totals={{
91
- subtotal: { amount: '', currencyCode: 'USD' },
92
- total: { amount: 'abc', currencyCode: 'USD' },
93
- }}
94
- formatMoney={formatMoney}
95
- />,
96
- )
97
- expect(html).not.toContain('USD 0')
98
- expect(html).toContain('Totales no disponibles')
99
- })
100
-
101
- it('preserves a legitimate zero total', () => {
102
- const html = renderToStaticMarkup(
103
- <ShoppingCart4
104
- items={items}
105
- promotions={[]}
106
- totals={{
107
- subtotal: { amount: '0', currencyCode: 'USD' },
108
- total: { amount: '0', currencyCode: 'USD' },
109
- }}
110
- formatMoney={formatMoney}
111
- />,
112
- )
113
- expect(html).toContain('USD 0')
114
- expect(html).not.toContain('Totales no disponibles')
115
- })
116
-
117
- it('shows an unavailable marker (not zero) for a supplied but malformed line total', () => {
118
- const html = renderToStaticMarkup(
119
- <ShoppingCart4
120
- items={[{ id: 'line_x', name: 'Pieza', price: 10, quantity: 1, lineTotal: { amount: '', currencyCode: 'USD' } }]}
121
- promotions={[]}
122
- totals={totals}
123
- formatMoney={formatMoney}
124
- />,
125
- )
126
- expect(html).toContain('—')
127
- expect(html).not.toContain('USD 0')
128
- })
129
-
130
- it('renders absolute quantities from props (cart contract, not a delta)', () => {
131
- const html = renderToStaticMarkup(
132
- <ShoppingCart4 items={items} promotions={[]} totals={totals} formatMoney={formatMoney} />,
133
- )
134
- expect(html).toContain('>2<')
135
- expect(html).toContain('>1<')
136
- })
137
- })
138
-
139
- describe('ShoppingCart4 — presentation and action availability', () => {
140
- it('defaults to the compact collapsible card', () => {
141
- const html = renderToStaticMarkup(
142
- <ShoppingCart4 items={items} promotions={[]} totals={totals} formatMoney={formatMoney} />,
143
- )
144
- expect(html).toContain('max-w-md')
145
- expect(html).toContain('Contraer carrito')
146
- expect(html).toContain('max-h-64')
147
- })
148
-
149
- it('renders page presentation as a wide, always-expanded review with a heading', () => {
150
- const html = renderToStaticMarkup(
151
- <ShoppingCart4
152
- items={items}
153
- promotions={[]}
154
- totals={totals}
155
- formatMoney={formatMoney}
156
- presentation='page'
157
- />,
158
- )
159
- expect(html).toContain('max-w-3xl')
160
- expect(html).toContain('<h1')
161
- expect(html).not.toContain('Contraer carrito')
162
- expect(html).not.toContain('max-h-64')
163
- })
164
-
165
- it('offers a continue-shopping action in page presentation', () => {
166
- const html = renderToStaticMarkup(
167
- <ShoppingCart4
168
- items={items}
169
- promotions={[]}
170
- totals={totals}
171
- formatMoney={formatMoney}
172
- presentation='page'
173
- onContinueShopping={() => {}}
174
- />,
175
- )
176
- expect(html).toContain('Seguir comprando')
177
- })
178
-
179
- it('suppresses checkout when the checkout feature is unavailable', () => {
180
- const html = renderToStaticMarkup(
181
- <ShoppingCart4
182
- items={items}
183
- promotions={[]}
184
- totals={totals}
185
- formatMoney={formatMoney}
186
- presentation='page'
187
- checkoutUnavailable
188
- />,
189
- )
190
- expect(html).not.toContain('Ver carrito y finalizar')
191
- })
192
-
193
- it('disables checkout while the cart is not ready', () => {
194
- const html = renderToStaticMarkup(
195
- <ShoppingCart4
196
- items={items}
197
- promotions={[]}
198
- totals={totals}
199
- formatMoney={formatMoney}
200
- presentation='page'
201
- checkoutDisabled
202
- />,
203
- )
204
- expect(html).toMatch(/<button[^>]*disabled[^>]*>[^<]*Ver carrito y finalizar/)
205
- })
206
- })
207
-
208
- describe('ShoppingCart4 — accessibility', () => {
209
- it('names every line mutation control', () => {
210
- const html = renderToStaticMarkup(
211
- <ShoppingCart4 items={items} promotions={[]} totals={totals} formatMoney={formatMoney} />,
212
- )
213
- expect(html).toContain('aria-label="Disminuir cantidad, Taladro"')
214
- expect(html).toContain('aria-label="Aumentar cantidad, Taladro"')
215
- expect(html).toContain('aria-label="Quitar Taladro"')
216
- })
217
-
218
- it('announces pending totals politely and errors as an alert', () => {
219
- const pending = renderToStaticMarkup(
220
- <ShoppingCart4 items={items} promotions={[]} totalsState='pending' />,
221
- )
222
- expect(pending).toContain('aria-live="polite"')
223
- expect(pending).toContain('Actualizando totales')
224
- const error = renderToStaticMarkup(
225
- <ShoppingCart4 items={items} promotions={[]} totalsState='error' />,
226
- )
227
- expect(error).toMatch(/role="alert"/)
228
- })
229
- })
@@ -1,72 +0,0 @@
1
- import { renderToStaticMarkup } from 'react-dom/server'
2
- import { describe, expect, it } from 'vitest'
3
- import StorefrontHero1, { type FeaturedProduct } from './storefront-hero-1'
4
-
5
- const product: FeaturedProduct = {
6
- id: 'p1',
7
- name: 'Taladro percutor',
8
- brand: 'INGCO',
9
- price: 69.99,
10
- originalPrice: 99.99,
11
- rating: 4.8,
12
- reviewCount: 236,
13
- inStock: true,
14
- isNew: false,
15
- isOnSale: true,
16
- image: '/products/drill.png',
17
- colors: ['#111111'],
18
- }
19
-
20
- const base = {
21
- title: 'Potencia para cada proyecto',
22
- description: 'Herramientas profesionales',
23
- products: [],
24
- }
25
-
26
- describe('StorefrontHero1 — optional editorial image', () => {
27
- it('renders no editorial image when none is supplied', () => {
28
- const html = renderToStaticMarkup(<StorefrontHero1 {...base} />)
29
- expect(html).not.toContain('<img')
30
- })
31
-
32
- it('renders a native editorial image with its src and alt text', () => {
33
- const html = renderToStaticMarkup(
34
- <StorefrontHero1 {...base} heroImage={{ src: '/hero-tools.png', alt: 'Taladro inalámbrico y accesorios' }} />,
35
- )
36
- expect(html).toContain('src="/hero-tools.png"')
37
- expect(html).toContain('alt="Taladro inalámbrico y accesorios"')
38
- })
39
-
40
- it('never renders an empty image src', () => {
41
- const blank = renderToStaticMarkup(
42
- <StorefrontHero1 {...base} heroImage={{ src: ' ', alt: 'Vacío' }} />,
43
- )
44
- expect(blank).not.toContain('<img')
45
- })
46
-
47
- it('preserves the product grid when the editorial image is omitted', () => {
48
- const html = renderToStaticMarkup(<StorefrontHero1 {...base} products={[product]} />)
49
- expect(html).toContain('Taladro percutor')
50
- expect(html).toContain('src="/products/drill.png"')
51
- expect(html).not.toContain('/hero-tools.png')
52
- })
53
-
54
- it('keeps the product grid when the editorial image is supplied', () => {
55
- const html = renderToStaticMarkup(
56
- <StorefrontHero1
57
- {...base}
58
- products={[product]}
59
- heroImage={{ src: '/hero-tools.png', alt: 'Taladro inalámbrico' }}
60
- />,
61
- )
62
- expect(html).toContain('src="/hero-tools.png"')
63
- expect(html).toContain('Taladro percutor')
64
- })
65
- })
66
-
67
- describe('StorefrontHero1 — product card secondary action label', () => {
68
- it('defaults the second product-card action to "Ver producto"', () => {
69
- const html = renderToStaticMarkup(<StorefrontHero1 {...base} products={[product]} />)
70
- expect(html).toContain('Ver producto')
71
- })
72
- })
@@ -1,263 +0,0 @@
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 HeroImage {
27
- src: string
28
- alt: string
29
- }
30
-
31
- export interface StorefrontHero1Props {
32
- title: string
33
- description: string
34
- products: FeaturedProduct[]
35
- titleAccent?: string
36
- badge?: string
37
- primaryCta?: CTA
38
- secondaryCta?: CTA
39
- /**
40
- * Optional editorial image. Rendered as a native `<img>` when `src` is a
41
- * non-empty string; omitted entirely when absent or blank (never an empty
42
- * `src`). The product grid is unaffected.
43
- */
44
- heroImage?: HeroImage
45
- onAddToCart?: (product: FeaturedProduct) => void
46
- onBuyNow?: (product: FeaturedProduct) => void
47
- onSelectColor?: (product: FeaturedProduct, index: number, color: string) => void
48
- addToCartLabel?: string
49
- buyNowLabel?: string
50
- newLabel?: string
51
- outOfStockLabel?: string
52
- colorsLabel?: string
53
- }
54
-
55
- function CtaButton({
56
- cta,
57
- variant,
58
- children,
59
- }: {
60
- cta: CTA
61
- variant?: ComponentProps<typeof Button>['variant']
62
- children: ReactNode
63
- }) {
64
- if (cta.href) {
65
- return (
66
- <Button
67
- variant={variant}
68
- size='lg'
69
- className='group relative cursor-pointer overflow-hidden px-8 py-6 text-base font-medium'
70
- asChild
71
- >
72
- <a href={cta.href}>{children}</a>
73
- </Button>
74
- )
75
- }
76
-
77
- return (
78
- <Button
79
- variant={variant}
80
- size='lg'
81
- className='group relative cursor-pointer overflow-hidden px-8 py-6 text-base font-medium'
82
- >
83
- {children}
84
- </Button>
85
- )
86
- }
87
-
88
- function StorefrontHero1({
89
- title,
90
- description,
91
- products,
92
- titleAccent,
93
- badge,
94
- primaryCta,
95
- secondaryCta,
96
- heroImage,
97
- onAddToCart,
98
- onBuyNow,
99
- onSelectColor,
100
- addToCartLabel = 'Agregar al carrito',
101
- buyNowLabel = 'Ver producto',
102
- newLabel = 'Nuevo',
103
- outOfStockLabel = 'Agotado',
104
- colorsLabel = 'Colores:',
105
- }: StorefrontHero1Props) {
106
- const reduceMotion = useReducedMotion()
107
-
108
- return (
109
- <div className='from-primary/5 to-background relative overflow-hidden bg-gradient-to-br'>
110
- <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'>
111
- <motion.div
112
- initial={reduceMotion ? false : { opacity: 0, y: 24 }}
113
- animate={{ opacity: 1, y: 0 }}
114
- transition={{ duration: 0.5, ease: 'easeOut' }}
115
- className='mx-auto max-w-xl text-center lg:mx-0 xl:text-left'
116
- >
117
- {badge && (
118
- <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'>
119
- {badge}
120
- </div>
121
- )}
122
- <h1 className='text-foreground mt-8 text-4xl font-bold tracking-tight sm:text-5xl md:text-7xl'>
123
- {title}{' '}
124
- {titleAccent && (
125
- <span className='from-primary to-primary/70 bg-gradient-to-r bg-clip-text text-transparent'>
126
- {titleAccent}
127
- </span>
128
- )}
129
- </h1>
130
- <p className='text-muted-foreground mt-8 text-lg'>{description}</p>
131
- {(primaryCta || secondaryCta) && (
132
- <div className='mt-10 flex flex-col gap-4 sm:flex-row sm:justify-center xl:justify-start'>
133
- {primaryCta && (
134
- <CtaButton cta={primaryCta}>
135
- <span className='relative z-10 flex items-center gap-2'>
136
- {primaryCta.label}
137
- <ArrowRight className='transition-transform group-hover:translate-x-1' />
138
- </span>
139
- <span className='bg-primary/5 absolute inset-0 -z-0 opacity-0 transition-opacity group-hover:opacity-100'></span>
140
- </CtaButton>
141
- )}
142
- {secondaryCta && (
143
- <CtaButton cta={secondaryCta} variant='outline'>
144
- <span className='relative z-10 flex items-center gap-2'>
145
- <ShoppingBag />
146
- {secondaryCta.label}
147
- </span>
148
- </CtaButton>
149
- )}
150
- </div>
151
- )}
152
- {heroImage && heroImage.src.trim().length > 0 ? (
153
- <img
154
- src={heroImage.src}
155
- alt={heroImage.alt}
156
- className='mt-10 aspect-video w-full rounded-lg object-cover'
157
- />
158
- ) : null}
159
- </motion.div>
160
-
161
- <div className='w-full max-w-2xl lg:mt-0'>
162
- <div className='grid gap-6 sm:grid-cols-2'>
163
- {products.map((product, index) => (
164
- <motion.div
165
- key={product.id}
166
- initial={reduceMotion ? false : { opacity: 0, y: 24 }}
167
- whileInView={{ opacity: 1, y: 0 }}
168
- viewport={{ once: true, margin: '-40px' }}
169
- transition={{
170
- duration: 0.45,
171
- delay: reduceMotion ? 0 : index * 0.1,
172
- ease: 'easeOut',
173
- }}
174
- className='bg-card relative h-full overflow-hidden rounded-lg border shadow-sm transition-shadow hover:shadow-md'
175
- >
176
- <div className='absolute top-4 left-4 z-10 flex flex-col gap-2'>
177
- {product.isNew && (
178
- <Badge variant='secondary' className='rounded-full'>{newLabel}</Badge>
179
- )}
180
- {product.isOnSale && (
181
- <Badge variant='destructive' className='rounded-full'>
182
- {Math.round(((product.originalPrice - product.price) / product.originalPrice) * 100)}% OFF
183
- </Badge>
184
- )}
185
- </div>
186
-
187
- <div className='relative aspect-square overflow-hidden'>
188
- <img
189
- src={product.image || undefined}
190
- alt={`${product.brand} ${product.name}`}
191
- className='size-full object-cover'
192
- loading='lazy'
193
- />
194
- {!product.inStock && (
195
- <div className='absolute inset-0 flex items-center justify-center bg-black/50'>
196
- <span className='rounded bg-white/90 px-3 py-1 text-sm font-medium'>{outOfStockLabel}</span>
197
- </div>
198
- )}
199
- </div>
200
-
201
- <div className='p-4'>
202
- <div className='text-muted-foreground mb-1 text-sm font-medium'>{product.brand}</div>
203
- <h3 className='mb-2 font-medium'>{product.name}</h3>
204
-
205
- <div className='mb-3'>
206
- <span className='text-lg font-bold'>${product.price.toFixed(2)}</span>
207
- {product.isOnSale && (
208
- <span className='text-muted-foreground ms-2 text-sm line-through'>
209
- ${product.originalPrice.toFixed(2)}
210
- </span>
211
- )}
212
- </div>
213
-
214
- <div className='mb-4 flex items-center'>
215
- <span className='text-muted-foreground me-2 text-sm'>{colorsLabel}</span>
216
- <div className='flex gap-1'>
217
- {product.colors.map((color, i) => (
218
- <button
219
- key={i}
220
- className='size-5 rounded-full border'
221
- style={{ backgroundColor: color }}
222
- aria-label={`Opción de color ${i + 1}`}
223
- onClick={onSelectColor ? () => onSelectColor(product, i, product.colors[i]) : undefined}
224
- />
225
- ))}
226
- </div>
227
- </div>
228
-
229
- <div className='mb-4 flex items-center'>
230
- <div className='flex'>
231
- {[...Array(5)].map((_, i) => (
232
- <Star
233
- key={i}
234
- className={`size-4 ${i < Math.floor(product.rating) ? 'fill-foreground text-foreground' : 'text-muted-foreground'}`}
235
- />
236
- ))}
237
- </div>
238
- <span className='text-muted-foreground ms-2 text-sm'>({product.reviewCount.toLocaleString()})</span>
239
- </div>
240
-
241
- <div className='flex flex-col gap-2'>
242
- <Button variant='outline' className="h-9 px-4 py-2 cursor-pointer" disabled={!product.inStock} onClick={onAddToCart ? () => onAddToCart(product) : undefined}>
243
- <ShoppingBag />
244
- {addToCartLabel}
245
- </Button>
246
- <Button className="h-9 px-4 py-2 cursor-pointer" disabled={!product.inStock} onClick={onBuyNow ? () => onBuyNow(product) : undefined}>
247
- {buyNowLabel}
248
- </Button>
249
- </div>
250
- </div>
251
- </motion.div>
252
- ))}
253
- </div>
254
- </div>
255
- </div>
256
-
257
- <div className='bg-primary/10 absolute -top-32 -right-32 size-96 rounded-full blur-3xl' />
258
- <div className='bg-secondary/30 absolute -bottom-32 -left-32 size-96 rounded-full blur-3xl' />
259
- </div>
260
- )
261
- }
262
-
263
- export default StorefrontHero1