@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,151 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { useTheme } from '../../hooks/useTheme';
|
|
5
|
+
|
|
6
|
+
export interface ThemeToggleProps {
|
|
7
|
+
/**
|
|
8
|
+
* Size of the toggle button
|
|
9
|
+
* @default 'md'
|
|
10
|
+
*/
|
|
11
|
+
size?: 'sm' | 'md' | 'lg';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Whether to show the mode label next to the icon
|
|
15
|
+
* @default false
|
|
16
|
+
*/
|
|
17
|
+
showLabel?: boolean;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Additional CSS classes
|
|
21
|
+
*/
|
|
22
|
+
className?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* ThemeToggle Molecule
|
|
27
|
+
*
|
|
28
|
+
* A button that toggles between light and dark modes with smooth icon transitions.
|
|
29
|
+
*
|
|
30
|
+
* Features:
|
|
31
|
+
* - Automatic mode detection from theme context
|
|
32
|
+
* - Smooth icon transition between sun (light) and moon (dark)
|
|
33
|
+
* - Three size variants
|
|
34
|
+
* - Optional text label
|
|
35
|
+
* - Full keyboard accessibility
|
|
36
|
+
* - ARIA labels for screen readers
|
|
37
|
+
* - Theme-aware colors
|
|
38
|
+
*
|
|
39
|
+
* Example:
|
|
40
|
+
* ```tsx
|
|
41
|
+
* // Simple icon-only toggle
|
|
42
|
+
* <ThemeToggle />
|
|
43
|
+
*
|
|
44
|
+
* // With label
|
|
45
|
+
* <ThemeToggle showLabel />
|
|
46
|
+
*
|
|
47
|
+
* // Large size with label
|
|
48
|
+
* <ThemeToggle size="lg" showLabel />
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export const ThemeToggle: React.FC<ThemeToggleProps> = ({
|
|
52
|
+
size = 'md',
|
|
53
|
+
showLabel = false,
|
|
54
|
+
className = '',
|
|
55
|
+
}) => {
|
|
56
|
+
const { mode, setMode } = useTheme();
|
|
57
|
+
|
|
58
|
+
const toggleMode = () => {
|
|
59
|
+
setMode(mode === 'light' ? 'dark' : 'light');
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const sizeClasses = {
|
|
63
|
+
sm: 'p-2',
|
|
64
|
+
md: 'p-2.5',
|
|
65
|
+
lg: 'p-3',
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const iconSizes = {
|
|
69
|
+
sm: 16,
|
|
70
|
+
md: 20,
|
|
71
|
+
lg: 24,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const iconSize = iconSizes[size];
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<button
|
|
78
|
+
onClick={toggleMode}
|
|
79
|
+
className={`
|
|
80
|
+
${sizeClasses[size]}
|
|
81
|
+
rounded-lg
|
|
82
|
+
bg-[var(--color-surface)]
|
|
83
|
+
border border-[var(--color-border)]
|
|
84
|
+
text-[var(--color-text-primary)]
|
|
85
|
+
hover:bg-[var(--color-hover)]
|
|
86
|
+
active:bg-[var(--color-active)]
|
|
87
|
+
transition-all duration-200
|
|
88
|
+
focus-visible:outline-none
|
|
89
|
+
focus-visible:ring-2
|
|
90
|
+
focus-visible:ring-[var(--color-focus)]
|
|
91
|
+
focus-visible:ring-offset-2
|
|
92
|
+
${showLabel ? 'flex items-center gap-2' : ''}
|
|
93
|
+
${className}
|
|
94
|
+
`}
|
|
95
|
+
aria-label={`Switch to ${mode === 'light' ? 'dark' : 'light'} mode`}
|
|
96
|
+
title={`Switch to ${mode === 'light' ? 'dark' : 'light'} mode`}
|
|
97
|
+
>
|
|
98
|
+
{/* Sun Icon (Light Mode) */}
|
|
99
|
+
{mode === 'light' && (
|
|
100
|
+
<svg
|
|
101
|
+
width={iconSize}
|
|
102
|
+
height={iconSize}
|
|
103
|
+
viewBox="0 0 24 24"
|
|
104
|
+
fill="none"
|
|
105
|
+
stroke="currentColor"
|
|
106
|
+
strokeWidth="2"
|
|
107
|
+
strokeLinecap="round"
|
|
108
|
+
strokeLinejoin="round"
|
|
109
|
+
className="transition-transform duration-200"
|
|
110
|
+
aria-hidden="true"
|
|
111
|
+
>
|
|
112
|
+
<circle cx="12" cy="12" r="5" />
|
|
113
|
+
<line x1="12" y1="1" x2="12" y2="3" />
|
|
114
|
+
<line x1="12" y1="21" x2="12" y2="23" />
|
|
115
|
+
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
|
|
116
|
+
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
|
|
117
|
+
<line x1="1" y1="12" x2="3" y2="12" />
|
|
118
|
+
<line x1="21" y1="12" x2="23" y2="12" />
|
|
119
|
+
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
|
|
120
|
+
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
|
|
121
|
+
</svg>
|
|
122
|
+
)}
|
|
123
|
+
|
|
124
|
+
{/* Moon Icon (Dark Mode) */}
|
|
125
|
+
{mode === 'dark' && (
|
|
126
|
+
<svg
|
|
127
|
+
width={iconSize}
|
|
128
|
+
height={iconSize}
|
|
129
|
+
viewBox="0 0 24 24"
|
|
130
|
+
fill="none"
|
|
131
|
+
stroke="currentColor"
|
|
132
|
+
strokeWidth="2"
|
|
133
|
+
strokeLinecap="round"
|
|
134
|
+
strokeLinejoin="round"
|
|
135
|
+
className="transition-transform duration-200"
|
|
136
|
+
aria-hidden="true"
|
|
137
|
+
>
|
|
138
|
+
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
|
|
139
|
+
</svg>
|
|
140
|
+
)}
|
|
141
|
+
|
|
142
|
+
{showLabel && (
|
|
143
|
+
<span className="text-sm font-medium">
|
|
144
|
+
{mode === 'light' ? 'Light' : 'Dark'}
|
|
145
|
+
</span>
|
|
146
|
+
)}
|
|
147
|
+
</button>
|
|
148
|
+
);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
ThemeToggle.displayName = 'ThemeToggle';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export * from './Checkbox';
|
|
2
|
+
export * from './ColorPicker';
|
|
3
|
+
export * from './Combobox';
|
|
4
|
+
export * from './DragDrop';
|
|
5
|
+
export * from './FilterButton';
|
|
6
|
+
export * from './Form';
|
|
7
|
+
export * from './Input';
|
|
8
|
+
export * from './InputOTP';
|
|
9
|
+
export * from './Label';
|
|
10
|
+
export * from './RadioGroup';
|
|
11
|
+
export * from './SearchBar';
|
|
12
|
+
export * from './Select';
|
|
13
|
+
export * from './Slider';
|
|
14
|
+
export * from './Switch';
|
|
15
|
+
export * from './TextField';
|
|
16
|
+
export * from './Textarea';
|
|
17
|
+
export * from './ThemeSwitcher';
|
|
18
|
+
export * from './ThemeToggle';
|
|
19
|
+
export * from './FileUpload';
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react'
|
|
2
|
+
import userEvent from '@testing-library/user-event'
|
|
3
|
+
import { describe, it, expect } from 'vitest'
|
|
4
|
+
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from './Accordion'
|
|
5
|
+
|
|
6
|
+
describe('Accordion', () => {
|
|
7
|
+
const renderAccordion = () =>
|
|
8
|
+
render(
|
|
9
|
+
<Accordion type="single" collapsible>
|
|
10
|
+
<AccordionItem value="item-1">
|
|
11
|
+
<AccordionTrigger>Section 1</AccordionTrigger>
|
|
12
|
+
<AccordionContent>Content for section 1</AccordionContent>
|
|
13
|
+
</AccordionItem>
|
|
14
|
+
<AccordionItem value="item-2">
|
|
15
|
+
<AccordionTrigger>Section 2</AccordionTrigger>
|
|
16
|
+
<AccordionContent>Content for section 2</AccordionContent>
|
|
17
|
+
</AccordionItem>
|
|
18
|
+
<AccordionItem value="item-3">
|
|
19
|
+
<AccordionTrigger>Section 3</AccordionTrigger>
|
|
20
|
+
<AccordionContent>Content for section 3</AccordionContent>
|
|
21
|
+
</AccordionItem>
|
|
22
|
+
</Accordion>
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
it('renders accordion items', () => {
|
|
26
|
+
renderAccordion()
|
|
27
|
+
expect(screen.getByText('Section 1')).toBeInTheDocument()
|
|
28
|
+
expect(screen.getByText('Section 2')).toBeInTheDocument()
|
|
29
|
+
expect(screen.getByText('Section 3')).toBeInTheDocument()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('expands item on click', async () => {
|
|
33
|
+
const user = userEvent.setup()
|
|
34
|
+
renderAccordion()
|
|
35
|
+
|
|
36
|
+
const trigger = screen.getByText('Section 1')
|
|
37
|
+
await user.click(trigger)
|
|
38
|
+
|
|
39
|
+
expect(screen.getByText('Content for section 1')).toBeVisible()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('collapses when another item is clicked (single mode)', async () => {
|
|
43
|
+
const user = userEvent.setup()
|
|
44
|
+
renderAccordion()
|
|
45
|
+
|
|
46
|
+
await user.click(screen.getByText('Section 1'))
|
|
47
|
+
expect(screen.getByText('Content for section 1')).toBeVisible()
|
|
48
|
+
|
|
49
|
+
await user.click(screen.getByText('Section 2'))
|
|
50
|
+
expect(screen.getByText('Content for section 2')).toBeVisible()
|
|
51
|
+
|
|
52
|
+
// In single mode, the first item's content should be hidden
|
|
53
|
+
// Radix adds hidden attribute which removes from accessible tree
|
|
54
|
+
const item1Content = screen.queryByText('Content for section 1')
|
|
55
|
+
if (item1Content) {
|
|
56
|
+
expect(item1Content).not.toBeVisible()
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('shows chevron icon', () => {
|
|
61
|
+
renderAccordion()
|
|
62
|
+
// The AccordionTrigger renders a ChevronDown SVG icon
|
|
63
|
+
const svgs = document.querySelectorAll('svg')
|
|
64
|
+
expect(svgs.length).toBeGreaterThanOrEqual(3) // One chevron per accordion item
|
|
65
|
+
})
|
|
66
|
+
})
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react"
|
|
3
|
+
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
|
4
|
+
import { ChevronDown } from "lucide-react"
|
|
5
|
+
|
|
6
|
+
import { cn } from "../../lib/utils"
|
|
7
|
+
|
|
8
|
+
const Accordion = AccordionPrimitive.Root
|
|
9
|
+
|
|
10
|
+
const AccordionItem = (
|
|
11
|
+
{
|
|
12
|
+
ref,
|
|
13
|
+
className,
|
|
14
|
+
...props
|
|
15
|
+
}: React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item> & {
|
|
16
|
+
ref?: React.Ref<React.ElementRef<typeof AccordionPrimitive.Item>>;
|
|
17
|
+
}
|
|
18
|
+
) => (<AccordionPrimitive.Item
|
|
19
|
+
ref={ref}
|
|
20
|
+
className={cn("border-b last:border-b-0", className)}
|
|
21
|
+
{...props}
|
|
22
|
+
/>)
|
|
23
|
+
|
|
24
|
+
const AccordionTrigger = (
|
|
25
|
+
{
|
|
26
|
+
ref,
|
|
27
|
+
className,
|
|
28
|
+
children,
|
|
29
|
+
...props
|
|
30
|
+
}: React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger> & {
|
|
31
|
+
ref?: React.Ref<React.ElementRef<typeof AccordionPrimitive.Trigger>>;
|
|
32
|
+
}
|
|
33
|
+
) => (<AccordionPrimitive.Header className="flex">
|
|
34
|
+
<AccordionPrimitive.Trigger
|
|
35
|
+
ref={ref}
|
|
36
|
+
className={cn(
|
|
37
|
+
"flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium hover:underline outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
|
38
|
+
className
|
|
39
|
+
)}
|
|
40
|
+
{...props}
|
|
41
|
+
>
|
|
42
|
+
{children}
|
|
43
|
+
<ChevronDown className="pointer-events-none h-4 w-4 shrink-0 translate-y-0.5 text-muted-foreground transition-transform duration-200" />
|
|
44
|
+
</AccordionPrimitive.Trigger>
|
|
45
|
+
</AccordionPrimitive.Header>)
|
|
46
|
+
|
|
47
|
+
const AccordionContent = (
|
|
48
|
+
{
|
|
49
|
+
ref,
|
|
50
|
+
className,
|
|
51
|
+
children,
|
|
52
|
+
...props
|
|
53
|
+
}: React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content> & {
|
|
54
|
+
ref?: React.Ref<React.ElementRef<typeof AccordionPrimitive.Content>>;
|
|
55
|
+
}
|
|
56
|
+
) => (<AccordionPrimitive.Content
|
|
57
|
+
ref={ref}
|
|
58
|
+
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
|
59
|
+
{...props}
|
|
60
|
+
>
|
|
61
|
+
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
|
62
|
+
</AccordionPrimitive.Content>)
|
|
63
|
+
|
|
64
|
+
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react"
|
|
3
|
+
import useEmblaCarousel, {
|
|
4
|
+
type UseEmblaCarouselType,
|
|
5
|
+
} from "embla-carousel-react"
|
|
6
|
+
import { ArrowLeft, ArrowRight } from "lucide-react"
|
|
7
|
+
|
|
8
|
+
import { cn } from "../../lib/utils"
|
|
9
|
+
import { Button } from "../actions/Button"
|
|
10
|
+
|
|
11
|
+
type CarouselApi = UseEmblaCarouselType[1]
|
|
12
|
+
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
|
|
13
|
+
type CarouselOptions = UseCarouselParameters[0]
|
|
14
|
+
type CarouselPlugin = UseCarouselParameters[1]
|
|
15
|
+
|
|
16
|
+
type CarouselProps = {
|
|
17
|
+
opts?: CarouselOptions
|
|
18
|
+
plugins?: CarouselPlugin
|
|
19
|
+
orientation?: "horizontal" | "vertical"
|
|
20
|
+
setApi?: (api: CarouselApi) => void
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type CarouselContextProps = {
|
|
24
|
+
carouselRef: ReturnType<typeof useEmblaCarousel>[0]
|
|
25
|
+
api: ReturnType<typeof useEmblaCarousel>[1]
|
|
26
|
+
scrollPrev: () => void
|
|
27
|
+
scrollNext: () => void
|
|
28
|
+
canScrollPrev: boolean
|
|
29
|
+
canScrollNext: boolean
|
|
30
|
+
} & CarouselProps
|
|
31
|
+
|
|
32
|
+
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
|
|
33
|
+
|
|
34
|
+
function useCarousel() {
|
|
35
|
+
const context = React.useContext(CarouselContext)
|
|
36
|
+
|
|
37
|
+
if (!context) {
|
|
38
|
+
throw new Error("useCarousel must be used within a <Carousel />")
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return context
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const Carousel = (
|
|
45
|
+
{
|
|
46
|
+
ref,
|
|
47
|
+
orientation = "horizontal",
|
|
48
|
+
opts,
|
|
49
|
+
setApi,
|
|
50
|
+
plugins,
|
|
51
|
+
className,
|
|
52
|
+
children,
|
|
53
|
+
...props
|
|
54
|
+
}: React.HTMLAttributes<HTMLDivElement> & CarouselProps & {
|
|
55
|
+
ref?: React.Ref<HTMLDivElement>;
|
|
56
|
+
}
|
|
57
|
+
) => {
|
|
58
|
+
const [carouselRef, api] = useEmblaCarousel(
|
|
59
|
+
{
|
|
60
|
+
...opts,
|
|
61
|
+
axis: orientation === "horizontal" ? "x" : "y",
|
|
62
|
+
},
|
|
63
|
+
plugins
|
|
64
|
+
)
|
|
65
|
+
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
|
|
66
|
+
const [canScrollNext, setCanScrollNext] = React.useState(false)
|
|
67
|
+
|
|
68
|
+
const onSelect = React.useCallback((api: CarouselApi) => {
|
|
69
|
+
if (!api) {
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
setCanScrollPrev(api.canScrollPrev())
|
|
74
|
+
setCanScrollNext(api.canScrollNext())
|
|
75
|
+
}, [])
|
|
76
|
+
|
|
77
|
+
const scrollPrev = React.useCallback(() => {
|
|
78
|
+
api?.scrollPrev()
|
|
79
|
+
}, [api])
|
|
80
|
+
|
|
81
|
+
const scrollNext = React.useCallback(() => {
|
|
82
|
+
api?.scrollNext()
|
|
83
|
+
}, [api])
|
|
84
|
+
|
|
85
|
+
const handleKeyDown = React.useCallback(
|
|
86
|
+
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
87
|
+
if (event.key === "ArrowLeft") {
|
|
88
|
+
event.preventDefault()
|
|
89
|
+
scrollPrev()
|
|
90
|
+
} else if (event.key === "ArrowRight") {
|
|
91
|
+
event.preventDefault()
|
|
92
|
+
scrollNext()
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
[scrollPrev, scrollNext]
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
React.useEffect(() => {
|
|
99
|
+
if (!api || !setApi) {
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
setApi(api)
|
|
104
|
+
}, [api, setApi])
|
|
105
|
+
|
|
106
|
+
React.useEffect(() => {
|
|
107
|
+
if (!api) {
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
onSelect(api)
|
|
112
|
+
api.on("reInit", onSelect)
|
|
113
|
+
api.on("select", onSelect)
|
|
114
|
+
|
|
115
|
+
return () => {
|
|
116
|
+
api?.off("select", onSelect)
|
|
117
|
+
}
|
|
118
|
+
}, [api, onSelect])
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<CarouselContext.Provider
|
|
122
|
+
value={{
|
|
123
|
+
carouselRef,
|
|
124
|
+
api: api,
|
|
125
|
+
opts,
|
|
126
|
+
orientation:
|
|
127
|
+
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
|
|
128
|
+
scrollPrev,
|
|
129
|
+
scrollNext,
|
|
130
|
+
canScrollPrev,
|
|
131
|
+
canScrollNext,
|
|
132
|
+
}}
|
|
133
|
+
>
|
|
134
|
+
<div
|
|
135
|
+
ref={ref}
|
|
136
|
+
onKeyDownCapture={handleKeyDown}
|
|
137
|
+
className={cn("relative", className)}
|
|
138
|
+
role="region"
|
|
139
|
+
aria-roledescription="carousel"
|
|
140
|
+
{...props}
|
|
141
|
+
>
|
|
142
|
+
{children}
|
|
143
|
+
</div>
|
|
144
|
+
</CarouselContext.Provider>
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const CarouselContent = (
|
|
149
|
+
{
|
|
150
|
+
ref,
|
|
151
|
+
className,
|
|
152
|
+
...props
|
|
153
|
+
}: React.HTMLAttributes<HTMLDivElement> & {
|
|
154
|
+
ref?: React.Ref<HTMLDivElement>;
|
|
155
|
+
}
|
|
156
|
+
) => {
|
|
157
|
+
const { carouselRef, orientation } = useCarousel()
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<div ref={carouselRef} className="overflow-hidden">
|
|
161
|
+
<div
|
|
162
|
+
ref={ref}
|
|
163
|
+
className={cn(
|
|
164
|
+
"flex",
|
|
165
|
+
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
|
|
166
|
+
className
|
|
167
|
+
)}
|
|
168
|
+
{...props}
|
|
169
|
+
/>
|
|
170
|
+
</div>
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const CarouselItem = (
|
|
175
|
+
{
|
|
176
|
+
ref,
|
|
177
|
+
className,
|
|
178
|
+
...props
|
|
179
|
+
}: React.HTMLAttributes<HTMLDivElement> & {
|
|
180
|
+
ref?: React.Ref<HTMLDivElement>;
|
|
181
|
+
}
|
|
182
|
+
) => {
|
|
183
|
+
const { orientation } = useCarousel()
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<div
|
|
187
|
+
ref={ref}
|
|
188
|
+
role="group"
|
|
189
|
+
aria-roledescription="slide"
|
|
190
|
+
className={cn(
|
|
191
|
+
"min-w-0 shrink-0 grow-0 basis-full",
|
|
192
|
+
orientation === "horizontal" ? "pl-4" : "pt-4",
|
|
193
|
+
className
|
|
194
|
+
)}
|
|
195
|
+
{...props}
|
|
196
|
+
/>
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const CarouselPrevious = (
|
|
201
|
+
{
|
|
202
|
+
ref,
|
|
203
|
+
className,
|
|
204
|
+
variant = "outline",
|
|
205
|
+
size = "icon",
|
|
206
|
+
...props
|
|
207
|
+
}: React.ComponentProps<typeof Button> & {
|
|
208
|
+
ref?: React.Ref<HTMLButtonElement>;
|
|
209
|
+
}
|
|
210
|
+
) => {
|
|
211
|
+
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
|
|
212
|
+
|
|
213
|
+
return (
|
|
214
|
+
<Button
|
|
215
|
+
ref={ref}
|
|
216
|
+
variant={variant}
|
|
217
|
+
size={size}
|
|
218
|
+
className={cn(
|
|
219
|
+
"absolute h-8 w-8 rounded-full",
|
|
220
|
+
orientation === "horizontal"
|
|
221
|
+
? "-left-12 top-1/2 -translate-y-1/2"
|
|
222
|
+
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
|
223
|
+
className
|
|
224
|
+
)}
|
|
225
|
+
disabled={!canScrollPrev}
|
|
226
|
+
onClick={scrollPrev}
|
|
227
|
+
{...props}
|
|
228
|
+
>
|
|
229
|
+
<ArrowLeft className="h-4 w-4" />
|
|
230
|
+
<span className="sr-only">Previous slide</span>
|
|
231
|
+
</Button>
|
|
232
|
+
)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const CarouselNext = (
|
|
236
|
+
{
|
|
237
|
+
ref,
|
|
238
|
+
className,
|
|
239
|
+
variant = "outline",
|
|
240
|
+
size = "icon",
|
|
241
|
+
...props
|
|
242
|
+
}: React.ComponentProps<typeof Button> & {
|
|
243
|
+
ref?: React.Ref<HTMLButtonElement>;
|
|
244
|
+
}
|
|
245
|
+
) => {
|
|
246
|
+
const { orientation, scrollNext, canScrollNext } = useCarousel()
|
|
247
|
+
|
|
248
|
+
return (
|
|
249
|
+
<Button
|
|
250
|
+
ref={ref}
|
|
251
|
+
variant={variant}
|
|
252
|
+
size={size}
|
|
253
|
+
className={cn(
|
|
254
|
+
"absolute h-8 w-8 rounded-full",
|
|
255
|
+
orientation === "horizontal"
|
|
256
|
+
? "-right-12 top-1/2 -translate-y-1/2"
|
|
257
|
+
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
|
258
|
+
className
|
|
259
|
+
)}
|
|
260
|
+
disabled={!canScrollNext}
|
|
261
|
+
onClick={scrollNext}
|
|
262
|
+
{...props}
|
|
263
|
+
>
|
|
264
|
+
<ArrowRight className="h-4 w-4" />
|
|
265
|
+
<span className="sr-only">Next slide</span>
|
|
266
|
+
</Button>
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export {
|
|
271
|
+
type CarouselApi,
|
|
272
|
+
Carousel,
|
|
273
|
+
CarouselContent,
|
|
274
|
+
CarouselItem,
|
|
275
|
+
CarouselPrevious,
|
|
276
|
+
CarouselNext,
|
|
277
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react'
|
|
2
|
+
import userEvent from '@testing-library/user-event'
|
|
3
|
+
import { describe, it, expect } from 'vitest'
|
|
4
|
+
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from './Collapsible'
|
|
5
|
+
|
|
6
|
+
describe('Collapsible', () => {
|
|
7
|
+
it('renders trigger', () => {
|
|
8
|
+
render(
|
|
9
|
+
<Collapsible>
|
|
10
|
+
<CollapsibleTrigger>Toggle</CollapsibleTrigger>
|
|
11
|
+
<CollapsibleContent>Hidden content</CollapsibleContent>
|
|
12
|
+
</Collapsible>
|
|
13
|
+
)
|
|
14
|
+
expect(screen.getByRole('button', { name: /toggle/i })).toBeInTheDocument()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('shows content when opened', async () => {
|
|
18
|
+
const user = userEvent.setup()
|
|
19
|
+
render(
|
|
20
|
+
<Collapsible>
|
|
21
|
+
<CollapsibleTrigger>Toggle</CollapsibleTrigger>
|
|
22
|
+
<CollapsibleContent>Hidden content</CollapsibleContent>
|
|
23
|
+
</Collapsible>
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
expect(screen.queryByText('Hidden content')).not.toBeInTheDocument()
|
|
27
|
+
await user.click(screen.getByRole('button'))
|
|
28
|
+
expect(screen.getByText('Hidden content')).toBeVisible()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('renders open by default when open prop is true', () => {
|
|
32
|
+
render(
|
|
33
|
+
<Collapsible open>
|
|
34
|
+
<CollapsibleTrigger>Toggle</CollapsibleTrigger>
|
|
35
|
+
<CollapsibleContent>Visible content</CollapsibleContent>
|
|
36
|
+
</Collapsible>
|
|
37
|
+
)
|
|
38
|
+
expect(screen.getByText('Visible content')).toBeVisible()
|
|
39
|
+
})
|
|
40
|
+
})
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react"
|
|
3
|
+
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
|
|
4
|
+
|
|
5
|
+
import { cn } from "../../lib/utils"
|
|
6
|
+
|
|
7
|
+
const Collapsible = CollapsiblePrimitive.Root
|
|
8
|
+
|
|
9
|
+
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
|
|
10
|
+
|
|
11
|
+
const CollapsibleContent = (
|
|
12
|
+
{
|
|
13
|
+
ref,
|
|
14
|
+
className,
|
|
15
|
+
children,
|
|
16
|
+
...props
|
|
17
|
+
}: React.ComponentPropsWithoutRef<typeof CollapsiblePrimitive.CollapsibleContent> & {
|
|
18
|
+
ref?: React.Ref<React.ElementRef<typeof CollapsiblePrimitive.CollapsibleContent>>;
|
|
19
|
+
}
|
|
20
|
+
) => (<CollapsiblePrimitive.CollapsibleContent
|
|
21
|
+
ref={ref}
|
|
22
|
+
className={cn(
|
|
23
|
+
"overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down",
|
|
24
|
+
className
|
|
25
|
+
)}
|
|
26
|
+
{...props}
|
|
27
|
+
>
|
|
28
|
+
{children}
|
|
29
|
+
</CollapsiblePrimitive.CollapsibleContent>)
|
|
30
|
+
|
|
31
|
+
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react'
|
|
2
|
+
import { describe, it, expect } from 'vitest'
|
|
3
|
+
import { Container } from './Container'
|
|
4
|
+
|
|
5
|
+
describe('Container', () => {
|
|
6
|
+
it('renders children', () => {
|
|
7
|
+
render(<Container>Content here</Container>)
|
|
8
|
+
expect(screen.getByText('Content here')).toBeInTheDocument()
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('defaults to standard max-width', () => {
|
|
12
|
+
const { container } = render(<Container>Content</Container>)
|
|
13
|
+
expect(container.firstChild).toHaveClass('max-w-7xl')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('renders wide variant', () => {
|
|
17
|
+
const { container } = render(<Container variant="wide">Content</Container>)
|
|
18
|
+
expect(container.firstChild).toHaveClass('max-w-[1440px]')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('renders narrow variant', () => {
|
|
22
|
+
const { container } = render(<Container variant="narrow">Content</Container>)
|
|
23
|
+
expect(container.firstChild).toHaveClass('max-w-4xl')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('includes padding by default', () => {
|
|
27
|
+
const { container } = render(<Container>Content</Container>)
|
|
28
|
+
expect(container.firstChild).toHaveClass('px-4')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('removes padding when padding=false', () => {
|
|
32
|
+
const { container } = render(<Container padding={false}>Content</Container>)
|
|
33
|
+
expect(container.firstChild).not.toHaveClass('px-4')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('renders as different HTML elements', () => {
|
|
37
|
+
render(<Container as="main">Main content</Container>)
|
|
38
|
+
expect(screen.getByText('Main content').tagName).toBe('MAIN')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('applies custom className', () => {
|
|
42
|
+
const { container } = render(<Container className="custom-container">Content</Container>)
|
|
43
|
+
expect(container.firstChild).toHaveClass('custom-container')
|
|
44
|
+
})
|
|
45
|
+
})
|