@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,434 @@
1
+ 'use client'
2
+
3
+ import type React from 'react'
4
+ import { useState, useEffect, useCallback, useRef, useMemo } from 'react'
5
+ import { motion, AnimatePresence, useReducedMotion } from 'framer-motion'
6
+ import { Search, Clock, Command, CornerDownLeft } from 'lucide-react'
7
+ import type { LucideIcon } from 'lucide-react'
8
+ import { cn } from '../utils'
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Types
12
+ // ---------------------------------------------------------------------------
13
+
14
+ /** A single command item in the palette. */
15
+ export interface CommandItem {
16
+ /** Unique identifier. */
17
+ id: string
18
+ /** Display label. */
19
+ label: string
20
+ /** Optional description shown below the label. */
21
+ description?: string
22
+ /** Optional icon component. */
23
+ icon?: LucideIcon
24
+ /** Keyboard shortcut display string (e.g. "Cmd+K"). */
25
+ shortcut?: string
26
+ /** Group name for sectioning items. */
27
+ group?: string
28
+ /** Callback when item is selected. */
29
+ onSelect: () => void
30
+ /** Additional search terms that match this item. */
31
+ keywords?: string[]
32
+ }
33
+
34
+ /** Props for the CommandBar component. */
35
+ export interface CommandBarProps {
36
+ /** Array of command items to display and search. */
37
+ items: CommandItem[]
38
+ /** Placeholder text for the search input. */
39
+ placeholder?: string
40
+ /** Hotkey letter (combined with Cmd/Ctrl). Default "k". */
41
+ hotkey?: string
42
+ /** Async search function for remote results. */
43
+ onSearch?: (query: string) => Promise<CommandItem[]>
44
+ /** localStorage key for persisting recent selections. */
45
+ recentKey?: string
46
+ /** Maximum number of recent items to store. Default 5. */
47
+ maxRecent?: number
48
+ /** Additional class name for the dialog. */
49
+ className?: string
50
+ }
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Fuzzy scoring
54
+ // ---------------------------------------------------------------------------
55
+
56
+ function fuzzyScore(query: string, target: string): number {
57
+ const q = query.toLowerCase()
58
+ const t = target.toLowerCase()
59
+ if (t === q) return 100
60
+ if (t.startsWith(q)) return 80
61
+ if (t.includes(q)) return 60
62
+ // Fuzzy subsequence match
63
+ let qi = 0
64
+ let score = 0
65
+ for (let ti = 0; ti < t.length && qi < q.length; ti++) {
66
+ if (t[ti] === q[qi]) {
67
+ score += 10
68
+ qi++
69
+ }
70
+ }
71
+ return qi === q.length ? score : 0
72
+ }
73
+
74
+ function scoreItem(query: string, item: CommandItem): number {
75
+ if (!query) return 0
76
+ let best = fuzzyScore(query, item.label)
77
+ if (item.description) best = Math.max(best, fuzzyScore(query, item.description) * 0.8)
78
+ if (item.keywords) {
79
+ for (const kw of item.keywords) {
80
+ best = Math.max(best, fuzzyScore(query, kw) * 0.9)
81
+ }
82
+ }
83
+ return best
84
+ }
85
+
86
+ // ---------------------------------------------------------------------------
87
+ // CommandBar
88
+ // ---------------------------------------------------------------------------
89
+
90
+ /**
91
+ * @description A universal command palette activated by Cmd+K (Mac) or Ctrl+K (Win).
92
+ * Features fuzzy search, grouped items, recent selections (localStorage),
93
+ * async search support, keyboard navigation, and Framer Motion animations.
94
+ * Fully configurable and not hardcoded to any app.
95
+ */
96
+ export function CommandBar({
97
+ items,
98
+ placeholder = 'Type a command\u2026',
99
+ hotkey = 'k',
100
+ onSearch,
101
+ recentKey = 'ui-kit-command-recent',
102
+ maxRecent = 5,
103
+ className,
104
+ }: CommandBarProps): React.JSX.Element {
105
+ const prefersReducedMotion = useReducedMotion()
106
+ const [open, setOpen] = useState(false)
107
+ const [query, setQuery] = useState('')
108
+ const [activeIndex, setActiveIndex] = useState(0)
109
+ const [asyncResults, setAsyncResults] = useState<CommandItem[]>([])
110
+ const [isSearching, setIsSearching] = useState(false)
111
+ const inputRef = useRef<HTMLInputElement>(null)
112
+ const listRef = useRef<HTMLDivElement>(null)
113
+
114
+ // Recent items from localStorage
115
+ const [recentIds, setRecentIds] = useState<string[]>(() => {
116
+ if (typeof window === 'undefined') return []
117
+ try {
118
+ return JSON.parse(localStorage.getItem(recentKey) ?? '[]') as string[]
119
+ } catch {
120
+ return []
121
+ }
122
+ })
123
+
124
+ const saveRecent = useCallback(
125
+ (id: string) => {
126
+ const updated = [id, ...recentIds.filter(r => r !== id)].slice(0, maxRecent)
127
+ setRecentIds(updated)
128
+ try {
129
+ localStorage.setItem(recentKey, JSON.stringify(updated))
130
+ } catch {
131
+ // localStorage might be full
132
+ }
133
+ },
134
+ [recentIds, recentKey, maxRecent],
135
+ )
136
+
137
+ // Hotkey listener
138
+ useEffect(() => {
139
+ const handler = (e: KeyboardEvent) => {
140
+ if ((e.metaKey || e.ctrlKey) && e.key === hotkey) {
141
+ e.preventDefault()
142
+ setOpen(o => !o)
143
+ }
144
+ if (e.key === 'Escape' && open) {
145
+ setOpen(false)
146
+ }
147
+ }
148
+ document.addEventListener('keydown', handler)
149
+ return () => document.removeEventListener('keydown', handler)
150
+ }, [hotkey, open])
151
+
152
+ // Focus input when opened
153
+ useEffect(() => {
154
+ if (open) {
155
+ setQuery('')
156
+ setActiveIndex(0)
157
+ setAsyncResults([])
158
+ requestAnimationFrame(() => inputRef.current?.focus())
159
+ }
160
+ }, [open])
161
+
162
+ // Prevent body scroll when open
163
+ useEffect(() => {
164
+ if (open) {
165
+ const prev = document.body.style.overflow
166
+ document.body.style.overflow = 'hidden'
167
+ return () => { document.body.style.overflow = prev }
168
+ }
169
+ }, [open])
170
+
171
+ // Async search debounce
172
+ useEffect(() => {
173
+ if (!onSearch || !query) {
174
+ setAsyncResults([])
175
+ return
176
+ }
177
+ setIsSearching(true)
178
+ const timer = setTimeout(async () => {
179
+ try {
180
+ const results = await onSearch(query)
181
+ setAsyncResults(results)
182
+ } catch {
183
+ setAsyncResults([])
184
+ } finally {
185
+ setIsSearching(false)
186
+ }
187
+ }, 200)
188
+ return () => clearTimeout(timer)
189
+ }, [query, onSearch])
190
+
191
+ // Build display list
192
+ const displayItems = useMemo(() => {
193
+ const allItems = [...items, ...asyncResults]
194
+
195
+ if (!query) {
196
+ // Show recent items first, then all items
197
+ const recentItems = recentIds
198
+ .map(id => allItems.find(i => i.id === id))
199
+ .filter((i): i is CommandItem => i !== undefined)
200
+ const rest = allItems.filter(i => !recentIds.includes(i.id))
201
+ return [
202
+ ...recentItems.map(i => ({ ...i, group: 'Recent' })),
203
+ ...rest,
204
+ ]
205
+ }
206
+
207
+ // Score and sort
208
+ return allItems
209
+ .map(item => ({ item, score: scoreItem(query, item) }))
210
+ .filter(({ score }) => score > 0)
211
+ .sort((a, b) => b.score - a.score)
212
+ .map(({ item }) => item)
213
+ }, [items, asyncResults, query, recentIds])
214
+
215
+ // Group items
216
+ const groups = useMemo(() => {
217
+ const grouped = new Map<string, CommandItem[]>()
218
+ for (const item of displayItems) {
219
+ const group = item.group ?? ''
220
+ const arr = grouped.get(group)
221
+ if (arr) arr.push(item)
222
+ else grouped.set(group, [item])
223
+ }
224
+ return grouped
225
+ }, [displayItems])
226
+
227
+ // Flatten for keyboard nav
228
+ const flatItems = displayItems
229
+
230
+ // Keep activeIndex in bounds
231
+ useEffect(() => {
232
+ setActiveIndex(0)
233
+ }, [query])
234
+
235
+ const handleSelect = useCallback(
236
+ (item: CommandItem) => {
237
+ saveRecent(item.id)
238
+ setOpen(false)
239
+ item.onSelect()
240
+ },
241
+ [saveRecent],
242
+ )
243
+
244
+ // Keyboard navigation
245
+ const handleKeyDown = useCallback(
246
+ (e: React.KeyboardEvent) => {
247
+ if (e.key === 'ArrowDown') {
248
+ e.preventDefault()
249
+ setActiveIndex(i => Math.min(i + 1, flatItems.length - 1))
250
+ } else if (e.key === 'ArrowUp') {
251
+ e.preventDefault()
252
+ setActiveIndex(i => Math.max(i - 1, 0))
253
+ } else if (e.key === 'Enter') {
254
+ e.preventDefault()
255
+ const item = flatItems[activeIndex]
256
+ if (item) handleSelect(item)
257
+ }
258
+ },
259
+ [flatItems, activeIndex, handleSelect],
260
+ )
261
+
262
+ // Scroll active item into view
263
+ useEffect(() => {
264
+ if (!listRef.current) return
265
+ const active = listRef.current.querySelector('[data-active="true"]')
266
+ active?.scrollIntoView({ block: 'nearest' })
267
+ }, [activeIndex])
268
+
269
+ const isMac = typeof navigator !== 'undefined' && /Mac|iPhone/.test(navigator.userAgent ?? '')
270
+
271
+ return (
272
+ <AnimatePresence>
273
+ {open && (
274
+ <div className="fixed inset-0 z-50">
275
+ {/* Backdrop */}
276
+ <motion.div
277
+ initial={{ opacity: 0 }}
278
+ animate={{ opacity: 1 }}
279
+ exit={{ opacity: 0 }}
280
+ transition={prefersReducedMotion ? { duration: 0 } : { duration: 0.15 }}
281
+ className="absolute inset-0 bg-[hsl(var(--bg-base)/0.6)] backdrop-blur-sm"
282
+ onClick={() => setOpen(false)}
283
+ />
284
+
285
+ {/* Dialog */}
286
+ <motion.div
287
+ initial={prefersReducedMotion ? undefined : { opacity: 0, scale: 0.96, y: -20 }}
288
+ animate={prefersReducedMotion ? undefined : { opacity: 1, scale: 1, y: 0 }}
289
+ exit={prefersReducedMotion ? undefined : { opacity: 0, scale: 0.96, y: -20 }}
290
+ transition={prefersReducedMotion ? { duration: 0 } : { duration: 0.2, ease: [0.16, 1, 0.3, 1] }}
291
+ className={cn(
292
+ 'absolute left-1/2 top-[15%] -translate-x-1/2',
293
+ 'w-full max-w-lg rounded-2xl overflow-hidden',
294
+ 'border border-[hsl(var(--border-default))]',
295
+ 'bg-[hsl(var(--bg-elevated))] shadow-2xl',
296
+ 'flex flex-col max-h-[70vh]',
297
+ className,
298
+ )}
299
+ onKeyDown={handleKeyDown}
300
+ >
301
+ {/* Search input */}
302
+ <div className="flex items-center gap-3 px-4 py-3 border-b border-[hsl(var(--border-subtle)/0.5)]">
303
+ <Search className="h-5 w-5 text-[hsl(var(--text-tertiary))] shrink-0" />
304
+ <input
305
+ ref={inputRef}
306
+ type="text"
307
+ value={query}
308
+ onChange={e => setQuery(e.target.value)}
309
+ placeholder={placeholder}
310
+ className="flex-1 bg-transparent text-[hsl(var(--text-primary))] text-sm placeholder:text-[hsl(var(--text-tertiary))] outline-none"
311
+ />
312
+ <kbd className="hidden sm:inline-flex items-center gap-1 rounded-md border border-[hsl(var(--border-subtle))] bg-[hsl(var(--bg-surface))] px-1.5 py-0.5 text-[10px] text-[hsl(var(--text-tertiary))] font-mono">
313
+ Esc
314
+ </kbd>
315
+ </div>
316
+
317
+ {/* Results list */}
318
+ <div ref={listRef} className="flex-1 overflow-y-auto py-2">
319
+ {flatItems.length === 0 && !isSearching && (
320
+ <div className="px-4 py-8 text-center text-sm text-[hsl(var(--text-tertiary))]">
321
+ {query ? 'No results found.' : 'No commands available.'}
322
+ </div>
323
+ )}
324
+
325
+ {isSearching && flatItems.length === 0 && (
326
+ <div className="px-4 py-8 flex items-center justify-center gap-2 text-sm text-[hsl(var(--text-tertiary))]">
327
+ <div className="h-4 w-4 rounded-full border-2 border-[hsl(var(--brand-primary))] border-t-transparent animate-spin" />
328
+ Searching...
329
+ </div>
330
+ )}
331
+
332
+ {[...groups.entries()].map(([groupName, groupItems]) => {
333
+ return (
334
+ <div key={groupName || '__ungrouped'}>
335
+ {groupName && (
336
+ <div className="px-4 pt-2 pb-1">
337
+ <span className="text-[10px] font-semibold uppercase tracking-wider text-[hsl(var(--text-tertiary))]">
338
+ {groupName}
339
+ </span>
340
+ </div>
341
+ )}
342
+ {groupItems.map(item => {
343
+ const globalIdx = flatItems.indexOf(item)
344
+ const isActive = globalIdx === activeIndex
345
+ const Icon = item.icon
346
+ const isRecent = item.group === 'Recent'
347
+ return (
348
+ <button
349
+ key={item.id}
350
+ data-active={isActive}
351
+ onClick={() => handleSelect(item)}
352
+ onMouseEnter={() => setActiveIndex(globalIdx)}
353
+ className={cn(
354
+ 'w-full flex items-center gap-3 px-4 py-2.5 text-left transition-colors',
355
+ isActive
356
+ ? 'bg-[hsl(var(--brand-primary)/0.1)]'
357
+ : 'hover:bg-[hsl(var(--bg-surface)/0.5)]',
358
+ )}
359
+ >
360
+ {Icon ? (
361
+ <Icon className={cn(
362
+ 'h-4 w-4 shrink-0',
363
+ isActive ? 'text-[hsl(var(--brand-primary))]' : 'text-[hsl(var(--text-tertiary))]',
364
+ )} />
365
+ ) : isRecent ? (
366
+ <Clock className={cn(
367
+ 'h-4 w-4 shrink-0',
368
+ isActive ? 'text-[hsl(var(--brand-primary))]' : 'text-[hsl(var(--text-tertiary))]',
369
+ )} />
370
+ ) : (
371
+ <div className="h-4 w-4 shrink-0" />
372
+ )}
373
+
374
+ <div className="flex-1 min-w-0">
375
+ <div className={cn(
376
+ 'text-sm font-medium truncate',
377
+ isActive ? 'text-[hsl(var(--text-primary))]' : 'text-[hsl(var(--text-primary))]',
378
+ )}>
379
+ {item.label}
380
+ </div>
381
+ {item.description && (
382
+ <div className="text-[11px] text-[hsl(var(--text-tertiary))] truncate mt-0.5">
383
+ {item.description}
384
+ </div>
385
+ )}
386
+ </div>
387
+
388
+ {item.shortcut && (
389
+ <kbd className="flex items-center gap-0.5 rounded-md border border-[hsl(var(--border-subtle))] bg-[hsl(var(--bg-surface))] px-1.5 py-0.5 text-[10px] text-[hsl(var(--text-tertiary))] font-mono shrink-0">
390
+ {item.shortcut}
391
+ </kbd>
392
+ )}
393
+
394
+ {isActive && (
395
+ <CornerDownLeft className="h-3.5 w-3.5 text-[hsl(var(--text-tertiary))] shrink-0" />
396
+ )}
397
+ </button>
398
+ )
399
+ })}
400
+ </div>
401
+ )
402
+ })}
403
+ </div>
404
+
405
+ {/* Footer */}
406
+ <div className="flex items-center gap-4 px-4 py-2 border-t border-[hsl(var(--border-subtle)/0.5)] text-[10px] text-[hsl(var(--text-tertiary))]">
407
+ <span className="inline-flex items-center gap-1">
408
+ <kbd className="rounded border border-[hsl(var(--border-subtle))] bg-[hsl(var(--bg-surface))] px-1 py-0.5 font-mono">&uarr;&darr;</kbd>
409
+ Navigate
410
+ </span>
411
+ <span className="inline-flex items-center gap-1">
412
+ <kbd className="rounded border border-[hsl(var(--border-subtle))] bg-[hsl(var(--bg-surface))] px-1 py-0.5 font-mono">&crarr;</kbd>
413
+ Select
414
+ </span>
415
+ <span className="inline-flex items-center gap-1">
416
+ <kbd className="rounded border border-[hsl(var(--border-subtle))] bg-[hsl(var(--bg-surface))] px-1 py-0.5 font-mono">Esc</kbd>
417
+ Close
418
+ </span>
419
+ <span className="ml-auto inline-flex items-center gap-1">
420
+ <kbd className="rounded border border-[hsl(var(--border-subtle))] bg-[hsl(var(--bg-surface))] px-1 py-0.5 font-mono">
421
+ {isMac ? '\u2318' : 'Ctrl+'}
422
+ </kbd>
423
+ <kbd className="rounded border border-[hsl(var(--border-subtle))] bg-[hsl(var(--bg-surface))] px-1 py-0.5 font-mono uppercase">
424
+ {hotkey}
425
+ </kbd>
426
+ Toggle
427
+ </span>
428
+ </div>
429
+ </motion.div>
430
+ </div>
431
+ )}
432
+ </AnimatePresence>
433
+ )
434
+ }
@@ -0,0 +1,115 @@
1
+ 'use client'
2
+
3
+ import type React from 'react'
4
+ import { useState } from 'react'
5
+ import { motion, useReducedMotion } from 'framer-motion'
6
+ import { cn } from '../utils'
7
+
8
+ export interface ConfidenceBarProps {
9
+ /** Confidence value between 0 and 1 (probability). */
10
+ value: number
11
+ /** Optional label displayed before the bar. */
12
+ label?: string
13
+ /** Show percentage text beside the bar. */
14
+ showPercentage?: boolean
15
+ /** Thresholds for color zones. */
16
+ thresholds?: { low: number; medium: number }
17
+ /** Size preset. */
18
+ size?: 'sm' | 'md'
19
+ className?: string
20
+ }
21
+
22
+ const SIZE_CLASSES = {
23
+ sm: 'h-1.5',
24
+ md: 'h-2.5',
25
+ } as const
26
+
27
+ function getBarColor(value: number, thresholds: { low: number; medium: number }): string {
28
+ if (value < thresholds.low) return 'bg-[hsl(var(--status-critical))]'
29
+ if (value < thresholds.medium) return 'bg-[hsl(var(--status-warning))]'
30
+ return 'bg-[hsl(var(--status-ok))]'
31
+ }
32
+
33
+ function getTextColor(value: number, thresholds: { low: number; medium: number }): string {
34
+ if (value < thresholds.low) return 'text-[hsl(var(--status-critical))]'
35
+ if (value < thresholds.medium) return 'text-[hsl(var(--status-warning))]'
36
+ return 'text-[hsl(var(--status-ok))]'
37
+ }
38
+
39
+ /**
40
+ * @description A horizontal confidence/probability bar colored by threshold zones:
41
+ * red (<low), yellow (low-medium), green (>medium). Animates fill on mount.
42
+ */
43
+ export function ConfidenceBar({
44
+ value,
45
+ label,
46
+ showPercentage = true,
47
+ thresholds = { low: 0.3, medium: 0.7 },
48
+ size = 'md',
49
+ className,
50
+ }: ConfidenceBarProps): React.JSX.Element {
51
+ const reduced = useReducedMotion()
52
+ const [hovered, setHovered] = useState(false)
53
+ const clamped = Math.min(1, Math.max(0, value))
54
+ const pct = clamped * 100
55
+ const barColor = getBarColor(clamped, thresholds)
56
+ const textColor = getTextColor(clamped, thresholds)
57
+
58
+ return (
59
+ <div
60
+ className={cn('w-full', className)}
61
+ onMouseEnter={() => setHovered(true)}
62
+ onMouseLeave={() => setHovered(false)}
63
+ >
64
+ {/* Label + percentage row */}
65
+ {(label || showPercentage) && (
66
+ <div className="flex items-center justify-between mb-1">
67
+ {label && (
68
+ <span className="text-xs font-medium text-[hsl(var(--text-secondary))]">
69
+ {label}
70
+ </span>
71
+ )}
72
+ {showPercentage && (
73
+ <span className={cn('text-xs font-medium tabular-nums', textColor)}>
74
+ {pct.toFixed(1)}%
75
+ </span>
76
+ )}
77
+ </div>
78
+ )}
79
+
80
+ {/* Track */}
81
+ <div
82
+ className={cn(
83
+ 'relative w-full overflow-hidden rounded-full bg-[hsl(var(--bg-overlay))]',
84
+ SIZE_CLASSES[size],
85
+ )}
86
+ role="meter"
87
+ aria-valuenow={pct}
88
+ aria-valuemin={0}
89
+ aria-valuemax={100}
90
+ aria-label={label ?? `Confidence: ${pct.toFixed(1)}%`}
91
+ >
92
+ {reduced ? (
93
+ <div
94
+ className={cn('h-full rounded-full', barColor)}
95
+ style={{ width: `${pct}%` }}
96
+ />
97
+ ) : (
98
+ <motion.div
99
+ className={cn('h-full rounded-full', barColor)}
100
+ initial={{ width: 0 }}
101
+ animate={{ width: `${pct}%` }}
102
+ transition={{ type: 'spring', stiffness: 80, damping: 20 }}
103
+ />
104
+ )}
105
+ </div>
106
+
107
+ {/* Tooltip on hover showing exact value */}
108
+ {hovered && (
109
+ <div className="mt-1 text-[10px] text-[hsl(var(--text-tertiary))] tabular-nums">
110
+ {clamped.toFixed(4)} ({pct.toFixed(2)}%)
111
+ </div>
112
+ )}
113
+ </div>
114
+ )
115
+ }
@@ -1,5 +1,6 @@
1
1
  'use client'
2
2
 
3
+ import type React from 'react'
3
4
  import * as AlertDialog from '@radix-ui/react-alert-dialog'
4
5
  import { motion, AnimatePresence } from 'framer-motion'
5
6
  import { AlertTriangle, Loader2 } from 'lucide-react'
@@ -55,7 +56,7 @@ export function ConfirmDialog({
55
56
  variant = 'danger',
56
57
  loading = false,
57
58
  onConfirm,
58
- }: ConfirmDialogProps) {
59
+ }: ConfirmDialogProps): React.JSX.Element {
59
60
  const styles = variantStyles[variant]
60
61
 
61
62
  return (