@annondeveloper/ui-kit 0.1.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 (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +215 -0
  3. package/dist/chunk-5OKSXPWK.js +270 -0
  4. package/dist/chunk-5OKSXPWK.js.map +1 -0
  5. package/dist/cli/index.js +430 -0
  6. package/dist/form.d.ts +65 -0
  7. package/dist/form.js +148 -0
  8. package/dist/form.js.map +1 -0
  9. package/dist/index.d.ts +942 -0
  10. package/dist/index.js +2812 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/select-nnBJUO8U.d.ts +26 -0
  13. package/package.json +114 -0
  14. package/src/components/animated-counter.stories.tsx +68 -0
  15. package/src/components/animated-counter.tsx +85 -0
  16. package/src/components/avatar.tsx +106 -0
  17. package/src/components/badge.stories.tsx +70 -0
  18. package/src/components/badge.tsx +97 -0
  19. package/src/components/button.stories.tsx +101 -0
  20. package/src/components/button.tsx +67 -0
  21. package/src/components/card.tsx +128 -0
  22. package/src/components/checkbox.stories.tsx +64 -0
  23. package/src/components/checkbox.tsx +58 -0
  24. package/src/components/confirm-dialog.stories.tsx +96 -0
  25. package/src/components/confirm-dialog.tsx +145 -0
  26. package/src/components/data-table.stories.tsx +125 -0
  27. package/src/components/data-table.tsx +791 -0
  28. package/src/components/dropdown-menu.tsx +111 -0
  29. package/src/components/empty-state.stories.tsx +42 -0
  30. package/src/components/empty-state.tsx +43 -0
  31. package/src/components/filter-pill.stories.tsx +71 -0
  32. package/src/components/filter-pill.tsx +45 -0
  33. package/src/components/form-input.stories.tsx +91 -0
  34. package/src/components/form-input.tsx +77 -0
  35. package/src/components/log-viewer.tsx +212 -0
  36. package/src/components/metric-card.tsx +141 -0
  37. package/src/components/pipeline-stage.tsx +134 -0
  38. package/src/components/popover.tsx +72 -0
  39. package/src/components/port-status-grid.tsx +102 -0
  40. package/src/components/progress.tsx +128 -0
  41. package/src/components/radio-group.tsx +162 -0
  42. package/src/components/select.stories.tsx +52 -0
  43. package/src/components/select.tsx +92 -0
  44. package/src/components/severity-timeline.tsx +125 -0
  45. package/src/components/sheet.tsx +164 -0
  46. package/src/components/skeleton.stories.tsx +64 -0
  47. package/src/components/skeleton.tsx +62 -0
  48. package/src/components/slider.tsx +208 -0
  49. package/src/components/sparkline.tsx +104 -0
  50. package/src/components/status-badge.stories.tsx +84 -0
  51. package/src/components/status-badge.tsx +71 -0
  52. package/src/components/status-pulse.stories.tsx +56 -0
  53. package/src/components/status-pulse.tsx +78 -0
  54. package/src/components/success-checkmark.stories.tsx +67 -0
  55. package/src/components/success-checkmark.tsx +53 -0
  56. package/src/components/tabs.tsx +177 -0
  57. package/src/components/threshold-gauge.tsx +149 -0
  58. package/src/components/time-range-selector.tsx +86 -0
  59. package/src/components/toast.stories.tsx +70 -0
  60. package/src/components/toast.tsx +48 -0
  61. package/src/components/toggle-switch.stories.tsx +66 -0
  62. package/src/components/toggle-switch.tsx +51 -0
  63. package/src/components/tooltip.tsx +62 -0
  64. package/src/components/truncated-text.stories.tsx +56 -0
  65. package/src/components/truncated-text.tsx +80 -0
  66. package/src/components/uptime-tracker.tsx +138 -0
  67. package/src/components/utilization-bar.tsx +103 -0
  68. package/src/theme.css +178 -0
  69. package/src/utils.ts +123 -0
@@ -0,0 +1,66 @@
1
+ import type { Meta, StoryObj } from '@storybook/react'
2
+ import { useState } from 'react'
3
+ import { ToggleSwitch } from './toggle-switch'
4
+
5
+ const meta: Meta<typeof ToggleSwitch> = {
6
+ title: 'Components/ToggleSwitch',
7
+ component: ToggleSwitch,
8
+ tags: ['autodocs'],
9
+ argTypes: {
10
+ size: { control: 'select', options: ['sm', 'md'] },
11
+ disabled: { control: 'boolean' },
12
+ },
13
+ }
14
+ export default meta
15
+ type Story = StoryObj<typeof ToggleSwitch>
16
+
17
+ export const Default: Story = {
18
+ render: (args) => {
19
+ const [enabled, setEnabled] = useState(false)
20
+ return <ToggleSwitch {...args} enabled={enabled} onChange={setEnabled} />
21
+ },
22
+ }
23
+
24
+ export const On: Story = {
25
+ render: () => {
26
+ const [enabled, setEnabled] = useState(true)
27
+ return <ToggleSwitch enabled={enabled} onChange={setEnabled} />
28
+ },
29
+ }
30
+
31
+ export const Off: Story = {
32
+ render: () => {
33
+ const [enabled, setEnabled] = useState(false)
34
+ return <ToggleSwitch enabled={enabled} onChange={setEnabled} />
35
+ },
36
+ }
37
+
38
+ export const SmallSize: Story = {
39
+ render: () => {
40
+ const [enabled, setEnabled] = useState(true)
41
+ return <ToggleSwitch size="sm" enabled={enabled} onChange={setEnabled} />
42
+ },
43
+ }
44
+
45
+ export const Disabled: Story = {
46
+ render: () => (
47
+ <div style={{ display: 'flex', gap: '1rem' }}>
48
+ <ToggleSwitch enabled={true} onChange={() => {}} disabled />
49
+ <ToggleSwitch enabled={false} onChange={() => {}} disabled />
50
+ </div>
51
+ ),
52
+ }
53
+
54
+ export const WithLabel: Story = {
55
+ render: () => {
56
+ const [enabled, setEnabled] = useState(false)
57
+ return (
58
+ <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
59
+ <ToggleSwitch enabled={enabled} onChange={setEnabled} label="Enable notifications" />
60
+ <span style={{ fontSize: '0.875rem', color: 'hsl(210 20% 70%)' }}>
61
+ Notifications {enabled ? 'on' : 'off'}
62
+ </span>
63
+ </div>
64
+ )
65
+ },
66
+ }
@@ -0,0 +1,51 @@
1
+ 'use client'
2
+
3
+ import { ToggleLeft, ToggleRight } from 'lucide-react'
4
+ import { cn } from '../utils'
5
+
6
+ export interface ToggleSwitchProps {
7
+ /** Whether the toggle is on. */
8
+ enabled: boolean
9
+ /** Callback when toggled. */
10
+ onChange: (enabled: boolean) => void
11
+ /** Size variant. */
12
+ size?: 'sm' | 'md'
13
+ /** Disable the toggle. */
14
+ disabled?: boolean
15
+ /** Accessible label. */
16
+ label?: string
17
+ className?: string
18
+ }
19
+
20
+ /**
21
+ * @description A themed toggle switch using lucide icons for on/off states.
22
+ * Supports dark/light mode via CSS custom property tokens.
23
+ */
24
+ export function ToggleSwitch({
25
+ enabled, onChange, size = 'md', disabled, label, className,
26
+ }: ToggleSwitchProps) {
27
+ const iconSize = size === 'sm' ? 'size-5' : 'size-6'
28
+
29
+ return (
30
+ <button
31
+ type="button"
32
+ role="switch"
33
+ aria-checked={enabled}
34
+ aria-label={label}
35
+ disabled={disabled}
36
+ onClick={() => onChange(!enabled)}
37
+ className={cn(
38
+ 'inline-flex items-center transition-colors',
39
+ 'text-[hsl(var(--text-secondary))] hover:text-[hsl(var(--text-primary))]',
40
+ 'disabled:opacity-50 disabled:cursor-not-allowed',
41
+ className,
42
+ )}
43
+ >
44
+ {enabled ? (
45
+ <ToggleRight className={cn(iconSize, 'text-[hsl(var(--status-ok))]')} />
46
+ ) : (
47
+ <ToggleLeft className={cn(iconSize, 'text-[hsl(var(--text-tertiary))]')} />
48
+ )}
49
+ </button>
50
+ )
51
+ }
@@ -0,0 +1,62 @@
1
+ 'use client'
2
+
3
+ import { type ReactNode } from 'react'
4
+ import * as TooltipPrimitive from '@radix-ui/react-tooltip'
5
+ import { cn } from '../utils'
6
+
7
+ export interface TooltipProps {
8
+ /** Tooltip content (can be text or ReactNode). */
9
+ content: ReactNode
10
+ /** Trigger element. */
11
+ children: ReactNode
12
+ /** Side of the trigger to display the tooltip. */
13
+ side?: 'top' | 'right' | 'bottom' | 'left'
14
+ /** Delay in milliseconds before the tooltip appears. */
15
+ delay?: number
16
+ /** Additional class name for the tooltip content. */
17
+ className?: string
18
+ }
19
+
20
+ /**
21
+ * @description A simple tooltip wrapper built on Radix Tooltip.
22
+ * Theme-styled content with arrow pointer and configurable delay.
23
+ * Requires a TooltipProvider ancestor (included by default).
24
+ */
25
+ export function Tooltip({
26
+ content,
27
+ children,
28
+ side = 'top',
29
+ delay = 200,
30
+ className,
31
+ }: TooltipProps) {
32
+ return (
33
+ <TooltipPrimitive.Provider delayDuration={delay}>
34
+ <TooltipPrimitive.Root>
35
+ <TooltipPrimitive.Trigger asChild>{children}</TooltipPrimitive.Trigger>
36
+ <TooltipPrimitive.Portal>
37
+ <TooltipPrimitive.Content
38
+ side={side}
39
+ sideOffset={6}
40
+ className={cn(
41
+ 'z-50 px-3 py-1.5 rounded-lg text-xs font-medium',
42
+ 'bg-[hsl(var(--bg-elevated))] text-[hsl(var(--text-primary))]',
43
+ 'border border-[hsl(var(--border-default))] shadow-lg',
44
+ 'animate-in fade-in-0 zoom-in-95',
45
+ 'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
46
+ 'data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2',
47
+ 'data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
48
+ className,
49
+ )}
50
+ >
51
+ {content}
52
+ <TooltipPrimitive.Arrow
53
+ className="fill-[hsl(var(--bg-elevated))]"
54
+ width={10}
55
+ height={5}
56
+ />
57
+ </TooltipPrimitive.Content>
58
+ </TooltipPrimitive.Portal>
59
+ </TooltipPrimitive.Root>
60
+ </TooltipPrimitive.Provider>
61
+ )
62
+ }
@@ -0,0 +1,56 @@
1
+ import type { Meta, StoryObj } from '@storybook/react'
2
+ import { TruncatedText } from './truncated-text'
3
+
4
+ const meta: Meta<typeof TruncatedText> = {
5
+ title: 'Components/TruncatedText',
6
+ component: TruncatedText,
7
+ tags: ['autodocs'],
8
+ }
9
+ export default meta
10
+ type Story = StoryObj<typeof TruncatedText>
11
+
12
+ export const ShortText: Story = {
13
+ args: {
14
+ text: 'core-sw-01',
15
+ maxWidth: 200,
16
+ },
17
+ }
18
+
19
+ export const LongText: Story = {
20
+ args: {
21
+ text: 'very-long-hostname-that-definitely-will-not-fit-in-a-small-column-width.example.datacenter.internal',
22
+ maxWidth: 200,
23
+ },
24
+ }
25
+
26
+ export const IPAddress: Story = {
27
+ args: {
28
+ text: '2001:0db8:85a3:0000:0000:8a2e:0370:7334',
29
+ maxWidth: 150,
30
+ },
31
+ }
32
+
33
+ export const InTableContext: Story = {
34
+ render: () => (
35
+ <div style={{ width: '400px', border: '1px solid hsl(222 25% 25%)', borderRadius: '0.5rem', overflow: 'hidden' }}>
36
+ <table style={{ width: '100%', borderCollapse: 'collapse' }}>
37
+ <thead>
38
+ <tr style={{ borderBottom: '1px solid hsl(222 30% 20%)' }}>
39
+ <th style={{ textAlign: 'left', padding: '0.5rem 1rem', fontSize: '0.75rem', color: 'hsl(210 15% 50%)' }}>Hostname</th>
40
+ <th style={{ textAlign: 'left', padding: '0.5rem 1rem', fontSize: '0.75rem', color: 'hsl(210 15% 50%)' }}>Description</th>
41
+ </tr>
42
+ </thead>
43
+ <tbody>
44
+ <tr>
45
+ <td style={{ padding: '0.5rem 1rem' }}><TruncatedText text="sw-core-01" maxWidth={120} /></td>
46
+ <td style={{ padding: '0.5rem 1rem' }}><TruncatedText text="Primary core switch in building A, floor 3, MDF closet" maxWidth={180} /></td>
47
+ </tr>
48
+ <tr>
49
+ <td style={{ padding: '0.5rem 1rem' }}><TruncatedText text="fw-edge-datacenter-primary-01" maxWidth={120} /></td>
50
+ <td style={{ padding: '0.5rem 1rem' }}><TruncatedText text="Edge firewall" maxWidth={180} /></td>
51
+ </tr>
52
+ </tbody>
53
+ </table>
54
+ </div>
55
+ ),
56
+ }
@@ -0,0 +1,80 @@
1
+ 'use client'
2
+
3
+ import { useRef, useState, useEffect } from 'react'
4
+ import * as Tooltip from '@radix-ui/react-tooltip'
5
+ import { Copy, Check } from 'lucide-react'
6
+
7
+ export interface TruncatedTextProps {
8
+ /** The full text to display (truncated if it overflows). */
9
+ text: string
10
+ /** Max width constraint for truncation. */
11
+ maxWidth?: string | number
12
+ className?: string
13
+ }
14
+
15
+ /**
16
+ * @description A text element that truncates with ellipsis and shows a tooltip with the
17
+ * full text on hover when truncated. Includes a copy-to-clipboard button in the tooltip.
18
+ */
19
+ export function TruncatedText({ text, maxWidth = '100%', className = '' }: TruncatedTextProps) {
20
+ const ref = useRef<HTMLSpanElement>(null)
21
+ const [isTruncated, setIsTruncated] = useState(false)
22
+ const [copied, setCopied] = useState(false)
23
+
24
+ useEffect(() => {
25
+ const el = ref.current
26
+ if (el) setIsTruncated(el.scrollWidth > el.clientWidth)
27
+ }, [text])
28
+
29
+ const handleCopy = async (e: React.MouseEvent) => {
30
+ e.stopPropagation()
31
+ await navigator.clipboard.writeText(text)
32
+ setCopied(true)
33
+ setTimeout(() => setCopied(false), 1500)
34
+ }
35
+
36
+ const inner = (
37
+ <span
38
+ ref={ref}
39
+ className={`block truncate ${className}`}
40
+ style={{ maxWidth }}
41
+ >
42
+ {text}
43
+ </span>
44
+ )
45
+
46
+ if (!isTruncated) return inner
47
+
48
+ return (
49
+ <Tooltip.Provider delayDuration={300}>
50
+ <Tooltip.Root>
51
+ <Tooltip.Trigger asChild>{inner}</Tooltip.Trigger>
52
+ <Tooltip.Portal>
53
+ <Tooltip.Content
54
+ side="top"
55
+ align="start"
56
+ sideOffset={6}
57
+ className="z-50 max-w-[400px] rounded-lg border border-[hsl(var(--border-default))]
58
+ bg-[hsl(var(--bg-elevated))] px-3 py-2 shadow-lg
59
+ animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out
60
+ data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95"
61
+ >
62
+ <p className="text-small text-[hsl(var(--text-primary))] break-all">{text}</p>
63
+ <div className="mt-1.5 flex justify-end">
64
+ <button
65
+ onClick={handleCopy}
66
+ className="flex items-center gap-1 rounded px-2 py-0.5
67
+ text-[11px] text-[hsl(var(--brand-primary))]
68
+ hover:bg-[hsl(var(--bg-surface))] transition-colors"
69
+ >
70
+ {copied ? <Check className="h-3 w-3" /> : <Copy className="h-3 w-3" />}
71
+ {copied ? 'Copied' : 'Copy'}
72
+ </button>
73
+ </div>
74
+ <Tooltip.Arrow className="fill-[hsl(var(--bg-elevated))]" />
75
+ </Tooltip.Content>
76
+ </Tooltip.Portal>
77
+ </Tooltip.Root>
78
+ </Tooltip.Provider>
79
+ )
80
+ }
@@ -0,0 +1,138 @@
1
+ 'use client'
2
+
3
+ import { motion, useReducedMotion } from 'framer-motion'
4
+ import { cn } from '../utils'
5
+
6
+ export interface DayStatus {
7
+ /** Date in YYYY-MM-DD format. */
8
+ date: string
9
+ /** Operational status for that day. */
10
+ status: 'ok' | 'degraded' | 'outage' | 'no-data'
11
+ /** Uptime percentage for that day (0-100). */
12
+ uptime?: number
13
+ }
14
+
15
+ export interface UptimeTrackerProps {
16
+ /** Array of day-status entries (oldest first). */
17
+ days: DayStatus[]
18
+ /** Show overall uptime percentage. */
19
+ showPercentage?: boolean
20
+ /** Optional label displayed above the bar. */
21
+ label?: string
22
+ /** Callback when a day bar is clicked. */
23
+ onDayClick?: (day: DayStatus) => void
24
+ className?: string
25
+ }
26
+
27
+ const dayColor: Record<string, string> = {
28
+ ok: 'bg-[hsl(var(--status-ok))]',
29
+ degraded: 'bg-[hsl(var(--status-warning))]',
30
+ outage: 'bg-[hsl(var(--status-critical))]',
31
+ 'no-data': 'bg-[hsl(var(--text-disabled))]',
32
+ }
33
+
34
+ const dayHover: Record<string, string> = {
35
+ ok: 'hover:bg-[hsl(var(--status-ok))]/80',
36
+ degraded: 'hover:bg-[hsl(var(--status-warning))]/80',
37
+ outage: 'hover:bg-[hsl(var(--status-critical))]/80',
38
+ 'no-data': 'hover:bg-[hsl(var(--text-disabled))]/80',
39
+ }
40
+
41
+ function formatDate(dateStr: string): string {
42
+ try {
43
+ const d = new Date(dateStr + 'T00:00:00')
44
+ return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' })
45
+ } catch {
46
+ return dateStr
47
+ }
48
+ }
49
+
50
+ /**
51
+ * @description A row of thin vertical bars representing daily uptime status,
52
+ * inspired by GitHub/Statuspage uptime indicators. Each bar is colored by
53
+ * operational status (ok/degraded/outage/no-data). Hover shows date and uptime
54
+ * percentage. Designed for SLA and availability tracking displays.
55
+ */
56
+ export function UptimeTracker({
57
+ days,
58
+ showPercentage = true,
59
+ label,
60
+ onDayClick,
61
+ className,
62
+ }: UptimeTrackerProps) {
63
+ const reduced = useReducedMotion()
64
+
65
+ // Calculate overall uptime
66
+ const daysWithUptime = days.filter(d => d.uptime != null)
67
+ const overallUptime = daysWithUptime.length > 0
68
+ ? daysWithUptime.reduce((sum, d) => sum + (d.uptime ?? 0), 0) / daysWithUptime.length
69
+ : null
70
+
71
+ return (
72
+ <div className={cn('space-y-2', className)}>
73
+ {/* Header */}
74
+ {(label || showPercentage) && (
75
+ <div className="flex items-center justify-between">
76
+ {label && (
77
+ <span className="text-[0.8125rem] font-medium text-[hsl(var(--text-primary))]">
78
+ {label}
79
+ </span>
80
+ )}
81
+ {showPercentage && overallUptime != null && (
82
+ <span className={cn(
83
+ 'text-[0.8125rem] font-semibold tabular-nums',
84
+ overallUptime >= 99.9
85
+ ? 'text-[hsl(var(--status-ok))]'
86
+ : overallUptime >= 99
87
+ ? 'text-[hsl(var(--status-warning))]'
88
+ : 'text-[hsl(var(--status-critical))]',
89
+ )}>
90
+ {overallUptime.toFixed(2)}% uptime
91
+ </span>
92
+ )}
93
+ </div>
94
+ )}
95
+
96
+ {/* Day bars */}
97
+ <div className="flex items-end gap-px h-8">
98
+ {days.map((day, i) => {
99
+ const tooltip = [
100
+ formatDate(day.date),
101
+ day.uptime != null ? `${day.uptime.toFixed(1)}%` : day.status,
102
+ ].join(' \u2014 ')
103
+
104
+ return (
105
+ <motion.button
106
+ key={day.date}
107
+ type="button"
108
+ initial={reduced ? false : { scaleY: 0 }}
109
+ animate={{ scaleY: 1 }}
110
+ transition={{ duration: 0.15, delay: reduced ? 0 : Math.min(i * 0.005, 0.3) }}
111
+ style={{ originY: '100%' }}
112
+ onClick={() => onDayClick?.(day)}
113
+ title={tooltip}
114
+ aria-label={tooltip}
115
+ className={cn(
116
+ 'flex-1 min-w-[2px] h-full rounded-[1px] transition-opacity cursor-pointer',
117
+ dayColor[day.status] ?? dayColor['no-data'],
118
+ dayHover[day.status] ?? dayHover['no-data'],
119
+ )}
120
+ />
121
+ )
122
+ })}
123
+ </div>
124
+
125
+ {/* Date range labels */}
126
+ {days.length > 0 && (
127
+ <div className="flex items-center justify-between">
128
+ <span className="text-[0.625rem] text-[hsl(var(--text-tertiary))]">
129
+ {formatDate(days[0].date)}
130
+ </span>
131
+ <span className="text-[0.625rem] text-[hsl(var(--text-tertiary))]">
132
+ {formatDate(days[days.length - 1].date)}
133
+ </span>
134
+ </div>
135
+ )}
136
+ </div>
137
+ )
138
+ }
@@ -0,0 +1,103 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+ import { useReducedMotion } from 'framer-motion'
5
+ import { cn, clamp } from '../utils'
6
+
7
+ export interface UtilizationBarProps {
8
+ /** Utilization value from 0 to 100. */
9
+ value: number
10
+ /** Warning and critical thresholds. */
11
+ thresholds?: { warning: number; critical: number }
12
+ /** Optional label displayed to the left. */
13
+ label?: string
14
+ /** Show percentage value text to the right. */
15
+ showValue?: boolean
16
+ /** Bar height size. */
17
+ size?: 'sm' | 'md' | 'lg'
18
+ /** Animate the fill width on mount. */
19
+ animated?: boolean
20
+ className?: string
21
+ }
22
+
23
+ const sizeMap = {
24
+ sm: 'h-1.5',
25
+ md: 'h-2.5',
26
+ lg: 'h-4',
27
+ }
28
+
29
+ function getBarColor(value: number, warning: number, critical: number): string {
30
+ if (value >= critical) return 'hsl(var(--util-high))'
31
+ if (value >= warning) return 'hsl(var(--util-medium))'
32
+ return 'hsl(var(--util-low))'
33
+ }
34
+
35
+ function getTextClass(value: number, warning: number, critical: number): string {
36
+ if (value >= critical) return 'text-[hsl(var(--status-critical))]'
37
+ if (value >= warning) return 'text-[hsl(var(--status-warning))]'
38
+ return 'text-[hsl(var(--text-secondary))]'
39
+ }
40
+
41
+ /**
42
+ * @description A segmented horizontal bar showing resource utilization with
43
+ * color-coded thresholds (green/yellow/red). Supports animated fill on mount.
44
+ */
45
+ export function UtilizationBar({
46
+ value: rawValue,
47
+ thresholds,
48
+ label,
49
+ showValue = true,
50
+ size = 'md',
51
+ animated = true,
52
+ className,
53
+ }: UtilizationBarProps) {
54
+ const reduced = useReducedMotion()
55
+ const value = clamp(rawValue, 0, 100)
56
+ const warning = thresholds?.warning ?? 70
57
+ const critical = thresholds?.critical ?? 90
58
+
59
+ const [displayWidth, setDisplayWidth] = useState(animated && !reduced ? 0 : value)
60
+
61
+ useEffect(() => {
62
+ if (!animated || reduced) {
63
+ setDisplayWidth(value)
64
+ return
65
+ }
66
+ // Trigger the animated width after mount
67
+ const raf = requestAnimationFrame(() => setDisplayWidth(value))
68
+ return () => cancelAnimationFrame(raf)
69
+ }, [value, animated, reduced])
70
+
71
+ const barColor = getBarColor(value, warning, critical)
72
+ const textClass = getTextClass(value, warning, critical)
73
+
74
+ return (
75
+ <div className={cn('flex items-center gap-2', className)} title={`${value.toFixed(1)}%`}>
76
+ {label && (
77
+ <span className="text-[0.75rem] font-medium text-[hsl(var(--text-secondary))] shrink-0 min-w-[3rem]">
78
+ {label}
79
+ </span>
80
+ )}
81
+ <div
82
+ className={cn(
83
+ 'flex-1 rounded-full bg-[hsl(var(--bg-elevated))] overflow-hidden',
84
+ sizeMap[size],
85
+ )}
86
+ >
87
+ <div
88
+ className="h-full rounded-full"
89
+ style={{
90
+ width: `${displayWidth}%`,
91
+ backgroundColor: barColor,
92
+ transition: animated && !reduced ? 'width 600ms cubic-bezier(0.4, 0, 0.2, 1)' : 'none',
93
+ }}
94
+ />
95
+ </div>
96
+ {showValue && (
97
+ <span className={cn('text-[0.75rem] font-medium tabular-nums shrink-0 min-w-[2.5rem] text-right', textClass)}>
98
+ {value.toFixed(0)}%
99
+ </span>
100
+ )}
101
+ </div>
102
+ )
103
+ }