@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
@@ -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
 
@@ -37,7 +38,7 @@ export interface StatusPulseProps {
37
38
  * @description An animated status indicator dot with optional pulse ring and label.
38
39
  * Accepts a configurable map to define custom statuses. Respects prefers-reduced-motion.
39
40
  */
40
- export function StatusPulse({ status, label = true, configMap, className }: StatusPulseProps) {
41
+ export function StatusPulse({ status, label = true, configMap, className }: StatusPulseProps): React.JSX.Element {
41
42
  const reduced = useReducedMotion()
42
43
  const map = configMap ?? defaultPulseConfigMap
43
44
  const cfg = map[status] ?? map['unknown'] ?? defaultPulseConfigMap['unknown']!
@@ -0,0 +1,372 @@
1
+ 'use client'
2
+
3
+ import type React from 'react'
4
+ import { useState, useCallback, useEffect, useRef, useMemo } from 'react'
5
+ import { motion, AnimatePresence, useReducedMotion } from 'framer-motion'
6
+ import { Check, ChevronRight, ChevronLeft } from 'lucide-react'
7
+ import type { LucideIcon } from 'lucide-react'
8
+ import { cn } from '../utils'
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Types
12
+ // ---------------------------------------------------------------------------
13
+
14
+ /** Definition of a single wizard step. */
15
+ export interface WizardStep {
16
+ /** Unique step identifier. */
17
+ id: string
18
+ /** Step title displayed in the step indicator. */
19
+ title: string
20
+ /** Optional description below the title. */
21
+ description?: string
22
+ /** Optional icon for the step indicator. */
23
+ icon?: LucideIcon
24
+ /** Step content to render. */
25
+ content: React.ReactNode
26
+ /** Validation function called before advancing. Return true to allow, false to block. */
27
+ validate?: () => boolean | Promise<boolean>
28
+ }
29
+
30
+ /** Props for the StepWizard component. */
31
+ export interface StepWizardProps {
32
+ /** Array of steps in order. */
33
+ steps: WizardStep[]
34
+ /** Called when the final step is completed. */
35
+ onComplete: () => void
36
+ /** Called whenever the active step changes. */
37
+ onStepChange?: (step: number) => void
38
+ /** Step indicator layout. Default "horizontal". */
39
+ orientation?: 'horizontal' | 'vertical'
40
+ /** Allow skipping forward to any uncompleted step. Default false. */
41
+ allowSkip?: boolean
42
+ /** Show a summary/completion state after the last step. Default false. */
43
+ showSummary?: boolean
44
+ /** Additional class name for the root container. */
45
+ className?: string
46
+ }
47
+
48
+ const SESSION_KEY_PREFIX = 'ui-kit-wizard-'
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // StepWizard
52
+ // ---------------------------------------------------------------------------
53
+
54
+ /**
55
+ * @description A multi-step form wizard with animated slide transitions, per-step validation,
56
+ * step indicator bar (horizontal or vertical), keyboard navigation, progress percentage,
57
+ * and auto-save to sessionStorage for resilience to page refresh.
58
+ */
59
+ export function StepWizard({
60
+ steps,
61
+ onComplete,
62
+ onStepChange,
63
+ orientation = 'horizontal',
64
+ allowSkip = false,
65
+ showSummary = false,
66
+ className,
67
+ }: StepWizardProps): React.JSX.Element {
68
+ const prefersReducedMotion = useReducedMotion()
69
+ const sessionKey = useMemo(() => SESSION_KEY_PREFIX + steps.map(s => s.id).join('-'), [steps])
70
+
71
+ // Restore state from sessionStorage
72
+ const [currentStep, setCurrentStep] = useState(() => {
73
+ if (typeof window === 'undefined') return 0
74
+ try {
75
+ const saved = sessionStorage.getItem(sessionKey)
76
+ if (saved) {
77
+ const parsed = JSON.parse(saved) as { step: number; completed: number[] }
78
+ return Math.min(parsed.step, steps.length - 1)
79
+ }
80
+ } catch { /* noop */ }
81
+ return 0
82
+ })
83
+
84
+ const [completed, setCompleted] = useState<Set<number>>(() => {
85
+ if (typeof window === 'undefined') return new Set()
86
+ try {
87
+ const saved = sessionStorage.getItem(sessionKey)
88
+ if (saved) {
89
+ const parsed = JSON.parse(saved) as { step: number; completed: number[] }
90
+ return new Set(parsed.completed)
91
+ }
92
+ } catch { /* noop */ }
93
+ return new Set()
94
+ })
95
+
96
+ const [isValidating, setIsValidating] = useState(false)
97
+ const [direction, setDirection] = useState(1) // 1=forward, -1=backward
98
+ const [isComplete, setIsComplete] = useState(false)
99
+ const contentRef = useRef<HTMLDivElement>(null)
100
+
101
+ // Save state to sessionStorage
102
+ useEffect(() => {
103
+ try {
104
+ sessionStorage.setItem(sessionKey, JSON.stringify({
105
+ step: currentStep,
106
+ completed: [...completed],
107
+ }))
108
+ } catch { /* noop */ }
109
+ }, [currentStep, completed, sessionKey])
110
+
111
+ const totalSteps = steps.length
112
+ const progress = totalSteps === 0 ? 100 : Math.round(((completed.size) / totalSteps) * 100)
113
+
114
+ const goToStep = useCallback((idx: number) => {
115
+ setDirection(idx > currentStep ? 1 : -1)
116
+ setCurrentStep(idx)
117
+ onStepChange?.(idx)
118
+ }, [currentStep, onStepChange])
119
+
120
+ const handleNext = useCallback(async () => {
121
+ const step = steps[currentStep]
122
+ if (step?.validate) {
123
+ setIsValidating(true)
124
+ try {
125
+ const valid = await step.validate()
126
+ if (!valid) {
127
+ setIsValidating(false)
128
+ return
129
+ }
130
+ } catch {
131
+ setIsValidating(false)
132
+ return
133
+ }
134
+ setIsValidating(false)
135
+ }
136
+
137
+ setCompleted(prev => new Set(prev).add(currentStep))
138
+
139
+ if (currentStep < totalSteps - 1) {
140
+ goToStep(currentStep + 1)
141
+ } else {
142
+ // Final step completed
143
+ if (showSummary) {
144
+ setIsComplete(true)
145
+ }
146
+ onComplete()
147
+ // Clear session storage
148
+ try { sessionStorage.removeItem(sessionKey) } catch { /* noop */ }
149
+ }
150
+ }, [currentStep, steps, totalSteps, goToStep, onComplete, showSummary, sessionKey])
151
+
152
+ const handleBack = useCallback(() => {
153
+ if (currentStep > 0) {
154
+ goToStep(currentStep - 1)
155
+ }
156
+ }, [currentStep, goToStep])
157
+
158
+ const handleStepClick = useCallback((idx: number) => {
159
+ // Can click on completed steps or if allowSkip
160
+ if (completed.has(idx) || allowSkip || idx < currentStep) {
161
+ goToStep(idx)
162
+ }
163
+ }, [completed, allowSkip, currentStep, goToStep])
164
+
165
+ // Keyboard navigation
166
+ useEffect(() => {
167
+ const handler = (e: KeyboardEvent) => {
168
+ if (e.key === 'Enter' && !e.shiftKey && !(e.target instanceof HTMLTextAreaElement)) {
169
+ handleNext()
170
+ }
171
+ }
172
+ document.addEventListener('keydown', handler)
173
+ return () => document.removeEventListener('keydown', handler)
174
+ }, [handleNext])
175
+
176
+ const slideVariants = {
177
+ enter: (dir: number) => ({
178
+ x: prefersReducedMotion ? 0 : dir > 0 ? 40 : -40,
179
+ opacity: prefersReducedMotion ? 1 : 0,
180
+ }),
181
+ center: { x: 0, opacity: 1 },
182
+ exit: (dir: number) => ({
183
+ x: prefersReducedMotion ? 0 : dir > 0 ? -40 : 40,
184
+ opacity: prefersReducedMotion ? 1 : 0,
185
+ }),
186
+ }
187
+
188
+ const isHorizontal = orientation === 'horizontal'
189
+
190
+ return (
191
+ <div className={cn('flex flex-col', className)}>
192
+ {/* Step indicator */}
193
+ <div className={cn(
194
+ 'mb-6',
195
+ isHorizontal ? 'flex items-center' : 'flex flex-col gap-1',
196
+ )}>
197
+ {steps.map((step, idx) => {
198
+ const isActive = idx === currentStep
199
+ const isDone = completed.has(idx) || isComplete
200
+ const isClickable = isDone || allowSkip || idx < currentStep
201
+ const Icon = step.icon
202
+
203
+ return (
204
+ <div
205
+ key={step.id}
206
+ className={cn(
207
+ isHorizontal ? 'flex items-center flex-1' : 'flex items-center gap-3',
208
+ )}
209
+ >
210
+ {/* Step circle + label */}
211
+ <button
212
+ onClick={() => handleStepClick(idx)}
213
+ disabled={!isClickable}
214
+ className={cn(
215
+ 'flex items-center gap-2 group',
216
+ isClickable ? 'cursor-pointer' : 'cursor-default',
217
+ )}
218
+ >
219
+ <div
220
+ className={cn(
221
+ 'flex items-center justify-center rounded-full transition-all',
222
+ 'h-8 w-8 text-xs font-semibold shrink-0',
223
+ isDone
224
+ ? 'bg-[hsl(var(--status-ok))] text-[hsl(var(--text-on-brand))]'
225
+ : isActive
226
+ ? 'bg-[hsl(var(--brand-primary))] text-[hsl(var(--text-on-brand))] ring-4 ring-[hsl(var(--brand-primary)/0.2)]'
227
+ : 'bg-[hsl(var(--bg-overlay))] text-[hsl(var(--text-tertiary))]',
228
+ isClickable && !isActive && !isDone && 'group-hover:bg-[hsl(var(--bg-elevated))]',
229
+ )}
230
+ >
231
+ {isDone ? (
232
+ <Check className="h-4 w-4" />
233
+ ) : Icon ? (
234
+ <Icon className="h-4 w-4" />
235
+ ) : (
236
+ idx + 1
237
+ )}
238
+ </div>
239
+
240
+ <div className={cn(
241
+ isHorizontal ? 'hidden sm:block' : 'block',
242
+ )}>
243
+ <div className={cn(
244
+ 'text-xs font-medium leading-tight',
245
+ isActive ? 'text-[hsl(var(--text-primary))]' : 'text-[hsl(var(--text-secondary))]',
246
+ )}>
247
+ {step.title}
248
+ </div>
249
+ {step.description && !isHorizontal && (
250
+ <div className="text-[10px] text-[hsl(var(--text-tertiary))]">
251
+ {step.description}
252
+ </div>
253
+ )}
254
+ </div>
255
+ </button>
256
+
257
+ {/* Connector line */}
258
+ {isHorizontal && idx < totalSteps - 1 && (
259
+ <div className={cn(
260
+ 'flex-1 h-0.5 mx-2 rounded-full transition-colors',
261
+ completed.has(idx) ? 'bg-[hsl(var(--status-ok))]' : 'bg-[hsl(var(--border-subtle))]',
262
+ )} />
263
+ )}
264
+ </div>
265
+ )
266
+ })}
267
+ </div>
268
+
269
+ {/* Progress bar */}
270
+ <div className="w-full h-1 rounded-full bg-[hsl(var(--bg-overlay))] mb-4 overflow-hidden">
271
+ <motion.div
272
+ className="h-full rounded-full bg-[hsl(var(--brand-primary))]"
273
+ initial={{ width: 0 }}
274
+ animate={{ width: `${progress}%` }}
275
+ transition={prefersReducedMotion ? { duration: 0 } : { type: 'spring', stiffness: 300, damping: 30 }}
276
+ />
277
+ </div>
278
+
279
+ {/* Step content */}
280
+ <div ref={contentRef} className="relative min-h-[200px]">
281
+ <AnimatePresence mode="wait" custom={direction}>
282
+ {isComplete && showSummary ? (
283
+ <motion.div
284
+ key="summary"
285
+ custom={1}
286
+ variants={slideVariants}
287
+ initial="enter"
288
+ animate="center"
289
+ exit="exit"
290
+ transition={prefersReducedMotion ? { duration: 0 } : { duration: 0.25 }}
291
+ className="flex flex-col items-center justify-center py-12 text-center"
292
+ >
293
+ <div className="flex items-center justify-center w-16 h-16 rounded-full bg-[hsl(var(--status-ok)/0.15)] mb-4">
294
+ <Check className="h-8 w-8 text-[hsl(var(--status-ok))]" />
295
+ </div>
296
+ <h3 className="text-lg font-semibold text-[hsl(var(--text-primary))] mb-1">
297
+ All steps completed
298
+ </h3>
299
+ <p className="text-sm text-[hsl(var(--text-secondary))]">
300
+ All {totalSteps} steps have been successfully completed.
301
+ </p>
302
+ </motion.div>
303
+ ) : (
304
+ <motion.div
305
+ key={currentStep}
306
+ custom={direction}
307
+ variants={slideVariants}
308
+ initial="enter"
309
+ animate="center"
310
+ exit="exit"
311
+ transition={prefersReducedMotion ? { duration: 0 } : { duration: 0.25 }}
312
+ >
313
+ {steps[currentStep]?.content}
314
+ </motion.div>
315
+ )}
316
+ </AnimatePresence>
317
+ </div>
318
+
319
+ {/* Navigation buttons */}
320
+ {!isComplete && (
321
+ <div className="flex items-center justify-between mt-6 pt-4 border-t border-[hsl(var(--border-subtle)/0.5)]">
322
+ <button
323
+ onClick={handleBack}
324
+ disabled={currentStep === 0}
325
+ className={cn(
326
+ 'inline-flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-medium transition-colors',
327
+ 'border border-[hsl(var(--border-default))]',
328
+ 'text-[hsl(var(--text-primary))]',
329
+ 'hover:bg-[hsl(var(--bg-overlay))]',
330
+ 'disabled:opacity-40 disabled:pointer-events-none',
331
+ )}
332
+ >
333
+ <ChevronLeft className="h-4 w-4" />
334
+ Back
335
+ </button>
336
+
337
+ <span className="text-[11px] text-[hsl(var(--text-tertiary))] tabular-nums">
338
+ Step {currentStep + 1} of {totalSteps}
339
+ </span>
340
+
341
+ <button
342
+ onClick={handleNext}
343
+ disabled={isValidating}
344
+ className={cn(
345
+ 'inline-flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-medium transition-colors',
346
+ 'bg-[hsl(var(--brand-primary))] text-[hsl(var(--text-on-brand))]',
347
+ 'hover:bg-[hsl(var(--brand-primary))]/90',
348
+ 'disabled:opacity-70 disabled:cursor-not-allowed',
349
+ )}
350
+ >
351
+ {isValidating ? (
352
+ <>
353
+ <div className="h-4 w-4 rounded-full border-2 border-[hsl(var(--text-on-brand))] border-t-transparent animate-spin" />
354
+ Validating...
355
+ </>
356
+ ) : currentStep === totalSteps - 1 ? (
357
+ <>
358
+ Complete
359
+ <Check className="h-4 w-4" />
360
+ </>
361
+ ) : (
362
+ <>
363
+ Next
364
+ <ChevronRight className="h-4 w-4" />
365
+ </>
366
+ )}
367
+ </button>
368
+ </div>
369
+ )}
370
+ </div>
371
+ )
372
+ }
@@ -0,0 +1,163 @@
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 { Copy, Check } from 'lucide-react'
8
+
9
+ export interface StreamingTextProps {
10
+ /** The text content — grows as tokens arrive. */
11
+ text: string
12
+ /** Whether tokens are still arriving. */
13
+ isStreaming: boolean
14
+ /** Cursor blink speed in ms. */
15
+ speed?: number
16
+ /** Show a blinking cursor at the end while streaming. */
17
+ showCursor?: boolean
18
+ /** Called when isStreaming transitions from true to false. */
19
+ onComplete?: () => void
20
+ className?: string
21
+ }
22
+
23
+ /** Simple inline formatter: **bold** and `code` spans. */
24
+ function formatSegments(text: string): React.ReactNode[] {
25
+ const segments: React.ReactNode[] = []
26
+ const regex = /(\*\*(.+?)\*\*|`([^`]+?)`)/g
27
+ let lastIndex = 0
28
+ let match: RegExpExecArray | null
29
+
30
+ while ((match = regex.exec(text)) !== null) {
31
+ if (match.index > lastIndex) {
32
+ segments.push(text.slice(lastIndex, match.index))
33
+ }
34
+ if (match[2]) {
35
+ segments.push(
36
+ <strong key={match.index} className="font-semibold">
37
+ {match[2]}
38
+ </strong>
39
+ )
40
+ } else if (match[3]) {
41
+ segments.push(
42
+ <code
43
+ key={match.index}
44
+ className="rounded px-1 py-0.5 text-[0.875em] bg-[hsl(var(--bg-overlay))] text-[hsl(var(--brand-primary))] font-mono"
45
+ >
46
+ {match[3]}
47
+ </code>
48
+ )
49
+ }
50
+ lastIndex = match.index + match[0].length
51
+ }
52
+
53
+ if (lastIndex < text.length) {
54
+ segments.push(text.slice(lastIndex))
55
+ }
56
+
57
+ return segments
58
+ }
59
+
60
+ /**
61
+ * @description Displays AI/LLM streaming text responses with a blinking cursor,
62
+ * markdown-like formatting (bold, code), copy button on completion, and auto-scroll.
63
+ */
64
+ export function StreamingText({
65
+ text,
66
+ isStreaming,
67
+ speed = 500,
68
+ showCursor = true,
69
+ onComplete,
70
+ className,
71
+ }: StreamingTextProps): React.JSX.Element {
72
+ const reduced = useReducedMotion()
73
+ const containerRef = useRef<HTMLDivElement>(null)
74
+ const prevStreamingRef = useRef(isStreaming)
75
+ const [copied, setCopied] = useState(false)
76
+
77
+ useEffect(() => {
78
+ if (prevStreamingRef.current && !isStreaming) {
79
+ onComplete?.()
80
+ }
81
+ prevStreamingRef.current = isStreaming
82
+ }, [isStreaming, onComplete])
83
+
84
+ useEffect(() => {
85
+ if (isStreaming && containerRef.current) {
86
+ const el = containerRef.current
87
+ el.scrollTop = el.scrollHeight
88
+ }
89
+ }, [text, isStreaming])
90
+
91
+ const handleCopy = useCallback(() => {
92
+ void navigator.clipboard.writeText(text).then(() => {
93
+ setCopied(true)
94
+ setTimeout(() => setCopied(false), 2000)
95
+ })
96
+ }, [text])
97
+
98
+ const formatted = formatSegments(text)
99
+
100
+ return (
101
+ <div className={cn('relative', className)}>
102
+ <div
103
+ ref={containerRef}
104
+ className="overflow-y-auto text-[hsl(var(--text-primary))] leading-relaxed whitespace-pre-wrap break-words"
105
+ >
106
+ {formatted}
107
+ {showCursor && isStreaming && (
108
+ <span
109
+ className="inline-block w-[2px] h-[1.1em] align-text-bottom ml-0.5 bg-[hsl(var(--brand-primary))]"
110
+ style={
111
+ reduced
112
+ ? { opacity: 1 }
113
+ : {
114
+ animation: `streaming-cursor-blink ${speed}ms step-end infinite`,
115
+ }
116
+ }
117
+ />
118
+ )}
119
+ <AnimatePresence>
120
+ {showCursor && !isStreaming && text.length > 0 && (
121
+ <motion.span
122
+ className="inline-block w-[2px] h-[1.1em] align-text-bottom ml-0.5 bg-[hsl(var(--brand-primary))]"
123
+ initial={{ opacity: 1 }}
124
+ exit={{ opacity: 0 }}
125
+ transition={{ duration: reduced ? 0 : 0.4 }}
126
+ />
127
+ )}
128
+ </AnimatePresence>
129
+ </div>
130
+
131
+ <AnimatePresence>
132
+ {!isStreaming && text.length > 0 && (
133
+ <motion.button
134
+ type="button"
135
+ initial={{ opacity: 0, scale: 0.8 }}
136
+ animate={{ opacity: 1, scale: 1 }}
137
+ exit={{ opacity: 0, scale: 0.8 }}
138
+ transition={{ duration: reduced ? 0 : 0.2 }}
139
+ onClick={handleCopy}
140
+ className={cn(
141
+ 'absolute top-0 right-0 p-1.5 rounded-lg',
142
+ 'text-[hsl(var(--text-tertiary))] hover:text-[hsl(var(--text-primary))]',
143
+ 'bg-[hsl(var(--bg-surface))]/80 hover:bg-[hsl(var(--bg-elevated))]',
144
+ 'transition-colors duration-150 cursor-pointer',
145
+ )}
146
+ aria-label={copied ? 'Copied' : 'Copy to clipboard'}
147
+ >
148
+ {copied ? <Check className="size-3.5" /> : <Copy className="size-3.5" />}
149
+ </motion.button>
150
+ )}
151
+ </AnimatePresence>
152
+
153
+ {showCursor && isStreaming && !reduced && (
154
+ <style>{`
155
+ @keyframes streaming-cursor-blink {
156
+ 0%, 100% { opacity: 1; }
157
+ 50% { opacity: 0; }
158
+ }
159
+ `}</style>
160
+ )}
161
+ </div>
162
+ )
163
+ }
@@ -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
 
5
6
  export interface SuccessCheckmarkProps {
@@ -12,7 +13,7 @@ export interface SuccessCheckmarkProps {
12
13
  * @description An animated success checkmark SVG with circle and path draw animations.
13
14
  * Uses Framer Motion spring physics. Respects prefers-reduced-motion.
14
15
  */
15
- export function SuccessCheckmark({ size = 20, className }: SuccessCheckmarkProps) {
16
+ export function SuccessCheckmark({ size = 20, className }: SuccessCheckmarkProps): React.JSX.Element {
16
17
  const reduced = useReducedMotion()
17
18
 
18
19
  return (
@@ -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 type { LucideIcon } from 'lucide-react'
@@ -49,7 +50,7 @@ export function Tabs({
49
50
  variant = 'underline',
50
51
  size = 'md',
51
52
  className,
52
- }: TabsProps) {
53
+ }: TabsProps): React.JSX.Element {
53
54
  const prefersReducedMotion = useReducedMotion()
54
55
  const tabListRef = useRef<HTMLDivElement>(null)
55
56
 
@@ -1,5 +1,6 @@
1
1
  'use client'
2
2
 
3
+ import type React from 'react'
3
4
  import { useEffect, useState } from 'react'
4
5
  import { useReducedMotion } from 'framer-motion'
5
6
  import { cn, clamp } from '../utils'
@@ -53,7 +54,7 @@ export function ThresholdGauge({
53
54
  showValue = true,
54
55
  format,
55
56
  className,
56
- }: ThresholdGaugeProps) {
57
+ }: ThresholdGaugeProps): React.JSX.Element {
57
58
  const reduced = useReducedMotion()
58
59
  const value = clamp(rawValue, 0, 100)
59
60
  const warning = thresholds?.warning ?? 70
@@ -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
 
@@ -40,7 +41,7 @@ export function TimeRangeSelector({
40
41
  onChange,
41
42
  ranges = defaultRanges,
42
43
  className,
43
- }: TimeRangeSelectorProps) {
44
+ }: TimeRangeSelectorProps): React.JSX.Element {
44
45
  const reduced = useReducedMotion()
45
46
 
46
47
  return (
@@ -1,5 +1,6 @@
1
1
  'use client'
2
2
 
3
+ import type React from 'react'
3
4
  import { Toaster as SonnerToaster } from 'sonner'
4
5
 
5
6
  export interface ToasterProps {
@@ -16,7 +17,7 @@ export interface ToasterProps {
16
17
  * instead of reading from a hook, making it portable across applications.
17
18
  * Import this Toaster once in your app layout, then use `toast()` from sonner anywhere.
18
19
  */
19
- export function Toaster({ theme = 'dark', position = 'bottom-right', duration = 4000 }: ToasterProps) {
20
+ export function Toaster({ theme = 'dark', position = 'bottom-right', duration = 4000 }: ToasterProps): React.JSX.Element {
20
21
  return (
21
22
  <SonnerToaster
22
23
  theme={theme}
@@ -1,5 +1,6 @@
1
1
  'use client'
2
2
 
3
+ import type React from 'react'
3
4
  import { ToggleLeft, ToggleRight } from 'lucide-react'
4
5
  import { cn } from '../utils'
5
6
 
@@ -23,7 +24,7 @@ export interface ToggleSwitchProps {
23
24
  */
24
25
  export function ToggleSwitch({
25
26
  enabled, onChange, size = 'md', disabled, label, className,
26
- }: ToggleSwitchProps) {
27
+ }: ToggleSwitchProps): React.JSX.Element {
27
28
  const iconSize = size === 'sm' ? 'size-5' : 'size-6'
28
29
 
29
30
  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 TooltipPrimitive from '@radix-ui/react-tooltip'
5
6
  import { cn } from '../utils'
@@ -28,7 +29,7 @@ export function Tooltip({
28
29
  side = 'top',
29
30
  delay = 200,
30
31
  className,
31
- }: TooltipProps) {
32
+ }: TooltipProps): React.JSX.Element {
32
33
  return (
33
34
  <TooltipPrimitive.Provider delayDuration={delay}>
34
35
  <TooltipPrimitive.Root>
@@ -1,5 +1,6 @@
1
1
  'use client'
2
2
 
3
+ import type React from 'react'
3
4
  import { useRef, useState, useEffect } from 'react'
4
5
  import * as Tooltip from '@radix-ui/react-tooltip'
5
6
  import { Copy, Check } from 'lucide-react'
@@ -16,7 +17,7 @@ export interface TruncatedTextProps {
16
17
  * @description A text element that truncates with ellipsis and shows a tooltip with the
17
18
  * full text on hover when truncated. Includes a copy-to-clipboard button in the tooltip.
18
19
  */
19
- export function TruncatedText({ text, maxWidth = '100%', className = '' }: TruncatedTextProps) {
20
+ export function TruncatedText({ text, maxWidth = '100%', className = '' }: TruncatedTextProps): React.JSX.Element {
20
21
  const ref = useRef<HTMLSpanElement>(null)
21
22
  const [isTruncated, setIsTruncated] = useState(false)
22
23
  const [copied, setCopied] = useState(false)