@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,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,7 @@
1
+ "use client"
2
+
3
+ import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
4
+
5
+ const AspectRatio = AspectRatioPrimitive.Root
6
+
7
+ export { AspectRatio }
@@ -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
+ })