@turtleclub/ui 0.7.0-beta.32 → 0.7.0-beta.34
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/dist/index.cjs +10331 -110
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +7652 -47844
- package/dist/index.js.map +1 -1
- package/dist/types/components/features/sidebar-layout.d.ts +2 -0
- package/dist/types/components/features/sidebar-layout.d.ts.map +1 -1
- package/dist/types/components/ui/chart.d.ts +1 -1
- package/dist/types/components/ui/chart.d.ts.map +1 -1
- package/package.json +26 -22
- package/.prettierrc.json +0 -4
- package/.turbo/turbo-build.log +0 -182
- package/CHANGELOG.md +0 -795
- package/components.json +0 -21
- package/src/components/charts/QUICK_REFERENCE.md +0 -323
- package/src/components/charts/README.md +0 -658
- package/src/components/charts/RECHARTS_FEATURES.md +0 -458
- package/src/components/charts/area-chart.tsx +0 -248
- package/src/components/charts/bar-chart.tsx +0 -362
- package/src/components/charts/index.ts +0 -4
- package/src/components/charts/pie-chart.tsx +0 -277
- package/src/components/charts/radial-chart.tsx +0 -312
- package/src/components/features/api-status/index.tsx +0 -23
- package/src/components/features/data-table/data-table.tsx +0 -538
- package/src/components/features/data-table/expand-toggle.tsx +0 -17
- package/src/components/features/data-table/fuzzy-filter.tsx +0 -34
- package/src/components/features/data-table/index.ts +0 -3
- package/src/components/features/data-table/item-info.tsx +0 -19
- package/src/components/features/data-table/skeleton.tsx +0 -23
- package/src/components/features/data-table/sort-dropdown.tsx +0 -118
- package/src/components/features/data-table/sortable-header.tsx +0 -37
- package/src/components/features/index.ts +0 -6
- package/src/components/features/page-heading.tsx +0 -27
- package/src/components/features/search-bar.tsx +0 -55
- package/src/components/features/segmented-navigation.tsx +0 -18
- package/src/components/features/sidebar-layout.tsx +0 -193
- package/src/components/features/turtle-tooltip.tsx +0 -67
- package/src/components/icons/arrow.tsx +0 -23
- package/src/components/icons/beta.tsx +0 -95
- package/src/components/icons/dot.tsx +0 -102
- package/src/components/icons/index.ts +0 -7
- package/src/components/icons/issue.tsx +0 -106
- package/src/components/icons/turtle.tsx +0 -156
- package/src/components/icons/update.tsx +0 -113
- package/src/components/icons/warning.tsx +0 -95
- package/src/components/molecules/index.ts +0 -9
- package/src/components/molecules/opportunity/index.ts +0 -10
- package/src/components/molecules/opportunity/opportunity-apr.tsx +0 -129
- package/src/components/molecules/opportunity/opportunity-disclaimer.tsx +0 -46
- package/src/components/molecules/opportunity/opportunity-rate-estimator.tsx +0 -62
- package/src/components/molecules/opportunity/opportunity-section.tsx +0 -113
- package/src/components/molecules/opportunity/opportunity-selector.tsx +0 -30
- package/src/components/molecules/opportunity/opportunity-type.tsx +0 -16
- package/src/components/molecules/route-details.tsx +0 -112
- package/src/components/molecules/slippage-selector.tsx +0 -200
- package/src/components/molecules/swap-details.tsx +0 -55
- package/src/components/molecules/swap-input.tsx +0 -186
- package/src/components/molecules/tabs.tsx +0 -79
- package/src/components/molecules/token-selector.tsx +0 -180
- package/src/components/molecules/tx-status.tsx +0 -312
- package/src/components/molecules/widget/asset-list/asset-filters.tsx +0 -113
- package/src/components/molecules/widget/asset-list/asset-list.tsx +0 -178
- package/src/components/molecules/widget/asset-list/asset-row.tsx +0 -45
- package/src/components/molecules/widget/asset-list/hooks/index.ts +0 -2
- package/src/components/molecules/widget/asset-list/hooks/use-asset-filtering.ts +0 -44
- package/src/components/molecules/widget/asset-list/hooks/use-asset-grouping.ts +0 -87
- package/src/components/molecules/widget/asset-list/index.ts +0 -3
- package/src/components/molecules/widget/base-selector.tsx +0 -121
- package/src/components/molecules/widget/campaign-item.tsx +0 -82
- package/src/components/molecules/widget/deal-item.tsx +0 -92
- package/src/components/molecules/widget/index.ts +0 -36
- package/src/components/molecules/widget/opportunity-item.tsx +0 -105
- package/src/components/molecules/widget/widget-item-stats.tsx +0 -50
- package/src/components/molecules/widget/widget-item.tsx +0 -139
- package/src/components/molecules/widget/widget-list-items.tsx +0 -86
- package/src/components/ui/alert-dialog.tsx +0 -163
- package/src/components/ui/animated-background/animated-background.tsx +0 -182
- package/src/components/ui/animated-background/index.ts +0 -1
- package/src/components/ui/avatar.tsx +0 -73
- package/src/components/ui/badge.tsx +0 -59
- package/src/components/ui/banner.tsx +0 -84
- package/src/components/ui/button.tsx +0 -100
- package/src/components/ui/card.tsx +0 -119
- package/src/components/ui/chart.tsx +0 -346
- package/src/components/ui/checkbox.tsx +0 -32
- package/src/components/ui/chip.tsx +0 -52
- package/src/components/ui/collapsible.tsx +0 -34
- package/src/components/ui/combobox.tsx +0 -730
- package/src/components/ui/command.tsx +0 -184
- package/src/components/ui/dialog.tsx +0 -129
- package/src/components/ui/dropdown.tsx +0 -316
- package/src/components/ui/field.tsx +0 -244
- package/src/components/ui/heading.tsx +0 -74
- package/src/components/ui/hover-card.tsx +0 -139
- package/src/components/ui/icon-animation.tsx +0 -82
- package/src/components/ui/icon-list.tsx +0 -168
- package/src/components/ui/index.ts +0 -48
- package/src/components/ui/info-card.tsx +0 -110
- package/src/components/ui/input-group.tsx +0 -170
- package/src/components/ui/input.tsx +0 -72
- package/src/components/ui/label-with-icon.tsx +0 -122
- package/src/components/ui/label.tsx +0 -24
- package/src/components/ui/multi-select.tsx +0 -1090
- package/src/components/ui/navigation-bar.tsx +0 -153
- package/src/components/ui/navigation-menu.tsx +0 -188
- package/src/components/ui/opportunity-details-v1.tsx +0 -104
- package/src/components/ui/pagination.tsx +0 -127
- package/src/components/ui/popover.tsx +0 -48
- package/src/components/ui/scroll-area.tsx +0 -64
- package/src/components/ui/segment-control.tsx +0 -146
- package/src/components/ui/select.tsx +0 -199
- package/src/components/ui/separator.tsx +0 -26
- package/src/components/ui/sheet.tsx +0 -139
- package/src/components/ui/sidebar.tsx +0 -728
- package/src/components/ui/skeleton.tsx +0 -14
- package/src/components/ui/slider.tsx +0 -58
- package/src/components/ui/sonner.tsx +0 -24
- package/src/components/ui/switch.tsx +0 -29
- package/src/components/ui/table-shadcn.tsx +0 -110
- package/src/components/ui/table.tsx +0 -117
- package/src/components/ui/textarea.tsx +0 -22
- package/src/components/ui/toggle-group.tsx +0 -71
- package/src/components/ui/toggle.tsx +0 -47
- package/src/components/ui/tooltip.tsx +0 -66
- package/src/hooks/index.ts +0 -1
- package/src/hooks/useIsMobile.ts +0 -77
- package/src/index.ts +0 -16
- package/src/lib/utils.ts +0 -6
- package/src/styles/globals.css +0 -181
- package/src/styles/themes/index.css +0 -9
- package/src/styles/themes/semantic.css +0 -117
- package/src/styles/tokens/colors.css +0 -124
- package/src/styles/tokens/index.css +0 -15
- package/src/styles/tokens/radius.css +0 -18
- package/src/styles/tokens/spacing.css +0 -58
- package/src/styles/tokens/typography.css +0 -87
- package/src/tokens/index.ts +0 -108
- package/tsconfig.json +0 -20
- package/vite.config.js +0 -49
- /package/{src/images/enso.png → dist/enso-22FJ4GNK.png} +0 -0
|
@@ -1,730 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import * as React from "react";
|
|
4
|
-
import { Check as CheckIcon, ChevronDown, WandSparkles, X as XIcon } from "lucide-react";
|
|
5
|
-
import { buttonVariants } from "@/components/ui/button";
|
|
6
|
-
import {
|
|
7
|
-
Command,
|
|
8
|
-
CommandEmpty,
|
|
9
|
-
CommandGroup,
|
|
10
|
-
CommandInput,
|
|
11
|
-
CommandItem,
|
|
12
|
-
CommandList,
|
|
13
|
-
} from "@/components/ui/command";
|
|
14
|
-
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
15
|
-
import { Separator } from "@/components/ui/separator";
|
|
16
|
-
import { cn } from "@/lib/utils";
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Option interface for Combobox component
|
|
20
|
-
*
|
|
21
|
-
* @example
|
|
22
|
-
* // String values (default)
|
|
23
|
-
* const stringOptions: ComboboxOption[] = [
|
|
24
|
-
* { label: "Option 1", value: "opt1" },
|
|
25
|
-
* { label: "Option 2", value: "opt2" }
|
|
26
|
-
* ];
|
|
27
|
-
*
|
|
28
|
-
* // Number values
|
|
29
|
-
* const numberOptions: ComboboxOption<number>[] = [
|
|
30
|
-
* { label: "First", value: 1 },
|
|
31
|
-
* { label: "Second", value: 2 }
|
|
32
|
-
* ];
|
|
33
|
-
*
|
|
34
|
-
* // Object values
|
|
35
|
-
* const objectOptions: ComboboxOption<{id: string, name: string}>[] = [
|
|
36
|
-
* { label: "User 1", value: { id: "u1", name: "John" } },
|
|
37
|
-
* { label: "User 2", value: { id: "u2", name: "Jane" } }
|
|
38
|
-
* ];
|
|
39
|
-
*/
|
|
40
|
-
interface ComboboxOption<T = string> {
|
|
41
|
-
/** The text to display for the option. */
|
|
42
|
-
label: string;
|
|
43
|
-
/** The unique value associated with the option. */
|
|
44
|
-
value: T;
|
|
45
|
-
/** Optional icon component to display alongside the option. */
|
|
46
|
-
icon?: React.ComponentType<{ className?: string }>;
|
|
47
|
-
/** Whether this option is disabled */
|
|
48
|
-
disabled?: boolean;
|
|
49
|
-
/** Custom styling for the option */
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Props for Combobox component
|
|
54
|
-
*
|
|
55
|
-
* @example
|
|
56
|
-
* // Basic usage with strings
|
|
57
|
-
* <Combobox
|
|
58
|
-
* options={[
|
|
59
|
-
* { label: "Option 1", value: "opt1" },
|
|
60
|
-
* { label: "Option 2", value: "opt2" }
|
|
61
|
-
* ]}
|
|
62
|
-
* onValueChange={(value: string) => console.log(value)}
|
|
63
|
-
* defaultValue="opt1"
|
|
64
|
-
* />
|
|
65
|
-
*
|
|
66
|
-
* // Usage with numbers
|
|
67
|
-
* <Combobox<number>
|
|
68
|
-
* options={[
|
|
69
|
-
* { label: "First", value: 1 },
|
|
70
|
-
* { label: "Second", value: 2 }
|
|
71
|
-
* ]}
|
|
72
|
-
* onValueChange={(value: number) => console.log(value)}
|
|
73
|
-
* defaultValue={1}
|
|
74
|
-
* />
|
|
75
|
-
*
|
|
76
|
-
* // Usage with objects
|
|
77
|
-
* <Combobox<User>
|
|
78
|
-
* options={[
|
|
79
|
-
* { label: "John Doe", value: { id: "1", name: "John" } },
|
|
80
|
-
* { label: "Jane Smith", value: { id: "2", name: "Jane" } }
|
|
81
|
-
* ]}
|
|
82
|
-
* onValueChange={(user: User) => console.log(user.name)}
|
|
83
|
-
* defaultValue={{ id: "1", name: "John" }}
|
|
84
|
-
* />
|
|
85
|
-
*/
|
|
86
|
-
interface ComboboxProps<T = string>
|
|
87
|
-
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "animationConfig" | "defaultValue"> {
|
|
88
|
-
/**
|
|
89
|
-
* An array of option objects or groups to be displayed in the multi-select component.
|
|
90
|
-
*/
|
|
91
|
-
options: ComboboxOption<T>[];
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Callback function triggered when the selected values change.
|
|
95
|
-
* Receives an array of the new selected values.
|
|
96
|
-
*/
|
|
97
|
-
onValueChange: (value: T) => void;
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Custom renderer for the selected value displayed in the input.
|
|
101
|
-
* @param option The currently selected option object.
|
|
102
|
-
*/
|
|
103
|
-
renderValue?: (option: ComboboxOption<T>) => React.ReactNode;
|
|
104
|
-
|
|
105
|
-
/** The default selected values when the component mounts. */
|
|
106
|
-
defaultValue?: T;
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Callback function triggered when the search input value changes.
|
|
110
|
-
* Useful for server-side filtering or handling large datasets externally.
|
|
111
|
-
*/
|
|
112
|
-
onInputValueChange?: (value: string) => void;
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* If true, disables the built-in filtering logic.
|
|
116
|
-
* Use this when you filter options externally (e.g. server-side search).
|
|
117
|
-
* Optional, defaults to false.
|
|
118
|
-
*/
|
|
119
|
-
disableLocalFiltering?: boolean;
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Placeholder text to be displayed when no values are selected.
|
|
123
|
-
* Optional, defaults to "Select options".
|
|
124
|
-
*/
|
|
125
|
-
placeholder?: string;
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Animation duration in seconds for the visual effects (e.g., bouncing badges).
|
|
129
|
-
* Optional, defaults to 0 (no animation).
|
|
130
|
-
*/
|
|
131
|
-
animation?: number;
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* The modality of the popover. When set to true, interaction with outside elements
|
|
135
|
-
* will be disabled and only popover content will be visible to screen readers.
|
|
136
|
-
* Optional, defaults to false.
|
|
137
|
-
*/
|
|
138
|
-
modalPopover?: boolean;
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* If true, renders the multi-select component as a child of another component.
|
|
142
|
-
* Optional, defaults to false.
|
|
143
|
-
*/
|
|
144
|
-
asChild?: boolean;
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Additional class names to apply custom styles to the multi-select component.
|
|
148
|
-
* Optional, can be used to add custom styles.
|
|
149
|
-
*/
|
|
150
|
-
className?: string;
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* If true, shows search functionality in the popover.
|
|
154
|
-
* If false, hides the search input completely.
|
|
155
|
-
* Optional, defaults to true.
|
|
156
|
-
*/
|
|
157
|
-
searchable?: boolean;
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* If true, searching will also check the 'value' field in addition to 'label'.
|
|
161
|
-
* Optional, defaults to false.
|
|
162
|
-
*/
|
|
163
|
-
searchByValue?: boolean;
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Custom empty state message when no options match search.
|
|
167
|
-
* Optional, defaults to "No results found."
|
|
168
|
-
*/
|
|
169
|
-
emptyIndicator?: React.ReactNode;
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* If true, allows the component to grow and shrink with its content.
|
|
173
|
-
* If false, uses fixed width behavior.
|
|
174
|
-
* Optional, defaults to false.
|
|
175
|
-
*/
|
|
176
|
-
autoSize?: boolean;
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Custom CSS class for the popover content.
|
|
180
|
-
* Optional, can be used to customize popover appearance.
|
|
181
|
-
*/
|
|
182
|
-
popoverClassName?: string;
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* If true, disables the component completely.
|
|
186
|
-
* Optional, defaults to false.
|
|
187
|
-
*/
|
|
188
|
-
disabled?: boolean;
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Responsive configuration for different screen sizes.
|
|
192
|
-
* Allows customizing maxCount and other properties based on viewport.
|
|
193
|
-
* Can be boolean true for default responsive behavior or an object for custom configuration.
|
|
194
|
-
*/
|
|
195
|
-
responsive?:
|
|
196
|
-
| boolean
|
|
197
|
-
| {
|
|
198
|
-
/** Configuration for mobile devices (< 640px) */
|
|
199
|
-
mobile?: {
|
|
200
|
-
maxCount?: number;
|
|
201
|
-
hideIcons?: boolean;
|
|
202
|
-
compactMode?: boolean;
|
|
203
|
-
};
|
|
204
|
-
/** Configuration for tablet devices (640px - 1024px) */
|
|
205
|
-
tablet?: {
|
|
206
|
-
maxCount?: number;
|
|
207
|
-
hideIcons?: boolean;
|
|
208
|
-
compactMode?: boolean;
|
|
209
|
-
};
|
|
210
|
-
/** Configuration for desktop devices (> 1024px) */
|
|
211
|
-
desktop?: {
|
|
212
|
-
maxCount?: number;
|
|
213
|
-
hideIcons?: boolean;
|
|
214
|
-
compactMode?: boolean;
|
|
215
|
-
};
|
|
216
|
-
};
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Minimum width for the component.
|
|
220
|
-
* Optional, defaults to auto-sizing based on content.
|
|
221
|
-
* When set, component will not shrink below this width.
|
|
222
|
-
*/
|
|
223
|
-
minWidth?: string;
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Maximum width for the component.
|
|
227
|
-
* Optional, defaults to 100% of container.
|
|
228
|
-
* Component will not exceed container boundaries.
|
|
229
|
-
*/
|
|
230
|
-
maxWidth?: string;
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* If true, automatically closes the popover after selecting an option.
|
|
234
|
-
* Useful for single-selection-like behavior or mobile UX.
|
|
235
|
-
* Optional, defaults to false.
|
|
236
|
-
*/
|
|
237
|
-
closeOnSelect?: boolean;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Imperative methods exposed through ref
|
|
242
|
-
*/
|
|
243
|
-
export interface ComboboxRef {
|
|
244
|
-
/**
|
|
245
|
-
* Programmatically reset the component to its default value
|
|
246
|
-
*/
|
|
247
|
-
reset: () => void;
|
|
248
|
-
/**
|
|
249
|
-
* Clear all selected values
|
|
250
|
-
*/
|
|
251
|
-
clear: () => void;
|
|
252
|
-
/**
|
|
253
|
-
* Focus the component
|
|
254
|
-
*/
|
|
255
|
-
focus: () => void;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
const ComboboxComponent = <T = string,>(props: ComboboxProps<T>, ref: React.Ref<ComboboxRef>) => {
|
|
259
|
-
const {
|
|
260
|
-
options,
|
|
261
|
-
onValueChange,
|
|
262
|
-
defaultValue = "" as T,
|
|
263
|
-
placeholder = "Select option",
|
|
264
|
-
animation = 0,
|
|
265
|
-
modalPopover = false,
|
|
266
|
-
className,
|
|
267
|
-
searchable = true,
|
|
268
|
-
emptyIndicator,
|
|
269
|
-
autoSize = false,
|
|
270
|
-
popoverClassName,
|
|
271
|
-
disabled = false,
|
|
272
|
-
responsive,
|
|
273
|
-
minWidth,
|
|
274
|
-
maxWidth,
|
|
275
|
-
closeOnSelect = false,
|
|
276
|
-
renderValue,
|
|
277
|
-
searchByValue = false,
|
|
278
|
-
onInputValueChange,
|
|
279
|
-
disableLocalFiltering = false,
|
|
280
|
-
...restProps
|
|
281
|
-
} = props;
|
|
282
|
-
|
|
283
|
-
const [selectedValue, setSelectedValue] = React.useState<T>(defaultValue);
|
|
284
|
-
const [isPopoverOpen, setIsPopoverOpen] = React.useState(false);
|
|
285
|
-
const [isAnimating, setIsAnimating] = React.useState(false);
|
|
286
|
-
const [searchValue, setSearchValue] = React.useState("");
|
|
287
|
-
|
|
288
|
-
const [politeMessage, setPoliteMessage] = React.useState("");
|
|
289
|
-
const [assertiveMessage, setAssertiveMessage] = React.useState("");
|
|
290
|
-
|
|
291
|
-
const prevIsOpen = React.useRef(isPopoverOpen);
|
|
292
|
-
const prevSearchValue = React.useRef(searchValue);
|
|
293
|
-
|
|
294
|
-
const toggleOption = (optionValue: T) => {
|
|
295
|
-
if (disabled) return;
|
|
296
|
-
const option = getOptionByValue(optionValue);
|
|
297
|
-
if (option?.disabled) return;
|
|
298
|
-
setSelectedValue(optionValue);
|
|
299
|
-
onValueChange(optionValue);
|
|
300
|
-
if (closeOnSelect) {
|
|
301
|
-
setIsPopoverOpen(false);
|
|
302
|
-
}
|
|
303
|
-
};
|
|
304
|
-
|
|
305
|
-
const announce = React.useCallback(
|
|
306
|
-
(message: string, priority: "polite" | "assertive" = "polite") => {
|
|
307
|
-
if (priority === "assertive") {
|
|
308
|
-
setAssertiveMessage(message);
|
|
309
|
-
setTimeout(() => setAssertiveMessage(""), 100);
|
|
310
|
-
} else {
|
|
311
|
-
setPoliteMessage(message);
|
|
312
|
-
setTimeout(() => setPoliteMessage(""), 100);
|
|
313
|
-
}
|
|
314
|
-
},
|
|
315
|
-
[]
|
|
316
|
-
);
|
|
317
|
-
|
|
318
|
-
const multiSelectId = React.useId();
|
|
319
|
-
const listboxId = `${multiSelectId}-listbox`;
|
|
320
|
-
const triggerDescriptionId = `${multiSelectId}-description`;
|
|
321
|
-
const selectedCountId = `${multiSelectId}-count`;
|
|
322
|
-
|
|
323
|
-
const resetToDefault = React.useCallback(() => {
|
|
324
|
-
setSelectedValue(defaultValue);
|
|
325
|
-
setIsPopoverOpen(false);
|
|
326
|
-
setSearchValue("");
|
|
327
|
-
onValueChange(defaultValue);
|
|
328
|
-
}, [defaultValue, onValueChange]);
|
|
329
|
-
|
|
330
|
-
const buttonRef = React.useRef<HTMLButtonElement>(null);
|
|
331
|
-
|
|
332
|
-
React.useImperativeHandle(
|
|
333
|
-
ref,
|
|
334
|
-
() => ({
|
|
335
|
-
reset: resetToDefault,
|
|
336
|
-
clear: () => {
|
|
337
|
-
setSelectedValue("" as T);
|
|
338
|
-
onValueChange("" as T);
|
|
339
|
-
},
|
|
340
|
-
focus: () => {
|
|
341
|
-
if (buttonRef.current) {
|
|
342
|
-
buttonRef.current.focus();
|
|
343
|
-
const originalOutline = buttonRef.current.style.outline;
|
|
344
|
-
const originalOutlineOffset = buttonRef.current.style.outlineOffset;
|
|
345
|
-
buttonRef.current.style.outline = "2px solid hsl(var(--ring))";
|
|
346
|
-
buttonRef.current.style.outlineOffset = "2px";
|
|
347
|
-
setTimeout(() => {
|
|
348
|
-
if (buttonRef.current) {
|
|
349
|
-
buttonRef.current.style.outline = originalOutline;
|
|
350
|
-
buttonRef.current.style.outlineOffset = originalOutlineOffset;
|
|
351
|
-
}
|
|
352
|
-
}, 1000);
|
|
353
|
-
}
|
|
354
|
-
},
|
|
355
|
-
}),
|
|
356
|
-
[resetToDefault, onValueChange]
|
|
357
|
-
);
|
|
358
|
-
|
|
359
|
-
const [screenSize, setScreenSize] = React.useState<"mobile" | "tablet" | "desktop">("desktop");
|
|
360
|
-
|
|
361
|
-
React.useEffect(() => {
|
|
362
|
-
if (typeof window === "undefined") return;
|
|
363
|
-
const handleResize = () => {
|
|
364
|
-
const width = window.innerWidth;
|
|
365
|
-
if (width < 640) {
|
|
366
|
-
setScreenSize("mobile");
|
|
367
|
-
} else if (width < 1024) {
|
|
368
|
-
setScreenSize("tablet");
|
|
369
|
-
} else {
|
|
370
|
-
setScreenSize("desktop");
|
|
371
|
-
}
|
|
372
|
-
};
|
|
373
|
-
handleResize();
|
|
374
|
-
window.addEventListener("resize", handleResize);
|
|
375
|
-
return () => {
|
|
376
|
-
if (typeof window !== "undefined") {
|
|
377
|
-
window.removeEventListener("resize", handleResize);
|
|
378
|
-
}
|
|
379
|
-
};
|
|
380
|
-
}, []);
|
|
381
|
-
|
|
382
|
-
const getResponsiveSettings = () => {
|
|
383
|
-
if (!responsive) {
|
|
384
|
-
return {
|
|
385
|
-
hideIcons: false,
|
|
386
|
-
compactMode: false,
|
|
387
|
-
};
|
|
388
|
-
}
|
|
389
|
-
if (responsive === true) {
|
|
390
|
-
const defaultResponsive = {
|
|
391
|
-
mobile: { hideIcons: false, compactMode: true },
|
|
392
|
-
tablet: { hideIcons: false, compactMode: false },
|
|
393
|
-
desktop: { hideIcons: false, compactMode: false },
|
|
394
|
-
};
|
|
395
|
-
const currentSettings = defaultResponsive[screenSize];
|
|
396
|
-
return {
|
|
397
|
-
hideIcons: currentSettings?.hideIcons ?? false,
|
|
398
|
-
compactMode: currentSettings?.compactMode ?? false,
|
|
399
|
-
};
|
|
400
|
-
}
|
|
401
|
-
const currentSettings = responsive[screenSize];
|
|
402
|
-
return {
|
|
403
|
-
hideIcons: currentSettings?.hideIcons ?? false,
|
|
404
|
-
compactMode: currentSettings?.compactMode ?? false,
|
|
405
|
-
};
|
|
406
|
-
};
|
|
407
|
-
|
|
408
|
-
const responsiveSettings = getResponsiveSettings();
|
|
409
|
-
|
|
410
|
-
const getAllOptions = React.useCallback((): ComboboxOption<T>[] => {
|
|
411
|
-
if (options.length === 0) return [];
|
|
412
|
-
|
|
413
|
-
const valueSet = new Set<T>();
|
|
414
|
-
const uniqueOptions: ComboboxOption<T>[] = [];
|
|
415
|
-
|
|
416
|
-
options.forEach((option) => {
|
|
417
|
-
if (!valueSet.has(option.value)) {
|
|
418
|
-
valueSet.add(option.value);
|
|
419
|
-
uniqueOptions.push(option);
|
|
420
|
-
}
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
return uniqueOptions;
|
|
424
|
-
}, [options]);
|
|
425
|
-
|
|
426
|
-
const getOptionByValue = React.useCallback(
|
|
427
|
-
(value: T): ComboboxOption<T> | undefined => {
|
|
428
|
-
const option = getAllOptions().find((option) => option.value === value);
|
|
429
|
-
if (!option && process.env.NODE_ENV === "development") {
|
|
430
|
-
console.warn(`Combobox: Option with value "${value}" not found in options list`);
|
|
431
|
-
}
|
|
432
|
-
return option;
|
|
433
|
-
},
|
|
434
|
-
[getAllOptions]
|
|
435
|
-
);
|
|
436
|
-
|
|
437
|
-
const filteredOptions = React.useMemo(() => {
|
|
438
|
-
if (disableLocalFiltering) return options;
|
|
439
|
-
if (!searchable || !searchValue) return options;
|
|
440
|
-
if (options.length === 0) return [];
|
|
441
|
-
|
|
442
|
-
return options.filter((option) => {
|
|
443
|
-
const labelMatch = option.label.toLowerCase().includes(searchValue.toLowerCase());
|
|
444
|
-
if (!searchByValue) return labelMatch;
|
|
445
|
-
|
|
446
|
-
const val = option.value;
|
|
447
|
-
const valueMatch =
|
|
448
|
-
typeof val === "string" || typeof val === "number"
|
|
449
|
-
? String(val).toLowerCase().includes(searchValue.toLowerCase())
|
|
450
|
-
: false;
|
|
451
|
-
|
|
452
|
-
return labelMatch || valueMatch;
|
|
453
|
-
});
|
|
454
|
-
}, [disableLocalFiltering, options, searchable, searchValue, searchByValue]);
|
|
455
|
-
|
|
456
|
-
// const handleInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
|
457
|
-
// if (event.key === "Enter") {
|
|
458
|
-
// setIsPopoverOpen(true);
|
|
459
|
-
// } else if (event.key === "Backspace" && !event.currentTarget.value) {
|
|
460
|
-
// const newSelectedValues = [...selectedValue];
|
|
461
|
-
// newSelectedValues.pop();
|
|
462
|
-
// setSelectedValue(newSelectedValues);
|
|
463
|
-
// onValueChange(newSelectedValues);
|
|
464
|
-
// }
|
|
465
|
-
// };
|
|
466
|
-
|
|
467
|
-
const handleClear = () => {
|
|
468
|
-
if (disabled) return;
|
|
469
|
-
setSelectedValue("" as T);
|
|
470
|
-
onValueChange("" as T);
|
|
471
|
-
};
|
|
472
|
-
|
|
473
|
-
const handleTogglePopover = () => {
|
|
474
|
-
if (disabled) return;
|
|
475
|
-
setIsPopoverOpen((prev) => !prev);
|
|
476
|
-
};
|
|
477
|
-
|
|
478
|
-
const getWidthConstraints = () => {
|
|
479
|
-
const defaultMinWidth = screenSize === "mobile" ? "0px" : "200px";
|
|
480
|
-
const effectiveMinWidth = minWidth || defaultMinWidth;
|
|
481
|
-
const effectiveMaxWidth = maxWidth || "100%";
|
|
482
|
-
return {
|
|
483
|
-
minWidth: effectiveMinWidth,
|
|
484
|
-
maxWidth: effectiveMaxWidth,
|
|
485
|
-
width: autoSize ? "auto" : "100%",
|
|
486
|
-
};
|
|
487
|
-
};
|
|
488
|
-
|
|
489
|
-
const widthConstraints = getWidthConstraints();
|
|
490
|
-
|
|
491
|
-
React.useEffect(() => {
|
|
492
|
-
if (!isPopoverOpen) {
|
|
493
|
-
setSearchValue("");
|
|
494
|
-
}
|
|
495
|
-
}, [isPopoverOpen]);
|
|
496
|
-
|
|
497
|
-
React.useEffect(() => {
|
|
498
|
-
const allOptions = getAllOptions();
|
|
499
|
-
const totalOptions = allOptions.filter((opt) => !opt.disabled).length;
|
|
500
|
-
|
|
501
|
-
if (isPopoverOpen !== prevIsOpen.current) {
|
|
502
|
-
if (isPopoverOpen) {
|
|
503
|
-
announce(`Dropdown opened. ${totalOptions} options available. Use arrow keys to navigate.`);
|
|
504
|
-
} else {
|
|
505
|
-
announce("Dropdown closed.");
|
|
506
|
-
}
|
|
507
|
-
prevIsOpen.current = isPopoverOpen;
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
if (searchValue !== prevSearchValue.current && searchValue !== undefined) {
|
|
511
|
-
if (searchValue && isPopoverOpen) {
|
|
512
|
-
const filteredCount = allOptions.filter(
|
|
513
|
-
(opt) =>
|
|
514
|
-
opt.label.toLowerCase().includes(searchValue.toLowerCase()) ||
|
|
515
|
-
String(opt.value).toLowerCase().includes(searchValue.toLowerCase())
|
|
516
|
-
).length;
|
|
517
|
-
|
|
518
|
-
announce(
|
|
519
|
-
`${filteredCount} option${filteredCount === 1 ? "" : "s"} found for "${searchValue}"`
|
|
520
|
-
);
|
|
521
|
-
}
|
|
522
|
-
prevSearchValue.current = searchValue;
|
|
523
|
-
}
|
|
524
|
-
}, [selectedValue, isPopoverOpen, searchValue, announce, getAllOptions]);
|
|
525
|
-
|
|
526
|
-
return (
|
|
527
|
-
<>
|
|
528
|
-
<div className="sr-only">
|
|
529
|
-
<div aria-live="polite" aria-atomic="true" role="status">
|
|
530
|
-
{politeMessage}
|
|
531
|
-
</div>
|
|
532
|
-
<div aria-live="assertive" aria-atomic="true" role="alert">
|
|
533
|
-
{assertiveMessage}
|
|
534
|
-
</div>
|
|
535
|
-
</div>
|
|
536
|
-
|
|
537
|
-
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen} modal={modalPopover}>
|
|
538
|
-
<div id={triggerDescriptionId} className="sr-only">
|
|
539
|
-
Multi-select dropdown. Use arrow keys to navigate, Enter to select, and Escape to close.
|
|
540
|
-
</div>
|
|
541
|
-
<PopoverTrigger
|
|
542
|
-
ref={buttonRef}
|
|
543
|
-
{...restProps}
|
|
544
|
-
onClick={handleTogglePopover}
|
|
545
|
-
disabled={disabled}
|
|
546
|
-
role="combobox"
|
|
547
|
-
aria-expanded={isPopoverOpen}
|
|
548
|
-
aria-haspopup="listbox"
|
|
549
|
-
aria-controls={isPopoverOpen ? listboxId : undefined}
|
|
550
|
-
aria-describedby={`${triggerDescriptionId} ${selectedCountId}`}
|
|
551
|
-
className={cn(
|
|
552
|
-
buttonVariants({
|
|
553
|
-
variant: "default",
|
|
554
|
-
size: "default",
|
|
555
|
-
border: "plain",
|
|
556
|
-
}),
|
|
557
|
-
"flex h-10 items-center justify-between [&_svg]:pointer-events-auto",
|
|
558
|
-
"!bg-neutral-alpha-2 text-base !font-normal shadow md:text-sm",
|
|
559
|
-
autoSize ? "w-auto" : "w-full",
|
|
560
|
-
responsiveSettings.compactMode && "h-8 text-sm",
|
|
561
|
-
screenSize === "mobile" && "h-12 text-base",
|
|
562
|
-
disabled && "cursor-not-allowed opacity-50",
|
|
563
|
-
className
|
|
564
|
-
)}
|
|
565
|
-
style={{
|
|
566
|
-
...widthConstraints,
|
|
567
|
-
maxWidth: `min(${widthConstraints.maxWidth}, 100%)`,
|
|
568
|
-
}}
|
|
569
|
-
>
|
|
570
|
-
{selectedValue ? (
|
|
571
|
-
<div className="mx-auto flex w-full items-center justify-between">
|
|
572
|
-
<div className="flex items-center gap-2">
|
|
573
|
-
{(() => {
|
|
574
|
-
const selectedOption = options.find((op) => op.value === selectedValue);
|
|
575
|
-
|
|
576
|
-
if (renderValue && selectedOption) {
|
|
577
|
-
return renderValue(selectedOption);
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
return (
|
|
581
|
-
<>
|
|
582
|
-
{selectedOption?.icon && (
|
|
583
|
-
<selectedOption.icon
|
|
584
|
-
className="text-muted-foreground size-3.5"
|
|
585
|
-
aria-hidden="true"
|
|
586
|
-
/>
|
|
587
|
-
)}
|
|
588
|
-
<span className="text-foreground text-sm">{selectedOption?.label}</span>
|
|
589
|
-
</>
|
|
590
|
-
);
|
|
591
|
-
})()}
|
|
592
|
-
</div>
|
|
593
|
-
<div className="flex items-center justify-between">
|
|
594
|
-
<div
|
|
595
|
-
role="button"
|
|
596
|
-
tabIndex={0}
|
|
597
|
-
onClick={(event) => {
|
|
598
|
-
event.stopPropagation();
|
|
599
|
-
handleClear();
|
|
600
|
-
}}
|
|
601
|
-
onKeyDown={(event) => {
|
|
602
|
-
if (event.key === "Enter" || event.key === " ") {
|
|
603
|
-
event.preventDefault();
|
|
604
|
-
event.stopPropagation();
|
|
605
|
-
handleClear();
|
|
606
|
-
}
|
|
607
|
-
}}
|
|
608
|
-
className="text-muted-foreground hover:text-foreground focus:ring-ring mx-2 flex size-3.5 cursor-pointer items-center justify-center rounded-sm focus:ring-2 focus:ring-offset-1 focus:outline-none"
|
|
609
|
-
>
|
|
610
|
-
<XIcon className="size-3.5" />
|
|
611
|
-
</div>
|
|
612
|
-
<Separator orientation="vertical" className="flex h-full min-h-6" />
|
|
613
|
-
<ChevronDown
|
|
614
|
-
className="text-muted-foreground mx-2 h-4 cursor-pointer"
|
|
615
|
-
aria-hidden="true"
|
|
616
|
-
/>
|
|
617
|
-
</div>
|
|
618
|
-
</div>
|
|
619
|
-
) : (
|
|
620
|
-
<div className="mx-auto flex w-full items-center justify-between">
|
|
621
|
-
<span className="text-muted-foreground text-sm">{placeholder}</span>
|
|
622
|
-
<ChevronDown className="text-muted-foreground mx-2 h-4 cursor-pointer" />
|
|
623
|
-
</div>
|
|
624
|
-
)}
|
|
625
|
-
</PopoverTrigger>
|
|
626
|
-
<PopoverContent
|
|
627
|
-
id={listboxId}
|
|
628
|
-
role="listbox"
|
|
629
|
-
aria-multiselectable="true"
|
|
630
|
-
aria-label="Available options"
|
|
631
|
-
className={cn(
|
|
632
|
-
"w-auto p-0",
|
|
633
|
-
screenSize === "mobile" && "w-[85vw] max-w-[280px]",
|
|
634
|
-
screenSize === "tablet" && "w-[70vw] max-w-md",
|
|
635
|
-
screenSize === "desktop" && "min-w-[300px]",
|
|
636
|
-
popoverClassName
|
|
637
|
-
)}
|
|
638
|
-
style={{
|
|
639
|
-
maxWidth: `min(${widthConstraints.maxWidth}, 85vw)`,
|
|
640
|
-
maxHeight: screenSize === "mobile" ? "70vh" : "60vh",
|
|
641
|
-
touchAction: "manipulation",
|
|
642
|
-
}}
|
|
643
|
-
align="start"
|
|
644
|
-
onEscapeKeyDown={() => setIsPopoverOpen(false)}
|
|
645
|
-
>
|
|
646
|
-
<Command shouldFilter={false}>
|
|
647
|
-
{searchable && (
|
|
648
|
-
<CommandInput
|
|
649
|
-
placeholder="Search options..."
|
|
650
|
-
// onKeyDown={handleInputKeyDown}
|
|
651
|
-
value={searchValue}
|
|
652
|
-
onValueChange={(val) => {
|
|
653
|
-
setSearchValue(val);
|
|
654
|
-
onInputValueChange?.(val);
|
|
655
|
-
}}
|
|
656
|
-
aria-label="Search through available options"
|
|
657
|
-
aria-describedby={`${multiSelectId}-search-help`}
|
|
658
|
-
/>
|
|
659
|
-
)}
|
|
660
|
-
{searchable && (
|
|
661
|
-
<div id={`${multiSelectId}-search-help`} className="sr-only">
|
|
662
|
-
Type to filter options. Use arrow keys to navigate results.
|
|
663
|
-
</div>
|
|
664
|
-
)}
|
|
665
|
-
<CommandList
|
|
666
|
-
className={cn(
|
|
667
|
-
"multiselect-scrollbar max-h-[40vh] overflow-y-auto",
|
|
668
|
-
screenSize === "mobile" && "max-h-[50vh]",
|
|
669
|
-
"overscroll-behavior-y-contain"
|
|
670
|
-
)}
|
|
671
|
-
>
|
|
672
|
-
<CommandEmpty>{emptyIndicator || "No results found."}</CommandEmpty>{" "}
|
|
673
|
-
<CommandGroup>
|
|
674
|
-
{filteredOptions.map((option) => {
|
|
675
|
-
const isSelected = selectedValue === option.value;
|
|
676
|
-
return (
|
|
677
|
-
<CommandItem
|
|
678
|
-
key={String(option.value)}
|
|
679
|
-
onSelect={() => toggleOption(option.value)}
|
|
680
|
-
role="option"
|
|
681
|
-
aria-selected={isSelected}
|
|
682
|
-
aria-disabled={option.disabled}
|
|
683
|
-
aria-label={`${option.label}${
|
|
684
|
-
isSelected ? ", selected" : ", not selected"
|
|
685
|
-
}${option.disabled ? ", disabled" : ""}`}
|
|
686
|
-
className={cn(
|
|
687
|
-
"cursor-pointer",
|
|
688
|
-
option.disabled && "cursor-not-allowed opacity-50"
|
|
689
|
-
)}
|
|
690
|
-
disabled={option.disabled}
|
|
691
|
-
>
|
|
692
|
-
{option.icon && (
|
|
693
|
-
<option.icon
|
|
694
|
-
className="text-muted-foreground mr-2 size-3.5"
|
|
695
|
-
aria-hidden="true"
|
|
696
|
-
/>
|
|
697
|
-
)}
|
|
698
|
-
<span className="grow">{option.label}</span>
|
|
699
|
-
{isSelected ? <CheckIcon className="text-muted-foreground size-3.5" /> : null}
|
|
700
|
-
</CommandItem>
|
|
701
|
-
);
|
|
702
|
-
})}
|
|
703
|
-
</CommandGroup>
|
|
704
|
-
</CommandList>
|
|
705
|
-
</Command>
|
|
706
|
-
</PopoverContent>
|
|
707
|
-
{animation > 0 && selectedValue && (
|
|
708
|
-
<WandSparkles
|
|
709
|
-
className={cn(
|
|
710
|
-
"text-foreground bg-background my-2 h-3 w-3 cursor-pointer",
|
|
711
|
-
isAnimating ? "" : "text-muted-foreground"
|
|
712
|
-
)}
|
|
713
|
-
onClick={() => setIsAnimating(!isAnimating)}
|
|
714
|
-
/>
|
|
715
|
-
)}
|
|
716
|
-
</Popover>
|
|
717
|
-
</>
|
|
718
|
-
);
|
|
719
|
-
};
|
|
720
|
-
|
|
721
|
-
// Create the forwardRef wrapper with proper generic typing
|
|
722
|
-
const ComboboxForwardRef = React.forwardRef(ComboboxComponent) as <T = string>(
|
|
723
|
-
props: ComboboxProps<T> & { ref?: React.Ref<ComboboxRef> }
|
|
724
|
-
) => React.ReactElement;
|
|
725
|
-
|
|
726
|
-
// Set displayName on a mutable object
|
|
727
|
-
(ComboboxForwardRef as any).displayName = "Combobox";
|
|
728
|
-
|
|
729
|
-
export { ComboboxForwardRef as Combobox };
|
|
730
|
-
export type { ComboboxOption, ComboboxProps };
|