@thorprovider/blocks 1.0.0 → 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 (91) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +156 -40
  3. package/package.json +1 -1
  4. package/src/blocks/application/login-forms/login-page.test.tsx +190 -0
  5. package/src/blocks/application/login-forms/login-page.tsx +240 -0
  6. package/src/blocks/application/password-recovery/forgot-password-page-1.tsx +6 -8
  7. package/src/blocks/application/signup-forms/signup-page-1.tsx +127 -129
  8. package/src/blocks/application/verification/verify-1.tsx +2 -2
  9. package/src/blocks/application/viewport-ownership.test.tsx +41 -0
  10. package/src/blocks/e-commerce/category-filters/category-listing-filter-fields.tsx +218 -0
  11. package/src/blocks/e-commerce/category-filters/{category-filter-2.test.tsx → category-listing.test.tsx} +287 -199
  12. package/src/blocks/e-commerce/category-filters/category-listing.tsx +934 -0
  13. package/src/blocks/e-commerce/checkout-forms/checkout-contact-step.tsx +149 -0
  14. package/src/blocks/e-commerce/checkout-forms/{checkout-form-1-country-field.tsx → checkout-country-field.tsx} +10 -8
  15. package/src/blocks/e-commerce/checkout-forms/checkout-order-summary.tsx +230 -0
  16. package/src/blocks/e-commerce/checkout-forms/checkout-payment-step.tsx +100 -0
  17. package/src/blocks/e-commerce/checkout-forms/checkout-progress.tsx +62 -0
  18. package/src/blocks/e-commerce/checkout-forms/checkout-review-step.tsx +59 -0
  19. package/src/blocks/e-commerce/checkout-forms/{checkout-form-1-shipping-step.tsx → checkout-shipping-step.tsx} +65 -41
  20. package/src/blocks/e-commerce/checkout-forms/checkout.test.tsx +478 -0
  21. package/src/blocks/e-commerce/checkout-forms/checkout.tsx +550 -0
  22. package/src/blocks/{marketing/navbars/navbar-4-account-menu.test.tsx → e-commerce/navigation/storefront-navbar-account-menu.test.tsx} +8 -8
  23. package/src/blocks/{marketing/navbars/navbar-4-account-menu.tsx → e-commerce/navigation/storefront-navbar-account-menu.tsx} +3 -3
  24. package/src/blocks/{marketing/navbars/navbar-4-cart-sheet.test.tsx → e-commerce/navigation/storefront-navbar-cart-sheet.test.tsx} +22 -22
  25. package/src/blocks/{marketing/navbars/navbar-4-cart-sheet.tsx → e-commerce/navigation/storefront-navbar-cart-sheet.tsx} +3 -3
  26. package/src/blocks/{marketing/navbars/navbar-4-mobile-nav.tsx → e-commerce/navigation/storefront-navbar-mobile-nav.tsx} +4 -12
  27. package/src/blocks/{marketing/navbars/navbar-4-search-bar.tsx → e-commerce/navigation/storefront-navbar-search-bar.tsx} +3 -3
  28. package/src/blocks/{marketing/navbars/navbar-4.test.tsx → e-commerce/navigation/storefront-navbar.test.tsx} +40 -40
  29. package/src/blocks/{marketing/navbars/navbar-4.tsx → e-commerce/navigation/storefront-navbar.tsx} +17 -17
  30. package/src/blocks/e-commerce/product-list/product-card.tsx +63 -40
  31. package/src/blocks/e-commerce/shopping-carts/shopping-cart.test.tsx +2 -2
  32. package/src/blocks/marketing/footers/footer-section-accessibility.test.tsx +118 -0
  33. package/src/blocks/marketing/footers/footer-section-actions.test.tsx +131 -0
  34. package/src/blocks/marketing/footers/footer-section-data.test.tsx +127 -0
  35. package/src/blocks/marketing/footers/footer-section-native-links.test.tsx +100 -0
  36. package/src/blocks/marketing/footers/footer-section-public-api.test.tsx +11 -0
  37. package/src/blocks/marketing/footers/footer-section.test.tsx +46 -0
  38. package/src/blocks/marketing/footers/footer-section.tsx +532 -0
  39. package/src/blocks/marketing/navbars/navbar-public-api.test.tsx +57 -0
  40. package/src/blocks/marketing/navbars/navbar.mega.test.tsx +149 -0
  41. package/src/blocks/marketing/navbars/navbar.test.tsx +155 -0
  42. package/src/blocks/marketing/navbars/navbar.tsx +735 -0
  43. package/src/blocks/marketing/testimonials/testimonial-section.test.tsx +171 -0
  44. package/src/blocks/marketing/testimonials/testimonial-section.tsx +237 -0
  45. package/src/index.ts +80 -81
  46. package/src/lib/contracts-curated.test.ts +5 -5
  47. package/src/lib/contracts.test.ts +55 -0
  48. package/src/lib/contracts.ts +64 -2
  49. package/tsup.config.ts +1 -1
  50. package/src/blocks/application/login-forms/login-page-1.tsx +0 -246
  51. package/src/blocks/application/login-forms/login-page-2.tsx +0 -207
  52. package/src/blocks/e-commerce/category-filters/category-filter-1.tsx +0 -260
  53. package/src/blocks/e-commerce/category-filters/category-filter-2-filter-panel.tsx +0 -222
  54. package/src/blocks/e-commerce/category-filters/category-filter-2.tsx +0 -921
  55. package/src/blocks/e-commerce/category-filters/category-filter-4-chip-rail.tsx +0 -57
  56. package/src/blocks/e-commerce/category-filters/category-filter-4-drawer.tsx +0 -124
  57. package/src/blocks/e-commerce/category-filters/category-filter-4-fields.tsx +0 -170
  58. package/src/blocks/e-commerce/category-filters/category-filter-4-sidebar.tsx +0 -52
  59. package/src/blocks/e-commerce/category-filters/category-filter-4-sort-sheet.tsx +0 -57
  60. package/src/blocks/e-commerce/category-filters/category-filter-4-state.ts +0 -68
  61. package/src/blocks/e-commerce/category-filters/category-filter-4.tsx +0 -298
  62. package/src/blocks/e-commerce/checkout-forms/card-formatting.test.ts +0 -47
  63. package/src/blocks/e-commerce/checkout-forms/card-formatting.ts +0 -9
  64. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-contact-step.tsx +0 -96
  65. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-order-summary.tsx +0 -252
  66. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-payment-step.tsx +0 -188
  67. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-progress.tsx +0 -36
  68. package/src/blocks/e-commerce/checkout-forms/checkout-form-1.test.tsx +0 -216
  69. package/src/blocks/e-commerce/checkout-forms/checkout-form-1.tsx +0 -338
  70. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-order-summary.tsx +0 -130
  71. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-payment-card.tsx +0 -175
  72. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-shipping-card.tsx +0 -190
  73. package/src/blocks/e-commerce/checkout-forms/checkout-form-2.tsx +0 -312
  74. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-information-step.tsx +0 -91
  75. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-order-summary.tsx +0 -110
  76. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-payment-step.tsx +0 -137
  77. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-progress.tsx +0 -68
  78. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-review-step.tsx +0 -79
  79. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-shipping-step.tsx +0 -172
  80. package/src/blocks/e-commerce/checkout-forms/checkout-form-3.tsx +0 -392
  81. package/src/blocks/marketing/footers/footer-section-1.test.tsx +0 -117
  82. package/src/blocks/marketing/footers/footer-section-1.tsx +0 -178
  83. package/src/blocks/marketing/footers/footer-section-3.tsx +0 -192
  84. package/src/blocks/marketing/footers/footer-section-4.tsx +0 -355
  85. package/src/blocks/marketing/navbars/navbar-1.tsx +0 -267
  86. package/src/blocks/marketing/navbars/navbar-2.tsx +0 -351
  87. package/src/blocks/marketing/navbars/navbar-3.tsx +0 -303
  88. package/src/blocks/marketing/navbars/navbar-4-like-button.tsx +0 -177
  89. package/src/blocks/marketing/navbars/navbar-4-product-grid.tsx +0 -219
  90. package/src/blocks/marketing/testimonials/testimonial-1.tsx +0 -141
  91. package/src/blocks/marketing/testimonials/testimonial-2.tsx +0 -108
@@ -0,0 +1,171 @@
1
+ // @vitest-environment jsdom
2
+ import { renderToStaticMarkup } from 'react-dom/server'
3
+ import { beforeAll, describe, expect, it } from 'vitest'
4
+ import { render, screen } from '@testing-library/react'
5
+ import userEvent from '@testing-library/user-event'
6
+ import * as blockExports from '../../../index'
7
+ import { TestimonialSection, type TestimonialSectionProps } from './testimonial-section'
8
+
9
+ beforeAll(() => {
10
+ ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
11
+
12
+ window.matchMedia = (query: string) =>
13
+ ({
14
+ matches: false,
15
+ media: query,
16
+ onchange: null,
17
+ addListener: () => {},
18
+ removeListener: () => {},
19
+ addEventListener: () => {},
20
+ removeEventListener: () => {},
21
+ dispatchEvent: () => true,
22
+ }) as MediaQueryList
23
+
24
+ window.IntersectionObserver = class {
25
+ disconnect() {}
26
+
27
+ observe() {}
28
+
29
+ takeRecords() {
30
+ return []
31
+ }
32
+
33
+ unobserve() {}
34
+ } as unknown as typeof IntersectionObserver
35
+
36
+ window.ResizeObserver = class {
37
+ disconnect() {}
38
+
39
+ observe() {}
40
+
41
+ unobserve() {}
42
+ } as unknown as typeof ResizeObserver
43
+ })
44
+
45
+ const renderToHtml = (props: TestimonialSectionProps) =>
46
+ renderToStaticMarkup(<TestimonialSection {...props} />)
47
+
48
+ describe('TestimonialSection — shared content', () => {
49
+ it('is the only public testimonial component export', () => {
50
+ expect(blockExports.TestimonialSection).toBe(TestimonialSection)
51
+ expect('Testimonial1' in blockExports).toBe(false)
52
+ expect('Testimonial2' in blockExports).toBe(false)
53
+ })
54
+
55
+ it('renders supplied testimonial identity, quote, and optional attribution', () => {
56
+ const html = renderToHtml({
57
+ presentation: 'bento',
58
+ items: [
59
+ {
60
+ id: 't-1',
61
+ name: 'Rosa Díaz',
62
+ role: 'Propietaria de taller',
63
+ avatar: '/rosa.png',
64
+ content: 'El servicio es excelente.',
65
+ rating: 4,
66
+ },
67
+ ],
68
+ })
69
+
70
+ expect(html).toContain('Rosa Díaz')
71
+ expect(html).toContain('Propietaria de taller')
72
+ expect(html).toContain('El servicio es excelente.')
73
+ expect(html).toContain('aria-label="4 out of 5 stars"')
74
+ expect(html).not.toContain('src=""')
75
+ })
76
+
77
+ it('does not invent absent attribution or emit an empty image source', () => {
78
+ const html = renderToHtml({
79
+ presentation: 'bento',
80
+ items: [{ id: 't-1', name: 'Rosa Díaz', content: 'El servicio es excelente.' }],
81
+ })
82
+
83
+ expect(html).toContain('Rosa Díaz')
84
+ expect(html).toContain('El servicio es excelente.')
85
+ expect(html).not.toContain('src=""')
86
+ expect(html).not.toContain('rating')
87
+ expect(html).not.toContain('Propietaria')
88
+ })
89
+
90
+ it('renders no section or controls when the collection is empty', () => {
91
+ expect(
92
+ renderToHtml({
93
+ presentation: 'carousel',
94
+ items: [],
95
+ carouselLabels: { previous: 'Prev', next: 'Next', slide: 'Slide {n}' },
96
+ }),
97
+ ).toBe('')
98
+ })
99
+
100
+ it('supports host-provided carousel labels', () => {
101
+ const html = renderToHtml({
102
+ presentation: 'carousel',
103
+ items: [{ id: 't-1', name: 'Rosa Díaz', content: 'El servicio es excelente.' }],
104
+ carouselLabels: {
105
+ previous: 'Testimonio anterior',
106
+ next: 'Testimonio siguiente',
107
+ slide: 'Ir al testimonio {n}',
108
+ },
109
+ })
110
+
111
+ expect(html).toContain('aria-label="Testimonio anterior"')
112
+ expect(html).toContain('aria-label="Testimonio siguiente"')
113
+ expect(html).toContain('aria-label="Ir al testimonio 1"')
114
+ })
115
+
116
+ it.each([1, 2, 3])('renders every bento entry when the collection has %i items', count => {
117
+ const items = Array.from({ length: count }, (_, index) => ({
118
+ id: `t-${index + 1}`,
119
+ name: `Cliente ${index + 1}`,
120
+ content: `Cita ${index + 1}`,
121
+ }))
122
+
123
+ render(<TestimonialSection presentation='bento' items={items} />)
124
+
125
+ for (const item of items) {
126
+ expect(screen.getByText(item.content)).toBeTruthy()
127
+ }
128
+ })
129
+
130
+ it('renders every bento entry when the collection has more than four items', () => {
131
+ const items = Array.from({ length: 6 }, (_, index) => ({
132
+ id: `t-${index + 1}`,
133
+ name: `Cliente ${index + 1}`,
134
+ content: `Cita ${index + 1}`,
135
+ }))
136
+
137
+ render(<TestimonialSection presentation='bento' items={items} />)
138
+
139
+ for (const item of items) {
140
+ expect(screen.getByText(item.content)).toBeTruthy()
141
+ }
142
+ })
143
+
144
+ it('lets keyboard users select carousel entries with localized controls', async () => {
145
+ const user = userEvent.setup()
146
+ render(
147
+ <TestimonialSection
148
+ presentation='carousel'
149
+ items={[
150
+ { id: 't-1', name: 'Cliente 1', content: 'Cita 1' },
151
+ { id: 't-2', name: 'Cliente 2', content: 'Cita 2' },
152
+ { id: 't-3', name: 'Cliente 3', content: 'Cita 3' },
153
+ ]}
154
+ carouselLabels={{
155
+ previous: 'Testimonio anterior',
156
+ next: 'Testimonio siguiente',
157
+ slide: 'Ir al testimonio {n}',
158
+ }}
159
+ />,
160
+ )
161
+
162
+ const lastSlide = screen.getByRole('button', { name: 'Ir al testimonio 3' })
163
+ while (document.activeElement !== lastSlide) {
164
+ await user.tab()
165
+ }
166
+ await user.keyboard('{Enter}')
167
+
168
+ expect(lastSlide.getAttribute('aria-current')).toBe('true')
169
+ expect(screen.getByText('Cita 3')).toBeTruthy()
170
+ })
171
+ })
@@ -0,0 +1,237 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+ import { Quote, Star } from 'lucide-react'
5
+ import { cn } from '../../../lib/utils'
6
+ import { Avatar, AvatarFallback, AvatarImage } from '../../../ui/avatar'
7
+ import { Card, CardContent, CardHeader, CardTitle } from '../../../ui/card'
8
+ import {
9
+ type CarouselApi,
10
+ Carousel,
11
+ CarouselContent,
12
+ CarouselItem,
13
+ CarouselNext,
14
+ CarouselPrevious,
15
+ } from '../../../ui/carousel'
16
+
17
+ export interface TestimonialItem {
18
+ id: string
19
+ name: string
20
+ content: string
21
+ role?: string
22
+ avatar?: string
23
+ rating?: number
24
+ }
25
+
26
+ export interface TestimonialCarouselLabels {
27
+ previous: string
28
+ next: string
29
+ slide: string
30
+ }
31
+
32
+ interface TestimonialSectionBaseProps {
33
+ items: TestimonialItem[]
34
+ title?: string
35
+ description?: string
36
+ className?: string
37
+ }
38
+
39
+ export type TestimonialSectionProps = TestimonialSectionBaseProps &
40
+ (
41
+ | {
42
+ presentation: 'carousel'
43
+ carouselLabels: TestimonialCarouselLabels
44
+ }
45
+ | {
46
+ presentation: 'bento'
47
+ carouselLabels?: never
48
+ }
49
+ )
50
+
51
+ const bentoGridClasses = [
52
+ 'grid grid-rows-[auto_1fr] gap-8 ring-1 ring-foreground/10 sm:p-6 md:col-span-2 lg:col-span-2 lg:row-span-2 shadow-sm',
53
+ 'ring-1 ring-foreground/10 md:col-span-2 lg:col-span-2 py-6 shadow-sm',
54
+ 'pt-0 ring-1 ring-foreground/10 lg:col-span-1 shadow-sm',
55
+ 'pt-0 ring-1 ring-foreground/10 lg:col-span-1 shadow-sm',
56
+ ]
57
+
58
+ const contentClasses = ['', 'h-full px-6', 'h-full pt-6 px-6', 'h-full pt-6 px-6']
59
+ const textClasses = ['text-xl text-balance', 'text-base text-balance', 'text-base text-balance', 'text-base text-balance']
60
+
61
+ function initials(name: string) {
62
+ return name
63
+ .split(' ')
64
+ .map(part => part[0])
65
+ .join('')
66
+ }
67
+
68
+ function Rating({ value }: { value: number }) {
69
+ return (
70
+ <div className='flex items-center gap-1' aria-label={`${value} out of 5 stars`}>
71
+ {Array.from({ length: 5 }, (_, index) => (
72
+ <Star
73
+ key={index}
74
+ aria-hidden='true'
75
+ className={cn('size-4', index < Math.floor(value) ? 'fill-foreground' : 'fill-none')}
76
+ />
77
+ ))}
78
+ <span className='text-muted-foreground ms-2 text-sm'>{value}</span>
79
+ </div>
80
+ )
81
+ }
82
+
83
+ function TestimonialAttribution({ item }: { item: TestimonialItem }) {
84
+ return (
85
+ <div className='grid grid-cols-[auto_1fr] items-center gap-3'>
86
+ <Avatar className='bg-muted size-12'>
87
+ {item.avatar && <AvatarImage src={item.avatar || undefined} alt={item.name} loading='lazy' />}
88
+ <AvatarFallback>{initials(item.name)}</AvatarFallback>
89
+ </Avatar>
90
+ <div>
91
+ <cite className='text-sm font-medium not-italic'>{item.name}</cite>
92
+ {item.role && <span className='text-muted-foreground block text-sm'>{item.role}</span>}
93
+ </div>
94
+ </div>
95
+ )
96
+ }
97
+
98
+ function BentoPresentation({
99
+ items,
100
+ title,
101
+ description,
102
+ className,
103
+ }: TestimonialSectionBaseProps) {
104
+ return (
105
+ <section className={cn('py-12 md:py-16', className)}>
106
+ <div className='mx-auto flex w-full max-w-7xl flex-col gap-8 px-4 sm:px-6 md:gap-16 lg:px-8'>
107
+ {(title || description) && (
108
+ <header className='relative z-10 mx-auto flex w-full max-w-4xl flex-col gap-6 text-center md:gap-12'>
109
+ {title && <h2 className='text-4xl font-medium max-lg:text-balance lg:text-5xl'>{title}</h2>}
110
+ {description && (
111
+ <p className='text-muted-foreground mx-auto max-w-2xl max-lg:text-balance'>
112
+ {description}
113
+ </p>
114
+ )}
115
+ </header>
116
+ )}
117
+
118
+ <div className='grid gap-4 md:grid-cols-2 lg:grid-cols-4 lg:grid-rows-2'>
119
+ {items.map((item, index) => {
120
+ const tileIndex = index % bentoGridClasses.length
121
+
122
+ return (
123
+ <Card key={item.id} className={bentoGridClasses[tileIndex]}>
124
+ {tileIndex === 0 && (
125
+ <CardHeader className='pb-6'>
126
+ <div className='flex items-center justify-start'>
127
+ <Quote className='text-muted-foreground/30 size-16 rotate-180' aria-hidden='true' />
128
+ </div>
129
+ </CardHeader>
130
+ )}
131
+ <CardContent className={contentClasses[tileIndex]}>
132
+ <blockquote className='grid h-full grid-rows-[1fr_auto] gap-6'>
133
+ <p className={textClasses[tileIndex]}>{item.content}</p>
134
+ <TestimonialAttribution item={item} />
135
+ {item.rating !== undefined && <Rating value={item.rating} />}
136
+ </blockquote>
137
+ </CardContent>
138
+ </Card>
139
+ )
140
+ })}
141
+ </div>
142
+ </div>
143
+ </section>
144
+ )
145
+ }
146
+
147
+ function CarouselPresentation({
148
+ items,
149
+ title,
150
+ description,
151
+ className,
152
+ carouselLabels,
153
+ }: TestimonialSectionBaseProps & {
154
+ carouselLabels: TestimonialCarouselLabels
155
+ }) {
156
+ const [carouselApi, setCarouselApi] = useState<CarouselApi>()
157
+ const [selectedSlide, setSelectedSlide] = useState(0)
158
+
159
+ useEffect(() => {
160
+ if (!carouselApi) return
161
+
162
+ const updateSelectedSlide = () => setSelectedSlide(carouselApi.selectedScrollSnap())
163
+ updateSelectedSlide()
164
+ carouselApi.on('select', updateSelectedSlide)
165
+ carouselApi.on('reInit', updateSelectedSlide)
166
+
167
+ return () => {
168
+ carouselApi.off('select', updateSelectedSlide)
169
+ carouselApi.off('reInit', updateSelectedSlide)
170
+ }
171
+ }, [carouselApi])
172
+
173
+ return (
174
+ <section className={cn('py-12 lg:py-20', className)}>
175
+ <div className='mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8'>
176
+ {(title || description) && (
177
+ <header className='mb-12 text-center md:mb-16'>
178
+ {title && <h2 className='text-3xl font-bold text-balance md:text-4xl'>{title}</h2>}
179
+ {description && <p className='text-muted-foreground mx-auto mt-4 max-w-2xl'>{description}</p>}
180
+ </header>
181
+ )}
182
+
183
+ <Carousel
184
+ className='w-full'
185
+ opts={{ align: 'start', loop: true }}
186
+ setApi={setCarouselApi}
187
+ aria-label={title}
188
+ >
189
+ <CarouselContent className='-ml-1'>
190
+ {items.map(item => (
191
+ <CarouselItem key={item.id} className='basis-full px-4 last:pe-0 sm:basis-1/2 lg:basis-1/3'>
192
+ <Card className='h-full overflow-hidden border border-border py-6'>
193
+ <CardHeader className='gap-0 px-6'>
194
+ <TestimonialAttribution item={item} />
195
+ </CardHeader>
196
+ <CardContent className='px-6'>
197
+ <blockquote className='text-muted-foreground text-base'>{item.content}</blockquote>
198
+ {item.rating !== undefined && <Rating value={item.rating} />}
199
+ </CardContent>
200
+ </Card>
201
+ </CarouselItem>
202
+ ))}
203
+ </CarouselContent>
204
+ <CarouselPrevious className='cursor-pointer' aria-label={carouselLabels.previous} />
205
+ <CarouselNext className='cursor-pointer' aria-label={carouselLabels.next} />
206
+ </Carousel>
207
+ <div className='mt-6 flex justify-center gap-2'>
208
+ {items.map((item, index) => (
209
+ <button
210
+ key={item.id}
211
+ type='button'
212
+ className={cn(
213
+ 'size-2 rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
214
+ selectedSlide === index ? 'bg-foreground' : 'bg-muted-foreground/40',
215
+ )}
216
+ aria-label={carouselLabels.slide.replace('{n}', String(index + 1))}
217
+ aria-current={selectedSlide === index ? 'true' : undefined}
218
+ onClick={() => carouselApi?.scrollTo(index)}
219
+ />
220
+ ))}
221
+ </div>
222
+ </div>
223
+ </section>
224
+ )
225
+ }
226
+
227
+ export function TestimonialSection(props: TestimonialSectionProps) {
228
+ if (props.items.length === 0) return null
229
+
230
+ if (props.presentation === 'carousel') {
231
+ return <CarouselPresentation {...props} />
232
+ }
233
+
234
+ return <BentoPresentation {...props} />
235
+ }
236
+
237
+ export default TestimonialSection
package/src/index.ts CHANGED
@@ -16,16 +16,13 @@ export {
16
16
  CardContent,
17
17
  } from './ui/card'
18
18
 
19
- export { default as Navbar1 } from './blocks/marketing/navbars/navbar-1'
20
- export { default as Navbar2 } from './blocks/marketing/navbars/navbar-2'
21
- export { default as Navbar3 } from './blocks/marketing/navbars/navbar-3'
22
- export { default as Navbar4 } from './blocks/marketing/navbars/navbar-4'
19
+ export { default as Navbar } from './blocks/marketing/navbars/navbar'
20
+ export { default as StorefrontNavbar } from './blocks/e-commerce/navigation/storefront-navbar'
23
21
  export { default as Banner1 } from './blocks/marketing/banners/banner-1'
24
22
  export { default as HeroSection1 } from './blocks/marketing/hero-sections/hero-section-1'
25
23
  export { default as CtaSection1 } from './blocks/marketing/cta/cta-section-1'
26
24
  export { default as Feature } from './blocks/marketing/features/feature'
27
- export { default as Testimonial1 } from './blocks/marketing/testimonials/testimonial-1'
28
- export { default as Testimonial2 } from './blocks/marketing/testimonials/testimonial-2'
25
+ export { TestimonialSection } from './blocks/marketing/testimonials/testimonial-section'
29
26
  export { default as FaqBlock1 } from './blocks/marketing/faq/faq-block-1'
30
27
  export { default as PricingSection1 } from './blocks/marketing/pricing/pricing-section-1'
31
28
  export type { CtaSection1Props } from './blocks/marketing/cta/cta-section-1'
@@ -38,22 +35,37 @@ export type {
38
35
  export type { LogoCloud1Props, LogoCloud1Platform } from './blocks/marketing/logo-clouds/logo-cloud-1'
39
36
  export type { PricingSection1Props, PricingTier } from './blocks/marketing/pricing/pricing-section-1'
40
37
  export type { Statistic1Props, StatisticItem } from './blocks/marketing/statistics/statistic-1'
41
- export type { Testimonial1Props, Testimonial1Item } from './blocks/marketing/testimonials/testimonial-1'
42
- export type { Testimonial2Props, Testimonial2Item } from './blocks/marketing/testimonials/testimonial-2'
38
+ export type {
39
+ TestimonialItem,
40
+ TestimonialCarouselLabels,
41
+ TestimonialSectionProps,
42
+ } from './blocks/marketing/testimonials/testimonial-section'
43
43
  export { default as Statistic1 } from './blocks/marketing/statistics/statistic-1'
44
44
  export { default as LogoCloud1 } from './blocks/marketing/logo-clouds/logo-cloud-1'
45
- export { default as FooterSection1 } from './blocks/marketing/footers/footer-section-1'
46
- export { default as FooterSection3 } from './blocks/marketing/footers/footer-section-3'
47
- export type { FooterSection3Props, FooterLinkColumn, FooterLink, SocialLink } from './blocks/marketing/footers/footer-section-3'
48
- export { default as FooterSection4 } from './blocks/marketing/footers/footer-section-4'
45
+ export { FooterSection } from './blocks/marketing/footers/footer-section'
46
+ export type {
47
+ FooterBrandAccent,
48
+ FooterContact,
49
+ FooterContactItem,
50
+ FooterLink as CanonicalFooterLink,
51
+ FooterLinkBehavior,
52
+ FooterLinkColumn as CanonicalFooterLinkColumn,
53
+ FooterNewsletter,
54
+ FooterPresentation,
55
+ FooterSectionProps,
56
+ FooterSocialLink,
57
+ } from './blocks/marketing/footers/footer-section'
49
58
  export type {
50
- FooterSection4Props,
51
- FooterSection4Contact,
52
- FooterSection4Link,
53
- FooterSection4LinkColumn,
54
- FooterSection4SocialIcon,
55
- FooterSection4SocialLink,
56
- } from './blocks/marketing/footers/footer-section-4'
59
+ NavbarFeatured,
60
+ NavbarGroup,
61
+ NavbarIcon,
62
+ NavbarIconMap,
63
+ NavbarItem,
64
+ NavbarLink,
65
+ NavbarPresentation,
66
+ NavbarProps,
67
+ } from './blocks/marketing/navbars/navbar'
68
+ export type { StorefrontNavbarProps } from './blocks/e-commerce/navigation/storefront-navbar'
57
69
  export { ProductCard } from './blocks/e-commerce/product-list/product-card'
58
70
  export type {
59
71
  ProductCardProps,
@@ -107,31 +119,22 @@ export { MobileBottomNav, mobileBottomNavItemSchema, mobileBottomNavPropsSchema
107
119
  export type { MobileBottomNavItem, MobileBottomNavProps } from './blocks/e-commerce/navigation/mobile-bottom-nav'
108
120
  export { menuItemSchema, collectionItemSchema } from './lib/types/menu'
109
121
  export type { MenuItem, CollectionItem } from './lib/types/menu'
110
- export { default as CategoryFilter1 } from './blocks/e-commerce/category-filters/category-filter-1'
122
+ export { CategoryListing, CategoryListingStatusView, buildFilterChange, reconcileSearchDraft, toggleFilterValue } from './blocks/e-commerce/category-filters/category-listing'
111
123
  export type {
112
- CategoryFilter1Category,
113
- CategoryFilter1PriceRange,
114
- CategoryFilter1SortOption,
115
- } from './blocks/e-commerce/category-filters/category-filter-1'
116
- export { default as CategoryFilter2 } from './blocks/e-commerce/category-filters/category-filter-2'
117
- export { CategoryFilter2StatusView } from './blocks/e-commerce/category-filters/category-filter-2'
118
- export type {
119
- FilterProduct,
120
- CategoryFilter2Props,
121
- CategoryFilter2Category,
122
- CategoryFilter2Collection,
123
- CategoryFilter2Color,
124
- CategoryFilter2PriceRange,
125
- CategoryFilter2SortOption,
126
- CategoryFilter2Filters,
127
- CategoryFilter2Status,
128
- CategoryFilter2StatusViewProps,
129
- CategoryFilter2QueryError,
130
- CategoryFilter2ActiveFilter,
131
- CategoryFilter2ActiveFilterKind,
132
- } from './blocks/e-commerce/category-filters/category-filter-2'
133
- export { default as CategoryFilter4 } from './blocks/e-commerce/category-filters/category-filter-4'
134
- export type { CategoryFilter4Props, CategoryFilter4Product, CategoryFilter4Facets } from './blocks/e-commerce/category-filters/category-filter-4'
124
+ CategoryListingActiveFilter,
125
+ CategoryListingActiveFilterKind,
126
+ CategoryListingFacetGroup,
127
+ CategoryListingFacetOption,
128
+ CategoryListingFilters,
129
+ CategoryListingOption,
130
+ CategoryListingPresentation,
131
+ CategoryListingPriceRange,
132
+ CategoryListingProps,
133
+ CategoryListingQueryError,
134
+ CategoryListingSortOption,
135
+ CategoryListingStatus,
136
+ CategoryListingStatusViewProps,
137
+ } from './blocks/e-commerce/category-filters/category-listing'
135
138
  export { default as ShoppingCart } from './blocks/e-commerce/shopping-carts/shopping-cart'
136
139
  export type {
137
140
  ShoppingCartProps,
@@ -141,38 +144,24 @@ export type {
141
144
  } from './blocks/e-commerce/shopping-carts/shopping-cart'
142
145
  export { default as StoreContainer1 } from './blocks/e-commerce/store-containers/store-container-1'
143
146
  export type { StoreContainer1Props } from './blocks/e-commerce/store-containers/store-container-1'
144
- export { default as CheckoutForm1 } from './blocks/e-commerce/checkout-forms/checkout-form-1'
145
- export { default as CheckoutForm2 } from './blocks/e-commerce/checkout-forms/checkout-form-2'
146
- export { default as CheckoutForm3 } from './blocks/e-commerce/checkout-forms/checkout-form-3'
147
- export { default as CheckoutConfirmation } from './blocks/e-commerce/checkout-forms/checkout-confirmation'
147
+ export { default as Checkout } from './blocks/e-commerce/checkout-forms/checkout'
148
148
  export type {
149
- CheckoutForm1Props,
150
- CheckoutForm1FormData,
151
- CheckoutForm1FieldErrors,
152
- CheckoutForm1FieldDescriptions,
153
- CheckoutForm1ChangeHandler,
154
- CheckoutForm1OrderSummary,
155
- CheckoutForm1OrderSummaryItem,
156
- CheckoutForm1CountryOption,
157
- CheckoutForm1ShippingOption,
158
- CheckoutForm1Totals,
159
- CheckoutForm1TotalsState,
160
- } from './blocks/e-commerce/checkout-forms/checkout-form-1'
161
- export type {
162
- CheckoutForm2Props,
163
- CheckoutForm2Item,
164
- CheckoutForm2Data,
165
- CheckoutForm2FieldErrors,
166
- CheckoutForm2FieldDescriptions,
167
- CheckoutForm2Totals,
168
- } from './blocks/e-commerce/checkout-forms/checkout-form-2'
169
- export type {
170
- CheckoutForm3Props,
171
- CheckoutForm3CartItem,
172
- CheckoutForm3FormData,
173
- CheckoutForm3ShippingMethod,
174
- CheckoutForm3CountryOption,
175
- } from './blocks/e-commerce/checkout-forms/checkout-form-3'
149
+ CheckoutProps,
150
+ CheckoutValues,
151
+ CheckoutPresentation,
152
+ CheckoutStepId,
153
+ CheckoutLabels,
154
+ CheckoutValuesFieldErrors,
155
+ CheckoutFieldDescriptions,
156
+ CheckoutChangeHandler,
157
+ CheckoutOrderSummary,
158
+ CheckoutOrderLine,
159
+ CheckoutTotals,
160
+ CheckoutTotalsState,
161
+ CheckoutCountryOption,
162
+ CheckoutShippingOption,
163
+ } from './blocks/e-commerce/checkout-forms/checkout'
164
+ export { default as CheckoutConfirmation } from './blocks/e-commerce/checkout-forms/checkout-confirmation'
176
165
  export type {
177
166
  CheckoutConfirmationProps,
178
167
  FulfilmentStage,
@@ -198,13 +187,11 @@ export type {
198
187
  StorefrontHeroProps,
199
188
  } from './blocks/e-commerce/storefront-hero/storefront-hero'
200
189
  export { default as AppShell1 } from './blocks/application/app-shells/app-shell-1'
201
- export { default as LoginPage1 } from './blocks/application/login-forms/login-page-1'
202
- export { default as LoginPage2 } from './blocks/application/login-forms/login-page-2'
190
+ export { default as LoginPage } from './blocks/application/login-forms/login-page'
203
191
  export { default as SignUpPage1 } from './blocks/application/signup-forms/signup-page-1'
204
192
  export { default as ForgotPasswordPage1 } from './blocks/application/password-recovery/forgot-password-page-1'
205
193
  export { default as Verify1 } from './blocks/application/verification/verify-1'
206
- export type { LoginPage1Props, LoginPage1FormData, LoginPage1SocialButton } from './blocks/application/login-forms/login-page-1'
207
- export type { LoginPage2Props, LoginPage2SocialButton } from './blocks/application/login-forms/login-page-2'
194
+ export type { LoginPageProps, LoginPageFormData, LoginPageProvider } from './blocks/application/login-forms/login-page'
208
195
  export type { SignUpPage1Props, SignUpPage1Data, SignUpPage1SocialButton } from './blocks/application/signup-forms/signup-page-1'
209
196
  export type { ForgotPasswordPage1Props } from './blocks/application/password-recovery/forgot-password-page-1'
210
197
  export type { Verify1Props } from './blocks/application/verification/verify-1'
@@ -235,7 +222,13 @@ export {
235
222
  shoppingCartPropsSchema,
236
223
  shoppingCartItemSchema,
237
224
  shoppingCartSuggestionSchema,
238
- navbar4PropsSchema,
225
+ navbarPresentationSchema,
226
+ navbarItemSchema,
227
+ navbarGroupSchema,
228
+ navbarLinkSchema,
229
+ navbarFeaturedSchema,
230
+ navbarPropsSchema,
231
+ storefrontNavbarPropsSchema,
239
232
  paymentMethodSchema,
240
233
  cardProductSchema,
241
234
  cartItemSchema,
@@ -266,7 +259,13 @@ export type {
266
259
  ShoppingCartItem,
267
260
  ShoppingCartSuggestion,
268
261
  ShoppingCartSpecProps,
269
- Navbar4SpecProps,
262
+ NavbarPresentationSpec,
263
+ NavbarItemSpec,
264
+ NavbarGroupSpec,
265
+ NavbarLinkSpec,
266
+ NavbarFeaturedSpec,
267
+ NavbarSpecProps,
268
+ StorefrontNavbarSpecProps,
270
269
  CardProduct,
271
270
  CartItem,
272
271
  Promotion,
@@ -282,4 +281,4 @@ export type {
282
281
  ProductCategorySpecProps,
283
282
  CategoryCardSpecProps,
284
283
  CategoryGridSpecProps,
285
- } from './lib/contracts'
284
+ } from './lib/contracts'
@@ -10,7 +10,7 @@ import {
10
10
  logoAccentSchema,
11
11
  moneySchema,
12
12
  navCartItemSchema,
13
- navbar4PropsSchema,
13
+ storefrontNavbarPropsSchema,
14
14
  productOverviewPropsSchema,
15
15
  shoppingCartPropsSchema,
16
16
  } from './contracts'
@@ -113,12 +113,12 @@ describe('optional brand seams — present, omitted, invalid', () => {
113
113
  utilityLinks: [],
114
114
  logoText: 'ElectroCuba',
115
115
  }
116
- const parsed = navbar4PropsSchema.parse(base)
116
+ const parsed = storefrontNavbarPropsSchema.parse(base)
117
117
  expect(parsed.logoAccent).toBeUndefined()
118
118
  })
119
119
 
120
120
  it('accepts navbar props with the accent present', () => {
121
- const parsed = navbar4PropsSchema.parse({
121
+ const parsed = storefrontNavbarPropsSchema.parse({
122
122
  storeCategories: [],
123
123
  shippingNote: 'Catálogo en línea',
124
124
  utilityLinks: [],
@@ -135,8 +135,8 @@ describe('optional brand seams — present, omitted, invalid', () => {
135
135
  utilityLinks: [],
136
136
  logoText: 'My Shop',
137
137
  }
138
- expect(navbar4PropsSchema.parse(base).className).toBeUndefined()
139
- expect(navbar4PropsSchema.parse({ ...base, className: 'bg-brand-nav text-brand-nav-foreground' }).className).toBe(
138
+ expect(storefrontNavbarPropsSchema.parse(base).className).toBeUndefined()
139
+ expect(storefrontNavbarPropsSchema.parse({ ...base, className: 'bg-brand-nav text-brand-nav-foreground' }).className).toBe(
140
140
  'bg-brand-nav text-brand-nav-foreground',
141
141
  )
142
142
  })