@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.
- package/.claude/CLAUDE.md +239 -0
- package/README.md +161 -0
- package/dist/cli.mjs +151 -0
- package/dist/dates.d.mts +20 -0
- package/dist/dates.d.ts +20 -0
- package/dist/dates.js +240 -0
- package/dist/dates.js.map +1 -0
- package/dist/dates.mjs +203 -0
- package/dist/dates.mjs.map +1 -0
- package/dist/dnd.d.mts +126 -0
- package/dist/dnd.d.ts +126 -0
- package/dist/dnd.js +274 -0
- package/dist/dnd.js.map +1 -0
- package/dist/dnd.mjs +250 -0
- package/dist/dnd.mjs.map +1 -0
- package/dist/fontThemes-Dh8mtXES.d.mts +868 -0
- package/dist/fontThemes-Dh8mtXES.d.ts +868 -0
- package/dist/forms.d.mts +38 -0
- package/dist/forms.d.ts +38 -0
- package/dist/forms.js +198 -0
- package/dist/forms.js.map +1 -0
- package/dist/forms.mjs +159 -0
- package/dist/forms.mjs.map +1 -0
- package/dist/hooks-1b8WaQf1.d.mts +225 -0
- package/dist/hooks-CKW8vE9H.d.ts +225 -0
- package/dist/hooks.d.mts +3 -0
- package/dist/hooks.d.ts +3 -0
- package/dist/hooks.js +971 -0
- package/dist/hooks.js.map +1 -0
- package/dist/hooks.mjs +943 -0
- package/dist/hooks.mjs.map +1 -0
- package/dist/index-DscTIrZ2.d.mts +29 -0
- package/dist/index-DscTIrZ2.d.ts +29 -0
- package/dist/index.d.mts +3382 -0
- package/dist/index.d.ts +3382 -0
- package/dist/index.js +15146 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +14802 -0
- package/dist/index.mjs.map +1 -0
- package/dist/providers-CXPDMsl7.d.mts +30 -0
- package/dist/providers-Dn_Msjvz.d.ts +30 -0
- package/dist/providers.d.mts +3 -0
- package/dist/providers.d.ts +3 -0
- package/dist/providers.js +1885 -0
- package/dist/providers.js.map +1 -0
- package/dist/providers.mjs +1859 -0
- package/dist/providers.mjs.map +1 -0
- package/dist/tables.d.mts +10 -0
- package/dist/tables.d.ts +10 -0
- package/dist/tables.js +248 -0
- package/dist/tables.js.map +1 -0
- package/dist/tables.mjs +218 -0
- package/dist/tables.mjs.map +1 -0
- package/dist/tokens.d.mts +1065 -0
- package/dist/tokens.d.ts +1065 -0
- package/dist/tokens.js +2637 -0
- package/dist/tokens.js.map +1 -0
- package/dist/tokens.mjs +2555 -0
- package/dist/tokens.mjs.map +1 -0
- package/dist/utils-CIIM7dAC.d.ts +986 -0
- package/dist/utils-Cs04sxth.d.mts +986 -0
- package/dist/utils.d.mts +4 -0
- package/dist/utils.d.ts +4 -0
- package/dist/utils.js +874 -0
- package/dist/utils.js.map +1 -0
- package/dist/utils.mjs +806 -0
- package/dist/utils.mjs.map +1 -0
- package/dist/validation-Bj1ye-v_.d.mts +114 -0
- package/dist/validation-Bj1ye-v_.d.ts +114 -0
- package/dist/webgl.d.mts +104 -0
- package/dist/webgl.d.ts +104 -0
- package/dist/webgl.js +226 -0
- package/dist/webgl.js.map +1 -0
- package/dist/webgl.mjs +195 -0
- package/dist/webgl.mjs.map +1 -0
- package/package.json +267 -0
- package/src/cli.ts +206 -0
- package/src/component-registry.ts +183 -0
- package/src/components/actions/Button.test.tsx +61 -0
- package/src/components/actions/Button.tsx +70 -0
- package/src/components/actions/Link.tsx +78 -0
- package/src/components/actions/Magnetic.tsx +68 -0
- package/src/components/actions/Toggle.test.tsx +40 -0
- package/src/components/actions/Toggle.tsx +47 -0
- package/src/components/actions/ToggleGroup.tsx +70 -0
- package/src/components/actions/index.ts +5 -0
- package/src/components/backgrounds/FaultyTerminal.tsx +426 -0
- package/src/components/backgrounds/OrbBackground.tsx +424 -0
- package/src/components/backgrounds/WarpBackground.tsx +358 -0
- package/src/components/backgrounds/index.ts +3 -0
- package/src/components/blocks/Hero.tsx +142 -0
- package/src/components/blocks/social/OpenGraphCard.tsx +243 -0
- package/src/components/cursor/SplashCursor.tsx +1315 -0
- package/src/components/cursor/TargetCursor.tsx +187 -0
- package/src/components/cursor/index.ts +2 -0
- package/src/components/data-display/AspectImage.tsx +73 -0
- package/src/components/data-display/Avatar.test.tsx +35 -0
- package/src/components/data-display/Avatar.tsx +55 -0
- package/src/components/data-display/Badge.test.tsx +43 -0
- package/src/components/data-display/Badge.tsx +84 -0
- package/src/components/data-display/Brand.tsx +123 -0
- package/src/components/data-display/Calendar.tsx +70 -0
- package/src/components/data-display/Card.test.tsx +92 -0
- package/src/components/data-display/Card.tsx +115 -0
- package/src/components/data-display/Code.tsx +210 -0
- package/src/components/data-display/CollapsibleCodeBlock.tsx +238 -0
- package/src/components/data-display/DataTable.tsx +119 -0
- package/src/components/data-display/DescriptionList.tsx +41 -0
- package/src/components/data-display/GitHubIcon.tsx +44 -0
- package/src/components/data-display/Heading.test.tsx +36 -0
- package/src/components/data-display/Heading.tsx +83 -0
- package/src/components/data-display/StatCard.tsx +195 -0
- package/src/components/data-display/Table.tsx +133 -0
- package/src/components/data-display/Text.test.tsx +48 -0
- package/src/components/data-display/Text.tsx +144 -0
- package/src/components/data-display/Timeline.tsx +194 -0
- package/src/components/data-display/TreeView.tsx +226 -0
- package/src/components/data-display/Typewriter.tsx +119 -0
- package/src/components/data-display/VariableWeightText.tsx +130 -0
- package/src/components/data-display/index.ts +19 -0
- package/src/components/feedback/Alert.test.tsx +44 -0
- package/src/components/feedback/Alert.tsx +65 -0
- package/src/components/feedback/EmptyState.tsx +113 -0
- package/src/components/feedback/Progress.test.tsx +60 -0
- package/src/components/feedback/Progress.tsx +30 -0
- package/src/components/feedback/ProgressBar.tsx +158 -0
- package/src/components/feedback/Skeleton.test.tsx +39 -0
- package/src/components/feedback/Skeleton.tsx +45 -0
- package/src/components/feedback/Sonner.tsx +28 -0
- package/src/components/feedback/Spinner.test.tsx +33 -0
- package/src/components/feedback/Spinner.tsx +99 -0
- package/src/components/feedback/Stepper.tsx +307 -0
- package/src/components/feedback/Toast/Toast.tsx +243 -0
- package/src/components/feedback/Toast/index.ts +2 -0
- package/src/components/feedback/index.ts +9 -0
- package/src/components/forms/Checkbox.test.tsx +40 -0
- package/src/components/forms/Checkbox.tsx +31 -0
- package/src/components/forms/ColorPicker.tsx +118 -0
- package/src/components/forms/Combobox.tsx +96 -0
- package/src/components/forms/DragDrop.tsx +440 -0
- package/src/components/forms/FileUpload.tsx +252 -0
- package/src/components/forms/FilterButton.tsx +65 -0
- package/src/components/forms/Form.tsx +197 -0
- package/src/components/forms/Input.test.tsx +46 -0
- package/src/components/forms/Input.tsx +43 -0
- package/src/components/forms/InputOTP.tsx +81 -0
- package/src/components/forms/Label.test.tsx +20 -0
- package/src/components/forms/Label.tsx +25 -0
- package/src/components/forms/RadioGroup.tsx +51 -0
- package/src/components/forms/SearchBar.tsx +215 -0
- package/src/components/forms/Select.test.tsx +118 -0
- package/src/components/forms/Select.tsx +274 -0
- package/src/components/forms/Slider.tsx +29 -0
- package/src/components/forms/Switch.test.tsx +76 -0
- package/src/components/forms/Switch.tsx +30 -0
- package/src/components/forms/TextField.tsx +152 -0
- package/src/components/forms/Textarea.test.tsx +41 -0
- package/src/components/forms/Textarea.tsx +29 -0
- package/src/components/forms/ThemeSwitcher.tsx +290 -0
- package/src/components/forms/ThemeToggle.tsx +151 -0
- package/src/components/forms/index.ts +19 -0
- package/src/components/layout/Accordion.test.tsx +66 -0
- package/src/components/layout/Accordion.tsx +64 -0
- package/src/components/layout/AspectRatio.tsx +7 -0
- package/src/components/layout/Carousel.tsx +277 -0
- package/src/components/layout/Collapsible.test.tsx +40 -0
- package/src/components/layout/Collapsible.tsx +31 -0
- package/src/components/layout/Container.test.tsx +45 -0
- package/src/components/layout/Container.tsx +99 -0
- package/src/components/layout/CustomizerPanel.tsx +400 -0
- package/src/components/layout/DatePicker.tsx +57 -0
- package/src/components/layout/Footer/Footer.tsx +175 -0
- package/src/components/layout/Footer/index.ts +2 -0
- package/src/components/layout/GlassSurface.tsx +82 -0
- package/src/components/layout/Grid.test.tsx +31 -0
- package/src/components/layout/Grid.tsx +130 -0
- package/src/components/layout/Header/Header.tsx +450 -0
- package/src/components/layout/Header/index.ts +2 -0
- package/src/components/layout/PageLayout.tsx +180 -0
- package/src/components/layout/PageTemplate.tsx +158 -0
- package/src/components/layout/Resizable.tsx +48 -0
- package/src/components/layout/ScrollArea.tsx +53 -0
- package/src/components/layout/Separator.test.tsx +28 -0
- package/src/components/layout/Separator.tsx +29 -0
- package/src/components/layout/Sidebar.tsx +171 -0
- package/src/components/layout/Stack.test.tsx +41 -0
- package/src/components/layout/Stack.tsx +89 -0
- package/src/components/layout/glass-surface.css +60 -0
- package/src/components/layout/index.ts +18 -0
- package/src/components/motion/AnimatedBeam.tsx +159 -0
- package/src/components/navigation/Breadcrumb.test.tsx +57 -0
- package/src/components/navigation/Breadcrumb.tsx +119 -0
- package/src/components/navigation/Breadcrumbs.tsx +221 -0
- package/src/components/navigation/Command.tsx +159 -0
- package/src/components/navigation/Menubar.tsx +115 -0
- package/src/components/navigation/NavLink.tsx +55 -0
- package/src/components/navigation/NavigationMenu.tsx +125 -0
- package/src/components/navigation/Pagination.tsx +121 -0
- package/src/components/navigation/SecondaryNav.tsx +100 -0
- package/src/components/navigation/Tabs.test.tsx +47 -0
- package/src/components/navigation/Tabs.tsx +60 -0
- package/src/components/navigation/TertiaryNav.tsx +90 -0
- package/src/components/navigation/index.ts +10 -0
- package/src/components/overlays/AlertDialog.test.tsx +69 -0
- package/src/components/overlays/AlertDialog.tsx +166 -0
- package/src/components/overlays/ContextMenu.tsx +243 -0
- package/src/components/overlays/Dialog.test.tsx +79 -0
- package/src/components/overlays/Dialog.tsx +158 -0
- package/src/components/overlays/Drawer.tsx +128 -0
- package/src/components/overlays/Dropdown.tsx +253 -0
- package/src/components/overlays/DropdownMenu.tsx +242 -0
- package/src/components/overlays/HoverCard.tsx +32 -0
- package/src/components/overlays/Modal.tsx +250 -0
- package/src/components/overlays/NotificationCenter.tsx +364 -0
- package/src/components/overlays/Popover.test.tsx +40 -0
- package/src/components/overlays/Popover.tsx +46 -0
- package/src/components/overlays/Sheet.tsx +163 -0
- package/src/components/overlays/Tooltip.test.tsx +33 -0
- package/src/components/overlays/Tooltip.tsx +32 -0
- package/src/components/overlays/index.ts +12 -0
- package/src/dates.ts +2 -0
- package/src/dnd.ts +1 -0
- package/src/forms.ts +1 -0
- package/src/globals.css +187 -0
- package/src/hooks/index.ts +6 -0
- package/src/hooks/useForm.ts +247 -0
- package/src/hooks/useMotionPreference.test.ts +102 -0
- package/src/hooks/useMotionPreference.ts +78 -0
- package/src/hooks/useTheme.ts +58 -0
- package/src/hooks.ts +9 -0
- package/src/index.ts +168 -0
- package/src/lib/animations.ts +356 -0
- package/src/lib/breadcrumbs.ts +94 -0
- package/src/lib/colors.ts +493 -0
- package/src/lib/store/customizer.ts +482 -0
- package/src/lib/store/index.ts +3 -0
- package/src/lib/store/theme.ts +55 -0
- package/src/lib/syntax-parser/index.ts +50 -0
- package/src/lib/syntax-parser/patterns.ts +64 -0
- package/src/lib/syntax-parser/tokenizer.ts +117 -0
- package/src/lib/syntax-parser/types.ts +27 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/validation.ts +204 -0
- package/src/lib/webgl/Color.ts +11 -0
- package/src/lib/webgl/Mesh.ts +41 -0
- package/src/lib/webgl/Program.ts +118 -0
- package/src/lib/webgl/Renderer.ts +51 -0
- package/src/lib/webgl/Triangle.ts +27 -0
- package/src/lib/webgl/Vec3.ts +18 -0
- package/src/lib/webgl/index.ts +13 -0
- package/src/nativewind-env.d.ts +1 -0
- package/src/providers/ThemeProvider.tsx +461 -0
- package/src/providers/index.ts +1 -0
- package/src/providers.ts +7 -0
- package/src/tables.ts +1 -0
- package/src/test/setup.ts +39 -0
- package/src/theme.css +158 -0
- package/src/tokens.ts +7 -0
- package/src/utils.ts +12 -0
- 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';
|