@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,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 }
|