@opencosmos/ui 1.3.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 (260) hide show
  1. package/.claude/CLAUDE.md +239 -0
  2. package/README.md +161 -0
  3. package/dist/cli.mjs +151 -0
  4. package/dist/dates.d.mts +20 -0
  5. package/dist/dates.d.ts +20 -0
  6. package/dist/dates.js +240 -0
  7. package/dist/dates.js.map +1 -0
  8. package/dist/dates.mjs +203 -0
  9. package/dist/dates.mjs.map +1 -0
  10. package/dist/dnd.d.mts +126 -0
  11. package/dist/dnd.d.ts +126 -0
  12. package/dist/dnd.js +274 -0
  13. package/dist/dnd.js.map +1 -0
  14. package/dist/dnd.mjs +250 -0
  15. package/dist/dnd.mjs.map +1 -0
  16. package/dist/fontThemes-Dh8mtXES.d.mts +868 -0
  17. package/dist/fontThemes-Dh8mtXES.d.ts +868 -0
  18. package/dist/forms.d.mts +38 -0
  19. package/dist/forms.d.ts +38 -0
  20. package/dist/forms.js +198 -0
  21. package/dist/forms.js.map +1 -0
  22. package/dist/forms.mjs +159 -0
  23. package/dist/forms.mjs.map +1 -0
  24. package/dist/hooks-1b8WaQf1.d.mts +225 -0
  25. package/dist/hooks-CKW8vE9H.d.ts +225 -0
  26. package/dist/hooks.d.mts +3 -0
  27. package/dist/hooks.d.ts +3 -0
  28. package/dist/hooks.js +971 -0
  29. package/dist/hooks.js.map +1 -0
  30. package/dist/hooks.mjs +943 -0
  31. package/dist/hooks.mjs.map +1 -0
  32. package/dist/index-DscTIrZ2.d.mts +29 -0
  33. package/dist/index-DscTIrZ2.d.ts +29 -0
  34. package/dist/index.d.mts +3382 -0
  35. package/dist/index.d.ts +3382 -0
  36. package/dist/index.js +15146 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/index.mjs +14802 -0
  39. package/dist/index.mjs.map +1 -0
  40. package/dist/providers-CXPDMsl7.d.mts +30 -0
  41. package/dist/providers-Dn_Msjvz.d.ts +30 -0
  42. package/dist/providers.d.mts +3 -0
  43. package/dist/providers.d.ts +3 -0
  44. package/dist/providers.js +1885 -0
  45. package/dist/providers.js.map +1 -0
  46. package/dist/providers.mjs +1859 -0
  47. package/dist/providers.mjs.map +1 -0
  48. package/dist/tables.d.mts +10 -0
  49. package/dist/tables.d.ts +10 -0
  50. package/dist/tables.js +248 -0
  51. package/dist/tables.js.map +1 -0
  52. package/dist/tables.mjs +218 -0
  53. package/dist/tables.mjs.map +1 -0
  54. package/dist/tokens.d.mts +1065 -0
  55. package/dist/tokens.d.ts +1065 -0
  56. package/dist/tokens.js +2637 -0
  57. package/dist/tokens.js.map +1 -0
  58. package/dist/tokens.mjs +2555 -0
  59. package/dist/tokens.mjs.map +1 -0
  60. package/dist/utils-CIIM7dAC.d.ts +986 -0
  61. package/dist/utils-Cs04sxth.d.mts +986 -0
  62. package/dist/utils.d.mts +4 -0
  63. package/dist/utils.d.ts +4 -0
  64. package/dist/utils.js +874 -0
  65. package/dist/utils.js.map +1 -0
  66. package/dist/utils.mjs +806 -0
  67. package/dist/utils.mjs.map +1 -0
  68. package/dist/validation-Bj1ye-v_.d.mts +114 -0
  69. package/dist/validation-Bj1ye-v_.d.ts +114 -0
  70. package/dist/webgl.d.mts +104 -0
  71. package/dist/webgl.d.ts +104 -0
  72. package/dist/webgl.js +226 -0
  73. package/dist/webgl.js.map +1 -0
  74. package/dist/webgl.mjs +195 -0
  75. package/dist/webgl.mjs.map +1 -0
  76. package/package.json +267 -0
  77. package/src/cli.ts +206 -0
  78. package/src/component-registry.ts +183 -0
  79. package/src/components/actions/Button.test.tsx +61 -0
  80. package/src/components/actions/Button.tsx +70 -0
  81. package/src/components/actions/Link.tsx +78 -0
  82. package/src/components/actions/Magnetic.tsx +68 -0
  83. package/src/components/actions/Toggle.test.tsx +40 -0
  84. package/src/components/actions/Toggle.tsx +47 -0
  85. package/src/components/actions/ToggleGroup.tsx +70 -0
  86. package/src/components/actions/index.ts +5 -0
  87. package/src/components/backgrounds/FaultyTerminal.tsx +426 -0
  88. package/src/components/backgrounds/OrbBackground.tsx +424 -0
  89. package/src/components/backgrounds/WarpBackground.tsx +358 -0
  90. package/src/components/backgrounds/index.ts +3 -0
  91. package/src/components/blocks/Hero.tsx +142 -0
  92. package/src/components/blocks/social/OpenGraphCard.tsx +243 -0
  93. package/src/components/cursor/SplashCursor.tsx +1315 -0
  94. package/src/components/cursor/TargetCursor.tsx +187 -0
  95. package/src/components/cursor/index.ts +2 -0
  96. package/src/components/data-display/AspectImage.tsx +73 -0
  97. package/src/components/data-display/Avatar.test.tsx +35 -0
  98. package/src/components/data-display/Avatar.tsx +55 -0
  99. package/src/components/data-display/Badge.test.tsx +43 -0
  100. package/src/components/data-display/Badge.tsx +84 -0
  101. package/src/components/data-display/Brand.tsx +123 -0
  102. package/src/components/data-display/Calendar.tsx +70 -0
  103. package/src/components/data-display/Card.test.tsx +92 -0
  104. package/src/components/data-display/Card.tsx +115 -0
  105. package/src/components/data-display/Code.tsx +210 -0
  106. package/src/components/data-display/CollapsibleCodeBlock.tsx +238 -0
  107. package/src/components/data-display/DataTable.tsx +119 -0
  108. package/src/components/data-display/DescriptionList.tsx +41 -0
  109. package/src/components/data-display/GitHubIcon.tsx +44 -0
  110. package/src/components/data-display/Heading.test.tsx +36 -0
  111. package/src/components/data-display/Heading.tsx +83 -0
  112. package/src/components/data-display/StatCard.tsx +195 -0
  113. package/src/components/data-display/Table.tsx +133 -0
  114. package/src/components/data-display/Text.test.tsx +48 -0
  115. package/src/components/data-display/Text.tsx +144 -0
  116. package/src/components/data-display/Timeline.tsx +194 -0
  117. package/src/components/data-display/TreeView.tsx +226 -0
  118. package/src/components/data-display/Typewriter.tsx +119 -0
  119. package/src/components/data-display/VariableWeightText.tsx +130 -0
  120. package/src/components/data-display/index.ts +19 -0
  121. package/src/components/feedback/Alert.test.tsx +44 -0
  122. package/src/components/feedback/Alert.tsx +65 -0
  123. package/src/components/feedback/EmptyState.tsx +113 -0
  124. package/src/components/feedback/Progress.test.tsx +60 -0
  125. package/src/components/feedback/Progress.tsx +30 -0
  126. package/src/components/feedback/ProgressBar.tsx +158 -0
  127. package/src/components/feedback/Skeleton.test.tsx +39 -0
  128. package/src/components/feedback/Skeleton.tsx +45 -0
  129. package/src/components/feedback/Sonner.tsx +28 -0
  130. package/src/components/feedback/Spinner.test.tsx +33 -0
  131. package/src/components/feedback/Spinner.tsx +99 -0
  132. package/src/components/feedback/Stepper.tsx +307 -0
  133. package/src/components/feedback/Toast/Toast.tsx +243 -0
  134. package/src/components/feedback/Toast/index.ts +2 -0
  135. package/src/components/feedback/index.ts +9 -0
  136. package/src/components/forms/Checkbox.test.tsx +40 -0
  137. package/src/components/forms/Checkbox.tsx +31 -0
  138. package/src/components/forms/ColorPicker.tsx +118 -0
  139. package/src/components/forms/Combobox.tsx +96 -0
  140. package/src/components/forms/DragDrop.tsx +440 -0
  141. package/src/components/forms/FileUpload.tsx +252 -0
  142. package/src/components/forms/FilterButton.tsx +65 -0
  143. package/src/components/forms/Form.tsx +197 -0
  144. package/src/components/forms/Input.test.tsx +46 -0
  145. package/src/components/forms/Input.tsx +43 -0
  146. package/src/components/forms/InputOTP.tsx +81 -0
  147. package/src/components/forms/Label.test.tsx +20 -0
  148. package/src/components/forms/Label.tsx +25 -0
  149. package/src/components/forms/RadioGroup.tsx +51 -0
  150. package/src/components/forms/SearchBar.tsx +215 -0
  151. package/src/components/forms/Select.test.tsx +118 -0
  152. package/src/components/forms/Select.tsx +274 -0
  153. package/src/components/forms/Slider.tsx +29 -0
  154. package/src/components/forms/Switch.test.tsx +76 -0
  155. package/src/components/forms/Switch.tsx +30 -0
  156. package/src/components/forms/TextField.tsx +152 -0
  157. package/src/components/forms/Textarea.test.tsx +41 -0
  158. package/src/components/forms/Textarea.tsx +29 -0
  159. package/src/components/forms/ThemeSwitcher.tsx +290 -0
  160. package/src/components/forms/ThemeToggle.tsx +151 -0
  161. package/src/components/forms/index.ts +19 -0
  162. package/src/components/layout/Accordion.test.tsx +66 -0
  163. package/src/components/layout/Accordion.tsx +64 -0
  164. package/src/components/layout/AspectRatio.tsx +7 -0
  165. package/src/components/layout/Carousel.tsx +277 -0
  166. package/src/components/layout/Collapsible.test.tsx +40 -0
  167. package/src/components/layout/Collapsible.tsx +31 -0
  168. package/src/components/layout/Container.test.tsx +45 -0
  169. package/src/components/layout/Container.tsx +99 -0
  170. package/src/components/layout/CustomizerPanel.tsx +400 -0
  171. package/src/components/layout/DatePicker.tsx +57 -0
  172. package/src/components/layout/Footer/Footer.tsx +175 -0
  173. package/src/components/layout/Footer/index.ts +2 -0
  174. package/src/components/layout/GlassSurface.tsx +82 -0
  175. package/src/components/layout/Grid.test.tsx +31 -0
  176. package/src/components/layout/Grid.tsx +130 -0
  177. package/src/components/layout/Header/Header.tsx +450 -0
  178. package/src/components/layout/Header/index.ts +2 -0
  179. package/src/components/layout/PageLayout.tsx +180 -0
  180. package/src/components/layout/PageTemplate.tsx +158 -0
  181. package/src/components/layout/Resizable.tsx +48 -0
  182. package/src/components/layout/ScrollArea.tsx +53 -0
  183. package/src/components/layout/Separator.test.tsx +28 -0
  184. package/src/components/layout/Separator.tsx +29 -0
  185. package/src/components/layout/Sidebar.tsx +171 -0
  186. package/src/components/layout/Stack.test.tsx +41 -0
  187. package/src/components/layout/Stack.tsx +89 -0
  188. package/src/components/layout/glass-surface.css +60 -0
  189. package/src/components/layout/index.ts +18 -0
  190. package/src/components/motion/AnimatedBeam.tsx +159 -0
  191. package/src/components/navigation/Breadcrumb.test.tsx +57 -0
  192. package/src/components/navigation/Breadcrumb.tsx +119 -0
  193. package/src/components/navigation/Breadcrumbs.tsx +221 -0
  194. package/src/components/navigation/Command.tsx +159 -0
  195. package/src/components/navigation/Menubar.tsx +115 -0
  196. package/src/components/navigation/NavLink.tsx +55 -0
  197. package/src/components/navigation/NavigationMenu.tsx +125 -0
  198. package/src/components/navigation/Pagination.tsx +121 -0
  199. package/src/components/navigation/SecondaryNav.tsx +100 -0
  200. package/src/components/navigation/Tabs.test.tsx +47 -0
  201. package/src/components/navigation/Tabs.tsx +60 -0
  202. package/src/components/navigation/TertiaryNav.tsx +90 -0
  203. package/src/components/navigation/index.ts +10 -0
  204. package/src/components/overlays/AlertDialog.test.tsx +69 -0
  205. package/src/components/overlays/AlertDialog.tsx +166 -0
  206. package/src/components/overlays/ContextMenu.tsx +243 -0
  207. package/src/components/overlays/Dialog.test.tsx +79 -0
  208. package/src/components/overlays/Dialog.tsx +158 -0
  209. package/src/components/overlays/Drawer.tsx +128 -0
  210. package/src/components/overlays/Dropdown.tsx +253 -0
  211. package/src/components/overlays/DropdownMenu.tsx +242 -0
  212. package/src/components/overlays/HoverCard.tsx +32 -0
  213. package/src/components/overlays/Modal.tsx +250 -0
  214. package/src/components/overlays/NotificationCenter.tsx +364 -0
  215. package/src/components/overlays/Popover.test.tsx +40 -0
  216. package/src/components/overlays/Popover.tsx +46 -0
  217. package/src/components/overlays/Sheet.tsx +163 -0
  218. package/src/components/overlays/Tooltip.test.tsx +33 -0
  219. package/src/components/overlays/Tooltip.tsx +32 -0
  220. package/src/components/overlays/index.ts +12 -0
  221. package/src/dates.ts +2 -0
  222. package/src/dnd.ts +1 -0
  223. package/src/forms.ts +1 -0
  224. package/src/globals.css +187 -0
  225. package/src/hooks/index.ts +6 -0
  226. package/src/hooks/useForm.ts +247 -0
  227. package/src/hooks/useMotionPreference.test.ts +102 -0
  228. package/src/hooks/useMotionPreference.ts +78 -0
  229. package/src/hooks/useTheme.ts +58 -0
  230. package/src/hooks.ts +9 -0
  231. package/src/index.ts +168 -0
  232. package/src/lib/animations.ts +356 -0
  233. package/src/lib/breadcrumbs.ts +94 -0
  234. package/src/lib/colors.ts +493 -0
  235. package/src/lib/store/customizer.ts +482 -0
  236. package/src/lib/store/index.ts +3 -0
  237. package/src/lib/store/theme.ts +55 -0
  238. package/src/lib/syntax-parser/index.ts +50 -0
  239. package/src/lib/syntax-parser/patterns.ts +64 -0
  240. package/src/lib/syntax-parser/tokenizer.ts +117 -0
  241. package/src/lib/syntax-parser/types.ts +27 -0
  242. package/src/lib/utils.ts +6 -0
  243. package/src/lib/validation.ts +204 -0
  244. package/src/lib/webgl/Color.ts +11 -0
  245. package/src/lib/webgl/Mesh.ts +41 -0
  246. package/src/lib/webgl/Program.ts +118 -0
  247. package/src/lib/webgl/Renderer.ts +51 -0
  248. package/src/lib/webgl/Triangle.ts +27 -0
  249. package/src/lib/webgl/Vec3.ts +18 -0
  250. package/src/lib/webgl/index.ts +13 -0
  251. package/src/nativewind-env.d.ts +1 -0
  252. package/src/providers/ThemeProvider.tsx +461 -0
  253. package/src/providers/index.ts +1 -0
  254. package/src/providers.ts +7 -0
  255. package/src/tables.ts +1 -0
  256. package/src/test/setup.ts +39 -0
  257. package/src/theme.css +158 -0
  258. package/src/tokens.ts +7 -0
  259. package/src/utils.ts +12 -0
  260. package/src/webgl.ts +1 -0
@@ -0,0 +1,130 @@
1
+ 'use client';
2
+
3
+ import { motion, HTMLMotionProps } from 'framer-motion';
4
+ import React from 'react';
5
+ import { useCustomizer } from '../../lib/store';
6
+
7
+ interface VariableWeightTextProps extends Omit<HTMLMotionProps<'div'>, 'children'> {
8
+ /**
9
+ * Minimum font weight for the animation
10
+ * @default 200
11
+ */
12
+ minWeight?: number;
13
+ /**
14
+ * Maximum font weight for the animation
15
+ * @default 700
16
+ */
17
+ maxWeight?: number;
18
+ /**
19
+ * Duration of one complete animation cycle (in seconds)
20
+ * @default 2
21
+ */
22
+ duration?: number;
23
+ /**
24
+ * Motion intensity override (0-10). If not provided, uses global customizer setting.
25
+ */
26
+ intensity?: number;
27
+ /**
28
+ * Font family to use. Recommended: 'Clash Display' or another variable font.
29
+ * @default 'Clash Display'
30
+ */
31
+ fontFamily?: string;
32
+ /**
33
+ * Text content to animate
34
+ */
35
+ children?: React.ReactNode;
36
+ }
37
+
38
+ /**
39
+ * VariableWeightText
40
+ *
41
+ * A motion component that creates a "breathing" effect by animating font weight.
42
+ * Works best with variable fonts like Clash Display that support smooth weight transitions.
43
+ *
44
+ * **Key Features:**
45
+ * - Animates font-weight in a continuous loop
46
+ * - Respects global motion intensity settings
47
+ * - Automatically centers text to prevent layout shifts during weight changes
48
+ * - Disables animation when motion intensity is 0 (accessibility)
49
+ *
50
+ * **Usage:**
51
+ * ```tsx
52
+ * <VariableWeightText minWeight={200} maxWeight={700}>
53
+ * Variable Font Text
54
+ * </VariableWeightText>
55
+ * ```
56
+ */
57
+ export const VariableWeightText = ({
58
+ children,
59
+ minWeight = 200,
60
+ maxWeight = 700,
61
+ duration = 2,
62
+ intensity,
63
+ fontFamily = 'Clash Display',
64
+ className,
65
+ style,
66
+ ...props
67
+ }: VariableWeightTextProps) => {
68
+ const { motion: motionIntensity } = useCustomizer();
69
+
70
+ // Use provided intensity or global intensity
71
+ const effectiveIntensity = intensity ?? motionIntensity;
72
+
73
+ // Scale duration based on intensity (higher intensity = faster animation)
74
+ const scaledDuration = effectiveIntensity > 0
75
+ ? duration * (5 / effectiveIntensity)
76
+ : duration;
77
+
78
+ // If motion is disabled (intensity 0), render static text at maxWeight
79
+ if (effectiveIntensity === 0) {
80
+ return (
81
+ <div
82
+ className={className}
83
+ style={{
84
+ fontFamily,
85
+ fontWeight: maxWeight,
86
+ fontVariationSettings: `'wght' ${maxWeight}`,
87
+ textAlign: 'center',
88
+ width: '100%',
89
+ WebkitFontSmoothing: 'antialiased',
90
+ MozOsxFontSmoothing: 'grayscale',
91
+ ...style,
92
+ }}
93
+ {...props as any}
94
+ >
95
+ {children}
96
+ </div>
97
+ );
98
+ }
99
+
100
+ return (
101
+ <motion.div
102
+ initial={{
103
+ fontVariationSettings: `'wght' ${minWeight}`
104
+ }}
105
+ animate={{
106
+ fontVariationSettings: `'wght' ${maxWeight}`
107
+ }}
108
+ transition={{
109
+ duration: scaledDuration,
110
+ repeat: Infinity,
111
+ repeatType: "reverse",
112
+ ease: "easeInOut",
113
+ }}
114
+ style={{
115
+ fontFamily,
116
+ fontWeight: minWeight, // Fallback
117
+ textAlign: 'center',
118
+ width: '100%',
119
+ willChange: 'font-variation-settings', // GPU acceleration hint
120
+ WebkitFontSmoothing: 'antialiased',
121
+ MozOsxFontSmoothing: 'grayscale',
122
+ ...style,
123
+ }}
124
+ className={className}
125
+ {...props}
126
+ >
127
+ {children}
128
+ </motion.div>
129
+ );
130
+ };
@@ -0,0 +1,19 @@
1
+ export * from './AspectImage';
2
+ export * from './Avatar';
3
+ export * from './Badge';
4
+ export * from './Brand';
5
+ export * from './Calendar';
6
+ export * from './Card';
7
+ export * from './Code';
8
+ export * from './CollapsibleCodeBlock';
9
+ export * from './DataTable';
10
+ export * from './DescriptionList';
11
+ export * from './GitHubIcon';
12
+ export * from './Heading';
13
+ export * from './Table';
14
+ export * from './Text';
15
+ export * from './VariableWeightText';
16
+ export * from './Typewriter';
17
+ export * from './StatCard';
18
+ export * from './Timeline';
19
+ export * from './TreeView';
@@ -0,0 +1,44 @@
1
+ import { render, screen } from '@testing-library/react'
2
+ import { describe, it, expect } from 'vitest'
3
+ import { Alert, AlertTitle, AlertDescription } from './Alert'
4
+
5
+ describe('Alert', () => {
6
+ it('renders with default variant', () => {
7
+ render(<Alert>Alert content</Alert>)
8
+ expect(screen.getByRole('alert')).toBeInTheDocument()
9
+ expect(screen.getByRole('alert')).toHaveClass('bg-card')
10
+ })
11
+
12
+ it('renders destructive variant', () => {
13
+ render(<Alert variant="destructive">Error</Alert>)
14
+ expect(screen.getByRole('alert')).toHaveClass('text-destructive')
15
+ })
16
+
17
+ it('renders title and description', () => {
18
+ render(
19
+ <Alert>
20
+ <AlertTitle>Error</AlertTitle>
21
+ <AlertDescription>Something went wrong.</AlertDescription>
22
+ </Alert>
23
+ )
24
+ expect(screen.getByText('Error')).toBeInTheDocument()
25
+ expect(screen.getByText('Something went wrong.')).toBeInTheDocument()
26
+ })
27
+
28
+ it('applies custom className', () => {
29
+ render(<Alert className="custom-alert">Content</Alert>)
30
+ expect(screen.getByRole('alert')).toHaveClass('custom-alert')
31
+ })
32
+
33
+ it('has correct data-slot attributes', () => {
34
+ render(
35
+ <Alert>
36
+ <AlertTitle>Title</AlertTitle>
37
+ <AlertDescription>Desc</AlertDescription>
38
+ </Alert>
39
+ )
40
+ expect(screen.getByRole('alert')).toHaveAttribute('data-slot', 'alert')
41
+ expect(screen.getByText('Title')).toHaveAttribute('data-slot', 'alert-title')
42
+ expect(screen.getByText('Desc')).toHaveAttribute('data-slot', 'alert-description')
43
+ })
44
+ })
@@ -0,0 +1,65 @@
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+ import { cn } from "../../lib/utils"
4
+
5
+ const alertVariants = cva(
6
+ "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
7
+ {
8
+ variants: {
9
+ variant: {
10
+ default: "bg-card text-card-foreground",
11
+ destructive:
12
+ "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
13
+ },
14
+ },
15
+ defaultVariants: {
16
+ variant: "default",
17
+ },
18
+ }
19
+ )
20
+
21
+ function Alert({
22
+ className,
23
+ variant,
24
+ ...props
25
+ }: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
26
+ return (
27
+ <div
28
+ data-slot="alert"
29
+ role="alert"
30
+ className={cn(alertVariants({ variant }), className)}
31
+ {...props}
32
+ />
33
+ )
34
+ }
35
+
36
+ function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
37
+ return (
38
+ <div
39
+ data-slot="alert-title"
40
+ className={cn(
41
+ "col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
42
+ className
43
+ )}
44
+ {...props}
45
+ />
46
+ )
47
+ }
48
+
49
+ function AlertDescription({
50
+ className,
51
+ ...props
52
+ }: React.ComponentProps<"div">) {
53
+ return (
54
+ <div
55
+ data-slot="alert-description"
56
+ className={cn(
57
+ "text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
58
+ className
59
+ )}
60
+ {...props}
61
+ />
62
+ )
63
+ }
64
+
65
+ export { Alert, alertVariants, AlertTitle, AlertDescription }
@@ -0,0 +1,113 @@
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+ import { cn } from "../../lib/utils"
4
+
5
+ const emptyStateVariants = cva(
6
+ "flex flex-col items-center justify-center text-center",
7
+ {
8
+ variants: {
9
+ size: {
10
+ sm: "py-8 px-4 gap-2",
11
+ default: "py-12 px-6 gap-3",
12
+ lg: "py-16 px-8 gap-4",
13
+ },
14
+ },
15
+ defaultVariants: {
16
+ size: "default",
17
+ },
18
+ }
19
+ )
20
+
21
+ const emptyStateIconVariants = cva(
22
+ "flex items-center justify-center rounded-full bg-muted text-foreground-secondary",
23
+ {
24
+ variants: {
25
+ size: {
26
+ sm: "h-10 w-10 [&>svg]:h-5 [&>svg]:w-5",
27
+ default: "h-12 w-12 [&>svg]:h-6 [&>svg]:w-6",
28
+ lg: "h-16 w-16 [&>svg]:h-8 [&>svg]:w-8",
29
+ },
30
+ },
31
+ defaultVariants: {
32
+ size: "default",
33
+ },
34
+ }
35
+ )
36
+
37
+ const emptyStateTitleVariants = cva("font-semibold font-heading tracking-tight text-foreground", {
38
+ variants: {
39
+ size: {
40
+ sm: "text-base",
41
+ default: "text-lg",
42
+ lg: "text-xl",
43
+ },
44
+ },
45
+ defaultVariants: {
46
+ size: "default",
47
+ },
48
+ })
49
+
50
+ export interface EmptyStateProps
51
+ extends React.HTMLAttributes<HTMLDivElement>,
52
+ VariantProps<typeof emptyStateVariants> {
53
+ /** Icon displayed above the title */
54
+ icon?: React.ReactNode
55
+ /** Primary message */
56
+ title: string
57
+ /** Secondary explanation text */
58
+ description?: string
59
+ /** Call-to-action element (e.g. Button) */
60
+ action?: React.ReactNode
61
+ }
62
+
63
+ function EmptyState({
64
+ className,
65
+ size,
66
+ icon,
67
+ title,
68
+ description,
69
+ action,
70
+ children,
71
+ ...props
72
+ }: EmptyStateProps) {
73
+ return (
74
+ <div
75
+ data-slot="empty-state"
76
+ role="status"
77
+ className={cn(emptyStateVariants({ size }), className)}
78
+ {...props}
79
+ >
80
+ {icon && (
81
+ <div
82
+ data-slot="empty-state-icon"
83
+ className={cn(emptyStateIconVariants({ size }))}
84
+ aria-hidden="true"
85
+ >
86
+ {icon}
87
+ </div>
88
+ )}
89
+ <h3
90
+ data-slot="empty-state-title"
91
+ className={cn(emptyStateTitleVariants({ size }))}
92
+ >
93
+ {title}
94
+ </h3>
95
+ {description && (
96
+ <p
97
+ data-slot="empty-state-description"
98
+ className="max-w-sm text-sm text-foreground-secondary"
99
+ >
100
+ {description}
101
+ </p>
102
+ )}
103
+ {action && (
104
+ <div data-slot="empty-state-action" className="mt-2">
105
+ {action}
106
+ </div>
107
+ )}
108
+ {children}
109
+ </div>
110
+ )
111
+ }
112
+
113
+ export { EmptyState, emptyStateVariants }
@@ -0,0 +1,60 @@
1
+ import { render, screen } from '@testing-library/react'
2
+ import { describe, it, expect } from 'vitest'
3
+ import { createRef } from 'react'
4
+ import { Progress } from './Progress'
5
+
6
+ describe('Progress', () => {
7
+ it('renders the progress bar', () => {
8
+ render(<Progress value={50} />)
9
+ const progressbar = screen.getByRole('progressbar')
10
+ expect(progressbar).toBeInTheDocument()
11
+ })
12
+
13
+ it('has a max value of 100 by default', () => {
14
+ render(<Progress value={50} />)
15
+ const progressbar = screen.getByRole('progressbar')
16
+ expect(progressbar).toHaveAttribute('data-max', '100')
17
+ })
18
+
19
+ it('shows indicator with correct transform based on value', () => {
20
+ render(<Progress value={60} />)
21
+ const progressbar = screen.getByRole('progressbar')
22
+ // The indicator is the child inside the progress root
23
+ const indicator = progressbar.firstElementChild as HTMLElement
24
+ expect(indicator).toBeInTheDocument()
25
+ expect(indicator.style.transform).toBe('translateX(-40%)')
26
+ })
27
+
28
+ it('shows indicator at 0% when value is 0', () => {
29
+ render(<Progress value={0} />)
30
+ const progressbar = screen.getByRole('progressbar')
31
+ const indicator = progressbar.firstElementChild as HTMLElement
32
+ expect(indicator.style.transform).toBe('translateX(-100%)')
33
+ })
34
+
35
+ it('shows indicator at 100% when value is 100', () => {
36
+ render(<Progress value={100} />)
37
+ const progressbar = screen.getByRole('progressbar')
38
+ const indicator = progressbar.firstElementChild as HTMLElement
39
+ expect(indicator.style.transform).toBe('translateX(-0%)')
40
+ })
41
+
42
+ it('defaults to 0 when no value is provided', () => {
43
+ render(<Progress />)
44
+ const progressbar = screen.getByRole('progressbar')
45
+ const indicator = progressbar.firstElementChild as HTMLElement
46
+ expect(indicator.style.transform).toBe('translateX(-100%)')
47
+ })
48
+
49
+ it('applies custom className', () => {
50
+ render(<Progress value={50} className="custom-progress" />)
51
+ const progressbar = screen.getByRole('progressbar')
52
+ expect(progressbar).toHaveClass('custom-progress')
53
+ })
54
+
55
+ it('forwards ref', () => {
56
+ const ref = createRef<HTMLDivElement>()
57
+ render(<Progress ref={ref} value={50} />)
58
+ expect(ref.current).toBeInstanceOf(HTMLDivElement)
59
+ })
60
+ })
@@ -0,0 +1,30 @@
1
+ "use client";
2
+ import * as React from "react"
3
+ import * as ProgressPrimitive from "@radix-ui/react-progress"
4
+
5
+ import { cn } from "../../lib/utils"
6
+
7
+ const Progress = (
8
+ {
9
+ ref,
10
+ className,
11
+ value,
12
+ ...props
13
+ }: React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> & {
14
+ ref?: React.Ref<React.ElementRef<typeof ProgressPrimitive.Root>>;
15
+ }
16
+ ) => (<ProgressPrimitive.Root
17
+ ref={ref}
18
+ className={cn(
19
+ "relative h-2 w-full overflow-hidden rounded-full bg-primary/20",
20
+ className
21
+ )}
22
+ {...props}
23
+ >
24
+ <ProgressPrimitive.Indicator
25
+ className="h-full w-full flex-1 bg-primary transition-all"
26
+ style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
27
+ />
28
+ </ProgressPrimitive.Root>)
29
+
30
+ export { Progress }
@@ -0,0 +1,158 @@
1
+ import React from 'react';
2
+
3
+ export interface ProgressBarProps extends React.HTMLAttributes<HTMLDivElement> {
4
+ /**
5
+ * Progress value (0-100)
6
+ */
7
+ value: number;
8
+
9
+ /**
10
+ * Maximum value
11
+ * @default 100
12
+ */
13
+ max?: number;
14
+
15
+ /**
16
+ * Size of the progress bar
17
+ * @default 'md'
18
+ */
19
+ size?: 'sm' | 'md' | 'lg';
20
+
21
+ /**
22
+ * Color variant
23
+ * @default 'primary'
24
+ */
25
+ variant?: 'primary' | 'success' | 'warning' | 'error' | 'info';
26
+
27
+ /**
28
+ * Whether to show the percentage label
29
+ * @default false
30
+ */
31
+ showLabel?: boolean;
32
+
33
+ /**
34
+ * Whether to animate the progress bar
35
+ * @default true
36
+ */
37
+ animated?: boolean;
38
+
39
+ /**
40
+ * Indeterminate state (ignores value)
41
+ * @default false
42
+ */
43
+ indeterminate?: boolean;
44
+ }
45
+
46
+ /**
47
+ * ProgressBar Component
48
+ *
49
+ * A visual indicator of progress or completion.
50
+ *
51
+ * Features:
52
+ * - Determinate and indeterminate modes
53
+ * - Five color variants
54
+ * - Three size options
55
+ * - Optional percentage label
56
+ * - Smooth animations
57
+ * - Theme-aware colors
58
+ * - Accessible ARIA attributes
59
+ *
60
+ * Example:
61
+ * ```tsx
62
+ * <ProgressBar value={65} showLabel />
63
+ * <ProgressBar value={100} variant="success" />
64
+ * <ProgressBar indeterminate variant="primary" />
65
+ * ```
66
+ */
67
+ export const ProgressBar = (
68
+ {
69
+ ref,
70
+ value,
71
+ max = 100,
72
+ size = 'md',
73
+ variant = 'primary',
74
+ showLabel = false,
75
+ animated = true,
76
+ indeterminate = false,
77
+ className = '',
78
+ ...props
79
+ }: ProgressBarProps & {
80
+ ref?: React.Ref<HTMLDivElement>;
81
+ }
82
+ ) => {
83
+ const percentage = Math.min(Math.max((value / max) * 100, 0), 100);
84
+
85
+ const sizes = {
86
+ sm: 'h-1',
87
+ md: 'h-2',
88
+ lg: 'h-3',
89
+ };
90
+
91
+ const variants = {
92
+ primary: 'bg-[var(--color-primary)]',
93
+ success: 'bg-[var(--color-success)]',
94
+ warning: 'bg-[var(--color-warning)]',
95
+ error: 'bg-[var(--color-error)]',
96
+ info: 'bg-[var(--color-info)]',
97
+ };
98
+
99
+ return (
100
+ <div ref={ref} className={`w-full ${className}`} {...props}>
101
+ {showLabel && !indeterminate && (
102
+ <div className="flex justify-between items-center mb-2">
103
+ <span className="text-sm font-medium text-[var(--color-text-primary)]">
104
+ {Math.round(percentage)}%
105
+ </span>
106
+ </div>
107
+ )}
108
+ <div
109
+ className={`
110
+ w-full ${sizes[size]}
111
+ bg-[var(--color-surface)]
112
+ rounded-full
113
+ overflow-hidden
114
+ border border-[var(--color-border)]
115
+ `}
116
+ role="progressbar"
117
+ aria-valuenow={indeterminate ? undefined : Math.round(percentage)}
118
+ aria-valuemin={0}
119
+ aria-valuemax={100}
120
+ aria-label={indeterminate ? 'Loading' : `${Math.round(percentage)}% complete`}
121
+ >
122
+ <div
123
+ className={`
124
+ h-full
125
+ ${variants[variant]}
126
+ ${animated ? 'transition-all duration-300 ease-out' : ''}
127
+ ${indeterminate ? 'animate-progress-indeterminate w-1/3' : ''}
128
+ `}
129
+ style={{
130
+ width: indeterminate ? undefined : `${percentage}%`,
131
+ }}
132
+ />
133
+ </div>
134
+ </div>
135
+ );
136
+ };
137
+
138
+ // Add indeterminate animation
139
+ if (typeof document !== 'undefined') {
140
+ const style = document.createElement('style');
141
+ style.textContent = `
142
+ @keyframes progress-indeterminate {
143
+ 0% {
144
+ transform: translateX(-100%);
145
+ }
146
+ 100% {
147
+ transform: translateX(400%);
148
+ }
149
+ }
150
+ .animate-progress-indeterminate {
151
+ animation: progress-indeterminate 1.5s ease-in-out infinite;
152
+ }
153
+ `;
154
+ if (!document.querySelector('style[data-progress-animations]')) {
155
+ style.setAttribute('data-progress-animations', 'true');
156
+ document.head.appendChild(style);
157
+ }
158
+ }
@@ -0,0 +1,39 @@
1
+ import { render } from '@testing-library/react'
2
+ import { describe, it, expect } from 'vitest'
3
+ import { Skeleton } from './Skeleton'
4
+
5
+ describe('Skeleton', () => {
6
+ it('renders with default variant', () => {
7
+ const { container } = render(<Skeleton />)
8
+ const skeleton = container.firstChild as HTMLElement
9
+ expect(skeleton).toBeInTheDocument()
10
+ expect(skeleton).toHaveClass('animate-pulse', 'bg-muted', 'rounded-md')
11
+ })
12
+
13
+ it('renders circular variant', () => {
14
+ const { container } = render(<Skeleton variant="circular" />)
15
+ expect(container.firstChild).toHaveClass('rounded-full')
16
+ })
17
+
18
+ it('renders rectangular variant', () => {
19
+ const { container } = render(<Skeleton variant="rectangular" />)
20
+ expect(container.firstChild).toHaveClass('rounded-none')
21
+ })
22
+
23
+ it('renders text variant', () => {
24
+ const { container } = render(<Skeleton variant="text" />)
25
+ expect(container.firstChild).toHaveClass('rounded-xs', 'h-4')
26
+ })
27
+
28
+ it('applies custom width and height', () => {
29
+ const { container } = render(<Skeleton width="200px" height="40px" />)
30
+ const skeleton = container.firstChild as HTMLElement
31
+ expect(skeleton.style.width).toBe('200px')
32
+ expect(skeleton.style.height).toBe('40px')
33
+ })
34
+
35
+ it('applies custom className', () => {
36
+ const { container } = render(<Skeleton className="custom-skeleton" />)
37
+ expect(container.firstChild).toHaveClass('custom-skeleton')
38
+ })
39
+ })