@hanzo/ui 4.7.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 (272) hide show
  1. package/assets/ai-icons.tsx +207 -0
  2. package/assets/crypto.tsx +33 -0
  3. package/assets/file-type-icon.tsx +66 -0
  4. package/assets/file.tsx +45 -0
  5. package/assets/general.tsx +2318 -0
  6. package/assets/hanzo-logo.svg +9 -0
  7. package/assets/hanzo-logo.tsx +17 -0
  8. package/assets/index.ts +122 -0
  9. package/assets/index.tsx +4 -0
  10. package/assets/llm-provider.tsx +1094 -0
  11. package/blocks/auth/index.ts +6 -0
  12. package/blocks/auth/login-2fa.tsx +165 -0
  13. package/blocks/auth/login-basic.tsx +94 -0
  14. package/blocks/auth/login-social.tsx +148 -0
  15. package/blocks/auth/magic-link.tsx +129 -0
  16. package/blocks/auth/password-reset.tsx +97 -0
  17. package/blocks/auth/signup.tsx +157 -0
  18. package/blocks/components/accordian-block.tsx +48 -0
  19. package/blocks/components/block-component-props.ts +11 -0
  20. package/blocks/components/bullet-cards-block.tsx +46 -0
  21. package/blocks/components/card-block/index.tsx +171 -0
  22. package/blocks/components/card-block/link-out-button.tsx +20 -0
  23. package/blocks/components/card-block/util.ts +28 -0
  24. package/blocks/components/carte-blanche-block/index.tsx +127 -0
  25. package/blocks/components/carte-blanche-block/variant-content-left.tsx +49 -0
  26. package/blocks/components/content.tsx +70 -0
  27. package/blocks/components/cta-block.tsx +115 -0
  28. package/blocks/components/enh-heading-block.tsx +204 -0
  29. package/blocks/components/grid-block/grid-block-mutator.ts +12 -0
  30. package/blocks/components/grid-block/index.tsx +83 -0
  31. package/blocks/components/grid-block/mutator-registry.ts +10 -0
  32. package/blocks/components/grid-block/table-borders.mutator.ts +47 -0
  33. package/blocks/components/group-block.tsx +83 -0
  34. package/blocks/components/heading-block.tsx +88 -0
  35. package/blocks/components/image-block.tsx +111 -0
  36. package/blocks/components/index.ts +30 -0
  37. package/blocks/components/screenful-block/content.tsx +123 -0
  38. package/blocks/components/screenful-block/index.tsx +107 -0
  39. package/blocks/components/screenful-block/poster-background.tsx +34 -0
  40. package/blocks/components/screenful-block/video-background.tsx +45 -0
  41. package/blocks/components/space-block.tsx +66 -0
  42. package/blocks/components/video-block.tsx +138 -0
  43. package/blocks/data-display/activity-feed.tsx +242 -0
  44. package/blocks/data-display/data-table.tsx +235 -0
  45. package/blocks/data-display/stats-grid.tsx +194 -0
  46. package/blocks/def/accordian-block.ts +14 -0
  47. package/blocks/def/block.ts +7 -0
  48. package/blocks/def/bullet-cards-block.ts +22 -0
  49. package/blocks/def/card-block.ts +22 -0
  50. package/blocks/def/carte-blanche-block.ts +21 -0
  51. package/blocks/def/cta-block.ts +19 -0
  52. package/blocks/def/element-block.ts +11 -0
  53. package/blocks/def/enh-heading-block.ts +44 -0
  54. package/blocks/def/grid-block.ts +16 -0
  55. package/blocks/def/group-block.ts +11 -0
  56. package/blocks/def/heading-block.ts +15 -0
  57. package/blocks/def/image-block.ts +31 -0
  58. package/blocks/def/index.ts +35 -0
  59. package/blocks/def/screenful-block.ts +54 -0
  60. package/blocks/def/space-block.ts +64 -0
  61. package/blocks/def/video-block.ts +9 -0
  62. package/blocks/ecommerce/checkout.tsx +242 -0
  63. package/blocks/ecommerce/index.ts +7 -0
  64. package/blocks/ecommerce/product-detail.tsx +257 -0
  65. package/blocks/ecommerce/product-grid.tsx +148 -0
  66. package/blocks/ecommerce/shopping-cart.tsx +181 -0
  67. package/blocks/index.ts +2 -0
  68. package/blocks/marketing/cta-section.tsx +207 -0
  69. package/blocks/marketing/faq.tsx +159 -0
  70. package/blocks/marketing/features-grid.tsx +156 -0
  71. package/blocks/marketing/hero-section.tsx +192 -0
  72. package/blocks/marketing/index.ts +6 -0
  73. package/blocks/marketing/pricing-table.tsx +121 -0
  74. package/blocks/marketing/testimonials.tsx +196 -0
  75. package/components/index.ts +9 -0
  76. package/dist/index.js +1407 -1514
  77. package/dist/index.mjs +1363 -1472
  78. package/dist/tailwind/index.js +3 -1
  79. package/dist/tailwind/index.mjs +3 -1
  80. package/dist/util/format-text.js +51 -0
  81. package/dist/util/format-text.mjs +32 -0
  82. package/dist/util/index.js +384 -0
  83. package/dist/util/index.mjs +363 -0
  84. package/frameworks/core/index.ts +6 -0
  85. package/frameworks/core/utils/index.ts +64 -0
  86. package/frameworks/react/components/button.tsx +26 -0
  87. package/frameworks/react/components/index.ts +5 -0
  88. package/frameworks/react/hooks/index.ts +5 -0
  89. package/frameworks/react/index.ts +9 -0
  90. package/frameworks/react/package.json +8 -0
  91. package/frameworks/react/utils/index.ts +2 -0
  92. package/frameworks/react-native/index.ts +9 -0
  93. package/frameworks/react-native/package.json +8 -0
  94. package/frameworks/registry.json +371 -0
  95. package/frameworks/setup.sh +69 -0
  96. package/frameworks/svelte/index.ts +9 -0
  97. package/frameworks/svelte/package.json +8 -0
  98. package/frameworks/tracker.json +1854 -0
  99. package/frameworks/vue/index.ts +9 -0
  100. package/frameworks/vue/package.json +8 -0
  101. package/helpers/file.ts +33 -0
  102. package/helpers/memoization.ts +40 -0
  103. package/package.json +49 -11
  104. package/primitives/accordion.tsx +74 -0
  105. package/primitives/action-button.tsx +42 -0
  106. package/primitives/alert-dialog.tsx +185 -0
  107. package/primitives/alert.tsx +74 -0
  108. package/primitives/apply-typography.tsx +55 -0
  109. package/primitives/aspect-ratio.tsx +5 -0
  110. package/primitives/avatar.tsx +57 -0
  111. package/primitives/background-beams.tsx +142 -0
  112. package/primitives/badge.tsx +45 -0
  113. package/primitives/breadcrumb.tsx +130 -0
  114. package/primitives/breakpoint-indicator.tsx +19 -0
  115. package/primitives/button.tsx +72 -0
  116. package/primitives/calendar.tsx +72 -0
  117. package/primitives/card.tsx +97 -0
  118. package/primitives/carousel.tsx +238 -0
  119. package/primitives/chat/chat-input-area.tsx +88 -0
  120. package/primitives/chat/chat-input.tsx +71 -0
  121. package/primitives/chat/files-preview.tsx +331 -0
  122. package/primitives/chat/index.ts +6 -0
  123. package/primitives/chat/json-form.tsx +8 -0
  124. package/primitives/chat/message-list.tsx +308 -0
  125. package/primitives/chat/message.tsx +569 -0
  126. package/primitives/chat/sqlite-preview.tsx +215 -0
  127. package/primitives/checkbox.tsx +32 -0
  128. package/primitives/collapsible.tsx +9 -0
  129. package/primitives/combobox.tsx +239 -0
  130. package/primitives/command.tsx +151 -0
  131. package/primitives/context-menu.tsx +206 -0
  132. package/primitives/copy-to-clipboard-icon.tsx +60 -0
  133. package/primitives/dialog-video-controller.tsx +38 -0
  134. package/primitives/dialog.tsx +128 -0
  135. package/primitives/dot-pattern.tsx +57 -0
  136. package/primitives/dots-loader.tsx +13 -0
  137. package/primitives/drawer.tsx +113 -0
  138. package/primitives/dropdown-menu.tsx +199 -0
  139. package/primitives/error-message.tsx +19 -0
  140. package/primitives/file-uploader.tsx +202 -0
  141. package/primitives/form.tsx +183 -0
  142. package/primitives/hover-card.tsx +28 -0
  143. package/primitives/icons/github.tsx +14 -0
  144. package/primitives/icons/index.ts +18 -0
  145. package/primitives/icons/youtube-logo.tsx +59 -0
  146. package/primitives/index-common.ts +304 -0
  147. package/primitives/index-next.ts +4 -0
  148. package/primitives/input-otp.tsx +65 -0
  149. package/primitives/input.tsx +126 -0
  150. package/primitives/label.tsx +21 -0
  151. package/primitives/list-adaptor.ts +12 -0
  152. package/primitives/list-box.tsx +74 -0
  153. package/primitives/loading-spinner.tsx +33 -0
  154. package/primitives/markdown-preview.tsx +612 -0
  155. package/primitives/mermaid.tsx +191 -0
  156. package/primitives/navigation-menu.tsx +147 -0
  157. package/primitives/next/image.tsx +91 -0
  158. package/primitives/next/index.ts +7 -0
  159. package/primitives/next/inline-icon.tsx +36 -0
  160. package/primitives/next/link-element.tsx +109 -0
  161. package/primitives/next/mdx-link.tsx +22 -0
  162. package/primitives/next/media-stack.tsx +52 -0
  163. package/primitives/next/nav-items.tsx +45 -0
  164. package/primitives/next/youtube-embed.tsx +83 -0
  165. package/primitives/pagination.tsx +117 -0
  166. package/primitives/popover.tsx +34 -0
  167. package/primitives/pretty-json-print.tsx +28 -0
  168. package/primitives/progress.tsx +27 -0
  169. package/primitives/prompt-textarea.tsx +72 -0
  170. package/primitives/qr-code.tsx +112 -0
  171. package/primitives/radio-group.tsx +42 -0
  172. package/primitives/resizable.tsx +47 -0
  173. package/primitives/scroll-area.tsx +57 -0
  174. package/primitives/search-input.tsx +66 -0
  175. package/primitives/select.tsx +122 -0
  176. package/primitives/separator.tsx +26 -0
  177. package/primitives/sheet.tsx +139 -0
  178. package/primitives/skeleton.tsx +18 -0
  179. package/primitives/slider.tsx +63 -0
  180. package/primitives/sonner.tsx +35 -0
  181. package/primitives/step-indicator.tsx +69 -0
  182. package/primitives/stepper.tsx +272 -0
  183. package/primitives/switch.tsx +27 -0
  184. package/primitives/table.tsx +105 -0
  185. package/primitives/tabs.tsx +50 -0
  186. package/primitives/text-area.tsx +26 -0
  187. package/primitives/text-link.tsx +25 -0
  188. package/primitives/textarea.tsx +62 -0
  189. package/primitives/textfield.tsx +76 -0
  190. package/primitives/toast.tsx +30 -0
  191. package/primitives/toggle-group.tsx +63 -0
  192. package/primitives/toggle.tsx +44 -0
  193. package/primitives/tooltip.tsx +47 -0
  194. package/primitives/video-player.tsx +23 -0
  195. package/src/button.ts +1 -0
  196. package/src/hooks/index.ts +7 -0
  197. package/src/hooks/use-click-away.ts +31 -0
  198. package/src/hooks/use-combined-refs.ts +22 -0
  199. package/src/hooks/use-copy-clipboard.ts +30 -0
  200. package/src/hooks/use-debounce.ts +17 -0
  201. package/src/hooks/use-fill-ids.ts +25 -0
  202. package/src/hooks/use-map.ts +26 -0
  203. package/src/hooks/use-measure.ts +42 -0
  204. package/src/hooks/use-reverse-video-playback.ts +43 -0
  205. package/src/hooks/use-scroll-restoration.ts +50 -0
  206. package/src/index-lean.ts +87 -0
  207. package/src/index.ts +54 -0
  208. package/src/mcp/README.md +141 -0
  209. package/src/mcp/enhanced-server.ts +1208 -0
  210. package/src/mcp/index.ts +518 -0
  211. package/src/mcp/package.json +10 -0
  212. package/src/registry/api.ts +164 -0
  213. package/src/registry/index.ts +60 -0
  214. package/src/registry/package.json +10 -0
  215. package/src/utils.ts +19 -0
  216. package/tailwind/colors.tailwind.js +53 -0
  217. package/tailwind/fontFamily.tailwind.ts +7 -0
  218. package/tailwind/fontSize.tailwind.ts +13 -0
  219. package/tailwind/index.ts +7 -0
  220. package/tailwind/safelist.tailwind.js +26 -0
  221. package/tailwind/screens.tailwind.js +8 -0
  222. package/tailwind/spacing.tailwind.js +65 -0
  223. package/tailwind/tailwind.config.hanzo-preset.d.ts +5 -0
  224. package/tailwind/tailwind.config.hanzo-preset.js +915 -0
  225. package/tailwind/tw-font-desc.ts +15 -0
  226. package/tailwind/typo-plugin/get-plugin-styles.js +679 -0
  227. package/tailwind/typo-plugin/index.d.ts +9 -0
  228. package/tailwind/typo-plugin/index.js +141 -0
  229. package/tailwind/typo-plugin/utils.js +60 -0
  230. package/tailwind/typography-test.mdx +35 -0
  231. package/tailwind/z-index.tailwind.js +71 -0
  232. package/types/animation-def.ts +3 -0
  233. package/types/breakpoints.ts +11 -0
  234. package/types/bullet-item.ts +10 -0
  235. package/types/button-def.ts +39 -0
  236. package/types/dimensions.ts +8 -0
  237. package/types/grid-def.ts +56 -0
  238. package/types/image-def.ts +32 -0
  239. package/types/index.ts +30 -0
  240. package/types/link-def.ts +56 -0
  241. package/types/media-stack-def.ts +31 -0
  242. package/types/t-shirt-size.ts +5 -0
  243. package/types/tshirt-dimensions.ts +20 -0
  244. package/types/video-def.ts +25 -0
  245. package/util/blob.ts +33 -0
  246. package/util/copy-to-clipboard.ts +17 -0
  247. package/util/create-shadow-root.ts +22 -0
  248. package/util/date.ts +84 -0
  249. package/util/debounce.ts +11 -0
  250. package/util/file.ts +15 -0
  251. package/util/format-and-abbreviate-as-currency.ts +125 -0
  252. package/util/format-text.ts +34 -0
  253. package/util/format-to-max-char.ts +68 -0
  254. package/util/index-client.ts +3 -0
  255. package/util/index.ts +112 -0
  256. package/util/number-abbreviate.ts +49 -0
  257. package/util/specifier.ts +43 -0
  258. package/util/spread-to-transform.ts +25 -0
  259. package/util/step-animation.ts +90 -0
  260. package/util/timing.ts +3 -0
  261. package/util/toasts.tsx +17 -0
  262. package/util/two-way-map.ts +19 -0
  263. package/dist/index.d.mts +0 -16
  264. package/dist/index.d.ts +0 -16
  265. package/dist/lib/utils.d.mts +0 -2
  266. package/dist/lib/utils.d.ts +0 -2
  267. package/dist/src/utils.d.mts +0 -7
  268. package/dist/src/utils.d.ts +0 -7
  269. package/dist/tailwind/index.d.mts +0 -2
  270. package/dist/tailwind/index.d.ts +0 -2
  271. package/dist/types/index.d.mts +0 -12
  272. package/dist/types/index.d.ts +0 -12
@@ -0,0 +1,138 @@
1
+ 'use client'
2
+ import React, { useEffect, useLayoutEffect, useState } from 'react'
3
+
4
+ import Image from 'next/image'
5
+
6
+ import type { Dimensions, TShirtSize, TShirtDimensions } from '../../types'
7
+ import { constrain, asNum, cn } from '../../util'
8
+ import type { VideoBlock } from '../def'
9
+ import { VideoPlayer } from '../../primitives/index-common'
10
+
11
+ import type BlockComponentProps from './block-component-props'
12
+
13
+ const VideoBlockComponent: React.FC<BlockComponentProps & {
14
+ usePoster?: boolean
15
+ size?: TShirtSize
16
+ constrainTo?: Dimensions
17
+ }> = ({
18
+ block,
19
+ className='',
20
+ agent,
21
+ usePoster=false,
22
+ size='md',
23
+ constrainTo
24
+ }) => {
25
+
26
+ const [_dim, setDim] = useState<Dimensions | undefined>(undefined)
27
+
28
+ const onResize = () => {
29
+ setDim({
30
+ w: window.innerWidth,
31
+ h: window.innerHeight
32
+ })
33
+ }
34
+
35
+ const windowDefined = typeof window !== 'undefined'
36
+
37
+ useEffect(() => {
38
+ if (window && agent === 'desktop') {
39
+ window.addEventListener('resize', onResize)
40
+ return () => window.removeEventListener('resize', onResize)
41
+ }
42
+ }, [windowDefined])
43
+
44
+ useLayoutEffect(() => {
45
+ onResize()
46
+ }, [])
47
+
48
+
49
+ if (block.blockType !== 'video') {
50
+ return <>video block required</>
51
+ }
52
+
53
+ const b = block as VideoBlock
54
+ const ar = b.dim.md.w / b.dim.md.h
55
+ if (agent === 'phone') {
56
+ if (b.sizing?.mobile?.vw) {
57
+ // serverside, or at least while the video is loading,
58
+ // generate the css for the correctly sized poster image
59
+ if (!_dim) {
60
+ const width = `${b.sizing.mobile.vw}vw`
61
+ return <div className='dummy-div' style={{
62
+ maxWidth: '100%',
63
+ maxHeight: '100%',
64
+ width,
65
+ height: `calc(${width}/${ar})`,
66
+ backgroundImage: `url(${b.poster!})`,
67
+ backgroundSize: 'contain',
68
+ backgroundRepeat: 'no-repeat',
69
+ }} />
70
+ }
71
+ else {
72
+ const width = ((b.sizing.mobile.vw / 100) * _dim.w)
73
+ const dim = {
74
+ h: width / ar,
75
+ w: width
76
+ }
77
+ return ((
78
+ <VideoPlayer
79
+ className={cn('mx-auto', className)}
80
+ sources={b.sources}
81
+ width={dim.w}
82
+ height={dim.h}
83
+ {...b.videoProps}
84
+ />
85
+ ))
86
+ }
87
+ }
88
+ }
89
+ else if (b.sizing?.vh) {
90
+ // serverside, generate the css for the correctly sized poster image
91
+ if (!_dim) {
92
+ const height = `${b.sizing.vh}vh`
93
+ return <div className='dummy-div' style={{
94
+ maxWidth: '100%',
95
+ maxHeight: '100%',
96
+ height: height,
97
+ width: `calc(${height}*${ar})`,
98
+ backgroundImage: `url(${b.poster!})`,
99
+ backgroundSize: 'contain',
100
+ backgroundRepeat: 'no-repeat',
101
+ }} />
102
+ }
103
+ else {
104
+
105
+ const height = ((b.sizing.vh / 100) * _dim.h)
106
+ const dim = {
107
+ h: height,
108
+ w: height * ar
109
+ }
110
+ return ((
111
+ <VideoPlayer
112
+ className={cn('mx-auto', className)}
113
+ sources={b.sources}
114
+ width={dim.w}
115
+ height={dim.h}
116
+ {...b.videoProps}
117
+ />
118
+ ))
119
+ }
120
+ }
121
+
122
+ const videoDims = b.dim as TShirtDimensions
123
+ const dim = ((size && size in videoDims) ? videoDims[size] : videoDims.md) as Dimensions
124
+ const conDim = (constrainTo ? constrain(dim, constrainTo) : dim)
125
+ return usePoster ? (
126
+ <Image src={b.poster!} alt='image' width={conDim.w} height={conDim.h} className={className}/>
127
+ ) : (
128
+ <VideoPlayer
129
+ className={cn('mx-auto', className)}
130
+ sources={b.sources}
131
+ width={conDim.w}
132
+ height={conDim.h}
133
+ {...b.videoProps}
134
+ />
135
+ )
136
+ }
137
+
138
+ export default VideoBlockComponent
@@ -0,0 +1,242 @@
1
+ 'use client'
2
+
3
+ import { cn } from '@hanzo/ui/util'
4
+ import { Avatar, AvatarFallback, AvatarImage } from '@hanzo/ui/primitives'
5
+ import { Badge } from '@hanzo/ui/primitives'
6
+ import { Card, CardContent, CardHeader, CardTitle } from '@hanzo/ui/primitives'
7
+ import type { LucideIcon } from 'lucide-react'
8
+
9
+ interface Activity {
10
+ id: string | number
11
+ user: {
12
+ name: string
13
+ avatar?: string
14
+ email?: string
15
+ }
16
+ action: string
17
+ target?: string
18
+ timestamp: Date | string
19
+ type?: 'default' | 'success' | 'warning' | 'error' | 'info'
20
+ icon?: LucideIcon
21
+ metadata?: Record<string, any>
22
+ }
23
+
24
+ interface ActivityFeedProps extends React.ComponentPropsWithoutRef<'div'> {
25
+ activities: Activity[]
26
+ title?: string
27
+ variant?: 'default' | 'timeline' | 'compact'
28
+ showDate?: boolean
29
+ }
30
+
31
+ export function ActivityFeed({
32
+ className,
33
+ activities,
34
+ title,
35
+ variant = 'default',
36
+ showDate = true,
37
+ ...props
38
+ }: ActivityFeedProps) {
39
+ const formatTime = (timestamp: Date | string) => {
40
+ const date = new Date(timestamp)
41
+ const now = new Date()
42
+ const diff = now.getTime() - date.getTime()
43
+
44
+ const minutes = Math.floor(diff / 60000)
45
+ const hours = Math.floor(diff / 3600000)
46
+ const days = Math.floor(diff / 86400000)
47
+
48
+ if (minutes < 1) return 'Just now'
49
+ if (minutes < 60) return `${minutes} minute${minutes > 1 ? 's' : ''} ago`
50
+ if (hours < 24) return `${hours} hour${hours > 1 ? 's' : ''} ago`
51
+ if (days < 7) return `${days} day${days > 1 ? 's' : ''} ago`
52
+
53
+ return date.toLocaleDateString()
54
+ }
55
+
56
+ const getTypeStyles = (type?: Activity['type']) => {
57
+ switch (type) {
58
+ case 'success':
59
+ return 'border-green-500 bg-green-50 text-green-900 dark:bg-green-950 dark:text-green-100'
60
+ case 'warning':
61
+ return 'border-yellow-500 bg-yellow-50 text-yellow-900 dark:bg-yellow-950 dark:text-yellow-100'
62
+ case 'error':
63
+ return 'border-red-500 bg-red-50 text-red-900 dark:bg-red-950 dark:text-red-100'
64
+ case 'info':
65
+ return 'border-blue-500 bg-blue-50 text-blue-900 dark:bg-blue-950 dark:text-blue-100'
66
+ default:
67
+ return ''
68
+ }
69
+ }
70
+
71
+ const getUserInitials = (name: string) => {
72
+ return name
73
+ .split(' ')
74
+ .map((n) => n[0])
75
+ .join('')
76
+ .toUpperCase()
77
+ }
78
+
79
+ if (variant === 'timeline') {
80
+ return (
81
+ <div className={cn('space-y-4', className)} {...props}>
82
+ {title && <h3 className="text-lg font-semibold">{title}</h3>}
83
+ <div className="relative">
84
+ <div className="absolute left-6 top-0 h-full w-0.5 bg-border" />
85
+ <div className="space-y-6">
86
+ {activities.map((activity, i) => {
87
+ const Icon = activity.icon
88
+ return (
89
+ <div key={activity.id || i} className="relative flex gap-4">
90
+ <div className="relative z-10 flex h-12 w-12 items-center justify-center">
91
+ <div className="absolute h-3 w-3 rounded-full bg-background border-2 border-primary" />
92
+ </div>
93
+ <div className="flex-1 space-y-1">
94
+ <div className="flex items-start justify-between">
95
+ <div className="space-y-1">
96
+ <p className="text-sm">
97
+ <span className="font-semibold">{activity.user.name}</span>{' '}
98
+ {activity.action}
99
+ {activity.target && (
100
+ <span className="font-medium"> {activity.target}</span>
101
+ )}
102
+ </p>
103
+ {activity.metadata && (
104
+ <div className="flex flex-wrap gap-2">
105
+ {Object.entries(activity.metadata).map(([key, value]) => (
106
+ <Badge key={key} variant="secondary" className="text-xs">
107
+ {key}: {value}
108
+ </Badge>
109
+ ))}
110
+ </div>
111
+ )}
112
+ </div>
113
+ {Icon && <Icon className="h-4 w-4 text-muted-foreground" />}
114
+ </div>
115
+ {showDate && (
116
+ <p className="text-xs text-muted-foreground">
117
+ {formatTime(activity.timestamp)}
118
+ </p>
119
+ )}
120
+ </div>
121
+ </div>
122
+ )
123
+ })}
124
+ </div>
125
+ </div>
126
+ </div>
127
+ )
128
+ }
129
+
130
+ if (variant === 'compact') {
131
+ return (
132
+ <Card className={className} {...props}>
133
+ {title && (
134
+ <CardHeader>
135
+ <CardTitle>{title}</CardTitle>
136
+ </CardHeader>
137
+ )}
138
+ <CardContent className="p-0">
139
+ <div className="divide-y">
140
+ {activities.map((activity, i) => {
141
+ const Icon = activity.icon
142
+ return (
143
+ <div
144
+ key={activity.id || i}
145
+ className={cn(
146
+ 'flex items-center gap-3 px-6 py-3',
147
+ getTypeStyles(activity.type)
148
+ )}
149
+ >
150
+ <Avatar className="h-8 w-8">
151
+ <AvatarImage src={activity.user.avatar} />
152
+ <AvatarFallback className="text-xs">
153
+ {getUserInitials(activity.user.name)}
154
+ </AvatarFallback>
155
+ </Avatar>
156
+ <div className="flex-1 min-w-0">
157
+ <p className="text-sm truncate">
158
+ <span className="font-medium">{activity.user.name}</span>{' '}
159
+ {activity.action}
160
+ {activity.target && (
161
+ <span className="font-medium"> {activity.target}</span>
162
+ )}
163
+ </p>
164
+ </div>
165
+ <div className="flex items-center gap-2">
166
+ {Icon && <Icon className="h-4 w-4 text-muted-foreground" />}
167
+ {showDate && (
168
+ <p className="text-xs text-muted-foreground">
169
+ {formatTime(activity.timestamp)}
170
+ </p>
171
+ )}
172
+ </div>
173
+ </div>
174
+ )
175
+ })}
176
+ </div>
177
+ </CardContent>
178
+ </Card>
179
+ )
180
+ }
181
+
182
+ // Default variant
183
+ return (
184
+ <div className={cn('space-y-4', className)} {...props}>
185
+ {title && <h3 className="text-lg font-semibold">{title}</h3>}
186
+ <div className="space-y-4">
187
+ {activities.map((activity, i) => {
188
+ const Icon = activity.icon
189
+ return (
190
+ <div
191
+ key={activity.id || i}
192
+ className={cn(
193
+ 'flex gap-4 rounded-lg border p-4',
194
+ getTypeStyles(activity.type)
195
+ )}
196
+ >
197
+ <Avatar>
198
+ <AvatarImage src={activity.user.avatar} />
199
+ <AvatarFallback>
200
+ {getUserInitials(activity.user.name)}
201
+ </AvatarFallback>
202
+ </Avatar>
203
+ <div className="flex-1 space-y-1">
204
+ <div className="flex items-start justify-between">
205
+ <div>
206
+ <p className="text-sm">
207
+ <span className="font-semibold">{activity.user.name}</span>{' '}
208
+ {activity.action}
209
+ {activity.target && (
210
+ <span className="font-medium"> {activity.target}</span>
211
+ )}
212
+ </p>
213
+ {activity.user.email && (
214
+ <p className="text-xs text-muted-foreground">
215
+ {activity.user.email}
216
+ </p>
217
+ )}
218
+ </div>
219
+ {Icon && <Icon className="h-4 w-4 text-muted-foreground" />}
220
+ </div>
221
+ {activity.metadata && (
222
+ <div className="flex flex-wrap gap-2 pt-2">
223
+ {Object.entries(activity.metadata).map(([key, value]) => (
224
+ <Badge key={key} variant="secondary" className="text-xs">
225
+ {key}: {value}
226
+ </Badge>
227
+ ))}
228
+ </div>
229
+ )}
230
+ {showDate && (
231
+ <p className="text-xs text-muted-foreground">
232
+ {formatTime(activity.timestamp)}
233
+ </p>
234
+ )}
235
+ </div>
236
+ </div>
237
+ )
238
+ })}
239
+ </div>
240
+ </div>
241
+ )
242
+ }
@@ -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
+ }