@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.
- package/.turbo/turbo-build.log +24 -0
- package/.turbo/turbo-test.log +10 -0
- package/.turbo/turbo-type-check.log +0 -0
- package/AGENTS.md +1 -0
- package/README.md +161 -0
- package/components.json +33 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/contracts.d.mts +563 -0
- package/dist/contracts.d.ts +563 -0
- package/dist/contracts.js +207 -0
- package/dist/contracts.js.map +1 -0
- package/dist/contracts.mjs +168 -0
- package/dist/contracts.mjs.map +1 -0
- package/dist/index.d.mts +2408 -0
- package/dist/index.d.ts +2408 -0
- package/dist/index.js +16422 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +16352 -0
- package/dist/index.mjs.map +1 -0
- package/docs/standards/data-contracts.md +70 -0
- package/docs/standards/theming.md +85 -0
- package/package.json +62 -0
- package/src/blocks/application/app-shells/app-shell-1-sidebar.tsx +198 -0
- package/src/blocks/application/app-shells/app-shell-1.tsx +196 -0
- package/src/blocks/application/login-forms/login-page-1.tsx +246 -0
- package/src/blocks/application/login-forms/login-page-2.tsx +207 -0
- package/src/blocks/application/password-recovery/forgot-password-page-1-brand-mark.tsx +38 -0
- package/src/blocks/application/password-recovery/forgot-password-page-1.tsx +320 -0
- package/src/blocks/application/signup-forms/signup-page-1.tsx +231 -0
- package/src/blocks/application/verification/verify-1.tsx +307 -0
- package/src/blocks/application/verification/verify-confetti.tsx +192 -0
- package/src/blocks/e-commerce/category-filters/category-filter-1.tsx +260 -0
- package/src/blocks/e-commerce/category-filters/category-filter-2-filter-panel.tsx +179 -0
- package/src/blocks/e-commerce/category-filters/category-filter-2.tsx +341 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4-chip-rail.tsx +57 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4-drawer.tsx +124 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4-fields.tsx +170 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4-sidebar.tsx +52 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4-sort-sheet.tsx +57 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4-state.ts +68 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4.tsx +298 -0
- package/src/blocks/e-commerce/checkout-forms/card-formatting.test.ts +47 -0
- package/src/blocks/e-commerce/checkout-forms/card-formatting.ts +9 -0
- package/src/blocks/e-commerce/checkout-forms/checkbox-check.tsx +74 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-confirmation-fulfilment-progress.tsx +189 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-confirmation.tsx +283 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-field-feedback.tsx +29 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-contact-step.tsx +96 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-country-field.tsx +51 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-order-summary.tsx +139 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-payment-step.tsx +184 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-progress.tsx +36 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-shipping-step.tsx +107 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1.tsx +235 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2-order-summary.tsx +130 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2-payment-card.tsx +175 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2-shipping-card.tsx +190 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2.tsx +312 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-information-step.tsx +91 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-order-summary.tsx +110 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-payment-step.tsx +137 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-progress.tsx +68 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-review-step.tsx +79 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-shipping-step.tsx +172 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3.tsx +392 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-types.ts +7 -0
- package/src/blocks/e-commerce/navigation/category-icon.test.tsx +54 -0
- package/src/blocks/e-commerce/navigation/category-icon.tsx +94 -0
- package/src/blocks/e-commerce/navigation/mega-menu.test.tsx +139 -0
- package/src/blocks/e-commerce/navigation/mega-menu.tsx +271 -0
- package/src/blocks/e-commerce/navigation/mobile-category-drawer.test.tsx +152 -0
- package/src/blocks/e-commerce/navigation/mobile-category-drawer.tsx +276 -0
- package/src/blocks/e-commerce/order-history/order-history-1.tsx +159 -0
- package/src/blocks/e-commerce/order-history/order-history-2.tsx +222 -0
- package/src/blocks/e-commerce/product-categories/avatar-group.tsx +86 -0
- package/src/blocks/e-commerce/product-categories/product-category-1.tsx +126 -0
- package/src/blocks/e-commerce/product-categories/product-category-2.tsx +100 -0
- package/src/blocks/e-commerce/product-categories/product-category-4.tsx +129 -0
- package/src/blocks/e-commerce/product-categories/product-category-5.tsx +218 -0
- package/src/blocks/e-commerce/product-list/product-card-1.tsx +131 -0
- package/src/blocks/e-commerce/product-list/product-card-2.tsx +71 -0
- package/src/blocks/e-commerce/product-list/product-card-3-reaction-toggle.tsx +166 -0
- package/src/blocks/e-commerce/product-list/product-card-3.tsx +215 -0
- package/src/blocks/e-commerce/product-list/product-card-5-checkbox-check.tsx +98 -0
- package/src/blocks/e-commerce/product-list/product-card-5-reaction-toggle.tsx +171 -0
- package/src/blocks/e-commerce/product-list/product-card-5.tsx +367 -0
- package/src/blocks/e-commerce/product-overview/product-overview-1.tsx +201 -0
- package/src/blocks/e-commerce/product-overview/product-overview-2.tsx +272 -0
- package/src/blocks/e-commerce/product-overview/product-overview-5-gallery.tsx +59 -0
- package/src/blocks/e-commerce/product-overview/product-overview-5-lightbox-tab.tsx +104 -0
- package/src/blocks/e-commerce/product-overview/product-overview-5-reviews-tab.tsx +82 -0
- package/src/blocks/e-commerce/product-overview/product-overview-5-tabs.tsx +69 -0
- package/src/blocks/e-commerce/product-overview/product-overview-5.tsx +274 -0
- package/src/blocks/e-commerce/product-overview/product-quick-view-card.tsx +135 -0
- package/src/blocks/e-commerce/product-overview/product-quick-view-dialog.tsx +393 -0
- package/src/blocks/e-commerce/product-overview/product-quick-view.tsx +136 -0
- package/src/blocks/e-commerce/reviews/review-rating-1-review-form.tsx +133 -0
- package/src/blocks/e-commerce/reviews/review-rating-1.tsx +178 -0
- package/src/blocks/e-commerce/reviews/review-rating-2.tsx +246 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-1-spinning-counter.tsx +142 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-1.tsx +291 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-item-row.tsx +110 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-recommendations.tsx +51 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-spinning-counter.tsx +146 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2.tsx +295 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-3-spinning-counter.tsx +142 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-3.tsx +342 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-4-spinning-counter.tsx +146 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.tsx +391 -0
- package/src/blocks/e-commerce/store-containers/store-container-1.tsx +29 -0
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.tsx +244 -0
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-2.tsx +283 -0
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-5.tsx +318 -0
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-6.tsx +205 -0
- package/src/blocks/marketing/banners/avatar-group.tsx +69 -0
- package/src/blocks/marketing/banners/banner-1.tsx +188 -0
- package/src/blocks/marketing/cta/cta-section-1.tsx +112 -0
- package/src/blocks/marketing/faq/faq-block-1.tsx +45 -0
- package/src/blocks/marketing/features/feature-1.tsx +97 -0
- package/src/blocks/marketing/features/feature-2.tsx +82 -0
- package/src/blocks/marketing/footers/footer-section-1.tsx +141 -0
- package/src/blocks/marketing/footers/footer-section-3.tsx +192 -0
- package/src/blocks/marketing/footers/footer-section-4.tsx +355 -0
- package/src/blocks/marketing/hero-sections/avatar-group.tsx +86 -0
- package/src/blocks/marketing/hero-sections/hero-section-1.tsx +135 -0
- package/src/blocks/marketing/logo-clouds/logo-cloud-1-logo-icon.tsx +52 -0
- package/src/blocks/marketing/logo-clouds/logo-cloud-1.tsx +56 -0
- package/src/blocks/marketing/navbars/navbar-1.tsx +267 -0
- package/src/blocks/marketing/navbars/navbar-2.tsx +351 -0
- package/src/blocks/marketing/navbars/navbar-3.tsx +303 -0
- package/src/blocks/marketing/navbars/navbar-4-account-menu.tsx +118 -0
- package/src/blocks/marketing/navbars/navbar-4-cart-sheet.tsx +248 -0
- package/src/blocks/marketing/navbars/navbar-4-like-button.tsx +177 -0
- package/src/blocks/marketing/navbars/navbar-4-mobile-nav.tsx +104 -0
- package/src/blocks/marketing/navbars/navbar-4-product-grid.tsx +219 -0
- package/src/blocks/marketing/navbars/navbar-4-search-bar.tsx +52 -0
- package/src/blocks/marketing/navbars/navbar-4.tsx +411 -0
- package/src/blocks/marketing/pricing/pricing-section-1.tsx +94 -0
- package/src/blocks/marketing/statistics/statistic-1.tsx +93 -0
- package/src/blocks/marketing/testimonials/testimonial-1.tsx +141 -0
- package/src/blocks/marketing/testimonials/testimonial-2.tsx +108 -0
- package/src/blocks/shared/reaction-toggle.tsx +145 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/index.ts +200 -0
- package/src/lib/contracts.test.ts +32 -0
- package/src/lib/contracts.ts +235 -0
- package/src/lib/types/menu.ts +43 -0
- package/src/lib/types.ts +4 -0
- package/src/lib/utils.ts +10 -0
- package/src/ui/accordion.tsx +65 -0
- package/src/ui/avatar.tsx +108 -0
- package/src/ui/badge.tsx +47 -0
- package/src/ui/breadcrumb.tsx +108 -0
- package/src/ui/button-group.tsx +83 -0
- package/src/ui/button.tsx +63 -0
- package/src/ui/card.tsx +91 -0
- package/src/ui/carousel.tsx +241 -0
- package/src/ui/dialog.tsx +158 -0
- package/src/ui/drawer.tsx +134 -0
- package/src/ui/dropdown-menu.tsx +256 -0
- package/src/ui/empty.tsx +103 -0
- package/src/ui/field.tsx +248 -0
- package/src/ui/input-group.tsx +170 -0
- package/src/ui/input-otp.tsx +76 -0
- package/src/ui/input.tsx +20 -0
- package/src/ui/label.tsx +23 -0
- package/src/ui/navigation-menu.tsx +167 -0
- package/src/ui/pagination.tsx +127 -0
- package/src/ui/progress.tsx +30 -0
- package/src/ui/radio-group.tsx +44 -0
- package/src/ui/select.tsx +189 -0
- package/src/ui/separator.tsx +27 -0
- package/src/ui/sheet.tsx +142 -0
- package/src/ui/sidebar.tsx +726 -0
- package/src/ui/skeleton.tsx +13 -0
- package/src/ui/table.tsx +115 -0
- package/src/ui/tabs.tsx +90 -0
- package/src/ui/textarea.tsx +17 -0
- package/src/ui/toggle-group.tsx +83 -0
- package/src/ui/toggle.tsx +46 -0
- package/src/ui/tooltip.tsx +56 -0
- package/tsconfig.json +20 -0
- package/tsup.config.ts +16 -0
- 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
|
+
})
|