@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.
- package/dist/{chunk-5OKSXPWK.js → chunk-2DWZVHZS.js} +2 -2
- package/dist/chunk-2DWZVHZS.js.map +1 -0
- package/dist/form.d.ts +6 -6
- package/dist/form.js +1 -1
- package/dist/form.js.map +1 -1
- package/dist/index.d.ts +508 -52
- package/dist/index.js +2927 -4
- package/dist/index.js.map +1 -1
- package/dist/{select-nnBJUO8U.d.ts → select-B2wXqqSM.d.ts} +2 -2
- package/package.json +1 -1
- package/src/components/animated-counter.tsx +2 -1
- package/src/components/avatar.tsx +2 -1
- package/src/components/badge.tsx +3 -2
- package/src/components/button.tsx +3 -2
- package/src/components/card.tsx +13 -12
- package/src/components/checkbox.tsx +3 -2
- package/src/components/color-input.tsx +414 -0
- package/src/components/command-bar.tsx +434 -0
- package/src/components/confidence-bar.tsx +115 -0
- package/src/components/confirm-dialog.tsx +2 -1
- package/src/components/copy-block.tsx +229 -0
- package/src/components/data-table.tsx +2 -1
- package/src/components/diff-viewer.tsx +319 -0
- package/src/components/dropdown-menu.tsx +2 -1
- package/src/components/empty-state.tsx +2 -1
- package/src/components/filter-pill.tsx +2 -1
- package/src/components/form-input.tsx +5 -4
- package/src/components/heatmap-calendar.tsx +213 -0
- package/src/components/infinite-scroll.tsx +243 -0
- package/src/components/kanban-column.tsx +198 -0
- package/src/components/live-feed.tsx +220 -0
- package/src/components/log-viewer.tsx +2 -1
- package/src/components/metric-card.tsx +2 -1
- package/src/components/notification-stack.tsx +226 -0
- package/src/components/pipeline-stage.tsx +2 -1
- package/src/components/popover.tsx +2 -1
- package/src/components/port-status-grid.tsx +2 -1
- package/src/components/progress.tsx +2 -1
- package/src/components/radio-group.tsx +2 -1
- package/src/components/realtime-value.tsx +283 -0
- package/src/components/select.tsx +2 -1
- package/src/components/severity-timeline.tsx +2 -1
- package/src/components/sheet.tsx +2 -1
- package/src/components/skeleton.tsx +4 -3
- package/src/components/slider.tsx +2 -1
- package/src/components/smart-table.tsx +383 -0
- package/src/components/sortable-list.tsx +268 -0
- package/src/components/sparkline.tsx +2 -1
- package/src/components/status-badge.tsx +2 -1
- package/src/components/status-pulse.tsx +2 -1
- package/src/components/step-wizard.tsx +372 -0
- package/src/components/streaming-text.tsx +163 -0
- package/src/components/success-checkmark.tsx +2 -1
- package/src/components/tabs.tsx +2 -1
- package/src/components/threshold-gauge.tsx +2 -1
- package/src/components/time-range-selector.tsx +2 -1
- package/src/components/toast.tsx +2 -1
- package/src/components/toggle-switch.tsx +2 -1
- package/src/components/tooltip.tsx +2 -1
- package/src/components/truncated-text.tsx +2 -1
- package/src/components/typing-indicator.tsx +123 -0
- package/src/components/uptime-tracker.tsx +2 -1
- package/src/components/utilization-bar.tsx +2 -1
- package/src/utils.ts +1 -1
- 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
|
|