@hanzo/ui 4.6.0 → 4.8.2

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/assets/general.tsx +1 -1
  2. package/assets/hanzo-logo.tsx +3 -1
  3. package/assets/index.ts +119 -5
  4. package/blocks/auth/index.ts +6 -0
  5. package/blocks/auth/login-2fa.tsx +165 -0
  6. package/blocks/auth/login-basic.tsx +94 -0
  7. package/blocks/auth/login-social.tsx +148 -0
  8. package/blocks/auth/magic-link.tsx +129 -0
  9. package/blocks/auth/password-reset.tsx +97 -0
  10. package/blocks/auth/signup.tsx +157 -0
  11. package/blocks/data-display/activity-feed.tsx +242 -0
  12. package/blocks/data-display/data-table.tsx +235 -0
  13. package/blocks/data-display/stats-grid.tsx +194 -0
  14. package/blocks/ecommerce/checkout.tsx +242 -0
  15. package/blocks/ecommerce/index.ts +7 -0
  16. package/blocks/ecommerce/product-detail.tsx +257 -0
  17. package/blocks/ecommerce/product-grid.tsx +148 -0
  18. package/blocks/ecommerce/shopping-cart.tsx +181 -0
  19. package/blocks/marketing/cta-section.tsx +207 -0
  20. package/blocks/marketing/faq.tsx +159 -0
  21. package/blocks/marketing/features-grid.tsx +156 -0
  22. package/blocks/marketing/hero-section.tsx +192 -0
  23. package/blocks/marketing/index.ts +6 -0
  24. package/blocks/marketing/pricing-table.tsx +121 -0
  25. package/blocks/marketing/testimonials.tsx +196 -0
  26. package/components/index.ts +4 -51
  27. package/dist/index.js +9351 -0
  28. package/dist/index.mjs +9340 -0
  29. package/dist/lib/utils.js +47 -0
  30. package/dist/lib/utils.mjs +28 -0
  31. package/dist/src/utils.js +47 -0
  32. package/dist/src/utils.mjs +28 -0
  33. package/dist/tailwind/index.js +2050 -0
  34. package/dist/tailwind/index.mjs +2019 -0
  35. package/dist/types/index.js +79 -0
  36. package/dist/types/index.mjs +56 -0
  37. package/dist/util/format-text.js +51 -0
  38. package/dist/util/format-text.mjs +32 -0
  39. package/dist/util/index.js +384 -0
  40. package/dist/util/index.mjs +363 -0
  41. package/frameworks/core/index.ts +6 -0
  42. package/frameworks/core/utils/index.ts +64 -0
  43. package/frameworks/react/components/button.tsx +26 -0
  44. package/frameworks/react/components/index.ts +5 -0
  45. package/frameworks/react/hooks/index.ts +5 -0
  46. package/frameworks/react/index.ts +9 -0
  47. package/frameworks/react/package.json +8 -0
  48. package/frameworks/react/utils/index.ts +2 -0
  49. package/frameworks/react-native/index.ts +9 -0
  50. package/frameworks/react-native/package.json +8 -0
  51. package/frameworks/registry.json +371 -0
  52. package/frameworks/setup.sh +69 -0
  53. package/frameworks/svelte/index.ts +9 -0
  54. package/frameworks/svelte/package.json +8 -0
  55. package/frameworks/tracker.json +1854 -0
  56. package/frameworks/vue/index.ts +9 -0
  57. package/frameworks/vue/package.json +8 -0
  58. package/package.json +192 -28
  59. package/primitives/accordion.tsx +1 -1
  60. package/primitives/alert-dialog.tsx +1 -1
  61. package/primitives/alert.tsx +1 -1
  62. package/primitives/avatar.tsx +1 -1
  63. package/primitives/badge.tsx +2 -1
  64. package/primitives/breadcrumb.tsx +1 -1
  65. package/primitives/button.tsx +37 -47
  66. package/primitives/card.tsx +1 -1
  67. package/primitives/carousel.tsx +3 -2
  68. package/primitives/chat/chat-input-area.tsx +5 -4
  69. package/primitives/chat/chat-input.tsx +2 -2
  70. package/primitives/chat/files-preview.tsx +5 -4
  71. package/primitives/chat/message-list.tsx +2 -1
  72. package/primitives/chat/sqlite-preview.tsx +8 -8
  73. package/primitives/checkbox.tsx +2 -1
  74. package/primitives/command.tsx +3 -1
  75. package/primitives/context-menu.tsx +1 -1
  76. package/primitives/dialog.tsx +6 -1
  77. package/primitives/drawer.tsx +4 -1
  78. package/primitives/dropdown-menu.tsx +1 -1
  79. package/primitives/file-uploader.tsx +4 -2
  80. package/primitives/form.tsx +1 -1
  81. package/primitives/hover-card.tsx +1 -1
  82. package/primitives/icons/github.tsx +2 -2
  83. package/primitives/icons/youtube-logo.tsx +1 -1
  84. package/primitives/index-common.ts +7 -6
  85. package/primitives/input-otp.tsx +1 -1
  86. package/primitives/input.tsx +2 -1
  87. package/primitives/label.tsx +2 -1
  88. package/primitives/markdown-preview.tsx +3 -0
  89. package/primitives/mermaid.tsx +13 -18
  90. package/primitives/next/image.tsx +2 -1
  91. package/primitives/next/inline-icon.tsx +14 -14
  92. package/primitives/next/media-stack.tsx +2 -19
  93. package/primitives/pagination.tsx +1 -1
  94. package/primitives/popover.tsx +4 -2
  95. package/primitives/progress.tsx +2 -1
  96. package/primitives/prompt-textarea.tsx +1 -1
  97. package/primitives/radio-group.tsx +1 -1
  98. package/primitives/scroll-area.tsx +1 -1
  99. package/primitives/search-input.tsx +1 -1
  100. package/primitives/select.tsx +1 -1
  101. package/primitives/separator.tsx +2 -1
  102. package/primitives/sheet.tsx +1 -1
  103. package/primitives/skeleton.tsx +1 -0
  104. package/primitives/slider.tsx +2 -1
  105. package/primitives/stepper.tsx +1 -1
  106. package/primitives/switch.tsx +2 -1
  107. package/primitives/table.tsx +1 -1
  108. package/primitives/tabs.tsx +1 -1
  109. package/primitives/textarea.tsx +2 -1
  110. package/primitives/textfield.tsx +1 -0
  111. package/primitives/toggle-group.tsx +1 -1
  112. package/primitives/toggle.tsx +1 -1
  113. package/primitives/tooltip.tsx +1 -1
  114. package/src/hooks/use-copy-clipboard.ts +1 -1
  115. package/src/index-lean.ts +87 -0
  116. package/src/index.ts +54 -0
  117. package/src/registry/api.ts +1 -1
  118. package/src/utils.ts +19 -1
  119. package/tailwind/tailwind.config.hanzo-preset.js +7 -7
  120. package/tailwind/typo-plugin/index.js +1 -1
  121. package/types/animation-def.ts +1 -1
  122. package/types/index.ts +2 -1
  123. package/util/blob.ts +9 -4
  124. package/util/date.ts +2 -1
  125. package/util/format-text.ts +2 -1
  126. package/util/index.ts +103 -0
  127. package/util/spread-to-transform.ts +9 -8
  128. package/MCP-INSTRUCTIONS.md +0 -73
  129. package/README-MCP.md +0 -175
  130. package/dist/button.d.ts +0 -1
  131. package/dist/button.js +0 -1
  132. package/dist/hooks/index.d.ts +0 -7
  133. package/dist/hooks/index.js +0 -7
  134. package/dist/hooks/use-click-away.d.ts +0 -2
  135. package/dist/hooks/use-click-away.js +0 -23
  136. package/dist/hooks/use-combined-refs.d.ts +0 -3
  137. package/dist/hooks/use-combined-refs.js +0 -18
  138. package/dist/hooks/use-copy-clipboard.d.ts +0 -9
  139. package/dist/hooks/use-copy-clipboard.js +0 -21
  140. package/dist/hooks/use-debounce.d.ts +0 -1
  141. package/dist/hooks/use-debounce.js +0 -13
  142. package/dist/hooks/use-fill-ids.d.ts +0 -8
  143. package/dist/hooks/use-fill-ids.js +0 -20
  144. package/dist/hooks/use-map.d.ts +0 -1
  145. package/dist/hooks/use-map.js +0 -20
  146. package/dist/hooks/use-measure.d.ts +0 -8
  147. package/dist/hooks/use-measure.js +0 -25
  148. package/dist/hooks/use-reverse-video-playback.d.ts +0 -1
  149. package/dist/hooks/use-reverse-video-playback.js +0 -41
  150. package/dist/hooks/use-scroll-restoration.d.ts +0 -8
  151. package/dist/hooks/use-scroll-restoration.js +0 -36
  152. package/dist/mcp/enhanced-server.d.ts +0 -29
  153. package/dist/mcp/enhanced-server.js +0 -1128
  154. package/dist/mcp/index.d.ts +0 -28
  155. package/dist/mcp/index.js +0 -436
  156. package/dist/registry/api.d.ts +0 -37
  157. package/dist/registry/api.js +0 -129
  158. package/dist/registry/index.d.ts +0 -353
  159. package/dist/registry/index.js +0 -45
  160. package/dist/utils.d.ts +0 -1
  161. package/dist/utils.js +0 -1
  162. package/environment.d.ts +0 -6
  163. package/public/r/accordion.json +0 -11
  164. package/public/r/alert.json +0 -11
  165. package/public/r/avatar.json +0 -11
  166. package/public/r/badge.json +0 -11
  167. package/public/r/button.json +0 -11
  168. package/public/r/card.json +0 -11
  169. package/public/r/checkbox.json +0 -11
  170. package/public/r/default.json +0 -6
  171. package/public/r/dialog.json +0 -11
  172. package/public/r/input.json +0 -11
  173. package/public/r/label.json +0 -11
  174. package/public/r/new-york.json +0 -6
  175. package/public/r/popover.json +0 -11
  176. package/public/r/select.json +0 -11
  177. package/public/r/table.json +0 -11
  178. package/public/r/tabs.json +0 -11
  179. package/public/r/toast.json +0 -11
  180. package/registry.json +0 -184
  181. package/test/test-registry.js +0 -73
  182. package/test-imports.mjs +0 -19
  183. package/tsconfig.json +0 -22
  184. package/utils.ts +0 -9
@@ -0,0 +1,257 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import { cn } from '@hanzo/ui/util'
5
+ import { Button } from '@hanzo/ui/primitives'
6
+ import { Badge } from '@hanzo/ui/primitives'
7
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@hanzo/ui/primitives'
8
+ import { Star, ShoppingCart, Heart, Share2 } from 'lucide-react'
9
+
10
+ interface ProductDetailProps extends React.ComponentPropsWithoutRef<'div'> {
11
+ product: {
12
+ id: string | number
13
+ name: string
14
+ description: string
15
+ price: number
16
+ originalPrice?: number
17
+ currency?: string
18
+ images: string[]
19
+ rating?: number
20
+ reviewCount?: number
21
+ badge?: string
22
+ inStock?: boolean
23
+ variants?: Array<{
24
+ id: string
25
+ name: string
26
+ value: string
27
+ }>
28
+ features?: string[]
29
+ specifications?: Record<string, string>
30
+ }
31
+ onAddToCart?: (quantity: number, variant?: string) => void
32
+ onAddToWishlist?: () => void
33
+ onShare?: () => void
34
+ }
35
+
36
+ export function ProductDetail({
37
+ className,
38
+ product,
39
+ onAddToCart,
40
+ onAddToWishlist,
41
+ onShare,
42
+ ...props
43
+ }: ProductDetailProps) {
44
+ const [selectedImage, setSelectedImage] = useState(0)
45
+ const [quantity, setQuantity] = useState(1)
46
+ const [selectedVariant, setSelectedVariant] = useState(
47
+ product.variants?.[0]?.id || ''
48
+ )
49
+
50
+ const formatPrice = (price: number, currency = '$') => {
51
+ return `${currency}${price.toFixed(2)}`
52
+ }
53
+
54
+ const calculateDiscount = (price: number, originalPrice: number) => {
55
+ return Math.round(((originalPrice - price) / originalPrice) * 100)
56
+ }
57
+
58
+ return (
59
+ <div className={cn('container py-8', className)} {...props}>
60
+ <div className="grid gap-8 lg:grid-cols-2">
61
+ {/* Images */}
62
+ <div className="space-y-4">
63
+ <div className="aspect-square overflow-hidden rounded-lg">
64
+ <img
65
+ src={product.images[selectedImage]}
66
+ alt={product.name}
67
+ className="h-full w-full object-cover"
68
+ />
69
+ </div>
70
+ {product.images.length > 1 && (
71
+ <div className="grid grid-cols-4 gap-4">
72
+ {product.images.map((image, i) => (
73
+ <button
74
+ key={i}
75
+ onClick={() => setSelectedImage(i)}
76
+ className={cn(
77
+ 'aspect-square overflow-hidden rounded-lg border-2',
78
+ selectedImage === i ? 'border-primary' : 'border-transparent'
79
+ )}
80
+ >
81
+ <img
82
+ src={image}
83
+ alt=""
84
+ className="h-full w-full object-cover"
85
+ />
86
+ </button>
87
+ ))}
88
+ </div>
89
+ )}
90
+ </div>
91
+
92
+ {/* Product Info */}
93
+ <div className="space-y-6">
94
+ <div>
95
+ {product.badge && (
96
+ <Badge className="mb-2">{product.badge}</Badge>
97
+ )}
98
+ <h1 className="text-3xl font-bold">{product.name}</h1>
99
+ {product.rating !== undefined && (
100
+ <div className="mt-4 flex items-center gap-2">
101
+ <div className="flex items-center gap-1">
102
+ {Array.from({ length: 5 }).map((_, i) => (
103
+ <Star
104
+ key={i}
105
+ className={cn(
106
+ 'h-5 w-5',
107
+ i < Math.floor(product.rating!)
108
+ ? 'fill-primary text-primary'
109
+ : 'text-muted-foreground'
110
+ )}
111
+ />
112
+ ))}
113
+ </div>
114
+ <span className="font-medium">{product.rating.toFixed(1)}</span>
115
+ {product.reviewCount !== undefined && (
116
+ <span className="text-muted-foreground">
117
+ ({product.reviewCount} reviews)
118
+ </span>
119
+ )}
120
+ </div>
121
+ )}
122
+ </div>
123
+
124
+ <div className="space-y-2">
125
+ <div className="flex items-center gap-2">
126
+ <span className="text-3xl font-bold">
127
+ {formatPrice(product.price, product.currency)}
128
+ </span>
129
+ {product.originalPrice && product.originalPrice > product.price && (
130
+ <>
131
+ <span className="text-xl text-muted-foreground line-through">
132
+ {formatPrice(product.originalPrice, product.currency)}
133
+ </span>
134
+ <Badge variant="destructive">
135
+ -{calculateDiscount(product.price, product.originalPrice)}%
136
+ </Badge>
137
+ </>
138
+ )}
139
+ </div>
140
+ {product.inStock === false && (
141
+ <p className="text-sm text-destructive">Out of stock</p>
142
+ )}
143
+ </div>
144
+
145
+ <p className="text-muted-foreground">{product.description}</p>
146
+
147
+ {/* Variants */}
148
+ {product.variants && product.variants.length > 0 && (
149
+ <div className="space-y-2">
150
+ <label className="text-sm font-medium">
151
+ {product.variants[0].name}
152
+ </label>
153
+ <div className="flex flex-wrap gap-2">
154
+ {product.variants.map((variant) => (
155
+ <Button
156
+ key={variant.id}
157
+ variant={selectedVariant === variant.id ? 'default' : 'outline'}
158
+ size="sm"
159
+ onClick={() => setSelectedVariant(variant.id)}
160
+ >
161
+ {variant.value}
162
+ </Button>
163
+ ))}
164
+ </div>
165
+ </div>
166
+ )}
167
+
168
+ {/* Quantity and Actions */}
169
+ <div className="space-y-4">
170
+ <div className="flex items-center gap-4">
171
+ <label className="text-sm font-medium">Quantity:</label>
172
+ <div className="flex items-center gap-2">
173
+ <Button
174
+ size="icon"
175
+ variant="outline"
176
+ onClick={() => setQuantity(Math.max(1, quantity - 1))}
177
+ disabled={quantity <= 1}
178
+ >
179
+ -
180
+ </Button>
181
+ <span className="w-12 text-center">{quantity}</span>
182
+ <Button
183
+ size="icon"
184
+ variant="outline"
185
+ onClick={() => setQuantity(quantity + 1)}
186
+ >
187
+ +
188
+ </Button>
189
+ </div>
190
+ </div>
191
+
192
+ <div className="flex gap-4">
193
+ <Button
194
+ className="flex-1"
195
+ size="lg"
196
+ disabled={product.inStock === false}
197
+ onClick={() => onAddToCart?.(quantity, selectedVariant)}
198
+ >
199
+ <ShoppingCart className="mr-2 h-5 w-5" />
200
+ Add to Cart
201
+ </Button>
202
+ <Button
203
+ size="lg"
204
+ variant="outline"
205
+ onClick={onAddToWishlist}
206
+ >
207
+ <Heart className="h-5 w-5" />
208
+ </Button>
209
+ <Button
210
+ size="lg"
211
+ variant="outline"
212
+ onClick={onShare}
213
+ >
214
+ <Share2 className="h-5 w-5" />
215
+ </Button>
216
+ </div>
217
+ </div>
218
+
219
+ {/* Tabs */}
220
+ {(product.features || product.specifications) && (
221
+ <Tabs defaultValue="features" className="w-full">
222
+ <TabsList className="grid w-full grid-cols-2">
223
+ {product.features && <TabsTrigger value="features">Features</TabsTrigger>}
224
+ {product.specifications && (
225
+ <TabsTrigger value="specifications">Specifications</TabsTrigger>
226
+ )}
227
+ </TabsList>
228
+ {product.features && (
229
+ <TabsContent value="features" className="space-y-2">
230
+ <ul className="list-inside list-disc space-y-1">
231
+ {product.features.map((feature, i) => (
232
+ <li key={i} className="text-sm">
233
+ {feature}
234
+ </li>
235
+ ))}
236
+ </ul>
237
+ </TabsContent>
238
+ )}
239
+ {product.specifications && (
240
+ <TabsContent value="specifications">
241
+ <dl className="space-y-2">
242
+ {Object.entries(product.specifications).map(([key, value]) => (
243
+ <div key={key} className="flex gap-4 text-sm">
244
+ <dt className="min-w-[120px] font-medium">{key}:</dt>
245
+ <dd className="text-muted-foreground">{value}</dd>
246
+ </div>
247
+ ))}
248
+ </dl>
249
+ </TabsContent>
250
+ )}
251
+ </Tabs>
252
+ )}
253
+ </div>
254
+ </div>
255
+ </div>
256
+ )
257
+ }
@@ -0,0 +1,148 @@
1
+ 'use client'
2
+
3
+ import { cn } from '@hanzo/ui/util'
4
+ import { Button } from '@hanzo/ui/primitives'
5
+ import { Card, CardContent, CardFooter } from '@hanzo/ui/primitives'
6
+ import { Badge } from '@hanzo/ui/primitives'
7
+ import { Star, ShoppingCart } from 'lucide-react'
8
+
9
+ export interface Product {
10
+ id: string | number
11
+ name: string
12
+ description?: string
13
+ price: number
14
+ originalPrice?: number
15
+ currency?: string
16
+ image: string
17
+ rating?: number
18
+ reviewCount?: number
19
+ badge?: string
20
+ inStock?: boolean
21
+ }
22
+
23
+ interface ProductGridProps extends React.ComponentPropsWithoutRef<'section'> {
24
+ products: Product[]
25
+ columns?: 2 | 3 | 4
26
+ onAddToCart?: (product: Product) => void
27
+ onProductClick?: (product: Product) => void
28
+ }
29
+
30
+ export function ProductGrid({
31
+ className,
32
+ products,
33
+ columns = 4,
34
+ onAddToCart,
35
+ onProductClick,
36
+ ...props
37
+ }: ProductGridProps) {
38
+ const gridCols = {
39
+ 2: 'sm:grid-cols-2',
40
+ 3: 'sm:grid-cols-2 lg:grid-cols-3',
41
+ 4: 'sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
42
+ }[columns]
43
+
44
+ const formatPrice = (price: number, currency = '$') => {
45
+ return `${currency}${price.toFixed(2)}`
46
+ }
47
+
48
+ const calculateDiscount = (price: number, originalPrice: number) => {
49
+ return Math.round(((originalPrice - price) / originalPrice) * 100)
50
+ }
51
+
52
+ return (
53
+ <section className={cn('py-8', className)} {...props}>
54
+ <div className="container">
55
+ <div className={cn('grid gap-6', gridCols)}>
56
+ {products.map((product) => (
57
+ <Card
58
+ key={product.id}
59
+ className="group relative overflow-hidden"
60
+ >
61
+ {product.badge && (
62
+ <Badge className="absolute left-2 top-2 z-10">
63
+ {product.badge}
64
+ </Badge>
65
+ )}
66
+ {product.originalPrice && product.originalPrice > product.price && (
67
+ <Badge
68
+ variant="destructive"
69
+ className="absolute right-2 top-2 z-10"
70
+ >
71
+ -{calculateDiscount(product.price, product.originalPrice)}%
72
+ </Badge>
73
+ )}
74
+
75
+ <div
76
+ className="aspect-square cursor-pointer overflow-hidden"
77
+ onClick={() => onProductClick?.(product)}
78
+ >
79
+ <img
80
+ src={product.image}
81
+ alt={product.name}
82
+ className="h-full w-full object-cover transition-transform group-hover:scale-105"
83
+ />
84
+ </div>
85
+
86
+ <CardContent className="p-4">
87
+ <h3
88
+ className="cursor-pointer font-semibold hover:underline"
89
+ onClick={() => onProductClick?.(product)}
90
+ >
91
+ {product.name}
92
+ </h3>
93
+
94
+ {product.description && (
95
+ <p className="mt-1 line-clamp-2 text-sm text-muted-foreground">
96
+ {product.description}
97
+ </p>
98
+ )}
99
+
100
+ {product.rating !== undefined && (
101
+ <div className="mt-2 flex items-center gap-2">
102
+ <div className="flex items-center gap-1">
103
+ <Star className="h-4 w-4 fill-primary text-primary" />
104
+ <span className="text-sm font-medium">
105
+ {product.rating.toFixed(1)}
106
+ </span>
107
+ </div>
108
+ {product.reviewCount !== undefined && (
109
+ <span className="text-sm text-muted-foreground">
110
+ ({product.reviewCount})
111
+ </span>
112
+ )}
113
+ </div>
114
+ )}
115
+
116
+ <div className="mt-3 flex items-center gap-2">
117
+ <span className="text-lg font-bold">
118
+ {formatPrice(product.price, product.currency)}
119
+ </span>
120
+ {product.originalPrice && product.originalPrice > product.price && (
121
+ <span className="text-sm text-muted-foreground line-through">
122
+ {formatPrice(product.originalPrice, product.currency)}
123
+ </span>
124
+ )}
125
+ </div>
126
+
127
+ {product.inStock === false && (
128
+ <p className="mt-2 text-sm text-destructive">Out of stock</p>
129
+ )}
130
+ </CardContent>
131
+
132
+ <CardFooter className="p-4 pt-0">
133
+ <Button
134
+ className="w-full"
135
+ disabled={product.inStock === false}
136
+ onClick={() => onAddToCart?.(product)}
137
+ >
138
+ <ShoppingCart className="mr-2 h-4 w-4" />
139
+ Add to cart
140
+ </Button>
141
+ </CardFooter>
142
+ </Card>
143
+ ))}
144
+ </div>
145
+ </div>
146
+ </section>
147
+ )
148
+ }
@@ -0,0 +1,181 @@
1
+ 'use client'
2
+
3
+ import { cn } from '@hanzo/ui/util'
4
+ import { Button } from '@hanzo/ui/primitives'
5
+ import { Input } from '@hanzo/ui/primitives'
6
+ import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@hanzo/ui/primitives'
7
+ import { Separator } from '@hanzo/ui/primitives'
8
+ import { Trash2, Plus, Minus } from 'lucide-react'
9
+
10
+ export interface CartItem {
11
+ id: string | number
12
+ name: string
13
+ price: number
14
+ quantity: number
15
+ image: string
16
+ variant?: string
17
+ currency?: string
18
+ }
19
+
20
+ interface ShoppingCartProps extends React.ComponentPropsWithoutRef<'div'> {
21
+ items: CartItem[]
22
+ onUpdateQuantity?: (id: string | number, quantity: number) => void
23
+ onRemoveItem?: (id: string | number) => void
24
+ onCheckout?: () => void
25
+ shippingCost?: number
26
+ taxRate?: number
27
+ currency?: string
28
+ }
29
+
30
+ export function ShoppingCart({
31
+ className,
32
+ items,
33
+ onUpdateQuantity,
34
+ onRemoveItem,
35
+ onCheckout,
36
+ shippingCost = 0,
37
+ taxRate = 0,
38
+ currency = '$',
39
+ ...props
40
+ }: ShoppingCartProps) {
41
+ const formatPrice = (price: number) => {
42
+ return `${currency}${price.toFixed(2)}`
43
+ }
44
+
45
+ const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0)
46
+ const tax = subtotal * taxRate
47
+ const total = subtotal + tax + shippingCost
48
+
49
+ return (
50
+ <div className={cn('grid gap-6 lg:grid-cols-3', className)} {...props}>
51
+ <div className="lg:col-span-2">
52
+ <Card>
53
+ <CardHeader>
54
+ <CardTitle>Shopping Cart ({items.length} items)</CardTitle>
55
+ </CardHeader>
56
+ <CardContent className="p-0">
57
+ {items.length === 0 ? (
58
+ <div className="p-6 text-center text-muted-foreground">
59
+ Your cart is empty
60
+ </div>
61
+ ) : (
62
+ <div className="divide-y">
63
+ {items.map((item) => (
64
+ <div key={item.id} className="flex gap-4 p-6">
65
+ <div className="h-24 w-24 flex-shrink-0 overflow-hidden rounded-lg">
66
+ <img
67
+ src={item.image}
68
+ alt={item.name}
69
+ className="h-full w-full object-cover"
70
+ />
71
+ </div>
72
+ <div className="flex flex-1 flex-col">
73
+ <div className="flex justify-between">
74
+ <div>
75
+ <h3 className="font-semibold">{item.name}</h3>
76
+ {item.variant && (
77
+ <p className="mt-1 text-sm text-muted-foreground">
78
+ {item.variant}
79
+ </p>
80
+ )}
81
+ </div>
82
+ <p className="font-semibold">
83
+ {formatPrice(item.price * item.quantity)}
84
+ </p>
85
+ </div>
86
+ <div className="mt-4 flex items-center justify-between">
87
+ <div className="flex items-center gap-2">
88
+ <Button
89
+ size="icon"
90
+ variant="outline"
91
+ className="h-8 w-8"
92
+ onClick={() =>
93
+ onUpdateQuantity?.(item.id, Math.max(1, item.quantity - 1))
94
+ }
95
+ disabled={item.quantity <= 1}
96
+ >
97
+ <Minus className="h-4 w-4" />
98
+ </Button>
99
+ <Input
100
+ type="number"
101
+ value={item.quantity}
102
+ onChange={(e) => {
103
+ const value = parseInt(e.target.value, 10)
104
+ if (!isNaN(value) && value > 0) {
105
+ onUpdateQuantity?.(item.id, value)
106
+ }
107
+ }}
108
+ className="h-8 w-16 text-center"
109
+ min="1"
110
+ />
111
+ <Button
112
+ size="icon"
113
+ variant="outline"
114
+ className="h-8 w-8"
115
+ onClick={() =>
116
+ onUpdateQuantity?.(item.id, item.quantity + 1)
117
+ }
118
+ >
119
+ <Plus className="h-4 w-4" />
120
+ </Button>
121
+ </div>
122
+ <Button
123
+ size="sm"
124
+ variant="ghost"
125
+ onClick={() => onRemoveItem?.(item.id)}
126
+ >
127
+ <Trash2 className="h-4 w-4" />
128
+ </Button>
129
+ </div>
130
+ </div>
131
+ </div>
132
+ ))}
133
+ </div>
134
+ )}
135
+ </CardContent>
136
+ </Card>
137
+ </div>
138
+
139
+ <div>
140
+ <Card>
141
+ <CardHeader>
142
+ <CardTitle>Order Summary</CardTitle>
143
+ </CardHeader>
144
+ <CardContent className="space-y-4">
145
+ <div className="flex justify-between">
146
+ <span>Subtotal</span>
147
+ <span>{formatPrice(subtotal)}</span>
148
+ </div>
149
+ {shippingCost > 0 && (
150
+ <div className="flex justify-between">
151
+ <span>Shipping</span>
152
+ <span>{formatPrice(shippingCost)}</span>
153
+ </div>
154
+ )}
155
+ {taxRate > 0 && (
156
+ <div className="flex justify-between">
157
+ <span>Tax</span>
158
+ <span>{formatPrice(tax)}</span>
159
+ </div>
160
+ )}
161
+ <Separator />
162
+ <div className="flex justify-between text-lg font-semibold">
163
+ <span>Total</span>
164
+ <span>{formatPrice(total)}</span>
165
+ </div>
166
+ </CardContent>
167
+ <CardFooter>
168
+ <Button
169
+ className="w-full"
170
+ size="lg"
171
+ onClick={onCheckout}
172
+ disabled={items.length === 0}
173
+ >
174
+ Proceed to Checkout
175
+ </Button>
176
+ </CardFooter>
177
+ </Card>
178
+ </div>
179
+ </div>
180
+ )
181
+ }