@opencosmos/ui 1.3.1

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 (260) hide show
  1. package/.claude/CLAUDE.md +239 -0
  2. package/README.md +161 -0
  3. package/dist/cli.mjs +151 -0
  4. package/dist/dates.d.mts +20 -0
  5. package/dist/dates.d.ts +20 -0
  6. package/dist/dates.js +240 -0
  7. package/dist/dates.js.map +1 -0
  8. package/dist/dates.mjs +203 -0
  9. package/dist/dates.mjs.map +1 -0
  10. package/dist/dnd.d.mts +126 -0
  11. package/dist/dnd.d.ts +126 -0
  12. package/dist/dnd.js +274 -0
  13. package/dist/dnd.js.map +1 -0
  14. package/dist/dnd.mjs +250 -0
  15. package/dist/dnd.mjs.map +1 -0
  16. package/dist/fontThemes-Dh8mtXES.d.mts +868 -0
  17. package/dist/fontThemes-Dh8mtXES.d.ts +868 -0
  18. package/dist/forms.d.mts +38 -0
  19. package/dist/forms.d.ts +38 -0
  20. package/dist/forms.js +198 -0
  21. package/dist/forms.js.map +1 -0
  22. package/dist/forms.mjs +159 -0
  23. package/dist/forms.mjs.map +1 -0
  24. package/dist/hooks-1b8WaQf1.d.mts +225 -0
  25. package/dist/hooks-CKW8vE9H.d.ts +225 -0
  26. package/dist/hooks.d.mts +3 -0
  27. package/dist/hooks.d.ts +3 -0
  28. package/dist/hooks.js +971 -0
  29. package/dist/hooks.js.map +1 -0
  30. package/dist/hooks.mjs +943 -0
  31. package/dist/hooks.mjs.map +1 -0
  32. package/dist/index-DscTIrZ2.d.mts +29 -0
  33. package/dist/index-DscTIrZ2.d.ts +29 -0
  34. package/dist/index.d.mts +3382 -0
  35. package/dist/index.d.ts +3382 -0
  36. package/dist/index.js +15146 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/index.mjs +14802 -0
  39. package/dist/index.mjs.map +1 -0
  40. package/dist/providers-CXPDMsl7.d.mts +30 -0
  41. package/dist/providers-Dn_Msjvz.d.ts +30 -0
  42. package/dist/providers.d.mts +3 -0
  43. package/dist/providers.d.ts +3 -0
  44. package/dist/providers.js +1885 -0
  45. package/dist/providers.js.map +1 -0
  46. package/dist/providers.mjs +1859 -0
  47. package/dist/providers.mjs.map +1 -0
  48. package/dist/tables.d.mts +10 -0
  49. package/dist/tables.d.ts +10 -0
  50. package/dist/tables.js +248 -0
  51. package/dist/tables.js.map +1 -0
  52. package/dist/tables.mjs +218 -0
  53. package/dist/tables.mjs.map +1 -0
  54. package/dist/tokens.d.mts +1065 -0
  55. package/dist/tokens.d.ts +1065 -0
  56. package/dist/tokens.js +2637 -0
  57. package/dist/tokens.js.map +1 -0
  58. package/dist/tokens.mjs +2555 -0
  59. package/dist/tokens.mjs.map +1 -0
  60. package/dist/utils-CIIM7dAC.d.ts +986 -0
  61. package/dist/utils-Cs04sxth.d.mts +986 -0
  62. package/dist/utils.d.mts +4 -0
  63. package/dist/utils.d.ts +4 -0
  64. package/dist/utils.js +874 -0
  65. package/dist/utils.js.map +1 -0
  66. package/dist/utils.mjs +806 -0
  67. package/dist/utils.mjs.map +1 -0
  68. package/dist/validation-Bj1ye-v_.d.mts +114 -0
  69. package/dist/validation-Bj1ye-v_.d.ts +114 -0
  70. package/dist/webgl.d.mts +104 -0
  71. package/dist/webgl.d.ts +104 -0
  72. package/dist/webgl.js +226 -0
  73. package/dist/webgl.js.map +1 -0
  74. package/dist/webgl.mjs +195 -0
  75. package/dist/webgl.mjs.map +1 -0
  76. package/package.json +267 -0
  77. package/src/cli.ts +206 -0
  78. package/src/component-registry.ts +183 -0
  79. package/src/components/actions/Button.test.tsx +61 -0
  80. package/src/components/actions/Button.tsx +70 -0
  81. package/src/components/actions/Link.tsx +78 -0
  82. package/src/components/actions/Magnetic.tsx +68 -0
  83. package/src/components/actions/Toggle.test.tsx +40 -0
  84. package/src/components/actions/Toggle.tsx +47 -0
  85. package/src/components/actions/ToggleGroup.tsx +70 -0
  86. package/src/components/actions/index.ts +5 -0
  87. package/src/components/backgrounds/FaultyTerminal.tsx +426 -0
  88. package/src/components/backgrounds/OrbBackground.tsx +424 -0
  89. package/src/components/backgrounds/WarpBackground.tsx +358 -0
  90. package/src/components/backgrounds/index.ts +3 -0
  91. package/src/components/blocks/Hero.tsx +142 -0
  92. package/src/components/blocks/social/OpenGraphCard.tsx +243 -0
  93. package/src/components/cursor/SplashCursor.tsx +1315 -0
  94. package/src/components/cursor/TargetCursor.tsx +187 -0
  95. package/src/components/cursor/index.ts +2 -0
  96. package/src/components/data-display/AspectImage.tsx +73 -0
  97. package/src/components/data-display/Avatar.test.tsx +35 -0
  98. package/src/components/data-display/Avatar.tsx +55 -0
  99. package/src/components/data-display/Badge.test.tsx +43 -0
  100. package/src/components/data-display/Badge.tsx +84 -0
  101. package/src/components/data-display/Brand.tsx +123 -0
  102. package/src/components/data-display/Calendar.tsx +70 -0
  103. package/src/components/data-display/Card.test.tsx +92 -0
  104. package/src/components/data-display/Card.tsx +115 -0
  105. package/src/components/data-display/Code.tsx +210 -0
  106. package/src/components/data-display/CollapsibleCodeBlock.tsx +238 -0
  107. package/src/components/data-display/DataTable.tsx +119 -0
  108. package/src/components/data-display/DescriptionList.tsx +41 -0
  109. package/src/components/data-display/GitHubIcon.tsx +44 -0
  110. package/src/components/data-display/Heading.test.tsx +36 -0
  111. package/src/components/data-display/Heading.tsx +83 -0
  112. package/src/components/data-display/StatCard.tsx +195 -0
  113. package/src/components/data-display/Table.tsx +133 -0
  114. package/src/components/data-display/Text.test.tsx +48 -0
  115. package/src/components/data-display/Text.tsx +144 -0
  116. package/src/components/data-display/Timeline.tsx +194 -0
  117. package/src/components/data-display/TreeView.tsx +226 -0
  118. package/src/components/data-display/Typewriter.tsx +119 -0
  119. package/src/components/data-display/VariableWeightText.tsx +130 -0
  120. package/src/components/data-display/index.ts +19 -0
  121. package/src/components/feedback/Alert.test.tsx +44 -0
  122. package/src/components/feedback/Alert.tsx +65 -0
  123. package/src/components/feedback/EmptyState.tsx +113 -0
  124. package/src/components/feedback/Progress.test.tsx +60 -0
  125. package/src/components/feedback/Progress.tsx +30 -0
  126. package/src/components/feedback/ProgressBar.tsx +158 -0
  127. package/src/components/feedback/Skeleton.test.tsx +39 -0
  128. package/src/components/feedback/Skeleton.tsx +45 -0
  129. package/src/components/feedback/Sonner.tsx +28 -0
  130. package/src/components/feedback/Spinner.test.tsx +33 -0
  131. package/src/components/feedback/Spinner.tsx +99 -0
  132. package/src/components/feedback/Stepper.tsx +307 -0
  133. package/src/components/feedback/Toast/Toast.tsx +243 -0
  134. package/src/components/feedback/Toast/index.ts +2 -0
  135. package/src/components/feedback/index.ts +9 -0
  136. package/src/components/forms/Checkbox.test.tsx +40 -0
  137. package/src/components/forms/Checkbox.tsx +31 -0
  138. package/src/components/forms/ColorPicker.tsx +118 -0
  139. package/src/components/forms/Combobox.tsx +96 -0
  140. package/src/components/forms/DragDrop.tsx +440 -0
  141. package/src/components/forms/FileUpload.tsx +252 -0
  142. package/src/components/forms/FilterButton.tsx +65 -0
  143. package/src/components/forms/Form.tsx +197 -0
  144. package/src/components/forms/Input.test.tsx +46 -0
  145. package/src/components/forms/Input.tsx +43 -0
  146. package/src/components/forms/InputOTP.tsx +81 -0
  147. package/src/components/forms/Label.test.tsx +20 -0
  148. package/src/components/forms/Label.tsx +25 -0
  149. package/src/components/forms/RadioGroup.tsx +51 -0
  150. package/src/components/forms/SearchBar.tsx +215 -0
  151. package/src/components/forms/Select.test.tsx +118 -0
  152. package/src/components/forms/Select.tsx +274 -0
  153. package/src/components/forms/Slider.tsx +29 -0
  154. package/src/components/forms/Switch.test.tsx +76 -0
  155. package/src/components/forms/Switch.tsx +30 -0
  156. package/src/components/forms/TextField.tsx +152 -0
  157. package/src/components/forms/Textarea.test.tsx +41 -0
  158. package/src/components/forms/Textarea.tsx +29 -0
  159. package/src/components/forms/ThemeSwitcher.tsx +290 -0
  160. package/src/components/forms/ThemeToggle.tsx +151 -0
  161. package/src/components/forms/index.ts +19 -0
  162. package/src/components/layout/Accordion.test.tsx +66 -0
  163. package/src/components/layout/Accordion.tsx +64 -0
  164. package/src/components/layout/AspectRatio.tsx +7 -0
  165. package/src/components/layout/Carousel.tsx +277 -0
  166. package/src/components/layout/Collapsible.test.tsx +40 -0
  167. package/src/components/layout/Collapsible.tsx +31 -0
  168. package/src/components/layout/Container.test.tsx +45 -0
  169. package/src/components/layout/Container.tsx +99 -0
  170. package/src/components/layout/CustomizerPanel.tsx +400 -0
  171. package/src/components/layout/DatePicker.tsx +57 -0
  172. package/src/components/layout/Footer/Footer.tsx +175 -0
  173. package/src/components/layout/Footer/index.ts +2 -0
  174. package/src/components/layout/GlassSurface.tsx +82 -0
  175. package/src/components/layout/Grid.test.tsx +31 -0
  176. package/src/components/layout/Grid.tsx +130 -0
  177. package/src/components/layout/Header/Header.tsx +450 -0
  178. package/src/components/layout/Header/index.ts +2 -0
  179. package/src/components/layout/PageLayout.tsx +180 -0
  180. package/src/components/layout/PageTemplate.tsx +158 -0
  181. package/src/components/layout/Resizable.tsx +48 -0
  182. package/src/components/layout/ScrollArea.tsx +53 -0
  183. package/src/components/layout/Separator.test.tsx +28 -0
  184. package/src/components/layout/Separator.tsx +29 -0
  185. package/src/components/layout/Sidebar.tsx +171 -0
  186. package/src/components/layout/Stack.test.tsx +41 -0
  187. package/src/components/layout/Stack.tsx +89 -0
  188. package/src/components/layout/glass-surface.css +60 -0
  189. package/src/components/layout/index.ts +18 -0
  190. package/src/components/motion/AnimatedBeam.tsx +159 -0
  191. package/src/components/navigation/Breadcrumb.test.tsx +57 -0
  192. package/src/components/navigation/Breadcrumb.tsx +119 -0
  193. package/src/components/navigation/Breadcrumbs.tsx +221 -0
  194. package/src/components/navigation/Command.tsx +159 -0
  195. package/src/components/navigation/Menubar.tsx +115 -0
  196. package/src/components/navigation/NavLink.tsx +55 -0
  197. package/src/components/navigation/NavigationMenu.tsx +125 -0
  198. package/src/components/navigation/Pagination.tsx +121 -0
  199. package/src/components/navigation/SecondaryNav.tsx +100 -0
  200. package/src/components/navigation/Tabs.test.tsx +47 -0
  201. package/src/components/navigation/Tabs.tsx +60 -0
  202. package/src/components/navigation/TertiaryNav.tsx +90 -0
  203. package/src/components/navigation/index.ts +10 -0
  204. package/src/components/overlays/AlertDialog.test.tsx +69 -0
  205. package/src/components/overlays/AlertDialog.tsx +166 -0
  206. package/src/components/overlays/ContextMenu.tsx +243 -0
  207. package/src/components/overlays/Dialog.test.tsx +79 -0
  208. package/src/components/overlays/Dialog.tsx +158 -0
  209. package/src/components/overlays/Drawer.tsx +128 -0
  210. package/src/components/overlays/Dropdown.tsx +253 -0
  211. package/src/components/overlays/DropdownMenu.tsx +242 -0
  212. package/src/components/overlays/HoverCard.tsx +32 -0
  213. package/src/components/overlays/Modal.tsx +250 -0
  214. package/src/components/overlays/NotificationCenter.tsx +364 -0
  215. package/src/components/overlays/Popover.test.tsx +40 -0
  216. package/src/components/overlays/Popover.tsx +46 -0
  217. package/src/components/overlays/Sheet.tsx +163 -0
  218. package/src/components/overlays/Tooltip.test.tsx +33 -0
  219. package/src/components/overlays/Tooltip.tsx +32 -0
  220. package/src/components/overlays/index.ts +12 -0
  221. package/src/dates.ts +2 -0
  222. package/src/dnd.ts +1 -0
  223. package/src/forms.ts +1 -0
  224. package/src/globals.css +187 -0
  225. package/src/hooks/index.ts +6 -0
  226. package/src/hooks/useForm.ts +247 -0
  227. package/src/hooks/useMotionPreference.test.ts +102 -0
  228. package/src/hooks/useMotionPreference.ts +78 -0
  229. package/src/hooks/useTheme.ts +58 -0
  230. package/src/hooks.ts +9 -0
  231. package/src/index.ts +168 -0
  232. package/src/lib/animations.ts +356 -0
  233. package/src/lib/breadcrumbs.ts +94 -0
  234. package/src/lib/colors.ts +493 -0
  235. package/src/lib/store/customizer.ts +482 -0
  236. package/src/lib/store/index.ts +3 -0
  237. package/src/lib/store/theme.ts +55 -0
  238. package/src/lib/syntax-parser/index.ts +50 -0
  239. package/src/lib/syntax-parser/patterns.ts +64 -0
  240. package/src/lib/syntax-parser/tokenizer.ts +117 -0
  241. package/src/lib/syntax-parser/types.ts +27 -0
  242. package/src/lib/utils.ts +6 -0
  243. package/src/lib/validation.ts +204 -0
  244. package/src/lib/webgl/Color.ts +11 -0
  245. package/src/lib/webgl/Mesh.ts +41 -0
  246. package/src/lib/webgl/Program.ts +118 -0
  247. package/src/lib/webgl/Renderer.ts +51 -0
  248. package/src/lib/webgl/Triangle.ts +27 -0
  249. package/src/lib/webgl/Vec3.ts +18 -0
  250. package/src/lib/webgl/index.ts +13 -0
  251. package/src/nativewind-env.d.ts +1 -0
  252. package/src/providers/ThemeProvider.tsx +461 -0
  253. package/src/providers/index.ts +1 -0
  254. package/src/providers.ts +7 -0
  255. package/src/tables.ts +1 -0
  256. package/src/test/setup.ts +39 -0
  257. package/src/theme.css +158 -0
  258. package/src/tokens.ts +7 -0
  259. package/src/utils.ts +12 -0
  260. package/src/webgl.ts +1 -0
@@ -0,0 +1,364 @@
1
+ 'use client'
2
+
3
+ import * as React from "react"
4
+ import { cn } from "../../lib/utils"
5
+
6
+ // ============================================================================
7
+ // Types
8
+ // ============================================================================
9
+
10
+ export interface NotificationItem {
11
+ id: string
12
+ title: string
13
+ description?: string
14
+ timestamp: string | Date
15
+ read?: boolean
16
+ icon?: React.ReactNode
17
+ action?: { label: string; onClick: () => void }
18
+ }
19
+
20
+ export interface NotificationCenterProps extends React.HTMLAttributes<HTMLDivElement> {
21
+ /** Array of notification items */
22
+ notifications: NotificationItem[]
23
+ /** Callback when a notification is marked as read */
24
+ onMarkRead?: (id: string) => void
25
+ /** Callback to mark all notifications as read */
26
+ onMarkAllRead?: () => void
27
+ /** Callback when a notification is dismissed */
28
+ onDismiss?: (id: string) => void
29
+ /** Custom trigger element (defaults to bell icon with badge) */
30
+ trigger?: React.ReactNode
31
+ /** Maximum height of the notification list */
32
+ maxHeight?: number
33
+ /** Empty state message */
34
+ emptyMessage?: string
35
+ }
36
+
37
+ // ============================================================================
38
+ // Inline Icons (avoiding external dependency)
39
+ // ============================================================================
40
+
41
+ const BellIcon = ({ className }: { className?: string }) => (
42
+ <svg
43
+ xmlns="http://www.w3.org/2000/svg"
44
+ width="20"
45
+ height="20"
46
+ viewBox="0 0 24 24"
47
+ fill="none"
48
+ stroke="currentColor"
49
+ strokeWidth="2"
50
+ strokeLinecap="round"
51
+ strokeLinejoin="round"
52
+ aria-hidden="true"
53
+ className={className}
54
+ >
55
+ <path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9" />
56
+ <path d="M10.3 21a1.94 1.94 0 0 0 3.4 0" />
57
+ </svg>
58
+ )
59
+
60
+ const XIcon = () => (
61
+ <svg
62
+ xmlns="http://www.w3.org/2000/svg"
63
+ width="14"
64
+ height="14"
65
+ viewBox="0 0 24 24"
66
+ fill="none"
67
+ stroke="currentColor"
68
+ strokeWidth="2"
69
+ strokeLinecap="round"
70
+ strokeLinejoin="round"
71
+ aria-hidden="true"
72
+ >
73
+ <line x1="18" y1="6" x2="6" y2="18" />
74
+ <line x1="6" y1="6" x2="18" y2="18" />
75
+ </svg>
76
+ )
77
+
78
+ const CheckIcon = () => (
79
+ <svg
80
+ xmlns="http://www.w3.org/2000/svg"
81
+ width="14"
82
+ height="14"
83
+ viewBox="0 0 24 24"
84
+ fill="none"
85
+ stroke="currentColor"
86
+ strokeWidth="2"
87
+ strokeLinecap="round"
88
+ strokeLinejoin="round"
89
+ aria-hidden="true"
90
+ >
91
+ <polyline points="20 6 9 17 4 12" />
92
+ </svg>
93
+ )
94
+
95
+ // ============================================================================
96
+ // Helper Functions
97
+ // ============================================================================
98
+
99
+ function formatTimestamp(timestamp: string | Date): string {
100
+ const date = typeof timestamp === 'string' ? new Date(timestamp) : timestamp
101
+ const now = new Date()
102
+ const diffMs = now.getTime() - date.getTime()
103
+ const diffMins = Math.floor(diffMs / 60000)
104
+ const diffHours = Math.floor(diffMs / 3600000)
105
+ const diffDays = Math.floor(diffMs / 86400000)
106
+
107
+ if (diffMins < 1) return 'Just now'
108
+ if (diffMins < 60) return `${diffMins}m ago`
109
+ if (diffHours < 24) return `${diffHours}h ago`
110
+ if (diffDays < 7) return `${diffDays}d ago`
111
+ return date.toLocaleDateString()
112
+ }
113
+
114
+ function groupNotifications(notifications: NotificationItem[]): Record<string, NotificationItem[]> {
115
+ const groups: Record<string, NotificationItem[]> = {}
116
+
117
+ for (const notification of notifications) {
118
+ const date = typeof notification.timestamp === 'string'
119
+ ? new Date(notification.timestamp)
120
+ : notification.timestamp
121
+ const now = new Date()
122
+ const diffMs = now.getTime() - date.getTime()
123
+ const diffDays = Math.floor(diffMs / 86400000)
124
+
125
+ let group: string
126
+ if (diffDays === 0) group = 'Today'
127
+ else if (diffDays === 1) group = 'Yesterday'
128
+ else if (diffDays < 7) group = 'This Week'
129
+ else group = 'Older'
130
+
131
+ if (!groups[group]) groups[group] = []
132
+ groups[group].push(notification)
133
+ }
134
+
135
+ return groups
136
+ }
137
+
138
+ // ============================================================================
139
+ // NotificationCenter Component
140
+ // ============================================================================
141
+
142
+ function NotificationCenter({
143
+ className,
144
+ notifications,
145
+ onMarkRead,
146
+ onMarkAllRead,
147
+ onDismiss,
148
+ trigger,
149
+ maxHeight = 400,
150
+ emptyMessage = 'No notifications',
151
+ ...props
152
+ }: NotificationCenterProps) {
153
+ const [open, setOpen] = React.useState(false)
154
+ const panelRef = React.useRef<HTMLDivElement>(null)
155
+
156
+ const unreadCount = notifications.filter(n => !n.read).length
157
+ const grouped = groupNotifications(notifications)
158
+ const groupOrder = ['Today', 'Yesterday', 'This Week', 'Older']
159
+
160
+ // Close on escape
161
+ React.useEffect(() => {
162
+ function handleKeyDown(e: KeyboardEvent) {
163
+ if (e.key === 'Escape' && open) {
164
+ setOpen(false)
165
+ }
166
+ }
167
+ document.addEventListener('keydown', handleKeyDown)
168
+ return () => document.removeEventListener('keydown', handleKeyDown)
169
+ }, [open])
170
+
171
+ // Close on click outside
172
+ React.useEffect(() => {
173
+ function handleClickOutside(e: MouseEvent) {
174
+ if (panelRef.current && !panelRef.current.contains(e.target as Node)) {
175
+ setOpen(false)
176
+ }
177
+ }
178
+ if (open) {
179
+ document.addEventListener('mousedown', handleClickOutside)
180
+ }
181
+ return () => document.removeEventListener('mousedown', handleClickOutside)
182
+ }, [open])
183
+
184
+ return (
185
+ <div
186
+ data-slot="notification-center"
187
+ className={cn("relative inline-block", className)}
188
+ ref={panelRef}
189
+ {...props}
190
+ >
191
+ {/* Trigger */}
192
+ <button
193
+ type="button"
194
+ onClick={() => setOpen(!open)}
195
+ className="relative inline-flex items-center justify-center rounded-md p-2 text-foreground hover:bg-muted transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
196
+ aria-label={`Notifications${unreadCount > 0 ? ` (${unreadCount} unread)` : ''}`}
197
+ aria-expanded={open}
198
+ aria-haspopup="true"
199
+ >
200
+ {trigger || <BellIcon />}
201
+ {unreadCount > 0 && (
202
+ <span
203
+ data-slot="notification-badge"
204
+ className="absolute -top-0.5 -right-0.5 flex h-4 min-w-4 items-center justify-center rounded-full bg-destructive px-1 text-[10px] font-medium text-destructive-foreground"
205
+ aria-hidden="true"
206
+ >
207
+ {unreadCount > 99 ? '99+' : unreadCount}
208
+ </span>
209
+ )}
210
+ </button>
211
+
212
+ {/* Panel */}
213
+ {open && (
214
+ <div
215
+ data-slot="notification-panel"
216
+ className="absolute right-0 top-full mt-2 z-50 w-80 rounded-lg border border-border bg-popover shadow-lg animate-in fade-in-0 zoom-in-95 slide-in-from-top-2"
217
+ role="dialog"
218
+ aria-label="Notifications"
219
+ >
220
+ {/* Header */}
221
+ <div className="flex items-center justify-between border-b border-border px-4 py-3">
222
+ <h3 className="text-sm font-semibold text-foreground">
223
+ Notifications
224
+ {unreadCount > 0 && (
225
+ <span className="ml-1.5 text-xs font-normal text-foreground-secondary">
226
+ ({unreadCount} unread)
227
+ </span>
228
+ )}
229
+ </h3>
230
+ {unreadCount > 0 && onMarkAllRead && (
231
+ <button
232
+ type="button"
233
+ onClick={() => {
234
+ onMarkAllRead()
235
+ }}
236
+ className="text-xs text-primary hover:text-primary/80 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded px-1"
237
+ >
238
+ Mark all read
239
+ </button>
240
+ )}
241
+ </div>
242
+
243
+ {/* Notification List */}
244
+ <div
245
+ className="overflow-y-auto"
246
+ style={{ maxHeight }}
247
+ role="list"
248
+ aria-label="Notification list"
249
+ >
250
+ {notifications.length === 0 ? (
251
+ <div className="flex flex-col items-center justify-center py-8 px-4 text-center">
252
+ <BellIcon className="text-foreground-secondary mb-2 opacity-40" />
253
+ <p className="text-sm text-foreground-secondary">{emptyMessage}</p>
254
+ </div>
255
+ ) : (
256
+ groupOrder
257
+ .filter(group => grouped[group]?.length > 0)
258
+ .map(group => (
259
+ <div key={group}>
260
+ <div className="sticky top-0 bg-popover/95 backdrop-blur-xs px-4 py-1.5 border-b border-border/50">
261
+ <span className="text-[11px] font-medium uppercase tracking-wider text-foreground-secondary">
262
+ {group}
263
+ </span>
264
+ </div>
265
+ {grouped[group].map(notification => (
266
+ <div
267
+ key={notification.id}
268
+ data-slot="notification-item"
269
+ className={cn(
270
+ "flex gap-3 px-4 py-3 border-b border-border/30 last:border-0 transition-colors",
271
+ !notification.read && "bg-primary/5"
272
+ )}
273
+ role="listitem"
274
+ >
275
+ {/* Icon */}
276
+ {notification.icon && (
277
+ <div className="shrink-0 mt-0.5 text-foreground-secondary" aria-hidden="true">
278
+ {notification.icon}
279
+ </div>
280
+ )}
281
+
282
+ {/* Content */}
283
+ <div className="flex-1 min-w-0">
284
+ <div className="flex items-start justify-between gap-2">
285
+ <p className={cn(
286
+ "text-sm truncate",
287
+ !notification.read ? "font-semibold text-foreground" : "font-medium text-foreground"
288
+ )}>
289
+ {notification.title}
290
+ </p>
291
+ {/* Unread dot */}
292
+ {!notification.read && (
293
+ <span
294
+ className="shrink-0 mt-1.5 h-2 w-2 rounded-full bg-primary"
295
+ aria-label="Unread"
296
+ />
297
+ )}
298
+ </div>
299
+ {notification.description && (
300
+ <p className="text-xs text-foreground-secondary mt-0.5 line-clamp-2">
301
+ {notification.description}
302
+ </p>
303
+ )}
304
+ <div className="flex items-center gap-2 mt-1.5">
305
+ <span className="text-[11px] text-foreground-secondary">
306
+ {formatTimestamp(notification.timestamp)}
307
+ </span>
308
+ {notification.action && (
309
+ <button
310
+ type="button"
311
+ onClick={(e) => {
312
+ e.stopPropagation()
313
+ notification.action!.onClick()
314
+ }}
315
+ className="text-[11px] font-medium text-primary hover:text-primary/80 transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring rounded"
316
+ >
317
+ {notification.action.label}
318
+ </button>
319
+ )}
320
+ </div>
321
+ </div>
322
+
323
+ {/* Actions */}
324
+ <div className="shrink-0 flex flex-col gap-1">
325
+ {!notification.read && onMarkRead && (
326
+ <button
327
+ type="button"
328
+ onClick={(e) => {
329
+ e.stopPropagation()
330
+ onMarkRead(notification.id)
331
+ }}
332
+ className="rounded-xs p-1 text-foreground-secondary hover:text-foreground hover:bg-muted transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
333
+ aria-label={`Mark "${notification.title}" as read`}
334
+ >
335
+ <CheckIcon />
336
+ </button>
337
+ )}
338
+ {onDismiss && (
339
+ <button
340
+ type="button"
341
+ onClick={(e) => {
342
+ e.stopPropagation()
343
+ onDismiss(notification.id)
344
+ }}
345
+ className="rounded-xs p-1 text-foreground-secondary hover:text-foreground hover:bg-muted transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
346
+ aria-label={`Dismiss "${notification.title}"`}
347
+ >
348
+ <XIcon />
349
+ </button>
350
+ )}
351
+ </div>
352
+ </div>
353
+ ))}
354
+ </div>
355
+ ))
356
+ )}
357
+ </div>
358
+ </div>
359
+ )}
360
+ </div>
361
+ )
362
+ }
363
+
364
+ export { NotificationCenter }
@@ -0,0 +1,40 @@
1
+ import { render, screen } from '@testing-library/react'
2
+ import userEvent from '@testing-library/user-event'
3
+ import { describe, it, expect } from 'vitest'
4
+ import { Popover, PopoverTrigger, PopoverContent } from './Popover'
5
+
6
+ describe('Popover', () => {
7
+ it('renders trigger', () => {
8
+ render(
9
+ <Popover>
10
+ <PopoverTrigger>Open</PopoverTrigger>
11
+ <PopoverContent>Popover content</PopoverContent>
12
+ </Popover>
13
+ )
14
+ expect(screen.getByRole('button', { name: /open/i })).toBeInTheDocument()
15
+ })
16
+
17
+ it('opens on click', async () => {
18
+ const user = userEvent.setup()
19
+ render(
20
+ <Popover>
21
+ <PopoverTrigger>Open</PopoverTrigger>
22
+ <PopoverContent>Popover content</PopoverContent>
23
+ </Popover>
24
+ )
25
+
26
+ expect(screen.queryByText('Popover content')).not.toBeInTheDocument()
27
+ await user.click(screen.getByRole('button', { name: /open/i }))
28
+ expect(screen.getByText('Popover content')).toBeInTheDocument()
29
+ })
30
+
31
+ it('renders open when controlled', () => {
32
+ render(
33
+ <Popover open>
34
+ <PopoverTrigger>Open</PopoverTrigger>
35
+ <PopoverContent>Visible content</PopoverContent>
36
+ </Popover>
37
+ )
38
+ expect(screen.getByText('Visible content')).toBeInTheDocument()
39
+ })
40
+ })
@@ -0,0 +1,46 @@
1
+ "use client";
2
+ import * as React from "react"
3
+ import * as PopoverPrimitive from "@radix-ui/react-popover"
4
+
5
+ import { cn } from "../../lib/utils"
6
+
7
+ const Popover = PopoverPrimitive.Root
8
+
9
+ const PopoverTrigger = PopoverPrimitive.Trigger
10
+
11
+ const PopoverAnchor = PopoverPrimitive.Anchor
12
+
13
+ const PopoverContent = (
14
+ {
15
+ ref,
16
+ className,
17
+ align = "center",
18
+ sideOffset = 4,
19
+ style,
20
+ ...props
21
+ }: React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> & {
22
+ ref?: React.Ref<React.ElementRef<typeof PopoverPrimitive.Content>>;
23
+ }
24
+ ) => (<PopoverPrimitive.Portal>
25
+ <PopoverPrimitive.Content
26
+ ref={ref}
27
+ align={align}
28
+ sideOffset={sideOffset}
29
+ className={cn(
30
+ "z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
31
+ className
32
+ )}
33
+ style={{
34
+ backgroundColor: 'var(--color-popover, #ffffff)',
35
+ color: 'var(--color-popover-foreground, #0a0a0a)',
36
+ border: '1px solid var(--color-border, #d4d4d4)',
37
+ borderRadius: 'var(--radius, 0.5rem)',
38
+ boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)',
39
+ zIndex: 50,
40
+ ...style,
41
+ }}
42
+ {...props}
43
+ />
44
+ </PopoverPrimitive.Portal>)
45
+
46
+ export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
@@ -0,0 +1,163 @@
1
+ "use client";
2
+ import * as React from "react"
3
+ import * as SheetPrimitive from "@radix-ui/react-dialog"
4
+ import { cva, type VariantProps } from "class-variance-authority"
5
+ import { X } from "lucide-react"
6
+
7
+ import { cn } from "../../lib/utils"
8
+
9
+ const Sheet = SheetPrimitive.Root
10
+
11
+ const SheetTrigger = SheetPrimitive.Trigger
12
+
13
+ const SheetClose = SheetPrimitive.Close
14
+
15
+ const SheetPortal = SheetPrimitive.Portal
16
+
17
+ const SheetOverlay = (
18
+ {
19
+ ref,
20
+ className,
21
+ style,
22
+ ...props
23
+ }: React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay> & {
24
+ ref?: React.Ref<React.ElementRef<typeof SheetPrimitive.Overlay>>;
25
+ }
26
+ ) => (<SheetPrimitive.Overlay
27
+ className={cn(
28
+ "fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
29
+ className
30
+ )}
31
+ style={{
32
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
33
+ zIndex: 50,
34
+ ...style,
35
+ }}
36
+ {...props}
37
+ ref={ref}
38
+ />)
39
+
40
+ const sheetVariants = cva(
41
+ "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
42
+ {
43
+ variants: {
44
+ side: {
45
+ top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
46
+ bottom:
47
+ "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
48
+ left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
49
+ right:
50
+ "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
51
+ },
52
+ },
53
+ defaultVariants: {
54
+ side: "right",
55
+ },
56
+ }
57
+ )
58
+
59
+ interface SheetContentProps
60
+ extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
61
+ VariantProps<typeof sheetVariants> {}
62
+
63
+ const SheetContent = (
64
+ {
65
+ ref,
66
+ side = "right",
67
+ className,
68
+ children,
69
+ style,
70
+ ...props
71
+ }: SheetContentProps & {
72
+ ref?: React.Ref<React.ElementRef<typeof SheetPrimitive.Content>>;
73
+ }
74
+ ) => (<SheetPortal>
75
+ <SheetOverlay />
76
+ <SheetPrimitive.Content
77
+ ref={ref}
78
+ className={cn(sheetVariants({ side }), className)}
79
+ style={{
80
+ backgroundColor: 'var(--color-background, #ffffff)',
81
+ boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)',
82
+ zIndex: 50,
83
+ ...style,
84
+ }}
85
+ {...props}
86
+ >
87
+ {children}
88
+ <SheetPrimitive.Close className="absolute right-4 top-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
89
+ <X className="h-4 w-4" />
90
+ <span className="sr-only">Close</span>
91
+ </SheetPrimitive.Close>
92
+ </SheetPrimitive.Content>
93
+ </SheetPortal>)
94
+
95
+ const SheetHeader = ({
96
+ className,
97
+ ...props
98
+ }: React.HTMLAttributes<HTMLDivElement>) => (
99
+ <div
100
+ className={cn(
101
+ "flex flex-col space-y-2 text-center sm:text-left",
102
+ className
103
+ )}
104
+ {...props}
105
+ />
106
+ )
107
+ SheetHeader.displayName = "SheetHeader"
108
+
109
+ const SheetFooter = ({
110
+ className,
111
+ ...props
112
+ }: React.HTMLAttributes<HTMLDivElement>) => (
113
+ <div
114
+ className={cn(
115
+ "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
116
+ className
117
+ )}
118
+ {...props}
119
+ />
120
+ )
121
+ SheetFooter.displayName = "SheetFooter"
122
+
123
+ const SheetTitle = (
124
+ {
125
+ ref,
126
+ className,
127
+ ...props
128
+ }: React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title> & {
129
+ ref?: React.Ref<React.ElementRef<typeof SheetPrimitive.Title>>;
130
+ }
131
+ ) => (<SheetPrimitive.Title
132
+ ref={ref}
133
+ className={cn("text-lg font-semibold text-foreground", className)}
134
+ {...props}
135
+ />)
136
+
137
+ const SheetDescription = (
138
+ {
139
+ ref,
140
+ className,
141
+ ...props
142
+ }: React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description> & {
143
+ ref?: React.Ref<React.ElementRef<typeof SheetPrimitive.Description>>;
144
+ }
145
+ ) => (<SheetPrimitive.Description
146
+ ref={ref}
147
+ className={cn("text-sm text-muted-foreground", className)}
148
+ {...props}
149
+ />)
150
+
151
+ export {
152
+ Sheet,
153
+ sheetVariants,
154
+ SheetPortal,
155
+ SheetOverlay,
156
+ SheetTrigger,
157
+ SheetClose,
158
+ SheetContent,
159
+ SheetHeader,
160
+ SheetFooter,
161
+ SheetTitle,
162
+ SheetDescription,
163
+ }
@@ -0,0 +1,33 @@
1
+ import { render, screen } from '@testing-library/react'
2
+ import userEvent from '@testing-library/user-event'
3
+ import { describe, it, expect } from 'vitest'
4
+ import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from './Tooltip'
5
+
6
+ describe('Tooltip', () => {
7
+ it('renders trigger', () => {
8
+ render(
9
+ <TooltipProvider>
10
+ <Tooltip>
11
+ <TooltipTrigger>Hover me</TooltipTrigger>
12
+ <TooltipContent>Tooltip text</TooltipContent>
13
+ </Tooltip>
14
+ </TooltipProvider>
15
+ )
16
+ expect(screen.getByText('Hover me')).toBeInTheDocument()
17
+ })
18
+
19
+ it('shows content on hover', async () => {
20
+ const user = userEvent.setup()
21
+ render(
22
+ <TooltipProvider delayDuration={0}>
23
+ <Tooltip>
24
+ <TooltipTrigger>Hover me</TooltipTrigger>
25
+ <TooltipContent>Helpful info</TooltipContent>
26
+ </Tooltip>
27
+ </TooltipProvider>
28
+ )
29
+
30
+ await user.hover(screen.getByText('Hover me'))
31
+ expect(await screen.findByRole('tooltip')).toBeInTheDocument()
32
+ })
33
+ })
@@ -0,0 +1,32 @@
1
+ "use client";
2
+ import * as React from "react"
3
+ import * as TooltipPrimitive from "@radix-ui/react-tooltip"
4
+
5
+ import { cn } from "../../lib/utils"
6
+
7
+ const TooltipProvider = TooltipPrimitive.Provider
8
+
9
+ const Tooltip = TooltipPrimitive.Root
10
+
11
+ const TooltipTrigger = TooltipPrimitive.Trigger
12
+
13
+ const TooltipContent = (
14
+ {
15
+ ref,
16
+ className,
17
+ sideOffset = 4,
18
+ ...props
19
+ }: React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content> & {
20
+ ref?: React.Ref<React.ElementRef<typeof TooltipPrimitive.Content>>;
21
+ }
22
+ ) => (<TooltipPrimitive.Content
23
+ ref={ref}
24
+ sideOffset={sideOffset}
25
+ className={cn(
26
+ "z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
27
+ className
28
+ )}
29
+ {...props}
30
+ />)
31
+
32
+ export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
@@ -0,0 +1,12 @@
1
+ export * from './AlertDialog';
2
+ export * from './ContextMenu';
3
+ export * from './Dialog';
4
+ export * from './Drawer';
5
+ export * from './Dropdown';
6
+ export * from './DropdownMenu';
7
+ export * from './HoverCard';
8
+ export * from './Modal';
9
+ export * from './Popover';
10
+ export * from './Sheet';
11
+ export * from './Tooltip';
12
+ export * from './NotificationCenter';