@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.
- package/assets/general.tsx +1 -1
- package/assets/hanzo-logo.tsx +3 -1
- package/assets/index.ts +119 -5
- package/blocks/auth/index.ts +6 -0
- package/blocks/auth/login-2fa.tsx +165 -0
- package/blocks/auth/login-basic.tsx +94 -0
- package/blocks/auth/login-social.tsx +148 -0
- package/blocks/auth/magic-link.tsx +129 -0
- package/blocks/auth/password-reset.tsx +97 -0
- package/blocks/auth/signup.tsx +157 -0
- package/blocks/data-display/activity-feed.tsx +242 -0
- package/blocks/data-display/data-table.tsx +235 -0
- package/blocks/data-display/stats-grid.tsx +194 -0
- package/blocks/ecommerce/checkout.tsx +242 -0
- package/blocks/ecommerce/index.ts +7 -0
- package/blocks/ecommerce/product-detail.tsx +257 -0
- package/blocks/ecommerce/product-grid.tsx +148 -0
- package/blocks/ecommerce/shopping-cart.tsx +181 -0
- package/blocks/marketing/cta-section.tsx +207 -0
- package/blocks/marketing/faq.tsx +159 -0
- package/blocks/marketing/features-grid.tsx +156 -0
- package/blocks/marketing/hero-section.tsx +192 -0
- package/blocks/marketing/index.ts +6 -0
- package/blocks/marketing/pricing-table.tsx +121 -0
- package/blocks/marketing/testimonials.tsx +196 -0
- package/components/index.ts +4 -51
- package/dist/index.js +9351 -0
- package/dist/index.mjs +9340 -0
- package/dist/lib/utils.js +47 -0
- package/dist/lib/utils.mjs +28 -0
- package/dist/src/utils.js +47 -0
- package/dist/src/utils.mjs +28 -0
- package/dist/tailwind/index.js +2050 -0
- package/dist/tailwind/index.mjs +2019 -0
- package/dist/types/index.js +79 -0
- package/dist/types/index.mjs +56 -0
- package/dist/util/format-text.js +51 -0
- package/dist/util/format-text.mjs +32 -0
- package/dist/util/index.js +384 -0
- package/dist/util/index.mjs +363 -0
- package/frameworks/core/index.ts +6 -0
- package/frameworks/core/utils/index.ts +64 -0
- package/frameworks/react/components/button.tsx +26 -0
- package/frameworks/react/components/index.ts +5 -0
- package/frameworks/react/hooks/index.ts +5 -0
- package/frameworks/react/index.ts +9 -0
- package/frameworks/react/package.json +8 -0
- package/frameworks/react/utils/index.ts +2 -0
- package/frameworks/react-native/index.ts +9 -0
- package/frameworks/react-native/package.json +8 -0
- package/frameworks/registry.json +371 -0
- package/frameworks/setup.sh +69 -0
- package/frameworks/svelte/index.ts +9 -0
- package/frameworks/svelte/package.json +8 -0
- package/frameworks/tracker.json +1854 -0
- package/frameworks/vue/index.ts +9 -0
- package/frameworks/vue/package.json +8 -0
- package/package.json +192 -28
- package/primitives/accordion.tsx +1 -1
- package/primitives/alert-dialog.tsx +1 -1
- package/primitives/alert.tsx +1 -1
- package/primitives/avatar.tsx +1 -1
- package/primitives/badge.tsx +2 -1
- package/primitives/breadcrumb.tsx +1 -1
- package/primitives/button.tsx +37 -47
- package/primitives/card.tsx +1 -1
- package/primitives/carousel.tsx +3 -2
- package/primitives/chat/chat-input-area.tsx +5 -4
- package/primitives/chat/chat-input.tsx +2 -2
- package/primitives/chat/files-preview.tsx +5 -4
- package/primitives/chat/message-list.tsx +2 -1
- package/primitives/chat/sqlite-preview.tsx +8 -8
- package/primitives/checkbox.tsx +2 -1
- package/primitives/command.tsx +3 -1
- package/primitives/context-menu.tsx +1 -1
- package/primitives/dialog.tsx +6 -1
- package/primitives/drawer.tsx +4 -1
- package/primitives/dropdown-menu.tsx +1 -1
- package/primitives/file-uploader.tsx +4 -2
- package/primitives/form.tsx +1 -1
- package/primitives/hover-card.tsx +1 -1
- package/primitives/icons/github.tsx +2 -2
- package/primitives/icons/youtube-logo.tsx +1 -1
- package/primitives/index-common.ts +7 -6
- package/primitives/input-otp.tsx +1 -1
- package/primitives/input.tsx +2 -1
- package/primitives/label.tsx +2 -1
- package/primitives/markdown-preview.tsx +3 -0
- package/primitives/mermaid.tsx +13 -18
- package/primitives/next/image.tsx +2 -1
- package/primitives/next/inline-icon.tsx +14 -14
- package/primitives/next/media-stack.tsx +2 -19
- package/primitives/pagination.tsx +1 -1
- package/primitives/popover.tsx +4 -2
- package/primitives/progress.tsx +2 -1
- package/primitives/prompt-textarea.tsx +1 -1
- package/primitives/radio-group.tsx +1 -1
- package/primitives/scroll-area.tsx +1 -1
- package/primitives/search-input.tsx +1 -1
- package/primitives/select.tsx +1 -1
- package/primitives/separator.tsx +2 -1
- package/primitives/sheet.tsx +1 -1
- package/primitives/skeleton.tsx +1 -0
- package/primitives/slider.tsx +2 -1
- package/primitives/stepper.tsx +1 -1
- package/primitives/switch.tsx +2 -1
- package/primitives/table.tsx +1 -1
- package/primitives/tabs.tsx +1 -1
- package/primitives/textarea.tsx +2 -1
- package/primitives/textfield.tsx +1 -0
- package/primitives/toggle-group.tsx +1 -1
- package/primitives/toggle.tsx +1 -1
- package/primitives/tooltip.tsx +1 -1
- package/src/hooks/use-copy-clipboard.ts +1 -1
- package/src/index-lean.ts +87 -0
- package/src/index.ts +54 -0
- package/src/registry/api.ts +1 -1
- package/src/utils.ts +19 -1
- package/tailwind/tailwind.config.hanzo-preset.js +7 -7
- package/tailwind/typo-plugin/index.js +1 -1
- package/types/animation-def.ts +1 -1
- package/types/index.ts +2 -1
- package/util/blob.ts +9 -4
- package/util/date.ts +2 -1
- package/util/format-text.ts +2 -1
- package/util/index.ts +103 -0
- package/util/spread-to-transform.ts +9 -8
- package/MCP-INSTRUCTIONS.md +0 -73
- package/README-MCP.md +0 -175
- package/dist/button.d.ts +0 -1
- package/dist/button.js +0 -1
- package/dist/hooks/index.d.ts +0 -7
- package/dist/hooks/index.js +0 -7
- package/dist/hooks/use-click-away.d.ts +0 -2
- package/dist/hooks/use-click-away.js +0 -23
- package/dist/hooks/use-combined-refs.d.ts +0 -3
- package/dist/hooks/use-combined-refs.js +0 -18
- package/dist/hooks/use-copy-clipboard.d.ts +0 -9
- package/dist/hooks/use-copy-clipboard.js +0 -21
- package/dist/hooks/use-debounce.d.ts +0 -1
- package/dist/hooks/use-debounce.js +0 -13
- package/dist/hooks/use-fill-ids.d.ts +0 -8
- package/dist/hooks/use-fill-ids.js +0 -20
- package/dist/hooks/use-map.d.ts +0 -1
- package/dist/hooks/use-map.js +0 -20
- package/dist/hooks/use-measure.d.ts +0 -8
- package/dist/hooks/use-measure.js +0 -25
- package/dist/hooks/use-reverse-video-playback.d.ts +0 -1
- package/dist/hooks/use-reverse-video-playback.js +0 -41
- package/dist/hooks/use-scroll-restoration.d.ts +0 -8
- package/dist/hooks/use-scroll-restoration.js +0 -36
- package/dist/mcp/enhanced-server.d.ts +0 -29
- package/dist/mcp/enhanced-server.js +0 -1128
- package/dist/mcp/index.d.ts +0 -28
- package/dist/mcp/index.js +0 -436
- package/dist/registry/api.d.ts +0 -37
- package/dist/registry/api.js +0 -129
- package/dist/registry/index.d.ts +0 -353
- package/dist/registry/index.js +0 -45
- package/dist/utils.d.ts +0 -1
- package/dist/utils.js +0 -1
- package/environment.d.ts +0 -6
- package/public/r/accordion.json +0 -11
- package/public/r/alert.json +0 -11
- package/public/r/avatar.json +0 -11
- package/public/r/badge.json +0 -11
- package/public/r/button.json +0 -11
- package/public/r/card.json +0 -11
- package/public/r/checkbox.json +0 -11
- package/public/r/default.json +0 -6
- package/public/r/dialog.json +0 -11
- package/public/r/input.json +0 -11
- package/public/r/label.json +0 -11
- package/public/r/new-york.json +0 -6
- package/public/r/popover.json +0 -11
- package/public/r/select.json +0 -11
- package/public/r/table.json +0 -11
- package/public/r/tabs.json +0 -11
- package/public/r/toast.json +0 -11
- package/registry.json +0 -184
- package/test/test-registry.js +0 -73
- package/test-imports.mjs +0 -19
- package/tsconfig.json +0 -22
- 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'
|