@thorprovider/blocks 0.1.3 → 1.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 (99) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/README.md +73 -29
  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/e-commerce/product-categories/category-card-grid.test.tsx +122 -0
  7. package/src/blocks/e-commerce/product-categories/category-card.test.tsx +68 -0
  8. package/src/blocks/e-commerce/product-categories/category-card.tsx +172 -0
  9. package/src/blocks/e-commerce/product-categories/category-grid.test.tsx +67 -0
  10. package/src/blocks/e-commerce/product-categories/category-shared.tsx +109 -0
  11. package/src/blocks/e-commerce/product-categories/product-category-grid.tsx +170 -0
  12. package/src/blocks/e-commerce/product-categories/product-category.test.tsx +266 -0
  13. package/src/blocks/e-commerce/product-categories/product-category.tsx +462 -0
  14. package/src/blocks/e-commerce/product-list/product-card.test.tsx +263 -0
  15. package/src/blocks/e-commerce/product-list/product-card.tsx +424 -0
  16. package/src/blocks/e-commerce/product-list/product-grid.test.tsx +162 -0
  17. package/src/blocks/e-commerce/product-list/product-grid.tsx +114 -0
  18. package/src/blocks/e-commerce/product-overview/product-overview-gallery.tsx +71 -0
  19. package/src/blocks/e-commerce/product-overview/{product-overview-5-lightbox-tab.tsx → product-overview-lightbox-tab.tsx} +35 -23
  20. package/src/blocks/e-commerce/product-overview/{product-overview-5-reviews-tab.tsx → product-overview-reviews-tab.tsx} +40 -17
  21. package/src/blocks/e-commerce/product-overview/{product-overview-5-tabs.tsx → product-overview-tabs.tsx} +8 -7
  22. package/src/blocks/e-commerce/product-overview/product-overview.test.tsx +283 -0
  23. package/src/blocks/e-commerce/product-overview/product-overview.tsx +1186 -0
  24. package/src/blocks/e-commerce/reviews/review-rating-2.tsx +5 -6
  25. package/src/blocks/e-commerce/shopping-carts/shopping-cart.test.tsx +399 -0
  26. package/src/blocks/e-commerce/shopping-carts/{shopping-cart-4.tsx → shopping-cart.tsx} +119 -85
  27. package/src/blocks/e-commerce/storefront-hero/storefront-hero.test.tsx +412 -0
  28. package/src/blocks/e-commerce/storefront-hero/storefront-hero.tsx +1283 -0
  29. package/src/blocks/marketing/features/feature.test.tsx +153 -0
  30. package/src/blocks/marketing/features/feature.tsx +195 -0
  31. package/src/blocks/marketing/navbars/navbar-4-account-menu.test.tsx +27 -9
  32. package/src/blocks/marketing/navbars/navbar-4.test.tsx +33 -2
  33. package/src/blocks/marketing/navbars/navbar-4.tsx +29 -11
  34. package/src/index.ts +77 -48
  35. package/src/lib/contracts-curated.test.ts +42 -24
  36. package/src/lib/contracts.test.ts +143 -2
  37. package/src/lib/contracts.ts +346 -14
  38. package/src/ui/button-group.tsx +1 -1
  39. package/src/ui/carousel.tsx +1 -1
  40. package/src/ui/dialog.tsx +1 -1
  41. package/src/ui/field.tsx +2 -2
  42. package/src/ui/input-group.tsx +3 -3
  43. package/src/ui/pagination.tsx +1 -1
  44. package/src/ui/sidebar.tsx +7 -7
  45. package/src/ui/toggle-group.tsx +1 -1
  46. package/tsconfig.build.json +12 -0
  47. package/tsup.config.ts +11 -7
  48. package/.turbo/turbo-build.log +0 -24
  49. package/.turbo/turbo-test.log +0 -47
  50. package/.turbo/turbo-type-check.log +0 -0
  51. package/dist/.tsbuildinfo +0 -1
  52. package/dist/contracts.d.mts +0 -1732
  53. package/dist/contracts.d.ts +0 -1732
  54. package/dist/contracts.js +0 -423
  55. package/dist/contracts.js.map +0 -1
  56. package/dist/contracts.mjs +0 -365
  57. package/dist/contracts.mjs.map +0 -1
  58. package/dist/index.d.mts +0 -3068
  59. package/dist/index.d.ts +0 -3068
  60. package/dist/index.js +0 -17834
  61. package/dist/index.js.map +0 -1
  62. package/dist/index.mjs +0 -17740
  63. package/dist/index.mjs.map +0 -1
  64. package/src/blocks/e-commerce/product-categories/avatar-group.tsx +0 -86
  65. package/src/blocks/e-commerce/product-categories/product-category-1.tsx +0 -126
  66. package/src/blocks/e-commerce/product-categories/product-category-2.test.tsx +0 -51
  67. package/src/blocks/e-commerce/product-categories/product-category-2.tsx +0 -120
  68. package/src/blocks/e-commerce/product-categories/product-category-4.tsx +0 -129
  69. package/src/blocks/e-commerce/product-categories/product-category-5.tsx +0 -218
  70. package/src/blocks/e-commerce/product-list/product-card-1.test.tsx +0 -88
  71. package/src/blocks/e-commerce/product-list/product-card-1.tsx +0 -192
  72. package/src/blocks/e-commerce/product-list/product-card-2.tsx +0 -71
  73. package/src/blocks/e-commerce/product-list/product-card-3-reaction-toggle.tsx +0 -166
  74. package/src/blocks/e-commerce/product-list/product-card-3.tsx +0 -215
  75. package/src/blocks/e-commerce/product-list/product-card-5-checkbox-check.tsx +0 -98
  76. package/src/blocks/e-commerce/product-list/product-card-5-reaction-toggle.tsx +0 -171
  77. package/src/blocks/e-commerce/product-list/product-card-5.tsx +0 -367
  78. package/src/blocks/e-commerce/product-overview/product-overview-1.test.tsx +0 -182
  79. package/src/blocks/e-commerce/product-overview/product-overview-1.tsx +0 -370
  80. package/src/blocks/e-commerce/product-overview/product-overview-2.tsx +0 -272
  81. package/src/blocks/e-commerce/product-overview/product-overview-5-gallery.tsx +0 -59
  82. package/src/blocks/e-commerce/product-overview/product-overview-5.tsx +0 -274
  83. package/src/blocks/e-commerce/shopping-carts/shopping-cart-1-spinning-counter.tsx +0 -142
  84. package/src/blocks/e-commerce/shopping-carts/shopping-cart-1.tsx +0 -291
  85. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-item-row.tsx +0 -110
  86. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-recommendations.tsx +0 -51
  87. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-spinning-counter.tsx +0 -146
  88. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2.tsx +0 -295
  89. package/src/blocks/e-commerce/shopping-carts/shopping-cart-3-spinning-counter.tsx +0 -142
  90. package/src/blocks/e-commerce/shopping-carts/shopping-cart-3.tsx +0 -342
  91. package/src/blocks/e-commerce/shopping-carts/shopping-cart-4-spinning-counter.tsx +0 -146
  92. package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.test.tsx +0 -229
  93. package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.test.tsx +0 -72
  94. package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.tsx +0 -263
  95. package/src/blocks/e-commerce/storefront-hero/storefront-hero-2.tsx +0 -283
  96. package/src/blocks/e-commerce/storefront-hero/storefront-hero-5.tsx +0 -318
  97. package/src/blocks/e-commerce/storefront-hero/storefront-hero-6.tsx +0 -205
  98. package/src/blocks/marketing/features/feature-1.tsx +0 -97
  99. package/src/blocks/marketing/features/feature-2.tsx +0 -82
@@ -0,0 +1,266 @@
1
+ // @vitest-environment jsdom
2
+ import { renderToStaticMarkup } from 'react-dom/server'
3
+ import { beforeAll, describe, expect, it, vi } from 'vitest'
4
+ import { render, screen } from '@testing-library/react'
5
+ import userEvent from '@testing-library/user-event'
6
+ import { ProductCategory } from './product-category'
7
+ import type { ProductCategoryItem } from '../../../lib/contracts'
8
+
9
+ beforeAll(() => {
10
+ ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
11
+ })
12
+
13
+ const base: ProductCategoryItem = {
14
+ id: 'pcat_herramientas',
15
+ handle: 'herramientas',
16
+ name: 'Herramientas',
17
+ description: 'Taladros y accesorios',
18
+ image: '/products/drill.png',
19
+ trending: false,
20
+ }
21
+
22
+ const renderGrid = (category: ProductCategoryItem, extra: Record<string, unknown> = {}) =>
23
+ renderToStaticMarkup(<ProductCategory categories={[category]} {...extra} />)
24
+
25
+ // The count is the only text node that ends in "items" (the `items-center`
26
+ // utility lives in a class attribute and is never followed by `</span>`).
27
+ const hasGridCount = (html: string) => html.includes('items</span>')
28
+
29
+ describe('ProductCategory — optional itemCount (grid)', () => {
30
+ it('renders no count when itemCount is omitted (unknown projection)', () => {
31
+ const html = renderGrid(base, { itemsLabel: 'items' })
32
+ expect(hasGridCount(html)).toBe(false)
33
+ // The tile itself still renders.
34
+ expect(html).toContain('Herramientas')
35
+ })
36
+
37
+ it('preserves a real zero count', () => {
38
+ expect(renderGrid({ ...base, itemCount: 0 }, { itemsLabel: 'items' })).toContain('0 items</span>')
39
+ })
40
+
41
+ it('renders a positive count', () => {
42
+ expect(renderGrid({ ...base, itemCount: 7 }, { itemsLabel: 'items' })).toContain('7 items</span>')
43
+ })
44
+
45
+ it('renders the bare number when no items label is supplied', () => {
46
+ const html = renderGrid({ ...base, itemCount: 7 })
47
+ expect(html).toContain('>7<')
48
+ expect(html).not.toContain('items</span>')
49
+ })
50
+
51
+ it('omits a negative count', () => {
52
+ expect(hasGridCount(renderGrid({ ...base, itemCount: -1 }, { itemsLabel: 'items' }))).toBe(false)
53
+ })
54
+
55
+ it('omits a non-finite count', () => {
56
+ expect(hasGridCount(renderGrid({ ...base, itemCount: Number.NaN }, { itemsLabel: 'items' }))).toBe(false)
57
+ expect(hasGridCount(renderGrid({ ...base, itemCount: Number.POSITIVE_INFINITY }, { itemsLabel: 'items' }))).toBe(
58
+ false,
59
+ )
60
+ })
61
+ })
62
+
63
+ describe('ProductCategory — no demo defaults or hardcoded locale copy', () => {
64
+ it('ships no copy when the host supplies none', () => {
65
+ const html = renderGrid(base)
66
+ expect(html).toContain('Herramientas')
67
+ expect(html).not.toContain('Browse')
68
+ expect(html).not.toContain('Trending')
69
+ expect(html).not.toContain('items</span>')
70
+ })
71
+
72
+ it('renders the browse action only when a handler and label are supplied', () => {
73
+ expect(renderGrid({ ...base, trending: true })).not.toContain('Browse')
74
+ expect(renderGrid(base, { onBrowse: () => {}, browseLabel: 'Explorar' })).toContain('Explorar')
75
+ })
76
+
77
+ it('renders the trending badge only when a label is supplied', () => {
78
+ expect(renderGrid({ ...base, trending: true })).not.toContain('Trending')
79
+ expect(renderGrid({ ...base, trending: true }, { trendingLabel: 'Tendencia' })).toContain('Tendencia')
80
+ })
81
+ })
82
+
83
+ describe('ProductCategory — honest imagery', () => {
84
+ it('never emits src="" and falls back to a neutral placeholder', () => {
85
+ const html = renderGrid({ ...base, image: undefined })
86
+ expect(html).not.toContain('src=""')
87
+ expect(html).toContain('data:image/svg+xml')
88
+ })
89
+
90
+ it('treats a blank image as missing', () => {
91
+ expect(renderGrid({ ...base, image: ' ' })).toContain('data:image/svg+xml')
92
+ })
93
+ })
94
+
95
+ describe('ProductCategory — bounded presentations', () => {
96
+ it('icons presentation renders no image and a different structure than grid', () => {
97
+ const category: ProductCategoryItem = { id: 'pcat_x', name: 'Categoría', icon: 'shirt' }
98
+ const icons = renderToStaticMarkup(<ProductCategory categories={[category]} presentation='icons' />)
99
+ expect(icons).toContain('Categoría')
100
+ expect(icons).not.toContain('<img')
101
+ expect(icons).not.toContain('aspect-[5/4]')
102
+ })
103
+
104
+ it('icons presentation ignores category counts (no demo metrics)', () => {
105
+ const category: ProductCategoryItem = { id: 'pcat_x', name: 'Categoría', icon: 'shirt', itemCount: 42 }
106
+ const icons = renderToStaticMarkup(
107
+ <ProductCategory categories={[category]} presentation='icons' itemsLabel='items' />,
108
+ )
109
+ expect(icons).not.toContain('42')
110
+ expect(icons).not.toContain('items</span>')
111
+ expect(icons).not.toContain('items</div>')
112
+ })
113
+
114
+ it('falls back to a neutral icon for an unknown or absent key', () => {
115
+ const unknown = renderToStaticMarkup(
116
+ <ProductCategory categories={[{ id: 'pcat_x', name: 'X', icon: 'not-a-real-icon' }]} presentation='icons' />,
117
+ )
118
+ const absent = renderToStaticMarkup(
119
+ <ProductCategory categories={[{ id: 'pcat_y', name: 'Y' }]} presentation='icons' />,
120
+ )
121
+ expect(unknown).toContain('X')
122
+ expect(absent).toContain('Y')
123
+ })
124
+ })
125
+
126
+ describe('ProductCategory — icons: keyboard-operable interactive category', () => {
127
+ const category: ProductCategoryItem = { id: 'pcat_herramientas', name: 'Herramientas', icon: 'wrench' }
128
+
129
+ it('renders a native button whose accessible name is the category name', () => {
130
+ render(<ProductCategory categories={[category]} presentation='icons' onBrowse={() => {}} />)
131
+ const control = screen.getByRole('button', { name: 'Herramientas' })
132
+ expect(control.tagName).toBe('BUTTON')
133
+ expect(control.getAttribute('type')).toBe('button')
134
+ })
135
+
136
+ it('is focusable and activates with the Enter key, reporting onBrowse', async () => {
137
+ const user = userEvent.setup()
138
+ const onBrowse = vi.fn()
139
+ render(<ProductCategory categories={[category]} presentation='icons' onBrowse={onBrowse} />)
140
+ await user.tab()
141
+ const control = screen.getByRole('button', { name: 'Herramientas' })
142
+ expect(document.activeElement).toBe(control)
143
+ await user.keyboard('{Enter}')
144
+ expect(onBrowse).toHaveBeenCalledWith(category)
145
+ })
146
+
147
+ it('activates with the Space key, reporting onBrowse', async () => {
148
+ const user = userEvent.setup()
149
+ const onBrowse = vi.fn()
150
+ render(<ProductCategory categories={[category]} presentation='icons' onBrowse={onBrowse} />)
151
+ await user.tab()
152
+ await user.keyboard(' ')
153
+ expect(onBrowse).toHaveBeenCalledWith(category)
154
+ })
155
+
156
+ it('is not a focusable control when no browse handler is supplied', async () => {
157
+ const user = userEvent.setup()
158
+ render(<ProductCategory categories={[category]} presentation='icons' />)
159
+ expect(screen.queryByRole('button')).toBeNull()
160
+ await user.tab()
161
+ expect(document.activeElement).toBe(document.body)
162
+ expect(screen.getByText('Herramientas')).toBeTruthy()
163
+ })
164
+
165
+ it('uses an inset focus ring so the indicator is not clipped by the card', () => {
166
+ render(<ProductCategory categories={[category]} presentation='icons' onBrowse={() => {}} />)
167
+ const control = screen.getByRole('button', { name: 'Herramientas' })
168
+ expect(control.className).toContain('focus-visible:inset-ring-2')
169
+ expect(control.className).toContain('focus-visible:inset-ring-ring')
170
+ expect(control.className).not.toContain('focus-visible:ring-2')
171
+ })
172
+ })
173
+
174
+ describe('ProductCategory — grid/list browse controls expose the category name', () => {
175
+ const category: ProductCategoryItem = { id: 'pcat_herramientas', name: 'Herramientas', image: '/products/drill.png' }
176
+
177
+ it('names the grid browse control with the host label and the category name', () => {
178
+ render(
179
+ <ProductCategory categories={[category]} presentation='grid' onBrowse={() => {}} browseLabel='Explorar' />,
180
+ )
181
+ const control = screen.getByRole('button', { name: 'Explorar Herramientas' })
182
+ expect(control.textContent).toContain('Explorar')
183
+ })
184
+
185
+ it('names the list browse control with the host label and the category name', () => {
186
+ render(
187
+ <ProductCategory categories={[category]} presentation='list' onBrowse={() => {}} browseLabel='Explorar' />,
188
+ )
189
+ const control = screen.getByRole('button', { name: 'Explorar Herramientas' })
190
+ expect(control.textContent).toContain('Explorar')
191
+ })
192
+ })
193
+
194
+ describe('ProductCategory — list presentation owns no filtering', () => {
195
+ const categories: ProductCategoryItem[] = [
196
+ { id: 'pcat_shirts', name: 'Camisas', icon: 'shirt', subcategories: ['Manga larga', 'Manga corta'], itemCount: 12 },
197
+ { id: 'pcat_boots', name: 'Botas', icon: 'shirt', featured: true },
198
+ ]
199
+
200
+ it('renders only the categories the host supplies for the current query', () => {
201
+ const html = renderToStaticMarkup(
202
+ <ProductCategory categories={[categories[1]]} presentation='list' query='bot' onQueryChange={() => {}} />,
203
+ )
204
+ expect(html).toContain('Botas')
205
+ expect(html).not.toContain('Camisas')
206
+ })
207
+
208
+ it('renders subcategory chips and the count with the host label', () => {
209
+ const html = renderToStaticMarkup(
210
+ <ProductCategory categories={[categories[0]]} presentation='list' itemsLabel='productos' />,
211
+ )
212
+ expect(html).toContain('Manga larga')
213
+ expect(html).toContain('12 productos')
214
+ })
215
+
216
+ it('renders the featured badge only when a label is supplied', () => {
217
+ const without = renderToStaticMarkup(<ProductCategory categories={[categories[1]]} presentation='list' />)
218
+ const with_ = renderToStaticMarkup(
219
+ <ProductCategory categories={[categories[1]]} presentation='list' featuredLabel='Destacado' />,
220
+ )
221
+ expect(without).not.toContain('Destacado')
222
+ expect(with_).toContain('Destacado')
223
+ })
224
+
225
+ it('reflects the controlled query in the search input', () => {
226
+ const html = renderToStaticMarkup(
227
+ <ProductCategory categories={categories} presentation='list' query='cam' onQueryChange={() => {}} />,
228
+ )
229
+ expect(html).toContain('value="cam"')
230
+ })
231
+
232
+ it('shows the no-results copy for an empty searched list and the empty copy otherwise', () => {
233
+ const searched = renderToStaticMarkup(
234
+ <ProductCategory
235
+ categories={[]}
236
+ presentation='list'
237
+ query='zzz'
238
+ onQueryChange={() => {}}
239
+ noResultsLabel='Sin resultados'
240
+ emptyLabel='Sin categorías'
241
+ />,
242
+ )
243
+ const empty = renderToStaticMarkup(
244
+ <ProductCategory categories={[]} presentation='list' emptyLabel='Sin categorías' noResultsLabel='Sin resultados' />,
245
+ )
246
+ expect(searched).toContain('Sin resultados')
247
+ expect(empty).toContain('Sin categorías')
248
+ })
249
+ })
250
+
251
+ describe('ProductCategory — reusable across sectors without demo content', () => {
252
+ it('renders distinct sector datasets through the same component and contract', () => {
253
+ const fashion: ProductCategoryItem[] = [
254
+ { id: 'pcat_fashion_shirts', name: 'Camisas', icon: 'shirt' },
255
+ ]
256
+ const electronics: ProductCategoryItem[] = [
257
+ { id: 'pcat_electronics_laptops', name: 'Portátiles', icon: 'laptop' },
258
+ ]
259
+ const fashionHtml = renderToStaticMarkup(<ProductCategory categories={fashion} presentation='icons' />)
260
+ const electronicsHtml = renderToStaticMarkup(<ProductCategory categories={electronics} presentation='icons' />)
261
+ // Same component and contract; only the supplied data differs (no branch on sector).
262
+ expect(fashionHtml).toContain('Camisas')
263
+ expect(electronicsHtml).toContain('Portátiles')
264
+ expect(fashionHtml).not.toContain('Portátiles')
265
+ })
266
+ })