@annondeveloper/ui-kit 0.1.0 → 0.2.0

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 (65) hide show
  1. package/dist/{chunk-5OKSXPWK.js → chunk-2DWZVHZS.js} +2 -2
  2. package/dist/chunk-2DWZVHZS.js.map +1 -0
  3. package/dist/form.d.ts +6 -6
  4. package/dist/form.js +1 -1
  5. package/dist/form.js.map +1 -1
  6. package/dist/index.d.ts +508 -52
  7. package/dist/index.js +2927 -4
  8. package/dist/index.js.map +1 -1
  9. package/dist/{select-nnBJUO8U.d.ts → select-B2wXqqSM.d.ts} +2 -2
  10. package/package.json +1 -1
  11. package/src/components/animated-counter.tsx +2 -1
  12. package/src/components/avatar.tsx +2 -1
  13. package/src/components/badge.tsx +3 -2
  14. package/src/components/button.tsx +3 -2
  15. package/src/components/card.tsx +13 -12
  16. package/src/components/checkbox.tsx +3 -2
  17. package/src/components/color-input.tsx +414 -0
  18. package/src/components/command-bar.tsx +434 -0
  19. package/src/components/confidence-bar.tsx +115 -0
  20. package/src/components/confirm-dialog.tsx +2 -1
  21. package/src/components/copy-block.tsx +229 -0
  22. package/src/components/data-table.tsx +2 -1
  23. package/src/components/diff-viewer.tsx +319 -0
  24. package/src/components/dropdown-menu.tsx +2 -1
  25. package/src/components/empty-state.tsx +2 -1
  26. package/src/components/filter-pill.tsx +2 -1
  27. package/src/components/form-input.tsx +5 -4
  28. package/src/components/heatmap-calendar.tsx +213 -0
  29. package/src/components/infinite-scroll.tsx +243 -0
  30. package/src/components/kanban-column.tsx +198 -0
  31. package/src/components/live-feed.tsx +220 -0
  32. package/src/components/log-viewer.tsx +2 -1
  33. package/src/components/metric-card.tsx +2 -1
  34. package/src/components/notification-stack.tsx +226 -0
  35. package/src/components/pipeline-stage.tsx +2 -1
  36. package/src/components/popover.tsx +2 -1
  37. package/src/components/port-status-grid.tsx +2 -1
  38. package/src/components/progress.tsx +2 -1
  39. package/src/components/radio-group.tsx +2 -1
  40. package/src/components/realtime-value.tsx +283 -0
  41. package/src/components/select.tsx +2 -1
  42. package/src/components/severity-timeline.tsx +2 -1
  43. package/src/components/sheet.tsx +2 -1
  44. package/src/components/skeleton.tsx +4 -3
  45. package/src/components/slider.tsx +2 -1
  46. package/src/components/smart-table.tsx +383 -0
  47. package/src/components/sortable-list.tsx +268 -0
  48. package/src/components/sparkline.tsx +2 -1
  49. package/src/components/status-badge.tsx +2 -1
  50. package/src/components/status-pulse.tsx +2 -1
  51. package/src/components/step-wizard.tsx +372 -0
  52. package/src/components/streaming-text.tsx +163 -0
  53. package/src/components/success-checkmark.tsx +2 -1
  54. package/src/components/tabs.tsx +2 -1
  55. package/src/components/threshold-gauge.tsx +2 -1
  56. package/src/components/time-range-selector.tsx +2 -1
  57. package/src/components/toast.tsx +2 -1
  58. package/src/components/toggle-switch.tsx +2 -1
  59. package/src/components/tooltip.tsx +2 -1
  60. package/src/components/truncated-text.tsx +2 -1
  61. package/src/components/typing-indicator.tsx +123 -0
  62. package/src/components/uptime-tracker.tsx +2 -1
  63. package/src/components/utilization-bar.tsx +2 -1
  64. package/src/utils.ts +1 -1
  65. package/dist/chunk-5OKSXPWK.js.map +0 -1
@@ -0,0 +1,220 @@
1
+ 'use client'
2
+
3
+ import type React from 'react'
4
+ import { useCallback, useEffect, useRef, useState } from 'react'
5
+ import { motion, AnimatePresence, useReducedMotion } from 'framer-motion'
6
+ import { cn } from '../utils'
7
+ import { Pause, Play, ChevronUp } from 'lucide-react'
8
+
9
+ export interface FeedItem {
10
+ id: string
11
+ content: React.ReactNode
12
+ timestamp: string | Date
13
+ type?: 'info' | 'success' | 'warning' | 'error'
14
+ }
15
+
16
+ export interface LiveFeedProps {
17
+ /** Array of feed items, newest first. */
18
+ items: FeedItem[]
19
+ /** Maximum visible items before oldest are faded out. */
20
+ maxVisible?: number
21
+ /** Show relative timestamps beside each item. */
22
+ showTimestamps?: boolean
23
+ /** Auto-scroll to newest items. */
24
+ autoScroll?: boolean
25
+ /** Callback when an item is clicked. */
26
+ onItemClick?: (item: FeedItem) => void
27
+ /** Message shown when items array is empty. */
28
+ emptyMessage?: string
29
+ className?: string
30
+ }
31
+
32
+ const TYPE_BORDER: Record<string, string> = {
33
+ info: 'border-l-[hsl(var(--brand-secondary))]',
34
+ success: 'border-l-[hsl(var(--status-ok))]',
35
+ warning: 'border-l-[hsl(var(--status-warning))]',
36
+ error: 'border-l-[hsl(var(--status-critical))]',
37
+ }
38
+
39
+ function relativeTime(ts: string | Date): string {
40
+ const diff = (Date.now() - new Date(ts).getTime()) / 1000
41
+ if (diff < 5) return 'now'
42
+ if (diff < 60) return `${Math.floor(diff)}s ago`
43
+ if (diff < 3600) return `${Math.floor(diff / 60)}m ago`
44
+ if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`
45
+ return `${Math.floor(diff / 86400)}d ago`
46
+ }
47
+
48
+ /**
49
+ * @description A real-time event feed with animated item entry, auto-scroll,
50
+ * pause/resume controls, type-colored borders, and relative timestamps.
51
+ */
52
+ export function LiveFeed({
53
+ items,
54
+ maxVisible = 50,
55
+ showTimestamps = true,
56
+ autoScroll: autoScrollProp = true,
57
+ onItemClick,
58
+ emptyMessage = 'No events yet',
59
+ className,
60
+ }: LiveFeedProps): React.JSX.Element {
61
+ const reduced = useReducedMotion()
62
+ const scrollRef = useRef<HTMLDivElement>(null)
63
+ const [paused, setPaused] = useState(false)
64
+ const [userScrolled, setUserScrolled] = useState(false)
65
+ const [newCount, setNewCount] = useState(0)
66
+ const prevCountRef = useRef(items.length)
67
+
68
+ // Track new items arriving while scrolled away
69
+ useEffect(() => {
70
+ const diff = items.length - prevCountRef.current
71
+ if (diff > 0 && (paused || userScrolled)) {
72
+ setNewCount((c) => c + diff)
73
+ }
74
+ prevCountRef.current = items.length
75
+ }, [items.length, paused, userScrolled])
76
+
77
+ // Auto-scroll
78
+ useEffect(() => {
79
+ if (autoScrollProp && !paused && !userScrolled && scrollRef.current) {
80
+ scrollRef.current.scrollTop = 0
81
+ }
82
+ }, [items, autoScrollProp, paused, userScrolled])
83
+
84
+ const handleScroll = useCallback(() => {
85
+ if (!scrollRef.current) return
86
+ const { scrollTop } = scrollRef.current
87
+ // scrollTop is 0 at top (newest), positive when scrolled down (older)
88
+ setUserScrolled(scrollTop > 40)
89
+ }, [])
90
+
91
+ const scrollToTop = useCallback(() => {
92
+ if (scrollRef.current) {
93
+ scrollRef.current.scrollTop = 0
94
+ }
95
+ setUserScrolled(false)
96
+ setNewCount(0)
97
+ }, [])
98
+
99
+ const togglePause = useCallback(() => {
100
+ setPaused((p) => {
101
+ if (p) {
102
+ setNewCount(0)
103
+ setUserScrolled(false)
104
+ }
105
+ return !p
106
+ })
107
+ }, [])
108
+
109
+ // Relative timestamp updater
110
+ const [, setTick] = useState(0)
111
+ useEffect(() => {
112
+ if (!showTimestamps) return
113
+ const id = setInterval(() => setTick((t) => t + 1), 10_000)
114
+ return () => clearInterval(id)
115
+ }, [showTimestamps])
116
+
117
+ const visibleItems = items.slice(0, maxVisible)
118
+
119
+ return (
120
+ <div className={cn('relative flex flex-col', className)}>
121
+ {/* Controls bar */}
122
+ <div className="flex items-center justify-between px-3 py-2 border-b border-[hsl(var(--border-subtle))]">
123
+ <span className="text-xs font-medium text-[hsl(var(--text-secondary))]">
124
+ {items.length} events
125
+ </span>
126
+ <button
127
+ type="button"
128
+ onClick={togglePause}
129
+ className={cn(
130
+ 'inline-flex items-center gap-1 px-2 py-1 rounded-md text-xs font-medium cursor-pointer',
131
+ 'text-[hsl(var(--text-secondary))] hover:text-[hsl(var(--text-primary))]',
132
+ 'hover:bg-[hsl(var(--bg-overlay))] transition-colors duration-150',
133
+ )}
134
+ aria-label={paused ? 'Resume auto-scroll' : 'Pause auto-scroll'}
135
+ >
136
+ {paused ? <Play className="size-3" /> : <Pause className="size-3" />}
137
+ {paused ? 'Resume' : 'Pause'}
138
+ </button>
139
+ </div>
140
+
141
+ {/* Feed container */}
142
+ <div
143
+ ref={scrollRef}
144
+ onScroll={handleScroll}
145
+ className="flex-1 overflow-y-auto"
146
+ >
147
+ {visibleItems.length === 0 ? (
148
+ <div className="flex items-center justify-center py-12 text-sm text-[hsl(var(--text-tertiary))]">
149
+ {emptyMessage}
150
+ </div>
151
+ ) : (
152
+ <AnimatePresence initial={false}>
153
+ {visibleItems.map((item) => (
154
+ <motion.div
155
+ key={item.id}
156
+ layout={!reduced}
157
+ initial={reduced ? undefined : { opacity: 0, y: -12, height: 0 }}
158
+ animate={{ opacity: 1, y: 0, height: 'auto' }}
159
+ exit={reduced ? undefined : { opacity: 0, height: 0 }}
160
+ transition={{ duration: reduced ? 0 : 0.2 }}
161
+ >
162
+ <div
163
+ role={onItemClick ? 'button' : undefined}
164
+ tabIndex={onItemClick ? 0 : undefined}
165
+ onClick={onItemClick ? () => onItemClick(item) : undefined}
166
+ onKeyDown={
167
+ onItemClick
168
+ ? (e) => { if (e.key === 'Enter' || e.key === ' ') onItemClick(item) }
169
+ : undefined
170
+ }
171
+ className={cn(
172
+ 'flex items-start gap-3 px-3 py-2.5 border-l-2',
173
+ 'border-b border-b-[hsl(var(--border-subtle))]',
174
+ TYPE_BORDER[item.type ?? 'info'],
175
+ onItemClick && 'cursor-pointer hover:bg-[hsl(var(--bg-surface))] transition-colors duration-100',
176
+ )}
177
+ >
178
+ <div className="flex-1 min-w-0 text-sm text-[hsl(var(--text-primary))]">
179
+ {item.content}
180
+ </div>
181
+ {showTimestamps && (
182
+ <span
183
+ className="shrink-0 text-[10px] tabular-nums text-[hsl(var(--text-tertiary))] mt-0.5"
184
+ title={new Date(item.timestamp).toISOString()}
185
+ >
186
+ {relativeTime(item.timestamp)}
187
+ </span>
188
+ )}
189
+ </div>
190
+ </motion.div>
191
+ ))}
192
+ </AnimatePresence>
193
+ )}
194
+ </div>
195
+
196
+ {/* "N new items" floating badge */}
197
+ <AnimatePresence>
198
+ {newCount > 0 && (userScrolled || paused) && (
199
+ <motion.button
200
+ type="button"
201
+ initial={reduced ? undefined : { opacity: 0, y: 8 }}
202
+ animate={{ opacity: 1, y: 0 }}
203
+ exit={reduced ? undefined : { opacity: 0, y: 8 }}
204
+ transition={{ duration: reduced ? 0 : 0.15 }}
205
+ onClick={scrollToTop}
206
+ className={cn(
207
+ 'absolute top-12 left-1/2 -translate-x-1/2 z-10',
208
+ 'inline-flex items-center gap-1 px-3 py-1.5 rounded-full',
209
+ 'bg-[hsl(var(--brand-primary))] text-white text-xs font-medium',
210
+ 'shadow-lg cursor-pointer hover:brightness-110 transition-[filter] duration-100',
211
+ )}
212
+ >
213
+ <ChevronUp className="size-3" />
214
+ {newCount} new {newCount === 1 ? 'item' : 'items'}
215
+ </motion.button>
216
+ )}
217
+ </AnimatePresence>
218
+ </div>
219
+ )
220
+ }
@@ -1,5 +1,6 @@
1
1
  'use client'
2
2
 
3
+ import type React from 'react'
3
4
  import { useCallback, useEffect, useRef, useState } from 'react'
4
5
  import { motion, AnimatePresence, useReducedMotion } from 'framer-motion'
5
6
  import { ArrowDown, Search } from 'lucide-react'
@@ -78,7 +79,7 @@ export function LogViewer({
78
79
  showLevel = true,
79
80
  onEntryClick,
80
81
  className,
81
- }: LogViewerProps) {
82
+ }: LogViewerProps): React.JSX.Element {
82
83
  const reduced = useReducedMotion()
83
84
  const containerRef = useRef<HTMLDivElement>(null)
84
85
  const [isAtBottom, setIsAtBottom] = useState(true)
@@ -1,5 +1,6 @@
1
1
  'use client'
2
2
 
3
+ import type React from 'react'
3
4
  import { type ElementType } from 'react'
4
5
  import { motion, useReducedMotion } from 'framer-motion'
5
6
  import { TrendingUp, TrendingDown } from 'lucide-react'
@@ -53,7 +54,7 @@ export function MetricCard({
53
54
  status,
54
55
  sparklineData,
55
56
  className,
56
- }: MetricCardProps) {
57
+ }: MetricCardProps): React.JSX.Element {
57
58
  const reduced = useReducedMotion()
58
59
 
59
60
  // Trend calculation
@@ -0,0 +1,226 @@
1
+ 'use client'
2
+
3
+ import type React from 'react'
4
+ import { useEffect, useRef, useCallback, useState } from 'react'
5
+ import { motion, AnimatePresence, useReducedMotion } from 'framer-motion'
6
+ import { cn } from '../utils'
7
+ import {
8
+ Info, CheckCircle2, AlertTriangle, XCircle, X,
9
+ } from 'lucide-react'
10
+
11
+ export interface Notification {
12
+ id: string
13
+ title: string
14
+ message?: string
15
+ type: 'info' | 'success' | 'warning' | 'error'
16
+ action?: { label: string; onClick: () => void }
17
+ /** Whether the notification can be dismissed. */
18
+ dismissible?: boolean
19
+ /** Auto-dismiss after this many ms. 0 = persistent. */
20
+ duration?: number
21
+ timestamp?: Date
22
+ }
23
+
24
+ export interface NotificationStackProps {
25
+ notifications: Notification[]
26
+ /** Callback to dismiss a notification by id. */
27
+ onDismiss: (id: string) => void
28
+ /** Screen corner positioning. */
29
+ position?: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left'
30
+ /** Max visible cards before stacking remainder. */
31
+ maxVisible?: number
32
+ className?: string
33
+ }
34
+
35
+ const TYPE_ICON: Record<Notification['type'], React.FC<{ className?: string }>> = {
36
+ info: Info,
37
+ success: CheckCircle2,
38
+ warning: AlertTriangle,
39
+ error: XCircle,
40
+ }
41
+
42
+ const TYPE_COLOR: Record<Notification['type'], string> = {
43
+ info: 'border-l-[hsl(var(--brand-secondary))]',
44
+ success: 'border-l-[hsl(var(--status-ok))]',
45
+ warning: 'border-l-[hsl(var(--status-warning))]',
46
+ error: 'border-l-[hsl(var(--status-critical))]',
47
+ }
48
+
49
+ const TYPE_ICON_COLOR: Record<Notification['type'], string> = {
50
+ info: 'text-[hsl(var(--brand-secondary))]',
51
+ success: 'text-[hsl(var(--status-ok))]',
52
+ warning: 'text-[hsl(var(--status-warning))]',
53
+ error: 'text-[hsl(var(--status-critical))]',
54
+ }
55
+
56
+ const POSITION_CLASSES: Record<NonNullable<NotificationStackProps['position']>, string> = {
57
+ 'top-right': 'top-4 right-4',
58
+ 'top-left': 'top-4 left-4',
59
+ 'bottom-right': 'bottom-4 right-4',
60
+ 'bottom-left': 'bottom-4 left-4',
61
+ }
62
+
63
+ const SLIDE_FROM: Record<NonNullable<NotificationStackProps['position']>, { x: number }> = {
64
+ 'top-right': { x: 80 },
65
+ 'top-left': { x: -80 },
66
+ 'bottom-right': { x: 80 },
67
+ 'bottom-left': { x: -80 },
68
+ }
69
+
70
+ /**
71
+ * @description A fixed-position notification stack with auto-dismiss progress bars,
72
+ * type-specific icons and colors, action buttons, and stacking overflow.
73
+ */
74
+ export function NotificationStack({
75
+ notifications,
76
+ onDismiss,
77
+ position = 'top-right',
78
+ maxVisible = 5,
79
+ className,
80
+ }: NotificationStackProps): React.JSX.Element {
81
+ const reduced = useReducedMotion()
82
+ const visible = notifications.slice(0, maxVisible)
83
+ const overflow = notifications.length - maxVisible
84
+
85
+ return (
86
+ <div
87
+ className={cn(
88
+ 'fixed z-50 flex flex-col gap-2 w-[360px] max-w-[calc(100vw-2rem)]',
89
+ POSITION_CLASSES[position],
90
+ className,
91
+ )}
92
+ role="region"
93
+ aria-label="Notifications"
94
+ >
95
+ <AnimatePresence initial={false}>
96
+ {visible.map((notification, idx) => (
97
+ <motion.div
98
+ key={notification.id}
99
+ layout={!reduced}
100
+ initial={reduced ? { opacity: 1 } : { opacity: 0, ...SLIDE_FROM[position] }}
101
+ animate={{ opacity: 1, x: 0 }}
102
+ exit={reduced ? { opacity: 0 } : { opacity: 0, ...SLIDE_FROM[position], transition: { duration: 0.15 } }}
103
+ transition={{ type: 'spring', stiffness: 300, damping: 30 }}
104
+ >
105
+ <NotificationCard
106
+ notification={notification}
107
+ onDismiss={onDismiss}
108
+ reduced={!!reduced}
109
+ />
110
+ </motion.div>
111
+ ))}
112
+ </AnimatePresence>
113
+
114
+ {/* Overflow indicator */}
115
+ {overflow > 0 && (
116
+ <div className="text-center text-xs text-[hsl(var(--text-tertiary))] py-1">
117
+ +{overflow} more {overflow === 1 ? 'notification' : 'notifications'}
118
+ </div>
119
+ )}
120
+ </div>
121
+ )
122
+ }
123
+
124
+ function NotificationCard({
125
+ notification,
126
+ onDismiss,
127
+ reduced,
128
+ }: {
129
+ notification: Notification
130
+ onDismiss: (id: string) => void
131
+ reduced: boolean
132
+ }): React.JSX.Element {
133
+ const { id, title, message, type, action, dismissible = true, duration = 0 } = notification
134
+ const Icon = TYPE_ICON[type]
135
+ const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
136
+ const [progress, setProgress] = useState(100)
137
+ const startTimeRef = useRef(Date.now())
138
+
139
+ // Auto-dismiss timer
140
+ useEffect(() => {
141
+ if (duration <= 0) return
142
+
143
+ startTimeRef.current = Date.now()
144
+
145
+ // Progress countdown
146
+ const intervalId = setInterval(() => {
147
+ const elapsed = Date.now() - startTimeRef.current
148
+ const remaining = Math.max(0, 100 - (elapsed / duration) * 100)
149
+ setProgress(remaining)
150
+ if (remaining <= 0) {
151
+ clearInterval(intervalId)
152
+ }
153
+ }, 50)
154
+
155
+ timerRef.current = setTimeout(() => {
156
+ onDismiss(id)
157
+ }, duration)
158
+
159
+ return () => {
160
+ clearInterval(intervalId)
161
+ if (timerRef.current) clearTimeout(timerRef.current)
162
+ }
163
+ }, [id, duration, onDismiss])
164
+
165
+ const handleDismiss = useCallback(() => {
166
+ onDismiss(id)
167
+ }, [id, onDismiss])
168
+
169
+ return (
170
+ <div
171
+ className={cn(
172
+ 'relative overflow-hidden rounded-xl border-l-[3px] shadow-lg',
173
+ 'bg-[hsl(var(--bg-elevated))] border border-[hsl(var(--border-subtle))]',
174
+ TYPE_COLOR[type],
175
+ )}
176
+ >
177
+ <div className="flex items-start gap-3 p-4">
178
+ <Icon className={cn('size-5 shrink-0 mt-0.5', TYPE_ICON_COLOR[type])} />
179
+ <div className="flex-1 min-w-0">
180
+ <p className="text-sm font-medium text-[hsl(var(--text-primary))]">{title}</p>
181
+ {message && (
182
+ <p className="mt-0.5 text-xs text-[hsl(var(--text-secondary))] line-clamp-2">
183
+ {message}
184
+ </p>
185
+ )}
186
+ {action && (
187
+ <button
188
+ type="button"
189
+ onClick={action.onClick}
190
+ className={cn(
191
+ 'mt-2 text-xs font-medium cursor-pointer',
192
+ 'text-[hsl(var(--brand-primary))] hover:underline',
193
+ )}
194
+ >
195
+ {action.label}
196
+ </button>
197
+ )}
198
+ </div>
199
+ {dismissible && (
200
+ <button
201
+ type="button"
202
+ onClick={handleDismiss}
203
+ className={cn(
204
+ 'shrink-0 p-0.5 rounded cursor-pointer',
205
+ 'text-[hsl(var(--text-tertiary))] hover:text-[hsl(var(--text-primary))]',
206
+ 'hover:bg-[hsl(var(--bg-overlay))] transition-colors duration-100',
207
+ )}
208
+ aria-label="Dismiss notification"
209
+ >
210
+ <X className="size-3.5" />
211
+ </button>
212
+ )}
213
+ </div>
214
+
215
+ {/* Auto-dismiss progress bar */}
216
+ {duration > 0 && (
217
+ <div className="h-0.5 bg-[hsl(var(--bg-overlay))]">
218
+ <div
219
+ className={cn('h-full transition-[width] duration-100', TYPE_COLOR[type].replace('border-l-', 'bg-'))}
220
+ style={{ width: `${progress}%` }}
221
+ />
222
+ </div>
223
+ )}
224
+ </div>
225
+ )
226
+ }
@@ -1,5 +1,6 @@
1
1
  'use client'
2
2
 
3
+ import type React from 'react'
3
4
  import { type ElementType } from 'react'
4
5
  import { motion, useReducedMotion } from 'framer-motion'
5
6
  import { ChevronRight } from 'lucide-react'
@@ -54,7 +55,7 @@ export function PipelineStage({
54
55
  stages,
55
56
  onStageClick,
56
57
  className,
57
- }: PipelineStageProps) {
58
+ }: PipelineStageProps): React.JSX.Element {
58
59
  const reduced = useReducedMotion()
59
60
 
60
61
  return (
@@ -1,5 +1,6 @@
1
1
  'use client'
2
2
 
3
+ import type React from 'react'
3
4
  import { type ReactNode } from 'react'
4
5
  import * as PopoverPrimitive from '@radix-ui/react-popover'
5
6
  import { motion, AnimatePresence, useReducedMotion } from 'framer-motion'
@@ -28,7 +29,7 @@ const contentVariants = {
28
29
  * @description A popover wrapper built on Radix Popover with Framer Motion entry animation.
29
30
  * Closes on outside click. Includes an arrow pointer.
30
31
  */
31
- export function Popover({ trigger, children, side = 'bottom', align = 'center', className }: PopoverProps) {
32
+ export function Popover({ trigger, children, side = 'bottom', align = 'center', className }: PopoverProps): React.JSX.Element {
32
33
  const prefersReducedMotion = useReducedMotion()
33
34
 
34
35
  return (
@@ -1,5 +1,6 @@
1
1
  'use client'
2
2
 
3
+ import type React from 'react'
3
4
  import { motion, useReducedMotion } from 'framer-motion'
4
5
  import { cn } from '../utils'
5
6
 
@@ -58,7 +59,7 @@ export function PortStatusGrid({
58
59
  size = 'sm',
59
60
  onPortClick,
60
61
  className,
61
- }: PortStatusGridProps) {
62
+ }: PortStatusGridProps): React.JSX.Element {
62
63
  const reduced = useReducedMotion()
63
64
 
64
65
  const gridStyle = columns
@@ -1,5 +1,6 @@
1
1
  'use client'
2
2
 
3
+ import type React from 'react'
3
4
  import { motion, useReducedMotion } from 'framer-motion'
4
5
  import { cn } from '../utils'
5
6
 
@@ -51,7 +52,7 @@ export function Progress({
51
52
  animated = true,
52
53
  indeterminate = false,
53
54
  className,
54
- }: ProgressProps) {
55
+ }: ProgressProps): React.JSX.Element {
55
56
  const prefersReducedMotion = useReducedMotion()
56
57
  const pct = Math.min(100, Math.max(0, (value / max) * 100))
57
58
 
@@ -1,5 +1,6 @@
1
1
  'use client'
2
2
 
3
+ import type React from 'react'
3
4
  import { useCallback, useRef, type KeyboardEvent } from 'react'
4
5
  import { motion, useReducedMotion } from 'framer-motion'
5
6
  import { cn } from '../utils'
@@ -39,7 +40,7 @@ export function RadioGroup({
39
40
  onChange,
40
41
  orientation = 'vertical',
41
42
  className,
42
- }: RadioGroupProps) {
43
+ }: RadioGroupProps): React.JSX.Element {
43
44
  const prefersReducedMotion = useReducedMotion()
44
45
  const groupRef = useRef<HTMLDivElement>(null)
45
46