@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,141 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { Star } from 'lucide-react'
5
+ import { cn } from '../../../lib/utils'
6
+ import { Card, CardContent, CardHeader, CardFooter, CardTitle } from '../../../ui/card'
7
+ import {
8
+ Carousel,
9
+ CarouselContent,
10
+ CarouselItem,
11
+ CarouselNext,
12
+ CarouselPrevious,
13
+ type CarouselApi,
14
+ } from '../../../ui/carousel'
15
+ import { Avatar, AvatarFallback, AvatarImage } from '../../../ui/avatar'
16
+ import { Button } from '../../../ui/button'
17
+
18
+ export interface Testimonial1Item {
19
+ id: string
20
+ name: string
21
+ role: string
22
+ avatar: string
23
+ content: string
24
+ rating: number
25
+ }
26
+
27
+ export interface Testimonial1Props {
28
+ items: Testimonial1Item[]
29
+ title?: string
30
+ prevLabel?: string
31
+ nextLabel?: string
32
+ slideAriaLabel?: string
33
+ }
34
+
35
+ function RatingStars({ rating }: { rating: number }) {
36
+ return (
37
+ <div className='flex items-center gap-1'>
38
+ {[...Array(5)].map((_, index) => (
39
+ <Star
40
+ key={index}
41
+ className={cn('size-4', index < Math.floor(rating) ? 'fill-foreground' : 'fill-none')}
42
+ />
43
+ ))}
44
+ <span className='text-muted-foreground ms-2 text-sm'>({rating})</span>
45
+ </div>
46
+ )
47
+ }
48
+
49
+ export function Testimonial1({
50
+ items,
51
+ title = 'Lo que dicen nuestros clientes',
52
+ prevLabel = 'Anterior',
53
+ nextLabel = 'Siguiente',
54
+ slideAriaLabel = 'Ir a la diapositiva {n}',
55
+ }: Testimonial1Props) {
56
+ const [api, setApi] = React.useState<CarouselApi | null>(null)
57
+ const [current, setCurrent] = React.useState(0)
58
+
59
+ React.useEffect(() => {
60
+ if (!api) return
61
+
62
+ const onSelect = () => {
63
+ setCurrent(api.selectedScrollSnap())
64
+ }
65
+
66
+ api.on('select', onSelect)
67
+ return () => {
68
+ api.off('select', onSelect)
69
+ }
70
+ }, [api])
71
+
72
+ return (
73
+ <section className='py-12 lg:py-20'>
74
+ <div className='mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8'>
75
+ <header className='mb-16 text-center'>
76
+ <h2 className='text-3xl font-bold text-balance md:text-4xl'>{title}</h2>
77
+ </header>
78
+
79
+ <Carousel className='w-full' setApi={setApi} opts={{ align: 'start', loop: true }}>
80
+ <CarouselContent className='-ml-1'>
81
+ {items.map(testimonial => (
82
+ <CarouselItem
83
+ key={testimonial.id}
84
+ className='basis-full px-4 last:pe-0 sm:basis-1/2 lg:basis-1/3'
85
+ >
86
+ <Card className='h-full overflow-hidden border border-border py-6'>
87
+ <CardHeader className='gap-0 px-6'>
88
+ <div className='flex items-center gap-4'>
89
+ <Avatar className='bg-muted size-12'>
90
+ <AvatarImage src={testimonial.avatar || undefined} alt={testimonial.name} className='size-12' />
91
+ <AvatarFallback className='bg-card'>{testimonial.name[0]}</AvatarFallback>
92
+ </Avatar>
93
+ <div>
94
+ <CardTitle className='text-foreground font-semibold'>{testimonial.name}</CardTitle>
95
+ <p className='text-muted-foreground text-sm'>{testimonial.role}</p>
96
+ </div>
97
+ </div>
98
+ </CardHeader>
99
+ <CardContent className='px-6'>
100
+ <p className='text-muted-foreground text-base'>{testimonial.content}</p>
101
+ </CardContent>
102
+ <CardFooter className='bg-transparent border-t-0 px-6 pb-6'>
103
+ <RatingStars rating={testimonial.rating} />
104
+ </CardFooter>
105
+ </Card>
106
+ </CarouselItem>
107
+ ))}
108
+ </CarouselContent>
109
+ <CarouselPrevious
110
+ variant='outline'
111
+ className='hidden cursor-pointer lg:flex'
112
+ aria-label={prevLabel}
113
+ />
114
+ <CarouselNext
115
+ variant='outline'
116
+ className='hidden cursor-pointer lg:flex'
117
+ aria-label={nextLabel}
118
+ />
119
+ <div className='mt-8 flex items-center justify-center gap-2'>
120
+ {items.map((_, index) => (
121
+ <Button
122
+ variant='ghost'
123
+ key={index}
124
+ onClick={() => api?.scrollTo(index)}
125
+ className={cn(
126
+ 'h-9 px-4 py-2',
127
+ 'size-2 cursor-pointer rounded-full !p-0 transition-all',
128
+ current === index ? 'bg-foreground w-6' : 'bg-muted',
129
+ )}
130
+ aria-label={slideAriaLabel.replace('{n}', String(index + 1))}
131
+ aria-current={current === index ? 'true' : 'false'}
132
+ />
133
+ ))}
134
+ </div>
135
+ </Carousel>
136
+ </div>
137
+ </section>
138
+ )
139
+ }
140
+
141
+ export default Testimonial1
@@ -0,0 +1,108 @@
1
+ import { Quote } from 'lucide-react'
2
+ import { Card, CardContent, CardHeader } from '../../../ui/card'
3
+ import { Avatar, AvatarFallback, AvatarImage } from '../../../ui/avatar'
4
+
5
+ export interface Testimonial2Item {
6
+ id: string
7
+ name: string
8
+ role: string
9
+ avatar: string
10
+ content: string
11
+ }
12
+
13
+ export interface Testimonial2Props {
14
+ items: Testimonial2Item[]
15
+ title?: string
16
+ description?: string
17
+ }
18
+
19
+ const bentoGridClasses = [
20
+ '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',
21
+ 'ring-1 ring-foreground/10 md:col-span-2 lg:col-span-2 py-6 shadow-sm',
22
+ 'pt-0 ring-1 ring-foreground/10 lg:col-span-1 shadow-sm',
23
+ 'pt-0 ring-1 ring-foreground/10 lg:col-span-1 shadow-sm',
24
+ ]
25
+
26
+ const contentClasses = [
27
+ '',
28
+ 'h-full px-6',
29
+ 'h-full pt-6 px-6',
30
+ 'h-full pt-6 px-6',
31
+ ]
32
+
33
+ const textClasses = [
34
+ 'text-xl text-balance',
35
+ 'text-base text-balance',
36
+ 'text-base text-balance',
37
+ 'text-base text-balance',
38
+ ]
39
+
40
+ function initials(name: string) {
41
+ return name
42
+ .split(' ')
43
+ .map(n => n[0])
44
+ .join('')
45
+ }
46
+
47
+ export function Testimonial2({
48
+ items,
49
+ title = 'Con la confianza de quienes construyen',
50
+ description,
51
+ }: Testimonial2Props) {
52
+ /** El diseño bento consume exactamente 4 tarjetas: la primera ocupa la celda
53
+ * grande (lg:row-span-2) y las tres restantes completan la cuadrícula. */
54
+ const bentoItems = items.slice(0, 4)
55
+
56
+ return (
57
+ <section className='py-12 md:py-16'>
58
+ <div className='mx-auto w-full max-w-7xl flex flex-col gap-8 px-4 sm:px-6 lg:px-8 md:gap-16'>
59
+ <div className='relative z-10 mx-auto max-w-4xl flex flex-col gap-6 text-center md:gap-12'>
60
+ <h2 className='text-4xl font-medium max-lg:text-balance lg:text-5xl'>{title}</h2>
61
+ {description && (
62
+ <p className='text-muted-foreground mx-auto max-w-2xl max-lg:text-balance'>
63
+ {description}
64
+ </p>
65
+ )}
66
+ </div>
67
+
68
+ <div className='grid gap-4 md:grid-cols-2 lg:grid-cols-4 lg:grid-rows-2'>
69
+ {bentoItems.map((testimonial, index) => (
70
+ <Card key={testimonial.id} className={bentoGridClasses[index]}>
71
+ {index === 0 && (
72
+ <CardHeader className='pb-6'>
73
+ <div className='flex items-center justify-start'>
74
+ <Quote className='text-muted-foreground/30 size-16 rotate-180' />
75
+ </div>
76
+ </CardHeader>
77
+ )}
78
+ <CardContent className={contentClasses[index]}>
79
+ <blockquote className='grid h-full grid-rows-[1fr_auto] gap-6'>
80
+ <p className={textClasses[index]}>{testimonial.content}</p>
81
+
82
+ <div className='grid grid-cols-[auto_1fr] items-center gap-3'>
83
+ <Avatar className='bg-muted size-12'>
84
+ <AvatarImage
85
+ src={testimonial.avatar || undefined}
86
+ alt={testimonial.name}
87
+ height='400'
88
+ width='400'
89
+ loading='lazy'
90
+ />
91
+ <AvatarFallback>{initials(testimonial.name)}</AvatarFallback>
92
+ </Avatar>
93
+ <div>
94
+ <cite className='text-sm font-medium not-italic'>{testimonial.name}</cite>
95
+ <span className='text-muted-foreground block text-sm'>{testimonial.role}</span>
96
+ </div>
97
+ </div>
98
+ </blockquote>
99
+ </CardContent>
100
+ </Card>
101
+ ))}
102
+ </div>
103
+ </div>
104
+ </section>
105
+ )
106
+ }
107
+
108
+ export default Testimonial2
@@ -0,0 +1,145 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useRef, useState, type ComponentType } from 'react'
4
+
5
+ import { cn } from '../../lib/utils'
6
+
7
+ const PARTICLE_COUNT = 8
8
+
9
+ const STYLE_MARKER = 'data-reaction-toggle-keyframes'
10
+ const STYLES = `
11
+ @keyframes reaction-toggle-pop {
12
+ 0% { transform: scale(1); }
13
+ 30% { transform: scale(0.82); }
14
+ 100% { transform: scale(1); }
15
+ }
16
+ @keyframes reaction-toggle-burst {
17
+ 0% { opacity: 0; transform: translate(0, 0) scale(0.4); }
18
+ 20% { opacity: 1; transform: translate(calc(var(--rx) * 0.25), calc(var(--ry) * 0.25)) scale(1); }
19
+ 100% { opacity: 0; transform: translate(var(--rx), var(--ry)) scale(0.6); }
20
+ }
21
+ .reaction-toggle-pop {
22
+ animation: reaction-toggle-pop 350ms cubic-bezier(0.34, 1.96, 0.64, 1);
23
+ }
24
+ .reaction-toggle-burst {
25
+ animation-name: reaction-toggle-burst;
26
+ animation-timing-function: ease-out;
27
+ animation-fill-mode: forwards;
28
+ }
29
+ @media (prefers-reduced-motion: reduce) {
30
+ .reaction-toggle-pop,
31
+ .reaction-toggle-burst { animation: none !important; }
32
+ }
33
+ `
34
+
35
+ function useReactionToggleStyles() {
36
+ useEffect(() => {
37
+ if (document.head.querySelector(`[${STYLE_MARKER}]`)) return
38
+
39
+ const style = document.createElement('style')
40
+ style.setAttribute(STYLE_MARKER, '')
41
+ style.textContent = STYLES
42
+ document.head.append(style)
43
+ }, [])
44
+ }
45
+
46
+ type ReactionIconProps = {
47
+ className?: string
48
+ }
49
+
50
+ type Particle = {
51
+ rx: number
52
+ ry: number
53
+ duration: number
54
+ delay: number
55
+ size: number
56
+ }
57
+
58
+ function buildParticles(): Particle[] {
59
+ return Array.from({ length: PARTICLE_COUNT }, (_, i) => {
60
+ const angle = (Math.PI * 2 * i) / PARTICLE_COUNT + (Math.random() - 0.5) * 0.4
61
+ const dist = 16 + Math.random() * 10
62
+
63
+ return {
64
+ rx: Math.round(Math.cos(angle) * dist * 10) / 10,
65
+ ry: Math.round(Math.sin(angle) * dist * 10) / 10,
66
+ duration: Math.round(500 + Math.random() * 200),
67
+ delay: Math.round(Math.random() * 60),
68
+ size: Math.round((0.7 + Math.random() * 0.8) * 100) / 100,
69
+ }
70
+ })
71
+ }
72
+
73
+ type ReactionToggleProps = {
74
+ active: boolean
75
+ icon: ComponentType<ReactionIconProps>
76
+ className?: string
77
+ iconClassName?: string
78
+ activeColorClassName?: string
79
+ }
80
+
81
+ export function ReactionToggle({
82
+ active,
83
+ icon: Icon,
84
+ className,
85
+ iconClassName,
86
+ activeColorClassName = 'text-primary',
87
+ }: ReactionToggleProps) {
88
+ const [isBursting, setIsBursting] = useState(false)
89
+ const wasActive = useRef(active)
90
+ const particlesRef = useRef<Particle[]>(buildParticles())
91
+
92
+ useReactionToggleStyles()
93
+
94
+ useEffect(() => {
95
+ if (active && !wasActive.current) {
96
+ particlesRef.current = buildParticles()
97
+ setIsBursting(true)
98
+ const timeout = setTimeout(() => setIsBursting(false), 700)
99
+ wasActive.current = active
100
+ return () => clearTimeout(timeout)
101
+ }
102
+
103
+ wasActive.current = active
104
+ }, [active])
105
+
106
+ return (
107
+ <span className={cn('relative inline-flex', className)}>
108
+ <span className={cn('inline-flex', active && 'reaction-toggle-pop')}>
109
+ <Icon
110
+ className={cn(
111
+ 'transition-[fill,stroke,color] duration-150 ease-[cubic-bezier(0.22,1,0.36,1)]',
112
+ active ? cn('fill-current', activeColorClassName) : 'fill-transparent',
113
+ iconClassName
114
+ )}
115
+ />
116
+ </span>
117
+
118
+ {isBursting && (
119
+ <span
120
+ aria-hidden='true'
121
+ className={cn('pointer-events-none absolute top-1/2 left-1/2 size-0', activeColorClassName)}
122
+ >
123
+ {particlesRef.current.map((particle, index) => (
124
+ <i
125
+ key={index}
126
+ className='reaction-toggle-burst absolute rounded-full bg-current opacity-0'
127
+ style={
128
+ {
129
+ '--rx': `${particle.rx}px`,
130
+ '--ry': `${particle.ry}px`,
131
+ animationDuration: `${particle.duration}ms`,
132
+ animationDelay: `${particle.delay}ms`,
133
+ width: `${2.5 * particle.size}px`,
134
+ height: `${2.5 * particle.size}px`,
135
+ left: `${-1.25 * particle.size}px`,
136
+ top: `${-1.25 * particle.size}px`,
137
+ } as React.CSSProperties
138
+ }
139
+ />
140
+ ))}
141
+ </span>
142
+ )}
143
+ </span>
144
+ )
145
+ }
@@ -0,0 +1,19 @@
1
+ import * as React from "react"
2
+
3
+ const MOBILE_BREAKPOINT = 768
4
+
5
+ export function useIsMobile() {
6
+ const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
7
+
8
+ React.useEffect(() => {
9
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
10
+ const onChange = () => {
11
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
12
+ }
13
+ mql.addEventListener("change", onChange)
14
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
15
+ return () => mql.removeEventListener("change", onChange)
16
+ }, [])
17
+
18
+ return !!isMobile
19
+ }
package/src/index.ts ADDED
@@ -0,0 +1,200 @@
1
+ export { cn } from './lib/utils'
2
+ export type { CTA } from './lib/types'
3
+
4
+ export { Badge, badgeVariants } from './ui/badge'
5
+ export { Button, buttonVariants } from './ui/button'
6
+ export { Avatar, AvatarImage, AvatarFallback } from './ui/avatar'
7
+ export {
8
+ Card,
9
+ CardHeader,
10
+ CardFooter,
11
+ CardTitle,
12
+ CardAction,
13
+ CardDescription,
14
+ CardContent,
15
+ } from './ui/card'
16
+
17
+ export { default as Navbar1 } from './blocks/marketing/navbars/navbar-1'
18
+ export { default as Navbar2 } from './blocks/marketing/navbars/navbar-2'
19
+ export { default as Navbar3 } from './blocks/marketing/navbars/navbar-3'
20
+ export { default as Navbar4 } from './blocks/marketing/navbars/navbar-4'
21
+ export { default as Banner1 } from './blocks/marketing/banners/banner-1'
22
+ export { default as HeroSection1 } from './blocks/marketing/hero-sections/hero-section-1'
23
+ export { default as CtaSection1 } from './blocks/marketing/cta/cta-section-1'
24
+ export { default as Feature1 } from './blocks/marketing/features/feature-1'
25
+ export { default as Feature2 } from './blocks/marketing/features/feature-2'
26
+ export { default as Testimonial1 } from './blocks/marketing/testimonials/testimonial-1'
27
+ export { default as Testimonial2 } from './blocks/marketing/testimonials/testimonial-2'
28
+ export { default as FaqBlock1 } from './blocks/marketing/faq/faq-block-1'
29
+ export { default as PricingSection1 } from './blocks/marketing/pricing/pricing-section-1'
30
+ export type { CtaSection1Props } from './blocks/marketing/cta/cta-section-1'
31
+ export type { FaqBlock1Props, FaqItem } from './blocks/marketing/faq/faq-block-1'
32
+ export type { Feature1Props, Feature1Item } from './blocks/marketing/features/feature-1'
33
+ export type { Feature2Props, Feature2Item } from './blocks/marketing/features/feature-2'
34
+ export type { LogoCloud1Props, LogoCloud1Platform } from './blocks/marketing/logo-clouds/logo-cloud-1'
35
+ export type { PricingSection1Props, PricingTier } from './blocks/marketing/pricing/pricing-section-1'
36
+ export type { Statistic1Props, StatisticItem } from './blocks/marketing/statistics/statistic-1'
37
+ export type { Testimonial1Props, Testimonial1Item } from './blocks/marketing/testimonials/testimonial-1'
38
+ export type { Testimonial2Props, Testimonial2Item } from './blocks/marketing/testimonials/testimonial-2'
39
+ export { default as Statistic1 } from './blocks/marketing/statistics/statistic-1'
40
+ export { default as LogoCloud1 } from './blocks/marketing/logo-clouds/logo-cloud-1'
41
+ export { default as FooterSection1 } from './blocks/marketing/footers/footer-section-1'
42
+ export { default as FooterSection3 } from './blocks/marketing/footers/footer-section-3'
43
+ export type { FooterSection3Props, FooterLinkColumn, FooterLink, SocialLink } from './blocks/marketing/footers/footer-section-3'
44
+ export { default as FooterSection4 } from './blocks/marketing/footers/footer-section-4'
45
+ export type {
46
+ FooterSection4Props,
47
+ FooterSection4Contact,
48
+ FooterSection4Link,
49
+ FooterSection4LinkColumn,
50
+ FooterSection4SocialIcon,
51
+ FooterSection4SocialLink,
52
+ } from './blocks/marketing/footers/footer-section-4'
53
+ export { default as ProductCard1 } from './blocks/e-commerce/product-list/product-card-1'
54
+ export { default as ProductCard2 } from './blocks/e-commerce/product-list/product-card-2'
55
+ export { default as ProductCard3 } from './blocks/e-commerce/product-list/product-card-3'
56
+ export { default as ProductCard5 } from './blocks/e-commerce/product-list/product-card-5'
57
+ export { default as ProductOverview1 } from './blocks/e-commerce/product-overview/product-overview-1'
58
+ export { default as ProductOverview2 } from './blocks/e-commerce/product-overview/product-overview-2'
59
+ export { default as ProductOverview5 } from './blocks/e-commerce/product-overview/product-overview-5'
60
+ export type { ProductOverview2Props, ProductOverview2Product } from './blocks/e-commerce/product-overview/product-overview-2'
61
+ export type { ProductOverview5Props, ProductOverview5Product, ProductOverview5Review } from './blocks/e-commerce/product-overview/product-overview-5'
62
+ export { default as ProductQuickView } from './blocks/e-commerce/product-overview/product-quick-view'
63
+ export { default as ReviewRating1 } from './blocks/e-commerce/reviews/review-rating-1'
64
+ export type { ReviewRating1Review } from './blocks/e-commerce/reviews/review-rating-1'
65
+ export { default as ReviewRating2 } from './blocks/e-commerce/reviews/review-rating-2'
66
+ export { default as ProductCategory1 } from './blocks/e-commerce/product-categories/product-category-1'
67
+ export type {
68
+ ProductCategory1Category,
69
+ ProductCategory1CategoryIcon,
70
+ ProductCategory1Props,
71
+ } from './blocks/e-commerce/product-categories/product-category-1'
72
+ export { default as ProductCategory2 } from './blocks/e-commerce/product-categories/product-category-2'
73
+ export type { ProductCategory2Category, ProductCategory2Props } from './blocks/e-commerce/product-categories/product-category-2'
74
+ export { default as ProductCategory4 } from './blocks/e-commerce/product-categories/product-category-4'
75
+ export { default as ProductCategory5 } from './blocks/e-commerce/product-categories/product-category-5'
76
+ export { MegaMenu, megaMenuPropsSchema } from './blocks/e-commerce/navigation/mega-menu'
77
+ export type { MegaMenuProps } from './blocks/e-commerce/navigation/mega-menu'
78
+ export { MobileCategoryDrawer, mobileCategoryDrawerPropsSchema } from './blocks/e-commerce/navigation/mobile-category-drawer'
79
+ export type { MobileCategoryDrawerProps } from './blocks/e-commerce/navigation/mobile-category-drawer'
80
+ export { CategoryIcon, categoryIconNameSchema, categoryIconPropsSchema } from './blocks/e-commerce/navigation/category-icon'
81
+ export type { CategoryIconName, CategoryIconProps } from './blocks/e-commerce/navigation/category-icon'
82
+ export { menuItemSchema, collectionItemSchema } from './lib/types/menu'
83
+ export type { MenuItem, CollectionItem } from './lib/types/menu'
84
+ export { default as CategoryFilter1 } from './blocks/e-commerce/category-filters/category-filter-1'
85
+ export { default as CategoryFilter2 } from './blocks/e-commerce/category-filters/category-filter-2'
86
+ export type {
87
+ FilterProduct,
88
+ CategoryFilter2Props,
89
+ CategoryFilter2Category,
90
+ CategoryFilter2Color,
91
+ CategoryFilter2PriceRange,
92
+ CategoryFilter2SortOption,
93
+ CategoryFilter2Filters,
94
+ } from './blocks/e-commerce/category-filters/category-filter-2'
95
+ export { default as CategoryFilter4 } from './blocks/e-commerce/category-filters/category-filter-4'
96
+ export type { CategoryFilter4Props, CategoryFilter4Product, CategoryFilter4Facets } from './blocks/e-commerce/category-filters/category-filter-4'
97
+ export { default as ShoppingCart1 } from './blocks/e-commerce/shopping-carts/shopping-cart-1'
98
+ export { default as ShoppingCart2 } from './blocks/e-commerce/shopping-carts/shopping-cart-2'
99
+ export { default as ShoppingCart4 } from './blocks/e-commerce/shopping-carts/shopping-cart-4'
100
+ export { default as ShoppingCart3 } from './blocks/e-commerce/shopping-carts/shopping-cart-3'
101
+ export type { ShoppingCart3Props, ShoppingCart3CartItem } from './blocks/e-commerce/shopping-carts/shopping-cart-3'
102
+ export type { ShoppingCart2Props, ShoppingCart2Item } from './blocks/e-commerce/shopping-carts/shopping-cart-2'
103
+ export type { ShoppingCart4Props, ShoppingCart4Item } from './blocks/e-commerce/shopping-carts/shopping-cart-4'
104
+ export { default as StoreContainer1 } from './blocks/e-commerce/store-containers/store-container-1'
105
+ export type { StoreContainer1Props } from './blocks/e-commerce/store-containers/store-container-1'
106
+ export { default as CheckoutForm1 } from './blocks/e-commerce/checkout-forms/checkout-form-1'
107
+ export { default as CheckoutForm2 } from './blocks/e-commerce/checkout-forms/checkout-form-2'
108
+ export { default as CheckoutForm3 } from './blocks/e-commerce/checkout-forms/checkout-form-3'
109
+ export { default as CheckoutConfirmation } from './blocks/e-commerce/checkout-forms/checkout-confirmation'
110
+ export type {
111
+ CheckoutForm1Props,
112
+ CheckoutForm1FormData,
113
+ CheckoutForm1FieldErrors,
114
+ CheckoutForm1FieldDescriptions,
115
+ CheckoutForm1ChangeHandler,
116
+ CheckoutForm1OrderSummary,
117
+ CheckoutForm1OrderSummaryItem,
118
+ CheckoutForm1CountryOption,
119
+ } from './blocks/e-commerce/checkout-forms/checkout-form-1'
120
+ export type {
121
+ CheckoutForm2Props,
122
+ CheckoutForm2Item,
123
+ CheckoutForm2Data,
124
+ CheckoutForm2FieldErrors,
125
+ CheckoutForm2FieldDescriptions,
126
+ CheckoutForm2Totals,
127
+ } from './blocks/e-commerce/checkout-forms/checkout-form-2'
128
+ export type {
129
+ CheckoutForm3Props,
130
+ CheckoutForm3CartItem,
131
+ CheckoutForm3FormData,
132
+ CheckoutForm3ShippingMethod,
133
+ CheckoutForm3CountryOption,
134
+ } from './blocks/e-commerce/checkout-forms/checkout-form-3'
135
+ export type {
136
+ CheckoutConfirmationProps,
137
+ FulfilmentStage,
138
+ FulfilmentStageDetail,
139
+ Order,
140
+ OrderLine,
141
+ OrderAddress,
142
+ OrderTotals,
143
+ } from './blocks/e-commerce/checkout-forms/checkout-confirmation'
144
+ export type { CheckoutFieldError, CheckoutFieldErrors } from './blocks/e-commerce/checkout-forms/checkout-form-types'
145
+ export { default as OrderHistory1 } from './blocks/e-commerce/order-history/order-history-1'
146
+ export { default as OrderHistory2 } from './blocks/e-commerce/order-history/order-history-2'
147
+ export { default as StorefrontHero1 } from './blocks/e-commerce/storefront-hero/storefront-hero-1'
148
+ export type {
149
+ FeaturedProduct,
150
+ StorefrontHero1Props,
151
+ } from './blocks/e-commerce/storefront-hero/storefront-hero-1'
152
+ export { default as StorefrontHero2 } from './blocks/e-commerce/storefront-hero/storefront-hero-2'
153
+ export { default as StorefrontHero5 } from './blocks/e-commerce/storefront-hero/storefront-hero-5'
154
+ export { default as StorefrontHero6 } from './blocks/e-commerce/storefront-hero/storefront-hero-6'
155
+ export { default as AppShell1 } from './blocks/application/app-shells/app-shell-1'
156
+ export { default as LoginPage1 } from './blocks/application/login-forms/login-page-1'
157
+ export { default as LoginPage2 } from './blocks/application/login-forms/login-page-2'
158
+ export { default as SignUpPage1 } from './blocks/application/signup-forms/signup-page-1'
159
+ export { default as ForgotPasswordPage1 } from './blocks/application/password-recovery/forgot-password-page-1'
160
+ export { default as Verify1 } from './blocks/application/verification/verify-1'
161
+ export type { LoginPage1Props, LoginPage1FormData, LoginPage1SocialButton } from './blocks/application/login-forms/login-page-1'
162
+ export type { LoginPage2Props, LoginPage2SocialButton } from './blocks/application/login-forms/login-page-2'
163
+ export type { SignUpPage1Props, SignUpPage1Data, SignUpPage1SocialButton } from './blocks/application/signup-forms/signup-page-1'
164
+ export type { ForgotPasswordPage1Props } from './blocks/application/password-recovery/forgot-password-page-1'
165
+ export type { Verify1Props } from './blocks/application/verification/verify-1'
166
+
167
+ // Spec-driven storefront contracts (Zod serializable subset, obeys @thorprovider/types)
168
+ export type { CanonMoney, CanonProduct } from './lib/contracts'
169
+ export { PRODUCT_IMAGE_FALLBACK } from './lib/contracts'
170
+ export {
171
+ buttonPropsSchema,
172
+ storeContainer1PropsSchema,
173
+ productCard1PropsSchema,
174
+ shoppingCart4PropsSchema,
175
+ navbar4PropsSchema,
176
+ paymentMethodSchema,
177
+ cardProductSchema,
178
+ cartItemSchema,
179
+ promotionSchema,
180
+ storeCategorySchema,
181
+ utilityLinkSchema,
182
+ navCartItemSchema,
183
+ storeUserSchema,
184
+ accountMenuItemSchema,
185
+ } from './lib/contracts'
186
+ export type {
187
+ ButtonSpecProps,
188
+ StoreContainer1SpecProps,
189
+ ProductCard1SpecProps,
190
+ ShoppingCart4SpecProps,
191
+ Navbar4SpecProps,
192
+ CardProduct,
193
+ CartItem,
194
+ Promotion,
195
+ StoreCategory,
196
+ UtilityLink,
197
+ NavCartItem,
198
+ StoreUser,
199
+ AccountMenuItem,
200
+ } from './lib/contracts'
@@ -0,0 +1,32 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { storeCategorySchema } from './contracts'
3
+
4
+ describe('storeCategorySchema', () => {
5
+ it('accepts flat categories (additive backward compatibility)', () => {
6
+ const result = storeCategorySchema.safeParse({ label: 'Herramientas', href: '/categorias/herramientas' })
7
+ expect(result.success).toBe(true)
8
+ })
9
+
10
+ it('accepts a three-level category tree', () => {
11
+ const result = storeCategorySchema.safeParse({
12
+ label: 'Mobile Devices',
13
+ href: '/categorias/mobile-devices-l1',
14
+ children: [
15
+ {
16
+ label: 'Smartphones',
17
+ href: '/categorias/smartphones',
18
+ children: [
19
+ { label: 'Flagship Phones', href: '/categorias/flagship-phones' },
20
+ { label: 'Mid-Range Phones', href: '/categorias/mid-range-phones' },
21
+ ],
22
+ },
23
+ ],
24
+ })
25
+ expect(result.success).toBe(true)
26
+ })
27
+
28
+ it('rejects categories missing label or href', () => {
29
+ expect(storeCategorySchema.safeParse({ label: 'Solo', href: '/categorias/x', children: [{}] }).success).toBe(false)
30
+ expect(storeCategorySchema.safeParse({}).success).toBe(false)
31
+ })
32
+ })