@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,411 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useMemo, useRef, useState } from 'react'
4
+ import { AnimatePresence, motion, useReducedMotion } from 'motion/react'
5
+ import { Menu, Search, ShoppingBag, X } from 'lucide-react'
6
+
7
+ import { Button } from '../../../ui/button'
8
+ import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '../../../ui/sheet'
9
+ import { cn } from '../../../lib/utils'
10
+
11
+ import { Navbar4AccountMenu, type AccountMenuItem, type StoreUser } from './navbar-4-account-menu'
12
+ import { Navbar4CartSheet, type CartItem } from './navbar-4-cart-sheet'
13
+ import type { UtilityLink } from './navbar-4-mobile-nav'
14
+ import { Navbar4SearchBar } from './navbar-4-search-bar'
15
+ import { MegaMenu } from '../../e-commerce/navigation/mega-menu'
16
+ import { MobileCategoryDrawer } from '../../e-commerce/navigation/mobile-category-drawer'
17
+ import type { MenuItem } from '../../../lib/types/menu'
18
+ import type { StoreCategory } from '../../../lib/contracts'
19
+
20
+ function toMenuItem(category: StoreCategory): MenuItem {
21
+ return {
22
+ id: category.href,
23
+ label: category.label,
24
+ href: category.href,
25
+ children: category.children?.map(toMenuItem),
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Drawer entries come from three sources (categories, utility links, account
31
+ * link). Hosts may reuse the same href across sources (e.g. a placeholder
32
+ * href), which would produce duplicate React keys if passed through as-is.
33
+ * Dedupe by href so every rendered item gets a unique id.
34
+ */
35
+ function uniqueById<T extends { id: string }>(items: T[]): T[] {
36
+ const seen = new Set<string>()
37
+ return items.filter((item) => {
38
+ if (seen.has(item.id)) return false
39
+ seen.add(item.id)
40
+ return true
41
+ })
42
+ }
43
+
44
+ function Logo({ text, href }: { text: string; href?: string }) {
45
+ const className =
46
+ 'focus-visible:ring-ring flex items-center gap-2 rounded-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none'
47
+
48
+ const content = (
49
+ <>
50
+ <svg viewBox='0 0 32 32' aria-hidden='true' className='text-primary size-7' fill='none' xmlns='http://www.w3.org/2000/svg'>
51
+ <path
52
+ d='M26 24.75C26.4142 24.75 26.75 24.4142 26.75 24C26.75 23.5858 26.4142 23.25 26 23.25V24.75ZM26 23.25H11V24.75H26V23.25ZM8.75 21V15H7.25V21H8.75ZM11 23.25C9.75736 23.25 8.75 22.2426 8.75 21H7.25C7.25 23.0711 8.92893 24.75 11 24.75V23.25Z'
53
+ fill='currentColor'
54
+ />
55
+ <path
56
+ d='M1.5 3.25C1.08579 3.25 0.75 3.58579 0.75 4C0.75 4.41421 1.08579 4.75 1.5 4.75V3.25ZM1.5 4.75H6V3.25H1.5V4.75ZM7.25 6V21H8.75V6H7.25ZM6 4.75C6.69036 4.75 7.25 5.30964 7.25 6H8.75C8.75 4.48122 7.51878 3.25 6 3.25V4.75Z'
57
+ fill='currentColor'
58
+ />
59
+ <path
60
+ d='M22 21.75C22.4142 21.75 22.75 21.4142 22.75 21C22.75 20.5858 22.4142 20.25 22 20.25V21.75ZM22 20.25H11V21.75H22V20.25ZM8.75 18V12H7.25V18H8.75ZM11 20.25C9.75736 20.25 8.75 19.2426 8.75 18H7.25C7.25 20.0711 8.92893 21.75 11 21.75V20.25Z'
61
+ fill='currentColor'
62
+ />
63
+ <path
64
+ d='M27.2057 19.754C27.0654 20.1438 26.6357 20.346 26.246 20.2057C25.8562 20.0654 25.654 19.6357 25.7943 19.246L27.2057 19.754ZM30.0361 9.67744L29.3305 9.4234L29.3305 9.4234L30.0361 9.67744ZM25.7943 19.246L29.3305 9.4234L30.7418 9.93148L27.2057 19.754L25.7943 19.246ZM28.1543 7.75L8 7.75V6.25L28.1543 6.25V7.75ZM29.3305 9.4234C29.6237 8.60882 29.0201 7.75 28.1543 7.75V6.25C30.059 6.25 31.3869 8.13941 30.7418 9.93148L29.3305 9.4234Z'
65
+ fill='currentColor'
66
+ />
67
+ <path
68
+ d='M13.5 21.75C13.0858 21.75 12.75 21.4142 12.75 21C12.75 20.5858 13.0858 20.25 13.5 20.25V21.75ZM26.7111 19.009L27.4174 19.2613L27.4174 19.2613L26.7111 19.009ZM13.5 20.25H23.8858V21.75H13.5V20.25ZM26.0048 18.7568L27.7937 13.7477L29.2063 14.2523L27.4174 19.2613L26.0048 18.7568ZM23.8858 20.25C24.8367 20.25 25.6849 19.6522 26.0048 18.7568L27.4174 19.2613C26.8843 20.7537 25.4706 21.75 23.8858 21.75V20.25Z'
69
+ fill='currentColor'
70
+ />
71
+ <path d='M21.1694 10.5806L14.5651 17.1849' stroke='currentColor' strokeWidth='1.5' strokeLinecap='round' />
72
+ <path d='M22.1694 14.5806L18.5632 18.1868' stroke='currentColor' strokeWidth='1.5' strokeLinecap='round' />
73
+ <circle cx='13.1' cy='26.1' r='1.7' stroke='currentColor' strokeWidth='1.2' />
74
+ <circle cx='22.1' cy='26.1' r='1.7' stroke='currentColor' strokeWidth='1.2' />
75
+ </svg>
76
+ <span className='text-base font-semibold tracking-tight'>{text}</span>
77
+ </>
78
+ )
79
+
80
+ if (!href) {
81
+ return <span className={className}>{content}</span>
82
+ }
83
+
84
+ return (
85
+ <a href={href} className={className}>
86
+ {content}
87
+ </a>
88
+ )
89
+ }
90
+
91
+ type CartBadgeProps = {
92
+ count: number
93
+ }
94
+
95
+ /**
96
+ * Transitions.dev `notification-badge`: diagonal slide + spring pop when the
97
+ * count appears or changes. Positioned outside the bag so it never covers the icon.
98
+ */
99
+ function CartBadge({ count }: CartBadgeProps) {
100
+ const reduceMotion = useReducedMotion()
101
+
102
+ return (
103
+ <AnimatePresence>
104
+ {count > 0 ? (
105
+ <motion.span
106
+ key='badge-shell'
107
+ className='bg-foreground text-background pointer-events-none absolute -top-2.5 -end-3.5 flex size-[1.125rem] items-center justify-center overflow-hidden rounded-full text-[10px] font-medium tabular-nums shadow-sm ring-2 ring-background'
108
+ initial={
109
+ reduceMotion
110
+ ? false
111
+ : { scale: 0, x: -8.2, y: 12.4, opacity: 0, filter: 'blur(2px)' }
112
+ }
113
+ animate={{ scale: 1, x: 0, y: 0, opacity: 1, filter: 'blur(0px)' }}
114
+ exit={
115
+ reduceMotion
116
+ ? { opacity: 0 }
117
+ : {
118
+ scale: 0,
119
+ opacity: 0,
120
+ filter: 'blur(2px)',
121
+ transition: { duration: 0.18, ease: [0.4, 0, 0.2, 1] },
122
+ }
123
+ }
124
+ transition={
125
+ reduceMotion
126
+ ? { duration: 0 }
127
+ : { type: 'spring', stiffness: 520, damping: 18, mass: 0.6 }
128
+ }
129
+ style={{ willChange: 'transform, opacity, filter' }}
130
+ >
131
+ <AnimatePresence mode='popLayout' initial={false}>
132
+ <motion.span
133
+ key={count}
134
+ className='block'
135
+ initial={reduceMotion ? false : { y: 8, opacity: 0, filter: 'blur(2px)' }}
136
+ animate={{ y: 0, opacity: 1, filter: 'blur(0px)' }}
137
+ exit={reduceMotion ? undefined : { y: -8, opacity: 0, filter: 'blur(2px)' }}
138
+ transition={{ duration: 0.22, ease: [0.22, 1, 0.36, 1] }}
139
+ >
140
+ {count > 99 ? '99+' : count}
141
+ </motion.span>
142
+ </AnimatePresence>
143
+ </motion.span>
144
+ ) : null}
145
+ </AnimatePresence>
146
+ )
147
+ }
148
+
149
+ export interface Navbar4Props {
150
+ storeCategories: StoreCategory[]
151
+ shippingNote: string
152
+ utilityLinks: UtilityLink[]
153
+ logoText: string
154
+ logoHref?: string
155
+ /** When provided, a mobile-only "Account" link is appended to the utility links. */
156
+ accountHref?: string
157
+ searchQuery?: string
158
+ onSearchChange?: (value: string) => void
159
+ items?: CartItem[]
160
+ onQtyChange?: (id: string, delta: number) => void
161
+ onRemove?: (id: string) => void
162
+ user?: StoreUser
163
+ accountMenuItems?: AccountMenuItem[]
164
+ onHelp?: () => void
165
+ onSignOut?: () => void
166
+ onCheckout?: () => void
167
+ onViewCart?: () => void
168
+ }
169
+
170
+ export function Navbar4({
171
+ storeCategories,
172
+ shippingNote,
173
+ utilityLinks,
174
+ logoText,
175
+ logoHref,
176
+ accountHref,
177
+ searchQuery: searchQueryProp,
178
+ onSearchChange,
179
+ items: itemsProp,
180
+ onQtyChange: onQtyChangeProp,
181
+ onRemove: onRemoveProp,
182
+ user,
183
+ accountMenuItems,
184
+ onHelp,
185
+ onSignOut,
186
+ onCheckout,
187
+ onViewCart,
188
+ }: Navbar4Props) {
189
+ /**
190
+ * Integration seam
191
+ * ----------------
192
+ * Pass cart + search from your storefront, or leave uncontrolled for a
193
+ * self-contained demo. Controlled props win when provided.
194
+ */
195
+ const [internalItems, setInternalItems] = useState<CartItem[]>([])
196
+ const [internalSearch, setInternalSearch] = useState('')
197
+ const [searchOpen, setSearchOpen] = useState(false)
198
+ const [menuOpen, setMenuOpen] = useState(false)
199
+ const searchTriggerRef = useRef<HTMLButtonElement>(null)
200
+
201
+ const items = itemsProp ?? internalItems
202
+ const searchQuery = searchQueryProp ?? internalSearch
203
+
204
+ const setSearchQuery = (value: string) => {
205
+ onSearchChange?.(value)
206
+ if (searchQueryProp === undefined) {
207
+ setInternalSearch(value)
208
+ }
209
+ }
210
+
211
+ const changeQty =
212
+ onQtyChangeProp ??
213
+ ((id: string, delta: number) =>
214
+ setInternalItems(current =>
215
+ current.flatMap(item => {
216
+ if (item.id !== id) {
217
+ return item
218
+ }
219
+
220
+ const qty = item.qty + delta
221
+ return qty <= 0 ? [] : [{ ...item, qty }]
222
+ }),
223
+ ))
224
+
225
+ const removeItem =
226
+ onRemoveProp ?? ((id: string) => setInternalItems(current => current.filter(item => item.id !== id)))
227
+
228
+ const count = useMemo(() => items.reduce((total, item) => total + item.qty, 0), [items])
229
+
230
+ useEffect(() => {
231
+ if (!searchOpen) {
232
+ return
233
+ }
234
+
235
+ document.getElementById('store-search-mobile')?.focus()
236
+ }, [searchOpen])
237
+
238
+ const dismissSearch = () => {
239
+ setSearchOpen(false)
240
+ searchTriggerRef.current?.focus()
241
+ }
242
+
243
+ return (
244
+ <header className='border-border bg-background/95 sticky top-0 z-40 w-full border-b backdrop-blur-md supports-backdrop-filter:bg-background/80'>
245
+ <div className='bg-foreground text-background hidden lg:block'>
246
+ <div className='mx-auto flex h-9 w-full max-w-7xl items-center justify-between px-4 text-xs tracking-wide sm:px-6 lg:px-8'>
247
+ <p className='text-background/80'>{shippingNote}</p>
248
+ <ul className='flex items-center gap-5'>
249
+ {utilityLinks.map(link => (
250
+ <li key={`${link.href}:${link.label}`}>
251
+ <a
252
+ href={link.href}
253
+ className='text-background/80 hover:text-background transition-colors duration-150'
254
+ >
255
+ {link.label}
256
+ </a>
257
+ </li>
258
+ ))}
259
+ </ul>
260
+ </div>
261
+ </div>
262
+
263
+ <div className='mx-auto flex h-16 w-full max-w-7xl items-center gap-3 px-4 sm:px-6 lg:px-8'>
264
+ <Button
265
+ variant='ghost'
266
+ size='icon'
267
+ className='lg:hidden'
268
+ aria-label='Open menu'
269
+ aria-expanded={menuOpen}
270
+ onClick={() => setMenuOpen(open => !open)}
271
+ >
272
+ <Menu />
273
+ </Button>
274
+
275
+ <MobileCategoryDrawer
276
+ categories={uniqueById([
277
+ ...storeCategories.map(toMenuItem),
278
+ ...utilityLinks.map(link => ({ id: link.href, label: link.label, href: link.href })),
279
+ ...(accountHref ? [{ id: accountHref, label: 'Account', href: accountHref }] : []),
280
+ ])}
281
+ collections={[]}
282
+ open={menuOpen}
283
+ onClose={() => setMenuOpen(false)}
284
+ viewAllHref=''
285
+ />
286
+
287
+ <div className='flex-1 lg:flex-none'>
288
+ <Logo text={logoText} href={logoHref} />
289
+ </div>
290
+
291
+ <nav aria-label='Store' className='hidden flex-1 lg:block'>
292
+ <ul className='flex items-center gap-0.5'>
293
+ {storeCategories.map(category =>
294
+ category.children && category.children.length > 0 ? (
295
+ <li key={category.href} className='group relative'>
296
+ <a
297
+ href={category.href}
298
+ className={cn(
299
+ 'hover:bg-muted focus-visible:ring-ring rounded-md px-3 py-2 text-sm font-medium transition-colors duration-150 focus-visible:ring-2 focus-visible:outline-none',
300
+ category.emphasis ? 'text-destructive' : 'text-foreground',
301
+ )}
302
+ >
303
+ {category.label}
304
+ </a>
305
+ <div className='invisible absolute left-0 top-full z-50 pt-2 opacity-0 transition-opacity duration-150 group-hover:visible group-hover:opacity-100'>
306
+ <MegaMenu
307
+ categories={[toMenuItem(category)]}
308
+ collections={[]}
309
+ collectionsHref=''
310
+ categoryColumnTitle={category.label}
311
+ />
312
+ </div>
313
+ </li>
314
+ ) : (
315
+ <li key={category.href}>
316
+ <a
317
+ href={category.href}
318
+ className={cn(
319
+ 'hover:bg-muted focus-visible:ring-ring rounded-md px-3 py-2 text-sm font-medium transition-colors duration-150 focus-visible:ring-2 focus-visible:outline-none',
320
+ category.emphasis ? 'text-destructive' : 'text-foreground',
321
+ )}
322
+ >
323
+ {category.label}
324
+ </a>
325
+ </li>
326
+ ),
327
+ )}
328
+ </ul>
329
+ </nav>
330
+
331
+ <div className='flex items-center gap-1.5'>
332
+ <div className='hidden lg:block lg:w-64'>
333
+ <Navbar4SearchBar
334
+ id='store-search'
335
+ value={searchQuery}
336
+ onValueChange={setSearchQuery}
337
+ />
338
+ </div>
339
+
340
+ <Button
341
+ ref={searchTriggerRef}
342
+ variant='ghost'
343
+ size='icon'
344
+ className='lg:hidden'
345
+ aria-label='Search products'
346
+ aria-expanded={searchOpen}
347
+ onClick={() => setSearchOpen(open => !open)}
348
+ >
349
+ {searchOpen ? <X /> : <Search />}
350
+ </Button>
351
+
352
+ {user ? (
353
+ <div className='hidden lg:block'>
354
+ <Navbar4AccountMenu
355
+ user={user}
356
+ items={accountMenuItems}
357
+ onHelp={onHelp}
358
+ onSignOut={onSignOut}
359
+ />
360
+ </div>
361
+ ) : null}
362
+
363
+ <Sheet>
364
+ <SheetTrigger asChild>
365
+ <Button
366
+ variant='ghost'
367
+ size='icon'
368
+ className='relative overflow-visible'
369
+ aria-label={`Cart, ${count} ${count === 1 ? 'item' : 'items'}`}
370
+ >
371
+ <ShoppingBag className='size-5' />
372
+ <CartBadge count={count} />
373
+ </Button>
374
+ </SheetTrigger>
375
+ <SheetContent side='right' className='flex w-full max-w-sm flex-col gap-0 p-0'>
376
+ <SheetHeader className='border-border border-b'>
377
+ <SheetTitle className='text-left text-base'>
378
+ Cart{count > 0 ? ` (${count})` : ''}
379
+ </SheetTitle>
380
+ </SheetHeader>
381
+ <Navbar4CartSheet
382
+ items={items}
383
+ onQtyChange={changeQty}
384
+ onRemove={removeItem}
385
+ onCheckout={onCheckout}
386
+ onViewCart={onViewCart}
387
+ />
388
+ </SheetContent>
389
+ </Sheet>
390
+ </div>
391
+ </div>
392
+
393
+ <p aria-live='polite' className='sr-only'>
394
+ {`Cart contains ${count} ${count === 1 ? 'item' : 'items'}`}
395
+ </p>
396
+
397
+ {searchOpen ? (
398
+ <div className='border-border flex flex-col gap-0 border-t p-4 lg:hidden'>
399
+ <Navbar4SearchBar
400
+ id='store-search-mobile'
401
+ value={searchQuery}
402
+ onValueChange={setSearchQuery}
403
+ onDismiss={dismissSearch}
404
+ />
405
+ </div>
406
+ ) : null}
407
+ </header>
408
+ )
409
+ }
410
+
411
+ export default Navbar4
@@ -0,0 +1,94 @@
1
+ import { ArrowRight, Check } from 'lucide-react'
2
+ import { Button } from '../../../ui/button'
3
+ import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '../../../ui/card'
4
+ import { cn } from '../../../lib/utils'
5
+ import { Badge } from '../../../ui/badge'
6
+
7
+ export interface PricingTier {
8
+ id: string
9
+ name: string
10
+ description: string
11
+ /** Precio sin símbolo de moneda; el símbolo se pasa aparte en `currencySymbol`. */
12
+ price: string
13
+ /** Frecuencia de facturación, p. ej. '/mes'. */
14
+ frequency: string
15
+ features: string[]
16
+ popular?: boolean
17
+ }
18
+
19
+ export interface PricingSection1Props {
20
+ tiers: PricingTier[]
21
+ currencySymbol?: string
22
+ sectionTitle?: string
23
+ sectionLabel?: string
24
+ popularBadgeLabel?: string
25
+ getStartedLabel?: string
26
+ onSelectTier?: (tier: PricingTier) => void
27
+ }
28
+
29
+ export function PricingSection1({
30
+ tiers,
31
+ currencySymbol = '$',
32
+ sectionTitle = 'El plan perfecto para tu negocio',
33
+ sectionLabel = 'Nuestros precios',
34
+ popularBadgeLabel = 'Popular',
35
+ getStartedLabel = 'Empezar',
36
+ onSelectTier,
37
+ }: PricingSection1Props) {
38
+ return (
39
+ <section className='py-16' id='pricing'>
40
+ <div className='mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8'>
41
+ <header className='mb-12 text-center'>
42
+ <p className='text-muted-foreground mb-2 text-sm font-medium'>{sectionLabel}</p>
43
+ <h2 className='text-3xl font-bold text-balance lg:text-5xl'>{sectionTitle}</h2>
44
+ </header>
45
+
46
+ <div className='grid gap-6 lg:grid-cols-3 lg:gap-8'>
47
+ {tiers.map(tier => (
48
+ <Card key={tier.id} className={cn('overflow-hidden py-6', { 'shadow-lg': tier.popular })}>
49
+ <CardHeader className='px-6'>
50
+ <div className='flex items-center justify-between gap-4'>
51
+ <CardTitle className='text-xl font-bold lg:text-2xl'>{tier.name}</CardTitle>
52
+ {tier.popular && (
53
+ <Badge className='px-2.5 py-0.5 font-semibold rounded-full'>{popularBadgeLabel}</Badge>
54
+ )}
55
+ </div>
56
+ <p className='text-muted-foreground text-sm'>{tier.description}</p>
57
+ </CardHeader>
58
+ <CardContent className='flex flex-col flex-1 px-6'>
59
+ <div className='mb-6 flex items-baseline gap-2'>
60
+ <span className='text-3xl font-bold lg:text-5xl'>
61
+ {currencySymbol}
62
+ {tier.price}
63
+ </span>
64
+ <span className='text-muted-foreground text-sm'>{tier.frequency}</span>
65
+ </div>
66
+ <ul className='flex flex-col gap-3'>
67
+ {tier.features.map(feature => (
68
+ <li key={feature} className='flex items-center gap-2'>
69
+ <Check className='size-4 shrink-0' />
70
+ <span className='text-muted-foreground text-sm'>{feature}</span>
71
+ </li>
72
+ ))}
73
+ </ul>
74
+ </CardContent>
75
+ <CardFooter className='border-0 bg-transparent px-6 pb-6'>
76
+ <Button
77
+ className='h-10 px-8 w-full cursor-pointer gap-2'
78
+ size='lg'
79
+ variant={tier.popular ? 'default' : 'outline'}
80
+ aria-label={`${getStartedLabel} con ${tier.name}`}
81
+ onClick={onSelectTier ? () => onSelectTier(tier) : undefined}
82
+ >
83
+ {getStartedLabel} <ArrowRight />
84
+ </Button>
85
+ </CardFooter>
86
+ </Card>
87
+ ))}
88
+ </div>
89
+ </div>
90
+ </section>
91
+ )
92
+ }
93
+
94
+ export default PricingSection1
@@ -0,0 +1,93 @@
1
+ import { Card, CardContent, CardHeader, CardTitle } from '../../../ui/card'
2
+ import { Badge } from '../../../ui/badge'
3
+ import { Progress } from '../../../ui/progress'
4
+ import { BarChart3 } from 'lucide-react'
5
+ import type { LucideIcon } from 'lucide-react'
6
+
7
+ export interface StatisticItem {
8
+ id: string
9
+ title: string
10
+ value: string
11
+ description: string
12
+ progress: number
13
+ progressLabel: string
14
+ icon: LucideIcon
15
+ }
16
+
17
+ export interface Statistic1Props {
18
+ items: StatisticItem[]
19
+ badge?: string
20
+ title?: string
21
+ description?: string
22
+ }
23
+
24
+ export function Statistic1({ items, badge, title, description }: Statistic1Props) {
25
+ return (
26
+ <section className='py-16 md:py-20'>
27
+ <div className='mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8'>
28
+ <div className='flex flex-col gap-16'>
29
+ {(badge || title || description) && (
30
+ <div className='flex flex-col gap-6 text-center'>
31
+ {badge && (
32
+ <Badge
33
+ variant='outline'
34
+ className='h-auto [&>svg]:size-4! rounded-md inline-flex items-center gap-2 px-4 py-2 text-sm font-medium self-center'
35
+ >
36
+ <BarChart3 />
37
+ {badge}
38
+ </Badge>
39
+ )}
40
+ {title && (
41
+ <h2 className='text-4xl font-bold tracking-tight text-balance lg:text-5xl'>
42
+ {title}
43
+ </h2>
44
+ )}
45
+ {description && (
46
+ <p className='text-muted-foreground mx-auto max-w-2xl text-lg leading-relaxed'>
47
+ {description}
48
+ </p>
49
+ )}
50
+ </div>
51
+ )}
52
+
53
+ <div className='grid gap-6 sm:grid-cols-2 md:grid-cols-3'>
54
+ {items.map(item => (
55
+ <Card
56
+ key={item.id}
57
+ className='group relative gap-2 overflow-hidden py-6 transition-all duration-300 hover:shadow-lg'
58
+ >
59
+ <CardHeader className='px-6'>
60
+ <div className='flex items-start justify-between'>
61
+ <div className='flex-1'>
62
+ <CardTitle className='text-muted-foreground text-sm font-medium text-balance'>
63
+ {item.title}
64
+ </CardTitle>
65
+ <div className='mt-2'>
66
+ <div className='text-2xl font-bold'>{item.value}</div>
67
+ </div>
68
+ </div>
69
+ <div className='bg-muted rounded-xl p-3'>
70
+ <item.icon className='text-muted-foreground size-6' />
71
+ </div>
72
+ </div>
73
+ <p className='text-muted-foreground mt-1 text-xs'>{item.description}</p>
74
+ </CardHeader>
75
+ <CardContent className='pt-0 px-6'>
76
+ <div className='flex flex-col gap-2'>
77
+ <div className='flex items-center justify-between text-xs'>
78
+ <span className='text-muted-foreground'>{item.progressLabel}</span>
79
+ <span className='font-medium'>{item.progress}%</span>
80
+ </div>
81
+ <Progress value={item.progress} className='h-2' />
82
+ </div>
83
+ </CardContent>
84
+ </Card>
85
+ ))}
86
+ </div>
87
+ </div>
88
+ </div>
89
+ </section>
90
+ )
91
+ }
92
+
93
+ export default Statistic1