@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,215 @@
1
+ 'use client';;
2
+ import React, { useState, useCallback, useEffect } from 'react';
3
+ import { TextField, type TextFieldProps } from './TextField';
4
+
5
+ export interface SearchBarProps extends Omit<TextFieldProps, 'variant'> {
6
+ /**
7
+ * Callback fired when search value changes (after debounce)
8
+ */
9
+ onSearch?: (value: string) => void;
10
+
11
+ /**
12
+ * Debounce delay in milliseconds
13
+ * @default 300
14
+ */
15
+ debounceMs?: number;
16
+
17
+ /**
18
+ * Show clear button when input has value
19
+ * @default true
20
+ */
21
+ showClearButton?: boolean;
22
+
23
+ /**
24
+ * Callback fired when clear button is clicked
25
+ */
26
+ onClear?: () => void;
27
+
28
+ /**
29
+ * Keyboard shortcut to display when empty (e.g. "⌘K")
30
+ * @default "⌘K"
31
+ */
32
+ shortcut?: React.ReactNode;
33
+ }
34
+
35
+ /**
36
+ * SearchBar Component
37
+ *
38
+ * A specialized text field for search functionality with built-in
39
+ * search icon, optional clear button, shortcut badge, and debounced onChange.
40
+ *
41
+ * **Note:** SearchBar always uses the `outlined` variant and does not
42
+ * accept a variant prop. This ensures consistent search field styling.
43
+ *
44
+ * Features:
45
+ * - Search icon on the left
46
+ * - Optional clear button (X) on the right
47
+ * - Shortcut badge (⌘K) when empty
48
+ * - Debounced search callback to reduce API calls
49
+ * - All TextField features (sizes, error states, etc.)
50
+ * - Theme-aware colors
51
+ * - Keyboard accessible (Escape to clear)
52
+ *
53
+ * Example:
54
+ * ```tsx
55
+ * <SearchBar
56
+ * placeholder="Search products..."
57
+ * onSearch={(query) => fetchResults(query)}
58
+ * debounceMs={500}
59
+ * />
60
+ * ```
61
+ */
62
+ export const SearchBar = (
63
+ {
64
+ ref,
65
+ onSearch,
66
+ debounceMs = 300,
67
+ showClearButton = true,
68
+ onClear,
69
+ value: controlledValue,
70
+ onChange,
71
+ placeholder = 'Search',
72
+ className = '',
73
+ shortcut = '⌘K',
74
+ ...props
75
+ }: SearchBarProps & {
76
+ ref?: React.Ref<HTMLInputElement>;
77
+ }
78
+ ) => {
79
+ const [internalValue, setInternalValue] = useState(controlledValue || '');
80
+
81
+ // Update internal value when controlled value changes
82
+ useEffect(() => {
83
+ if (controlledValue !== undefined) {
84
+ setInternalValue(controlledValue);
85
+ }
86
+ }, [controlledValue]);
87
+
88
+ // Debounced search callback
89
+ useEffect(() => {
90
+ if (!onSearch) return;
91
+
92
+ const timer = setTimeout(() => {
93
+ onSearch(String(internalValue));
94
+ }, debounceMs);
95
+
96
+ return () => clearTimeout(timer);
97
+ }, [internalValue, debounceMs, onSearch]);
98
+
99
+ const handleChange = useCallback(
100
+ (e: React.ChangeEvent<HTMLInputElement>) => {
101
+ const newValue = e.target.value;
102
+ setInternalValue(newValue);
103
+ onChange?.(e);
104
+ },
105
+ [onChange]
106
+ );
107
+
108
+ const handleClear = useCallback(() => {
109
+ setInternalValue('');
110
+ onClear?.();
111
+ // Create synthetic event for controlled components
112
+ if (onChange) {
113
+ const syntheticEvent = {
114
+ target: { value: '' },
115
+ } as React.ChangeEvent<HTMLInputElement>;
116
+ onChange(syntheticEvent);
117
+ }
118
+ }, [onChange, onClear]);
119
+
120
+ const handleKeyDown = useCallback(
121
+ (e: React.KeyboardEvent<HTMLInputElement>) => {
122
+ if (e.key === 'Escape' && internalValue) {
123
+ handleClear();
124
+ }
125
+ props.onKeyDown?.(e);
126
+ },
127
+ [internalValue, handleClear, props]
128
+ );
129
+
130
+ const value = controlledValue !== undefined ? controlledValue : internalValue;
131
+ const showClear = showClearButton && value;
132
+ const showShortcut = !value && shortcut;
133
+
134
+ return (
135
+ <div className={`relative w-full ${className}`}>
136
+ {/* Search Icon */}
137
+ <div className="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none">
138
+ <svg
139
+ xmlns="http://www.w3.org/2000/svg"
140
+ width="18"
141
+ height="18"
142
+ viewBox="0 0 24 24"
143
+ fill="none"
144
+ stroke="currentColor"
145
+ strokeWidth="2"
146
+ strokeLinecap="round"
147
+ strokeLinejoin="round"
148
+ className="text-[var(--color-text-muted)]"
149
+ aria-hidden="true"
150
+ >
151
+ <circle cx="11" cy="11" r="8" />
152
+ <path d="m21 21-4.35-4.35" />
153
+ </svg>
154
+ </div>
155
+
156
+ <TextField
157
+ ref={ref}
158
+ value={value}
159
+ onChange={handleChange}
160
+ onKeyDown={handleKeyDown}
161
+ placeholder={placeholder}
162
+ variant="outlined"
163
+ className="pl-10 bg-[var(--color-surface)]! border! border-[var(--color-border)]!"
164
+ style={{ paddingRight: (showClear || showShortcut) ? '3rem' : undefined }}
165
+ {...props}
166
+ />
167
+
168
+ {/* Shortcut Badge */}
169
+ {showShortcut && (
170
+ <div className="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none">
171
+ <kbd className="px-2 py-0.5 text-xs font-mono text-[var(--color-text-primary)] bg-[var(--color-background)] border border-[var(--color-border)] rounded shadow-xs">
172
+ {shortcut}
173
+ </kbd>
174
+ </div>
175
+ )}
176
+
177
+ {/* Clear Button */}
178
+ {showClear && (
179
+ <button
180
+ type="button"
181
+ onClick={handleClear}
182
+ className="
183
+ absolute right-3 top-1/2 -translate-y-1/2
184
+ p-1 rounded-full
185
+ text-[var(--color-text-muted)]
186
+ hover:text-[var(--color-text-primary)]
187
+ hover:bg-[var(--color-hover)]
188
+ transition-colors
189
+ focus:outline-none
190
+ focus:ring-2
191
+ focus:ring-[var(--color-focus)]
192
+ focus:ring-opacity-50
193
+ "
194
+ aria-label="Clear search"
195
+ >
196
+ <svg
197
+ xmlns="http://www.w3.org/2000/svg"
198
+ width="16"
199
+ height="16"
200
+ viewBox="0 0 24 24"
201
+ fill="none"
202
+ stroke="currentColor"
203
+ strokeWidth="2"
204
+ strokeLinecap="round"
205
+ strokeLinejoin="round"
206
+ aria-hidden="true"
207
+ >
208
+ <line x1="18" y1="6" x2="6" y2="18" />
209
+ <line x1="6" y1="6" x2="18" y2="18" />
210
+ </svg>
211
+ </button>
212
+ )}
213
+ </div>
214
+ );
215
+ };
@@ -0,0 +1,118 @@
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 {
5
+ Select,
6
+ SelectTrigger,
7
+ SelectContent,
8
+ SelectItem,
9
+ SelectValue,
10
+ } from './Select'
11
+
12
+ function renderSelect() {
13
+ return render(
14
+ <Select>
15
+ <SelectTrigger>
16
+ <SelectValue placeholder="Pick" />
17
+ </SelectTrigger>
18
+ <SelectContent>
19
+ <SelectItem value="a">Option A</SelectItem>
20
+ <SelectItem value="b">Option B</SelectItem>
21
+ <SelectItem value="c">Option C</SelectItem>
22
+ </SelectContent>
23
+ </Select>
24
+ )
25
+ }
26
+
27
+ describe('Select', () => {
28
+ it('renders the select trigger with placeholder', () => {
29
+ renderSelect()
30
+ const trigger = screen.getByRole('combobox')
31
+ expect(trigger).toBeInTheDocument()
32
+ expect(trigger).toHaveTextContent('Pick')
33
+ })
34
+
35
+ it('opens dropdown on click and shows options', async () => {
36
+ const user = userEvent.setup()
37
+ renderSelect()
38
+
39
+ const trigger = screen.getByRole('combobox')
40
+
41
+ // Focus then use keyboard to open (more reliable than click in jsdom for Radix Select)
42
+ await user.click(trigger)
43
+
44
+ // Radix Select renders options in a portal
45
+ const optionA = await screen.findByRole('option', { name: 'Option A' })
46
+ const optionB = await screen.findByRole('option', { name: 'Option B' })
47
+ const optionC = await screen.findByRole('option', { name: 'Option C' })
48
+
49
+ expect(optionA).toBeInTheDocument()
50
+ expect(optionB).toBeInTheDocument()
51
+ expect(optionC).toBeInTheDocument()
52
+ })
53
+
54
+ it('selects an option when clicked', async () => {
55
+ const user = userEvent.setup()
56
+ renderSelect()
57
+
58
+ const trigger = screen.getByRole('combobox')
59
+ await user.click(trigger)
60
+
61
+ const optionA = await screen.findByRole('option', { name: 'Option A' })
62
+ await user.click(optionA)
63
+
64
+ // After selection, the trigger should display the selected value
65
+ expect(trigger).toHaveTextContent('Option A')
66
+ })
67
+
68
+ it('applies custom className to trigger', () => {
69
+ render(
70
+ <Select>
71
+ <SelectTrigger className="custom-trigger">
72
+ <SelectValue placeholder="Pick" />
73
+ </SelectTrigger>
74
+ <SelectContent>
75
+ <SelectItem value="a">Option A</SelectItem>
76
+ </SelectContent>
77
+ </Select>
78
+ )
79
+
80
+ const trigger = screen.getByRole('combobox')
81
+ expect(trigger).toHaveClass('custom-trigger')
82
+ })
83
+
84
+ it('renders with a default value', () => {
85
+ render(
86
+ <Select defaultValue="b">
87
+ <SelectTrigger>
88
+ <SelectValue placeholder="Pick" />
89
+ </SelectTrigger>
90
+ <SelectContent>
91
+ <SelectItem value="a">Option A</SelectItem>
92
+ <SelectItem value="b">Option B</SelectItem>
93
+ </SelectContent>
94
+ </Select>
95
+ )
96
+
97
+ const trigger = screen.getByRole('combobox')
98
+ expect(trigger).toHaveTextContent('Option B')
99
+ })
100
+
101
+ it('opens with the open prop', () => {
102
+ render(
103
+ <Select open>
104
+ <SelectTrigger>
105
+ <SelectValue placeholder="Pick" />
106
+ </SelectTrigger>
107
+ <SelectContent>
108
+ <SelectItem value="a">Option A</SelectItem>
109
+ <SelectItem value="b">Option B</SelectItem>
110
+ </SelectContent>
111
+ </Select>
112
+ )
113
+
114
+ // When open prop is true, options should be visible
115
+ expect(screen.getByRole('option', { name: 'Option A' })).toBeInTheDocument()
116
+ expect(screen.getByRole('option', { name: 'Option B' })).toBeInTheDocument()
117
+ })
118
+ })
@@ -0,0 +1,274 @@
1
+ "use client";
2
+ import * as React from "react"
3
+ import type {
4
+ ComponentPropsWithoutRef,
5
+ ElementRef,
6
+ } from "react"
7
+ import {
8
+ Root,
9
+ Group,
10
+ Value,
11
+ Trigger,
12
+ Content,
13
+ Label,
14
+ Item,
15
+ ItemText,
16
+ ItemIndicator,
17
+ Separator as SelectSeparator,
18
+ ScrollUpButton,
19
+ ScrollDownButton,
20
+ Viewport,
21
+ Portal,
22
+ Icon as SelectIcon,
23
+ } from "@radix-ui/react-select"
24
+ import { Check, ChevronDown, ChevronUp } from "lucide-react"
25
+
26
+ import { cn } from "../../lib/utils"
27
+ import { Label as FormLabel } from "./Label"
28
+
29
+ const Select = Root
30
+
31
+ const SelectGroup = Group
32
+
33
+ const SelectValue = Value
34
+
35
+ const SelectTrigger = (
36
+ {
37
+ ref,
38
+ className,
39
+ children,
40
+ style,
41
+ label,
42
+ labelClassName,
43
+ ...props
44
+ }: ComponentPropsWithoutRef<typeof Trigger> & {
45
+ ref?: React.Ref<ElementRef<typeof Trigger>>;
46
+ /** Optional text label rendered above the trigger. */
47
+ label?: string;
48
+ /** Override classes on the label. */
49
+ labelClassName?: string;
50
+ }
51
+ ) => {
52
+ const trigger = (
53
+ <Trigger
54
+ ref={ref}
55
+ className={cn(
56
+ "flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
57
+ className
58
+ )}
59
+ style={{
60
+ height: '2.25rem',
61
+ width: '100%',
62
+ display: 'flex',
63
+ alignItems: 'center',
64
+ justifyContent: 'space-between',
65
+ border: '1px solid var(--color-input, #DFDFDF)',
66
+ borderRadius: 'var(--radius-md, 0.375rem)',
67
+ padding: '0.5rem 0.75rem',
68
+ fontSize: 'var(--text-sm, 0.875rem)',
69
+ backgroundColor: 'transparent',
70
+ boxShadow: '0 1px 2px 0 rgb(0 0 0 / 0.05)',
71
+ outline: 'none',
72
+ ...style,
73
+ }}
74
+ {...props}
75
+ >
76
+ {children}
77
+ <SelectIcon asChild>
78
+ <ChevronDown className="h-4 w-4 opacity-50" />
79
+ </SelectIcon>
80
+ </Trigger>
81
+ )
82
+
83
+ if (!label) return trigger
84
+
85
+ return (
86
+ <div>
87
+ <FormLabel className={cn("text-xs font-medium text-muted-foreground", labelClassName)}>
88
+ {label}
89
+ </FormLabel>
90
+ {trigger}
91
+ </div>
92
+ )
93
+ }
94
+
95
+ const SelectScrollUpButton = (
96
+ {
97
+ ref,
98
+ className,
99
+ ...props
100
+ }: ComponentPropsWithoutRef<typeof ScrollUpButton> & {
101
+ ref?: React.Ref<ElementRef<typeof ScrollUpButton>>;
102
+ }
103
+ ) => (<ScrollUpButton
104
+ ref={ref}
105
+ className={cn(
106
+ "flex cursor-default items-center justify-center py-1",
107
+ className
108
+ )}
109
+ {...props}
110
+ >
111
+ <ChevronUp className="h-4 w-4" />
112
+ </ScrollUpButton>)
113
+
114
+ const SelectScrollDownButton = (
115
+ {
116
+ ref,
117
+ className,
118
+ ...props
119
+ }: ComponentPropsWithoutRef<typeof ScrollDownButton> & {
120
+ ref?: React.Ref<ElementRef<typeof ScrollDownButton>>;
121
+ }
122
+ ) => (<ScrollDownButton
123
+ ref={ref}
124
+ className={cn(
125
+ "flex cursor-default items-center justify-center py-1",
126
+ className
127
+ )}
128
+ {...props}
129
+ >
130
+ <ChevronDown className="h-4 w-4" />
131
+ </ScrollDownButton>)
132
+
133
+ const SelectContent = (
134
+ {
135
+ ref,
136
+ className,
137
+ children,
138
+ position = "popper",
139
+ style,
140
+ ...props
141
+ }: ComponentPropsWithoutRef<typeof Content> & {
142
+ ref?: React.Ref<ElementRef<typeof Content>>;
143
+ }
144
+ ) => (<Portal>
145
+ <Content
146
+ ref={ref}
147
+ className={cn(
148
+ "relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
149
+ position === "popper" &&
150
+ "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
151
+ className
152
+ )}
153
+ style={style}
154
+ position={position}
155
+ {...props}
156
+ >
157
+ <div
158
+ style={{
159
+ backgroundColor: 'var(--color-popover, #ffffff)',
160
+ color: 'var(--color-popover-foreground, #0a0a0a)',
161
+ borderWidth: '1px',
162
+ borderStyle: 'solid',
163
+ borderColor: 'var(--color-border, #d4d4d4)',
164
+ borderRadius: 'var(--radius, 0.5rem)',
165
+ boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)',
166
+ overflow: 'hidden',
167
+ }}
168
+ >
169
+ <SelectScrollUpButton />
170
+ <Viewport
171
+ className={cn(
172
+ "p-1",
173
+ position === "popper" &&
174
+ "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
175
+ )}
176
+ style={{
177
+ padding: '0.25rem',
178
+ }}
179
+ >
180
+ {children}
181
+ </Viewport>
182
+ <SelectScrollDownButton />
183
+ </div>
184
+ </Content>
185
+ </Portal>)
186
+
187
+ const SelectLabel = (
188
+ {
189
+ ref,
190
+ className,
191
+ ...props
192
+ }: ComponentPropsWithoutRef<typeof Label> & {
193
+ ref?: React.Ref<ElementRef<typeof Label>>;
194
+ }
195
+ ) => (<Label
196
+ ref={ref}
197
+ className={cn("px-2 py-1.5 text-sm font-semibold", className)}
198
+ {...props}
199
+ />)
200
+
201
+ const SelectItem = (
202
+ {
203
+ ref,
204
+ className,
205
+ children,
206
+ ...props
207
+ }: ComponentPropsWithoutRef<typeof Item> & {
208
+ ref?: React.Ref<ElementRef<typeof Item>>;
209
+ }
210
+ ) => (<Item
211
+ ref={ref}
212
+ className={cn(
213
+ "relative flex w-full cursor-default select-none items-center rounded-xs py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
214
+ className
215
+ )}
216
+ style={{
217
+ position: 'relative',
218
+ display: 'flex',
219
+ width: '100%',
220
+ cursor: 'default',
221
+ userSelect: 'none',
222
+ alignItems: 'center',
223
+ borderRadius: 'var(--radius-sm, 0.125rem)',
224
+ padding: '0.375rem 2rem 0.375rem 0.5rem',
225
+ fontSize: 'var(--text-sm, 0.875rem)',
226
+ outline: 'none',
227
+ }}
228
+ {...props}
229
+ >
230
+ <span
231
+ className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center"
232
+ style={{
233
+ position: 'absolute',
234
+ right: '0.5rem',
235
+ display: 'flex',
236
+ height: '0.875rem',
237
+ width: '0.875rem',
238
+ alignItems: 'center',
239
+ justifyContent: 'center',
240
+ }}
241
+ >
242
+ <ItemIndicator>
243
+ <Check className="h-4 w-4" style={{ height: '1rem', width: '1rem' }} />
244
+ </ItemIndicator>
245
+ </span>
246
+ <ItemText>{children}</ItemText>
247
+ </Item>)
248
+
249
+ const SelectSeparatorComp = (
250
+ {
251
+ ref,
252
+ className,
253
+ ...props
254
+ }: ComponentPropsWithoutRef<typeof SelectSeparator> & {
255
+ ref?: React.Ref<ElementRef<typeof SelectSeparator>>;
256
+ }
257
+ ) => (<SelectSeparator
258
+ ref={ref}
259
+ className={cn("-mx-1 my-1 h-px bg-muted", className)}
260
+ {...props}
261
+ />)
262
+
263
+ export {
264
+ Select,
265
+ SelectGroup,
266
+ SelectValue,
267
+ SelectTrigger,
268
+ SelectContent,
269
+ SelectLabel,
270
+ SelectItem,
271
+ SelectSeparatorComp as SelectSeparator,
272
+ SelectScrollUpButton,
273
+ SelectScrollDownButton,
274
+ }
@@ -0,0 +1,29 @@
1
+ "use client";
2
+ import * as React from "react"
3
+ import * as SliderPrimitive from "@radix-ui/react-slider"
4
+
5
+ import { cn } from "../../lib/utils"
6
+
7
+ const Slider = (
8
+ {
9
+ ref,
10
+ className,
11
+ ...props
12
+ }: React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root> & {
13
+ ref?: React.Ref<React.ElementRef<typeof SliderPrimitive.Root>>;
14
+ }
15
+ ) => (<SliderPrimitive.Root
16
+ ref={ref}
17
+ className={cn(
18
+ "relative flex w-full touch-none select-none items-center",
19
+ className
20
+ )}
21
+ {...props}
22
+ >
23
+ <SliderPrimitive.Track className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-primary/20">
24
+ <SliderPrimitive.Range className="absolute h-full bg-primary" />
25
+ </SliderPrimitive.Track>
26
+ <SliderPrimitive.Thumb className="block h-4 w-4 rounded-full border border-primary/50 bg-background shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50" />
27
+ </SliderPrimitive.Root>)
28
+
29
+ export { Slider }