@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.
- package/.claude/CLAUDE.md +239 -0
- package/README.md +161 -0
- package/dist/cli.mjs +151 -0
- package/dist/dates.d.mts +20 -0
- package/dist/dates.d.ts +20 -0
- package/dist/dates.js +240 -0
- package/dist/dates.js.map +1 -0
- package/dist/dates.mjs +203 -0
- package/dist/dates.mjs.map +1 -0
- package/dist/dnd.d.mts +126 -0
- package/dist/dnd.d.ts +126 -0
- package/dist/dnd.js +274 -0
- package/dist/dnd.js.map +1 -0
- package/dist/dnd.mjs +250 -0
- package/dist/dnd.mjs.map +1 -0
- package/dist/fontThemes-Dh8mtXES.d.mts +868 -0
- package/dist/fontThemes-Dh8mtXES.d.ts +868 -0
- package/dist/forms.d.mts +38 -0
- package/dist/forms.d.ts +38 -0
- package/dist/forms.js +198 -0
- package/dist/forms.js.map +1 -0
- package/dist/forms.mjs +159 -0
- package/dist/forms.mjs.map +1 -0
- package/dist/hooks-1b8WaQf1.d.mts +225 -0
- package/dist/hooks-CKW8vE9H.d.ts +225 -0
- package/dist/hooks.d.mts +3 -0
- package/dist/hooks.d.ts +3 -0
- package/dist/hooks.js +971 -0
- package/dist/hooks.js.map +1 -0
- package/dist/hooks.mjs +943 -0
- package/dist/hooks.mjs.map +1 -0
- package/dist/index-DscTIrZ2.d.mts +29 -0
- package/dist/index-DscTIrZ2.d.ts +29 -0
- package/dist/index.d.mts +3382 -0
- package/dist/index.d.ts +3382 -0
- package/dist/index.js +15146 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +14802 -0
- package/dist/index.mjs.map +1 -0
- package/dist/providers-CXPDMsl7.d.mts +30 -0
- package/dist/providers-Dn_Msjvz.d.ts +30 -0
- package/dist/providers.d.mts +3 -0
- package/dist/providers.d.ts +3 -0
- package/dist/providers.js +1885 -0
- package/dist/providers.js.map +1 -0
- package/dist/providers.mjs +1859 -0
- package/dist/providers.mjs.map +1 -0
- package/dist/tables.d.mts +10 -0
- package/dist/tables.d.ts +10 -0
- package/dist/tables.js +248 -0
- package/dist/tables.js.map +1 -0
- package/dist/tables.mjs +218 -0
- package/dist/tables.mjs.map +1 -0
- package/dist/tokens.d.mts +1065 -0
- package/dist/tokens.d.ts +1065 -0
- package/dist/tokens.js +2637 -0
- package/dist/tokens.js.map +1 -0
- package/dist/tokens.mjs +2555 -0
- package/dist/tokens.mjs.map +1 -0
- package/dist/utils-CIIM7dAC.d.ts +986 -0
- package/dist/utils-Cs04sxth.d.mts +986 -0
- package/dist/utils.d.mts +4 -0
- package/dist/utils.d.ts +4 -0
- package/dist/utils.js +874 -0
- package/dist/utils.js.map +1 -0
- package/dist/utils.mjs +806 -0
- package/dist/utils.mjs.map +1 -0
- package/dist/validation-Bj1ye-v_.d.mts +114 -0
- package/dist/validation-Bj1ye-v_.d.ts +114 -0
- package/dist/webgl.d.mts +104 -0
- package/dist/webgl.d.ts +104 -0
- package/dist/webgl.js +226 -0
- package/dist/webgl.js.map +1 -0
- package/dist/webgl.mjs +195 -0
- package/dist/webgl.mjs.map +1 -0
- package/package.json +267 -0
- package/src/cli.ts +206 -0
- package/src/component-registry.ts +183 -0
- package/src/components/actions/Button.test.tsx +61 -0
- package/src/components/actions/Button.tsx +70 -0
- package/src/components/actions/Link.tsx +78 -0
- package/src/components/actions/Magnetic.tsx +68 -0
- package/src/components/actions/Toggle.test.tsx +40 -0
- package/src/components/actions/Toggle.tsx +47 -0
- package/src/components/actions/ToggleGroup.tsx +70 -0
- package/src/components/actions/index.ts +5 -0
- package/src/components/backgrounds/FaultyTerminal.tsx +426 -0
- package/src/components/backgrounds/OrbBackground.tsx +424 -0
- package/src/components/backgrounds/WarpBackground.tsx +358 -0
- package/src/components/backgrounds/index.ts +3 -0
- package/src/components/blocks/Hero.tsx +142 -0
- package/src/components/blocks/social/OpenGraphCard.tsx +243 -0
- package/src/components/cursor/SplashCursor.tsx +1315 -0
- package/src/components/cursor/TargetCursor.tsx +187 -0
- package/src/components/cursor/index.ts +2 -0
- package/src/components/data-display/AspectImage.tsx +73 -0
- package/src/components/data-display/Avatar.test.tsx +35 -0
- package/src/components/data-display/Avatar.tsx +55 -0
- package/src/components/data-display/Badge.test.tsx +43 -0
- package/src/components/data-display/Badge.tsx +84 -0
- package/src/components/data-display/Brand.tsx +123 -0
- package/src/components/data-display/Calendar.tsx +70 -0
- package/src/components/data-display/Card.test.tsx +92 -0
- package/src/components/data-display/Card.tsx +115 -0
- package/src/components/data-display/Code.tsx +210 -0
- package/src/components/data-display/CollapsibleCodeBlock.tsx +238 -0
- package/src/components/data-display/DataTable.tsx +119 -0
- package/src/components/data-display/DescriptionList.tsx +41 -0
- package/src/components/data-display/GitHubIcon.tsx +44 -0
- package/src/components/data-display/Heading.test.tsx +36 -0
- package/src/components/data-display/Heading.tsx +83 -0
- package/src/components/data-display/StatCard.tsx +195 -0
- package/src/components/data-display/Table.tsx +133 -0
- package/src/components/data-display/Text.test.tsx +48 -0
- package/src/components/data-display/Text.tsx +144 -0
- package/src/components/data-display/Timeline.tsx +194 -0
- package/src/components/data-display/TreeView.tsx +226 -0
- package/src/components/data-display/Typewriter.tsx +119 -0
- package/src/components/data-display/VariableWeightText.tsx +130 -0
- package/src/components/data-display/index.ts +19 -0
- package/src/components/feedback/Alert.test.tsx +44 -0
- package/src/components/feedback/Alert.tsx +65 -0
- package/src/components/feedback/EmptyState.tsx +113 -0
- package/src/components/feedback/Progress.test.tsx +60 -0
- package/src/components/feedback/Progress.tsx +30 -0
- package/src/components/feedback/ProgressBar.tsx +158 -0
- package/src/components/feedback/Skeleton.test.tsx +39 -0
- package/src/components/feedback/Skeleton.tsx +45 -0
- package/src/components/feedback/Sonner.tsx +28 -0
- package/src/components/feedback/Spinner.test.tsx +33 -0
- package/src/components/feedback/Spinner.tsx +99 -0
- package/src/components/feedback/Stepper.tsx +307 -0
- package/src/components/feedback/Toast/Toast.tsx +243 -0
- package/src/components/feedback/Toast/index.ts +2 -0
- package/src/components/feedback/index.ts +9 -0
- package/src/components/forms/Checkbox.test.tsx +40 -0
- package/src/components/forms/Checkbox.tsx +31 -0
- package/src/components/forms/ColorPicker.tsx +118 -0
- package/src/components/forms/Combobox.tsx +96 -0
- package/src/components/forms/DragDrop.tsx +440 -0
- package/src/components/forms/FileUpload.tsx +252 -0
- package/src/components/forms/FilterButton.tsx +65 -0
- package/src/components/forms/Form.tsx +197 -0
- package/src/components/forms/Input.test.tsx +46 -0
- package/src/components/forms/Input.tsx +43 -0
- package/src/components/forms/InputOTP.tsx +81 -0
- package/src/components/forms/Label.test.tsx +20 -0
- package/src/components/forms/Label.tsx +25 -0
- package/src/components/forms/RadioGroup.tsx +51 -0
- package/src/components/forms/SearchBar.tsx +215 -0
- package/src/components/forms/Select.test.tsx +118 -0
- package/src/components/forms/Select.tsx +274 -0
- package/src/components/forms/Slider.tsx +29 -0
- package/src/components/forms/Switch.test.tsx +76 -0
- package/src/components/forms/Switch.tsx +30 -0
- package/src/components/forms/TextField.tsx +152 -0
- package/src/components/forms/Textarea.test.tsx +41 -0
- package/src/components/forms/Textarea.tsx +29 -0
- package/src/components/forms/ThemeSwitcher.tsx +290 -0
- package/src/components/forms/ThemeToggle.tsx +151 -0
- package/src/components/forms/index.ts +19 -0
- package/src/components/layout/Accordion.test.tsx +66 -0
- package/src/components/layout/Accordion.tsx +64 -0
- package/src/components/layout/AspectRatio.tsx +7 -0
- package/src/components/layout/Carousel.tsx +277 -0
- package/src/components/layout/Collapsible.test.tsx +40 -0
- package/src/components/layout/Collapsible.tsx +31 -0
- package/src/components/layout/Container.test.tsx +45 -0
- package/src/components/layout/Container.tsx +99 -0
- package/src/components/layout/CustomizerPanel.tsx +400 -0
- package/src/components/layout/DatePicker.tsx +57 -0
- package/src/components/layout/Footer/Footer.tsx +175 -0
- package/src/components/layout/Footer/index.ts +2 -0
- package/src/components/layout/GlassSurface.tsx +82 -0
- package/src/components/layout/Grid.test.tsx +31 -0
- package/src/components/layout/Grid.tsx +130 -0
- package/src/components/layout/Header/Header.tsx +450 -0
- package/src/components/layout/Header/index.ts +2 -0
- package/src/components/layout/PageLayout.tsx +180 -0
- package/src/components/layout/PageTemplate.tsx +158 -0
- package/src/components/layout/Resizable.tsx +48 -0
- package/src/components/layout/ScrollArea.tsx +53 -0
- package/src/components/layout/Separator.test.tsx +28 -0
- package/src/components/layout/Separator.tsx +29 -0
- package/src/components/layout/Sidebar.tsx +171 -0
- package/src/components/layout/Stack.test.tsx +41 -0
- package/src/components/layout/Stack.tsx +89 -0
- package/src/components/layout/glass-surface.css +60 -0
- package/src/components/layout/index.ts +18 -0
- package/src/components/motion/AnimatedBeam.tsx +159 -0
- package/src/components/navigation/Breadcrumb.test.tsx +57 -0
- package/src/components/navigation/Breadcrumb.tsx +119 -0
- package/src/components/navigation/Breadcrumbs.tsx +221 -0
- package/src/components/navigation/Command.tsx +159 -0
- package/src/components/navigation/Menubar.tsx +115 -0
- package/src/components/navigation/NavLink.tsx +55 -0
- package/src/components/navigation/NavigationMenu.tsx +125 -0
- package/src/components/navigation/Pagination.tsx +121 -0
- package/src/components/navigation/SecondaryNav.tsx +100 -0
- package/src/components/navigation/Tabs.test.tsx +47 -0
- package/src/components/navigation/Tabs.tsx +60 -0
- package/src/components/navigation/TertiaryNav.tsx +90 -0
- package/src/components/navigation/index.ts +10 -0
- package/src/components/overlays/AlertDialog.test.tsx +69 -0
- package/src/components/overlays/AlertDialog.tsx +166 -0
- package/src/components/overlays/ContextMenu.tsx +243 -0
- package/src/components/overlays/Dialog.test.tsx +79 -0
- package/src/components/overlays/Dialog.tsx +158 -0
- package/src/components/overlays/Drawer.tsx +128 -0
- package/src/components/overlays/Dropdown.tsx +253 -0
- package/src/components/overlays/DropdownMenu.tsx +242 -0
- package/src/components/overlays/HoverCard.tsx +32 -0
- package/src/components/overlays/Modal.tsx +250 -0
- package/src/components/overlays/NotificationCenter.tsx +364 -0
- package/src/components/overlays/Popover.test.tsx +40 -0
- package/src/components/overlays/Popover.tsx +46 -0
- package/src/components/overlays/Sheet.tsx +163 -0
- package/src/components/overlays/Tooltip.test.tsx +33 -0
- package/src/components/overlays/Tooltip.tsx +32 -0
- package/src/components/overlays/index.ts +12 -0
- package/src/dates.ts +2 -0
- package/src/dnd.ts +1 -0
- package/src/forms.ts +1 -0
- package/src/globals.css +187 -0
- package/src/hooks/index.ts +6 -0
- package/src/hooks/useForm.ts +247 -0
- package/src/hooks/useMotionPreference.test.ts +102 -0
- package/src/hooks/useMotionPreference.ts +78 -0
- package/src/hooks/useTheme.ts +58 -0
- package/src/hooks.ts +9 -0
- package/src/index.ts +168 -0
- package/src/lib/animations.ts +356 -0
- package/src/lib/breadcrumbs.ts +94 -0
- package/src/lib/colors.ts +493 -0
- package/src/lib/store/customizer.ts +482 -0
- package/src/lib/store/index.ts +3 -0
- package/src/lib/store/theme.ts +55 -0
- package/src/lib/syntax-parser/index.ts +50 -0
- package/src/lib/syntax-parser/patterns.ts +64 -0
- package/src/lib/syntax-parser/tokenizer.ts +117 -0
- package/src/lib/syntax-parser/types.ts +27 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/validation.ts +204 -0
- package/src/lib/webgl/Color.ts +11 -0
- package/src/lib/webgl/Mesh.ts +41 -0
- package/src/lib/webgl/Program.ts +118 -0
- package/src/lib/webgl/Renderer.ts +51 -0
- package/src/lib/webgl/Triangle.ts +27 -0
- package/src/lib/webgl/Vec3.ts +18 -0
- package/src/lib/webgl/index.ts +13 -0
- package/src/nativewind-env.d.ts +1 -0
- package/src/providers/ThemeProvider.tsx +461 -0
- package/src/providers/index.ts +1 -0
- package/src/providers.ts +7 -0
- package/src/tables.ts +1 -0
- package/src/test/setup.ts +39 -0
- package/src/theme.css +158 -0
- package/src/tokens.ts +7 -0
- package/src/utils.ts +12 -0
- 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
|
+
})
|