@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,235 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import { cn } from '@hanzo/ui/util'
5
+ import {
6
+ Table,
7
+ TableBody,
8
+ TableCell,
9
+ TableHead,
10
+ TableHeader,
11
+ TableRow,
12
+ } from '@hanzo/ui/primitives'
13
+ import { Button } from '@hanzo/ui/primitives'
14
+ import { Input } from '@hanzo/ui/primitives'
15
+ import {
16
+ Select,
17
+ SelectContent,
18
+ SelectItem,
19
+ SelectTrigger,
20
+ SelectValue,
21
+ } from '@hanzo/ui/primitives'
22
+ import { ChevronUp, ChevronDown, Search } from 'lucide-react'
23
+
24
+ interface Column<T> {
25
+ key: string
26
+ header: string
27
+ accessor: (row: T) => any
28
+ sortable?: boolean
29
+ searchable?: boolean
30
+ render?: (value: any, row: T) => React.ReactNode
31
+ }
32
+
33
+ interface DataTableProps<T> extends React.ComponentPropsWithoutRef<'div'> {
34
+ data: T[]
35
+ columns: Column<T>[]
36
+ pageSize?: number
37
+ searchable?: boolean
38
+ title?: string
39
+ description?: string
40
+ onRowClick?: (row: T) => void
41
+ }
42
+
43
+ export function DataTable<T>({
44
+ className,
45
+ data,
46
+ columns,
47
+ pageSize = 10,
48
+ searchable = true,
49
+ title,
50
+ description,
51
+ onRowClick,
52
+ ...props
53
+ }: DataTableProps<T>) {
54
+ const [currentPage, setCurrentPage] = useState(1)
55
+ const [searchTerm, setSearchTerm] = useState('')
56
+ const [sortColumn, setSortColumn] = useState<string | null>(null)
57
+ const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc')
58
+ const [itemsPerPage, setItemsPerPage] = useState(pageSize)
59
+
60
+ // Filter data based on search
61
+ const filteredData = searchTerm
62
+ ? data.filter((row) =>
63
+ columns.some((column) => {
64
+ if (column.searchable === false) return false
65
+ const value = column.accessor(row)
66
+ return value?.toString().toLowerCase().includes(searchTerm.toLowerCase())
67
+ })
68
+ )
69
+ : data
70
+
71
+ // Sort data
72
+ const sortedData = [...filteredData].sort((a, b) => {
73
+ if (!sortColumn) return 0
74
+
75
+ const column = columns.find((col) => col.key === sortColumn)
76
+ if (!column) return 0
77
+
78
+ const aValue = column.accessor(a)
79
+ const bValue = column.accessor(b)
80
+
81
+ if (aValue === bValue) return 0
82
+
83
+ const comparison = aValue < bValue ? -1 : 1
84
+ return sortDirection === 'asc' ? comparison : -comparison
85
+ })
86
+
87
+ // Paginate data
88
+ const totalPages = Math.ceil(sortedData.length / itemsPerPage)
89
+ const startIndex = (currentPage - 1) * itemsPerPage
90
+ const paginatedData = sortedData.slice(startIndex, startIndex + itemsPerPage)
91
+
92
+ const handleSort = (columnKey: string) => {
93
+ if (sortColumn === columnKey) {
94
+ setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc')
95
+ } else {
96
+ setSortColumn(columnKey)
97
+ setSortDirection('asc')
98
+ }
99
+ }
100
+
101
+ return (
102
+ <div className={cn('space-y-4', className)} {...props}>
103
+ {(title || description || searchable) && (
104
+ <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
105
+ <div>
106
+ {title && <h3 className="text-lg font-semibold">{title}</h3>}
107
+ {description && (
108
+ <p className="text-sm text-muted-foreground">{description}</p>
109
+ )}
110
+ </div>
111
+ {searchable && (
112
+ <div className="relative w-full sm:w-64">
113
+ <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
114
+ <Input
115
+ placeholder="Search..."
116
+ value={searchTerm}
117
+ onChange={(e) => {
118
+ setSearchTerm(e.target.value)
119
+ setCurrentPage(1)
120
+ }}
121
+ className="pl-8"
122
+ />
123
+ </div>
124
+ )}
125
+ </div>
126
+ )}
127
+
128
+ <div className="rounded-md border">
129
+ <Table>
130
+ <TableHeader>
131
+ <TableRow>
132
+ {columns.map((column) => (
133
+ <TableHead
134
+ key={column.key}
135
+ className={column.sortable ? 'cursor-pointer select-none' : ''}
136
+ onClick={() => column.sortable && handleSort(column.key)}
137
+ >
138
+ <div className="flex items-center gap-2">
139
+ {column.header}
140
+ {column.sortable && sortColumn === column.key && (
141
+ <>
142
+ {sortDirection === 'asc' ? (
143
+ <ChevronUp className="h-4 w-4" />
144
+ ) : (
145
+ <ChevronDown className="h-4 w-4" />
146
+ )}
147
+ </>
148
+ )}
149
+ </div>
150
+ </TableHead>
151
+ ))}
152
+ </TableRow>
153
+ </TableHeader>
154
+ <TableBody>
155
+ {paginatedData.length === 0 ? (
156
+ <TableRow>
157
+ <TableCell
158
+ colSpan={columns.length}
159
+ className="h-24 text-center"
160
+ >
161
+ No data available
162
+ </TableCell>
163
+ </TableRow>
164
+ ) : (
165
+ paginatedData.map((row, i) => (
166
+ <TableRow
167
+ key={i}
168
+ className={onRowClick ? 'cursor-pointer' : ''}
169
+ onClick={() => onRowClick?.(row)}
170
+ >
171
+ {columns.map((column) => (
172
+ <TableCell key={column.key}>
173
+ {column.render
174
+ ? column.render(column.accessor(row), row)
175
+ : column.accessor(row)}
176
+ </TableCell>
177
+ ))}
178
+ </TableRow>
179
+ ))
180
+ )}
181
+ </TableBody>
182
+ </Table>
183
+ </div>
184
+
185
+ {totalPages > 1 && (
186
+ <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
187
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
188
+ <span>Rows per page:</span>
189
+ <Select
190
+ value={itemsPerPage.toString()}
191
+ onValueChange={(value) => {
192
+ setItemsPerPage(parseInt(value))
193
+ setCurrentPage(1)
194
+ }}
195
+ >
196
+ <SelectTrigger className="h-8 w-16">
197
+ <SelectValue />
198
+ </SelectTrigger>
199
+ <SelectContent>
200
+ <SelectItem value="5">5</SelectItem>
201
+ <SelectItem value="10">10</SelectItem>
202
+ <SelectItem value="25">25</SelectItem>
203
+ <SelectItem value="50">50</SelectItem>
204
+ </SelectContent>
205
+ </Select>
206
+ </div>
207
+
208
+ <div className="flex items-center gap-2">
209
+ <span className="text-sm text-muted-foreground">
210
+ Showing {startIndex + 1}-{Math.min(startIndex + itemsPerPage, sortedData.length)} of {sortedData.length}
211
+ </span>
212
+ <div className="flex gap-1">
213
+ <Button
214
+ variant="outline"
215
+ size="sm"
216
+ onClick={() => setCurrentPage(currentPage - 1)}
217
+ disabled={currentPage === 1}
218
+ >
219
+ Previous
220
+ </Button>
221
+ <Button
222
+ variant="outline"
223
+ size="sm"
224
+ onClick={() => setCurrentPage(currentPage + 1)}
225
+ disabled={currentPage === totalPages}
226
+ >
227
+ Next
228
+ </Button>
229
+ </div>
230
+ </div>
231
+ </div>
232
+ )}
233
+ </div>
234
+ )
235
+ }
@@ -0,0 +1,194 @@
1
+ 'use client'
2
+
3
+ import { cn } from '@hanzo/ui/util'
4
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@hanzo/ui/primitives'
5
+ import type { LucideIcon } from 'lucide-react'
6
+ import { ArrowUp, ArrowDown, Minus } from 'lucide-react'
7
+
8
+ interface Stat {
9
+ title: string
10
+ value: string | number
11
+ description?: string
12
+ change?: number
13
+ changeLabel?: string
14
+ icon?: LucideIcon
15
+ trend?: 'up' | 'down' | 'neutral'
16
+ }
17
+
18
+ interface StatsGridProps extends React.ComponentPropsWithoutRef<'div'> {
19
+ stats: Stat[]
20
+ columns?: 2 | 3 | 4
21
+ variant?: 'default' | 'compact' | 'detailed'
22
+ }
23
+
24
+ export function StatsGrid({
25
+ className,
26
+ stats,
27
+ columns = 4,
28
+ variant = 'default',
29
+ ...props
30
+ }: StatsGridProps) {
31
+ const gridCols = {
32
+ 2: 'sm:grid-cols-2',
33
+ 3: 'sm:grid-cols-2 lg:grid-cols-3',
34
+ 4: 'sm:grid-cols-2 lg:grid-cols-4',
35
+ }[columns]
36
+
37
+ const getTrendIcon = (trend?: 'up' | 'down' | 'neutral') => {
38
+ switch (trend) {
39
+ case 'up':
40
+ return <ArrowUp className="h-4 w-4 text-green-500" />
41
+ case 'down':
42
+ return <ArrowDown className="h-4 w-4 text-red-500" />
43
+ case 'neutral':
44
+ return <Minus className="h-4 w-4 text-muted-foreground" />
45
+ default:
46
+ return null
47
+ }
48
+ }
49
+
50
+ const getTrendColor = (change?: number) => {
51
+ if (!change) return 'text-muted-foreground'
52
+ return change > 0 ? 'text-green-500' : 'text-red-500'
53
+ }
54
+
55
+ if (variant === 'compact') {
56
+ return (
57
+ <div className={cn('grid gap-4', gridCols, className)} {...props}>
58
+ {stats.map((stat, i) => {
59
+ const Icon = stat.icon
60
+ return (
61
+ <Card key={i}>
62
+ <CardHeader className="pb-2">
63
+ <CardDescription className="flex items-center justify-between">
64
+ {stat.title}
65
+ {Icon && <Icon className="h-4 w-4 text-muted-foreground" />}
66
+ </CardDescription>
67
+ </CardHeader>
68
+ <CardContent>
69
+ <div className="flex items-baseline justify-between">
70
+ <div className="text-2xl font-bold">{stat.value}</div>
71
+ {(stat.change !== undefined || stat.trend) && (
72
+ <div className="flex items-center gap-1">
73
+ {getTrendIcon(stat.trend)}
74
+ {stat.change !== undefined && (
75
+ <span className={cn('text-xs', getTrendColor(stat.change))}>
76
+ {stat.change > 0 && '+'}
77
+ {stat.change}%
78
+ </span>
79
+ )}
80
+ </div>
81
+ )}
82
+ </div>
83
+ {stat.changeLabel && (
84
+ <p className="mt-1 text-xs text-muted-foreground">
85
+ {stat.changeLabel}
86
+ </p>
87
+ )}
88
+ </CardContent>
89
+ </Card>
90
+ )
91
+ })}
92
+ </div>
93
+ )
94
+ }
95
+
96
+ if (variant === 'detailed') {
97
+ return (
98
+ <div className={cn('grid gap-6', gridCols, className)} {...props}>
99
+ {stats.map((stat, i) => {
100
+ const Icon = stat.icon
101
+ return (
102
+ <Card key={i}>
103
+ <CardHeader>
104
+ <div className="flex items-center justify-between">
105
+ <CardTitle className="text-sm font-medium">
106
+ {stat.title}
107
+ </CardTitle>
108
+ {Icon && (
109
+ <div className="rounded-lg bg-primary/10 p-2">
110
+ <Icon className="h-5 w-5 text-primary" />
111
+ </div>
112
+ )}
113
+ </div>
114
+ </CardHeader>
115
+ <CardContent>
116
+ <div className="space-y-1">
117
+ <div className="text-3xl font-bold">{stat.value}</div>
118
+ {stat.description && (
119
+ <CardDescription>{stat.description}</CardDescription>
120
+ )}
121
+ {(stat.change !== undefined || stat.trend || stat.changeLabel) && (
122
+ <div className="flex items-center gap-2 pt-2">
123
+ <div className="flex items-center gap-1">
124
+ {getTrendIcon(stat.trend)}
125
+ {stat.change !== undefined && (
126
+ <span
127
+ className={cn(
128
+ 'text-sm font-medium',
129
+ getTrendColor(stat.change)
130
+ )}
131
+ >
132
+ {stat.change > 0 && '+'}
133
+ {stat.change}%
134
+ </span>
135
+ )}
136
+ </div>
137
+ {stat.changeLabel && (
138
+ <span className="text-sm text-muted-foreground">
139
+ {stat.changeLabel}
140
+ </span>
141
+ )}
142
+ </div>
143
+ )}
144
+ </div>
145
+ </CardContent>
146
+ </Card>
147
+ )
148
+ })}
149
+ </div>
150
+ )
151
+ }
152
+
153
+ // Default variant
154
+ return (
155
+ <div className={cn('grid gap-4', gridCols, className)} {...props}>
156
+ {stats.map((stat, i) => {
157
+ const Icon = stat.icon
158
+ return (
159
+ <Card key={i}>
160
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
161
+ <CardTitle className="text-sm font-medium">
162
+ {stat.title}
163
+ </CardTitle>
164
+ {Icon && <Icon className="h-4 w-4 text-muted-foreground" />}
165
+ </CardHeader>
166
+ <CardContent>
167
+ <div className="text-2xl font-bold">{stat.value}</div>
168
+ {stat.description && (
169
+ <p className="text-xs text-muted-foreground">
170
+ {stat.description}
171
+ </p>
172
+ )}
173
+ {(stat.change !== undefined || stat.changeLabel) && (
174
+ <div className="mt-2 flex items-center text-xs">
175
+ {stat.change !== undefined && (
176
+ <span className={getTrendColor(stat.change)}>
177
+ {stat.change > 0 && '+'}
178
+ {stat.change}%
179
+ </span>
180
+ )}
181
+ {stat.changeLabel && (
182
+ <span className="ml-1 text-muted-foreground">
183
+ {stat.changeLabel}
184
+ </span>
185
+ )}
186
+ </div>
187
+ )}
188
+ </CardContent>
189
+ </Card>
190
+ )
191
+ })}
192
+ </div>
193
+ )
194
+ }
@@ -0,0 +1,242 @@
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 { Label } from '@hanzo/ui/primitives'
7
+ import { RadioGroup, RadioGroupItem } from '@hanzo/ui/primitives'
8
+ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@hanzo/ui/primitives'
9
+ import { Separator } from '@hanzo/ui/primitives'
10
+ import { Checkbox } from '@hanzo/ui/primitives'
11
+
12
+ interface CheckoutProps extends React.ComponentPropsWithoutRef<'div'> {
13
+ orderSummary: {
14
+ items: Array<{
15
+ name: string
16
+ quantity: number
17
+ price: number
18
+ }>
19
+ subtotal: number
20
+ shipping: number
21
+ tax: number
22
+ total: number
23
+ currency?: string
24
+ }
25
+ onSubmit?: (data: any) => void
26
+ onBack?: () => void
27
+ }
28
+
29
+ export function Checkout({
30
+ className,
31
+ orderSummary,
32
+ onSubmit,
33
+ onBack,
34
+ ...props
35
+ }: CheckoutProps) {
36
+ const formatPrice = (price: number, currency = '$') => {
37
+ return `${currency}${price.toFixed(2)}`
38
+ }
39
+
40
+ const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
41
+ e.preventDefault()
42
+ const formData = new FormData(e.currentTarget)
43
+ const data = Object.fromEntries(formData.entries())
44
+ onSubmit?.(data)
45
+ }
46
+
47
+ return (
48
+ <div className={cn('container py-8', className)} {...props}>
49
+ <form onSubmit={handleSubmit} className="grid gap-8 lg:grid-cols-3">
50
+ <div className="lg:col-span-2 space-y-6">
51
+ {/* Shipping Information */}
52
+ <Card>
53
+ <CardHeader>
54
+ <CardTitle>Shipping Information</CardTitle>
55
+ <CardDescription>
56
+ Enter your shipping address details
57
+ </CardDescription>
58
+ </CardHeader>
59
+ <CardContent className="grid gap-4">
60
+ <div className="grid gap-4 sm:grid-cols-2">
61
+ <div className="grid gap-2">
62
+ <Label htmlFor="firstName">First Name</Label>
63
+ <Input id="firstName" name="firstName" required />
64
+ </div>
65
+ <div className="grid gap-2">
66
+ <Label htmlFor="lastName">Last Name</Label>
67
+ <Input id="lastName" name="lastName" required />
68
+ </div>
69
+ </div>
70
+ <div className="grid gap-2">
71
+ <Label htmlFor="email">Email</Label>
72
+ <Input id="email" name="email" type="email" required />
73
+ </div>
74
+ <div className="grid gap-2">
75
+ <Label htmlFor="address">Street Address</Label>
76
+ <Input id="address" name="address" required />
77
+ </div>
78
+ <div className="grid gap-2">
79
+ <Label htmlFor="address2">Apartment, suite, etc. (optional)</Label>
80
+ <Input id="address2" name="address2" />
81
+ </div>
82
+ <div className="grid gap-4 sm:grid-cols-3">
83
+ <div className="grid gap-2">
84
+ <Label htmlFor="city">City</Label>
85
+ <Input id="city" name="city" required />
86
+ </div>
87
+ <div className="grid gap-2">
88
+ <Label htmlFor="state">State / Province</Label>
89
+ <Input id="state" name="state" required />
90
+ </div>
91
+ <div className="grid gap-2">
92
+ <Label htmlFor="zip">ZIP / Postal Code</Label>
93
+ <Input id="zip" name="zip" required />
94
+ </div>
95
+ </div>
96
+ <div className="grid gap-2">
97
+ <Label htmlFor="phone">Phone Number</Label>
98
+ <Input id="phone" name="phone" type="tel" />
99
+ </div>
100
+ </CardContent>
101
+ </Card>
102
+
103
+ {/* Payment Method */}
104
+ <Card>
105
+ <CardHeader>
106
+ <CardTitle>Payment Method</CardTitle>
107
+ <CardDescription>
108
+ Select your payment method
109
+ </CardDescription>
110
+ </CardHeader>
111
+ <CardContent className="grid gap-6">
112
+ <RadioGroup defaultValue="card" name="paymentMethod">
113
+ <div className="flex items-center space-x-2">
114
+ <RadioGroupItem value="card" id="card" />
115
+ <Label htmlFor="card">Credit Card</Label>
116
+ </div>
117
+ <div className="flex items-center space-x-2">
118
+ <RadioGroupItem value="paypal" id="paypal" />
119
+ <Label htmlFor="paypal">PayPal</Label>
120
+ </div>
121
+ <div className="flex items-center space-x-2">
122
+ <RadioGroupItem value="crypto" id="crypto" />
123
+ <Label htmlFor="crypto">Cryptocurrency</Label>
124
+ </div>
125
+ </RadioGroup>
126
+
127
+ <div className="grid gap-4">
128
+ <div className="grid gap-2">
129
+ <Label htmlFor="cardNumber">Card Number</Label>
130
+ <Input
131
+ id="cardNumber"
132
+ name="cardNumber"
133
+ placeholder="1234 5678 9012 3456"
134
+ />
135
+ </div>
136
+ <div className="grid gap-4 sm:grid-cols-3">
137
+ <div className="grid gap-2 sm:col-span-2">
138
+ <Label htmlFor="expiry">Expiry Date</Label>
139
+ <Input
140
+ id="expiry"
141
+ name="expiry"
142
+ placeholder="MM/YY"
143
+ />
144
+ </div>
145
+ <div className="grid gap-2">
146
+ <Label htmlFor="cvc">CVC</Label>
147
+ <Input
148
+ id="cvc"
149
+ name="cvc"
150
+ placeholder="123"
151
+ />
152
+ </div>
153
+ </div>
154
+ </div>
155
+ </CardContent>
156
+ </Card>
157
+
158
+ {/* Billing Address */}
159
+ <Card>
160
+ <CardHeader>
161
+ <CardTitle>Billing Address</CardTitle>
162
+ </CardHeader>
163
+ <CardContent>
164
+ <div className="flex items-center space-x-2">
165
+ <Checkbox id="sameAsShipping" name="sameAsShipping" defaultChecked />
166
+ <label
167
+ htmlFor="sameAsShipping"
168
+ className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
169
+ >
170
+ Same as shipping address
171
+ </label>
172
+ </div>
173
+ </CardContent>
174
+ </Card>
175
+ </div>
176
+
177
+ {/* Order Summary */}
178
+ <div>
179
+ <Card>
180
+ <CardHeader>
181
+ <CardTitle>Order Summary</CardTitle>
182
+ </CardHeader>
183
+ <CardContent className="space-y-4">
184
+ <div className="space-y-2">
185
+ {orderSummary.items.map((item, i) => (
186
+ <div key={i} className="flex justify-between text-sm">
187
+ <span className="text-muted-foreground">
188
+ {item.name} x {item.quantity}
189
+ </span>
190
+ <span>
191
+ {formatPrice(item.price * item.quantity, orderSummary.currency)}
192
+ </span>
193
+ </div>
194
+ ))}
195
+ </div>
196
+ <Separator />
197
+ <div className="space-y-2">
198
+ <div className="flex justify-between">
199
+ <span>Subtotal</span>
200
+ <span>{formatPrice(orderSummary.subtotal, orderSummary.currency)}</span>
201
+ </div>
202
+ <div className="flex justify-between">
203
+ <span>Shipping</span>
204
+ <span>{formatPrice(orderSummary.shipping, orderSummary.currency)}</span>
205
+ </div>
206
+ <div className="flex justify-between">
207
+ <span>Tax</span>
208
+ <span>{formatPrice(orderSummary.tax, orderSummary.currency)}</span>
209
+ </div>
210
+ </div>
211
+ <Separator />
212
+ <div className="flex justify-between text-lg font-semibold">
213
+ <span>Total</span>
214
+ <span>{formatPrice(orderSummary.total, orderSummary.currency)}</span>
215
+ </div>
216
+ </CardContent>
217
+ <CardFooter className="flex-col gap-2">
218
+ <Button type="submit" className="w-full" size="lg">
219
+ Complete Order
220
+ </Button>
221
+ {onBack && (
222
+ <Button
223
+ type="button"
224
+ variant="outline"
225
+ className="w-full"
226
+ onClick={onBack}
227
+ >
228
+ Back to Cart
229
+ </Button>
230
+ )}
231
+ </CardFooter>
232
+ </Card>
233
+
234
+ <div className="mt-4 text-xs text-center text-muted-foreground">
235
+ Your payment information is encrypted and secure.
236
+ We never store your credit card details.
237
+ </div>
238
+ </div>
239
+ </form>
240
+ </div>
241
+ )
242
+ }
@@ -0,0 +1,7 @@
1
+ export { ProductGrid } from './product-grid'
2
+ export { ProductDetail } from './product-detail'
3
+ export { ShoppingCart } from './shopping-cart'
4
+ export { Checkout } from './checkout'
5
+
6
+ export type { Product } from './product-grid'
7
+ export type { CartItem } from './shopping-cart'