@annondeveloper/ui-kit 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +1463 -127
  2. package/dist/{chunk-5OKSXPWK.js → chunk-2DWZVHZS.js} +2 -2
  3. package/dist/chunk-2DWZVHZS.js.map +1 -0
  4. package/dist/form.d.ts +6 -6
  5. package/dist/form.js +2 -3
  6. package/dist/form.js.map +1 -1
  7. package/dist/index.d.ts +510 -52
  8. package/dist/index.js +2996 -15
  9. package/dist/index.js.map +1 -1
  10. package/dist/{select-nnBJUO8U.d.ts → select-B2wXqqSM.d.ts} +2 -2
  11. package/package.json +24 -26
  12. package/src/components/animated-counter.tsx +8 -5
  13. package/src/components/avatar.tsx +2 -1
  14. package/src/components/badge.tsx +3 -2
  15. package/src/components/button.tsx +3 -2
  16. package/src/components/card.tsx +13 -12
  17. package/src/components/checkbox.tsx +3 -2
  18. package/src/components/color-input.tsx +427 -0
  19. package/src/components/command-bar.tsx +435 -0
  20. package/src/components/confidence-bar.tsx +115 -0
  21. package/src/components/confirm-dialog.tsx +2 -1
  22. package/src/components/copy-block.tsx +224 -0
  23. package/src/components/data-table.tsx +9 -13
  24. package/src/components/diff-viewer.tsx +340 -0
  25. package/src/components/dropdown-menu.tsx +2 -1
  26. package/src/components/empty-state.tsx +2 -1
  27. package/src/components/filter-pill.tsx +2 -1
  28. package/src/components/form-input.tsx +5 -4
  29. package/src/components/heatmap-calendar.tsx +218 -0
  30. package/src/components/infinite-scroll.tsx +248 -0
  31. package/src/components/kanban-column.tsx +198 -0
  32. package/src/components/live-feed.tsx +222 -0
  33. package/src/components/log-viewer.tsx +4 -1
  34. package/src/components/metric-card.tsx +2 -1
  35. package/src/components/notification-stack.tsx +233 -0
  36. package/src/components/pipeline-stage.tsx +2 -1
  37. package/src/components/popover.tsx +2 -1
  38. package/src/components/port-status-grid.tsx +2 -1
  39. package/src/components/progress.tsx +2 -1
  40. package/src/components/radio-group.tsx +2 -1
  41. package/src/components/realtime-value.tsx +283 -0
  42. package/src/components/select.tsx +2 -1
  43. package/src/components/severity-timeline.tsx +2 -1
  44. package/src/components/sheet.tsx +2 -1
  45. package/src/components/skeleton.tsx +4 -3
  46. package/src/components/slider.tsx +2 -1
  47. package/src/components/smart-table.tsx +383 -0
  48. package/src/components/sortable-list.tsx +272 -0
  49. package/src/components/sparkline.tsx +2 -1
  50. package/src/components/status-badge.tsx +2 -1
  51. package/src/components/status-pulse.tsx +2 -1
  52. package/src/components/step-wizard.tsx +380 -0
  53. package/src/components/streaming-text.tsx +160 -0
  54. package/src/components/success-checkmark.tsx +2 -1
  55. package/src/components/tabs.tsx +2 -1
  56. package/src/components/threshold-gauge.tsx +2 -1
  57. package/src/components/time-range-selector.tsx +2 -1
  58. package/src/components/toast.tsx +2 -1
  59. package/src/components/toggle-switch.tsx +2 -1
  60. package/src/components/tooltip.tsx +2 -1
  61. package/src/components/truncated-text.tsx +2 -1
  62. package/src/components/typing-indicator.tsx +123 -0
  63. package/src/components/uptime-tracker.tsx +2 -1
  64. package/src/components/utilization-bar.tsx +2 -1
  65. package/src/theme.css +6 -0
  66. package/src/utils.ts +1 -1
  67. 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,380 @@
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
+ const wizardRef = useRef<HTMLDivElement>(null)
101
+ const validatingRef = useRef(false)
102
+
103
+ // Save state to sessionStorage
104
+ useEffect(() => {
105
+ try {
106
+ sessionStorage.setItem(sessionKey, JSON.stringify({
107
+ step: currentStep,
108
+ completed: [...completed],
109
+ }))
110
+ } catch { /* noop */ }
111
+ }, [currentStep, completed, sessionKey])
112
+
113
+ const totalSteps = steps.length
114
+ const progress = totalSteps === 0 ? 100 : Math.round(((completed.size) / totalSteps) * 100)
115
+
116
+ const goToStep = useCallback((idx: number) => {
117
+ setDirection(idx > currentStep ? 1 : -1)
118
+ setCurrentStep(idx)
119
+ onStepChange?.(idx)
120
+ }, [currentStep, onStepChange])
121
+
122
+ const handleNext = useCallback(async () => {
123
+ if (validatingRef.current) return
124
+ const step = steps[currentStep]
125
+ if (step?.validate) {
126
+ validatingRef.current = true
127
+ setIsValidating(true)
128
+ try {
129
+ const valid = await step.validate()
130
+ if (!valid) {
131
+ setIsValidating(false)
132
+ validatingRef.current = false
133
+ return
134
+ }
135
+ } catch {
136
+ setIsValidating(false)
137
+ validatingRef.current = false
138
+ return
139
+ }
140
+ setIsValidating(false)
141
+ validatingRef.current = false
142
+ }
143
+
144
+ setCompleted(prev => new Set(prev).add(currentStep))
145
+
146
+ if (currentStep < totalSteps - 1) {
147
+ goToStep(currentStep + 1)
148
+ } else {
149
+ // Final step completed
150
+ if (showSummary) {
151
+ setIsComplete(true)
152
+ }
153
+ onComplete()
154
+ // Clear session storage
155
+ try { sessionStorage.removeItem(sessionKey) } catch { /* noop */ }
156
+ }
157
+ }, [currentStep, steps, totalSteps, goToStep, onComplete, showSummary, sessionKey])
158
+
159
+ const handleBack = useCallback(() => {
160
+ if (currentStep > 0) {
161
+ goToStep(currentStep - 1)
162
+ }
163
+ }, [currentStep, goToStep])
164
+
165
+ const handleStepClick = useCallback((idx: number) => {
166
+ // Can click on completed steps or if allowSkip
167
+ if (completed.has(idx) || allowSkip || idx < currentStep) {
168
+ goToStep(idx)
169
+ }
170
+ }, [completed, allowSkip, currentStep, goToStep])
171
+
172
+ // Keyboard navigation
173
+ useEffect(() => {
174
+ const handler = (e: KeyboardEvent) => {
175
+ if (!wizardRef.current?.contains(e.target as Node)) return
176
+ if (e.key === 'Enter' && !e.shiftKey && !(e.target instanceof HTMLTextAreaElement)) {
177
+ handleNext()
178
+ }
179
+ }
180
+ document.addEventListener('keydown', handler)
181
+ return () => document.removeEventListener('keydown', handler)
182
+ }, [handleNext])
183
+
184
+ const slideVariants = {
185
+ enter: (dir: number) => ({
186
+ x: prefersReducedMotion ? 0 : dir > 0 ? 40 : -40,
187
+ opacity: prefersReducedMotion ? 1 : 0,
188
+ }),
189
+ center: { x: 0, opacity: 1 },
190
+ exit: (dir: number) => ({
191
+ x: prefersReducedMotion ? 0 : dir > 0 ? -40 : 40,
192
+ opacity: prefersReducedMotion ? 1 : 0,
193
+ }),
194
+ }
195
+
196
+ const isHorizontal = orientation === 'horizontal'
197
+
198
+ return (
199
+ <div ref={wizardRef} className={cn('flex flex-col', className)}>
200
+ {/* Step indicator */}
201
+ <div className={cn(
202
+ 'mb-6',
203
+ isHorizontal ? 'flex items-center' : 'flex flex-col gap-1',
204
+ )}>
205
+ {steps.map((step, idx) => {
206
+ const isActive = idx === currentStep
207
+ const isDone = completed.has(idx) || isComplete
208
+ const isClickable = isDone || allowSkip || idx < currentStep
209
+ const Icon = step.icon
210
+
211
+ return (
212
+ <div
213
+ key={step.id}
214
+ className={cn(
215
+ isHorizontal ? 'flex items-center flex-1' : 'flex items-center gap-3',
216
+ )}
217
+ >
218
+ {/* Step circle + label */}
219
+ <button
220
+ onClick={() => handleStepClick(idx)}
221
+ disabled={!isClickable}
222
+ className={cn(
223
+ 'flex items-center gap-2 group',
224
+ isClickable ? 'cursor-pointer' : 'cursor-default',
225
+ )}
226
+ >
227
+ <div
228
+ className={cn(
229
+ 'flex items-center justify-center rounded-full transition-all',
230
+ 'h-8 w-8 text-xs font-semibold shrink-0',
231
+ isDone
232
+ ? 'bg-[hsl(var(--status-ok))] text-[hsl(var(--text-on-brand))]'
233
+ : isActive
234
+ ? 'bg-[hsl(var(--brand-primary))] text-[hsl(var(--text-on-brand))] ring-4 ring-[hsl(var(--brand-primary)/0.2)]'
235
+ : 'bg-[hsl(var(--bg-overlay))] text-[hsl(var(--text-tertiary))]',
236
+ isClickable && !isActive && !isDone && 'group-hover:bg-[hsl(var(--bg-elevated))]',
237
+ )}
238
+ >
239
+ {isDone ? (
240
+ <Check className="h-4 w-4" />
241
+ ) : Icon ? (
242
+ <Icon className="h-4 w-4" />
243
+ ) : (
244
+ idx + 1
245
+ )}
246
+ </div>
247
+
248
+ <div className={cn(
249
+ isHorizontal ? 'hidden sm:block' : 'block',
250
+ )}>
251
+ <div className={cn(
252
+ 'text-xs font-medium leading-tight',
253
+ isActive ? 'text-[hsl(var(--text-primary))]' : 'text-[hsl(var(--text-secondary))]',
254
+ )}>
255
+ {step.title}
256
+ </div>
257
+ {step.description && !isHorizontal && (
258
+ <div className="text-[10px] text-[hsl(var(--text-tertiary))]">
259
+ {step.description}
260
+ </div>
261
+ )}
262
+ </div>
263
+ </button>
264
+
265
+ {/* Connector line */}
266
+ {isHorizontal && idx < totalSteps - 1 && (
267
+ <div className={cn(
268
+ 'flex-1 h-0.5 mx-2 rounded-full transition-colors',
269
+ completed.has(idx) ? 'bg-[hsl(var(--status-ok))]' : 'bg-[hsl(var(--border-subtle))]',
270
+ )} />
271
+ )}
272
+ </div>
273
+ )
274
+ })}
275
+ </div>
276
+
277
+ {/* Progress bar */}
278
+ <div className="w-full h-1 rounded-full bg-[hsl(var(--bg-overlay))] mb-4 overflow-hidden">
279
+ <motion.div
280
+ className="h-full rounded-full bg-[hsl(var(--brand-primary))]"
281
+ initial={{ width: 0 }}
282
+ animate={{ width: `${progress}%` }}
283
+ transition={prefersReducedMotion ? { duration: 0 } : { type: 'spring', stiffness: 300, damping: 30 }}
284
+ />
285
+ </div>
286
+
287
+ {/* Step content */}
288
+ <div ref={contentRef} className="relative min-h-[200px]">
289
+ <AnimatePresence mode="wait" custom={direction}>
290
+ {isComplete && showSummary ? (
291
+ <motion.div
292
+ key="summary"
293
+ custom={1}
294
+ variants={slideVariants}
295
+ initial="enter"
296
+ animate="center"
297
+ exit="exit"
298
+ transition={prefersReducedMotion ? { duration: 0 } : { duration: 0.25 }}
299
+ className="flex flex-col items-center justify-center py-12 text-center"
300
+ >
301
+ <div className="flex items-center justify-center w-16 h-16 rounded-full bg-[hsl(var(--status-ok)/0.15)] mb-4">
302
+ <Check className="h-8 w-8 text-[hsl(var(--status-ok))]" />
303
+ </div>
304
+ <h3 className="text-lg font-semibold text-[hsl(var(--text-primary))] mb-1">
305
+ All steps completed
306
+ </h3>
307
+ <p className="text-sm text-[hsl(var(--text-secondary))]">
308
+ All {totalSteps} steps have been successfully completed.
309
+ </p>
310
+ </motion.div>
311
+ ) : (
312
+ <motion.div
313
+ key={currentStep}
314
+ custom={direction}
315
+ variants={slideVariants}
316
+ initial="enter"
317
+ animate="center"
318
+ exit="exit"
319
+ transition={prefersReducedMotion ? { duration: 0 } : { duration: 0.25 }}
320
+ >
321
+ {steps[currentStep]?.content}
322
+ </motion.div>
323
+ )}
324
+ </AnimatePresence>
325
+ </div>
326
+
327
+ {/* Navigation buttons */}
328
+ {!isComplete && (
329
+ <div className="flex items-center justify-between mt-6 pt-4 border-t border-[hsl(var(--border-subtle)/0.5)]">
330
+ <button
331
+ onClick={handleBack}
332
+ disabled={currentStep === 0}
333
+ className={cn(
334
+ 'inline-flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-medium transition-colors',
335
+ 'border border-[hsl(var(--border-default))]',
336
+ 'text-[hsl(var(--text-primary))]',
337
+ 'hover:bg-[hsl(var(--bg-overlay))]',
338
+ 'disabled:opacity-40 disabled:pointer-events-none',
339
+ )}
340
+ >
341
+ <ChevronLeft className="h-4 w-4" />
342
+ Back
343
+ </button>
344
+
345
+ <span className="text-[11px] text-[hsl(var(--text-tertiary))] tabular-nums">
346
+ Step {currentStep + 1} of {totalSteps}
347
+ </span>
348
+
349
+ <button
350
+ onClick={handleNext}
351
+ disabled={isValidating}
352
+ className={cn(
353
+ 'inline-flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-medium transition-colors',
354
+ 'bg-[hsl(var(--brand-primary))] text-[hsl(var(--text-on-brand))]',
355
+ 'hover:bg-[hsl(var(--brand-primary))]/90',
356
+ 'disabled:opacity-70 disabled:cursor-not-allowed',
357
+ )}
358
+ >
359
+ {isValidating ? (
360
+ <>
361
+ <div className="h-4 w-4 rounded-full border-2 border-[hsl(var(--text-on-brand))] border-t-transparent animate-spin" />
362
+ Validating...
363
+ </>
364
+ ) : currentStep === totalSteps - 1 ? (
365
+ <>
366
+ Complete
367
+ <Check className="h-4 w-4" />
368
+ </>
369
+ ) : (
370
+ <>
371
+ Next
372
+ <ChevronRight className="h-4 w-4" />
373
+ </>
374
+ )}
375
+ </button>
376
+ </div>
377
+ )}
378
+ </div>
379
+ )
380
+ }
@@ -0,0 +1,160 @@
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
+ const copyTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
77
+
78
+ useEffect(() => () => { if (copyTimerRef.current) clearTimeout(copyTimerRef.current) }, [])
79
+
80
+ useEffect(() => {
81
+ if (prevStreamingRef.current && !isStreaming) {
82
+ onComplete?.()
83
+ }
84
+ prevStreamingRef.current = isStreaming
85
+ }, [isStreaming, onComplete])
86
+
87
+ useEffect(() => {
88
+ if (isStreaming && containerRef.current) {
89
+ const el = containerRef.current
90
+ el.scrollTop = el.scrollHeight
91
+ }
92
+ }, [text, isStreaming])
93
+
94
+ const handleCopy = useCallback(() => {
95
+ void navigator.clipboard.writeText(text).then(() => {
96
+ setCopied(true)
97
+ if (copyTimerRef.current) clearTimeout(copyTimerRef.current)
98
+ copyTimerRef.current = setTimeout(() => setCopied(false), 2000)
99
+ })
100
+ }, [text])
101
+
102
+ const formatted = formatSegments(text)
103
+
104
+ return (
105
+ <div className={cn('relative', className)}>
106
+ <div
107
+ ref={containerRef}
108
+ className="overflow-y-auto text-[hsl(var(--text-primary))] leading-relaxed whitespace-pre-wrap break-words"
109
+ >
110
+ {formatted}
111
+ {showCursor && isStreaming && (
112
+ <span
113
+ className="inline-block w-[2px] h-[1.1em] align-text-bottom ml-0.5 bg-[hsl(var(--brand-primary))]"
114
+ style={
115
+ reduced
116
+ ? { opacity: 1 }
117
+ : {
118
+ animation: `streaming-cursor-blink ${speed}ms step-end infinite`,
119
+ }
120
+ }
121
+ aria-hidden="true"
122
+ />
123
+ )}
124
+ <AnimatePresence>
125
+ {showCursor && !isStreaming && text.length > 0 && (
126
+ <motion.span
127
+ className="inline-block w-[2px] h-[1.1em] align-text-bottom ml-0.5 bg-[hsl(var(--brand-primary))]"
128
+ initial={{ opacity: 1 }}
129
+ exit={{ opacity: 0 }}
130
+ transition={{ duration: reduced ? 0 : 0.4 }}
131
+ />
132
+ )}
133
+ </AnimatePresence>
134
+ </div>
135
+
136
+ <AnimatePresence>
137
+ {!isStreaming && text.length > 0 && (
138
+ <motion.button
139
+ type="button"
140
+ initial={{ opacity: 0, scale: 0.8 }}
141
+ animate={{ opacity: 1, scale: 1 }}
142
+ exit={{ opacity: 0, scale: 0.8 }}
143
+ transition={{ duration: reduced ? 0 : 0.2 }}
144
+ onClick={handleCopy}
145
+ className={cn(
146
+ 'absolute top-0 right-0 p-1.5 rounded-lg',
147
+ 'text-[hsl(var(--text-tertiary))] hover:text-[hsl(var(--text-primary))]',
148
+ 'bg-[hsl(var(--bg-surface))]/80 hover:bg-[hsl(var(--bg-elevated))]',
149
+ 'transition-colors duration-150 cursor-pointer',
150
+ )}
151
+ aria-label={copied ? 'Copied' : 'Copy to clipboard'}
152
+ >
153
+ {copied ? <Check className="size-3.5" /> : <Copy className="size-3.5" />}
154
+ </motion.button>
155
+ )}
156
+ </AnimatePresence>
157
+
158
+ </div>
159
+ )
160
+ }
@@ -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)