kmod-cli 1.0.10
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/README.md +53 -0
- package/bin/gen-components.js +68 -0
- package/bin/index.js +153 -0
- package/component-templates/components/access-denied.tsx +130 -0
- package/component-templates/components/breadcumb.tsx +42 -0
- package/component-templates/components/count-down.tsx +94 -0
- package/component-templates/components/count-input.tsx +221 -0
- package/component-templates/components/date-range-calendar/button.tsx +61 -0
- package/component-templates/components/date-range-calendar/calendar.tsx +132 -0
- package/component-templates/components/date-range-calendar/date-input.tsx +259 -0
- package/component-templates/components/date-range-calendar/date-range-picker.tsx +594 -0
- package/component-templates/components/date-range-calendar/label.tsx +31 -0
- package/component-templates/components/date-range-calendar/popover.tsx +32 -0
- package/component-templates/components/date-range-calendar/select.tsx +125 -0
- package/component-templates/components/date-range-calendar/switch.tsx +30 -0
- package/component-templates/components/datetime-picker/button.tsx +61 -0
- package/component-templates/components/datetime-picker/calendar.tsx +156 -0
- package/component-templates/components/datetime-picker/datetime-picker.tsx +75 -0
- package/component-templates/components/datetime-picker/input.tsx +20 -0
- package/component-templates/components/datetime-picker/label.tsx +18 -0
- package/component-templates/components/datetime-picker/period-input.tsx +62 -0
- package/component-templates/components/datetime-picker/popover.tsx +32 -0
- package/component-templates/components/datetime-picker/select.tsx +125 -0
- package/component-templates/components/datetime-picker/time-picker-input.tsx +131 -0
- package/component-templates/components/datetime-picker/time-picker-utils.tsx +204 -0
- package/component-templates/components/datetime-picker/time-picker.tsx +59 -0
- package/component-templates/components/gradient-outline.tsx +233 -0
- package/component-templates/components/gradient-svg.tsx +157 -0
- package/component-templates/components/grid-layout.tsx +69 -0
- package/component-templates/components/hydrate-guard.tsx +40 -0
- package/component-templates/components/image.tsx +92 -0
- package/component-templates/components/loader-slash-gradient.tsx +85 -0
- package/component-templates/components/masonry-gallery.tsx +221 -0
- package/component-templates/components/modal.tsx +110 -0
- package/component-templates/components/multi-select.tsx +447 -0
- package/component-templates/components/non-hydration.tsx +27 -0
- package/component-templates/components/portal.tsx +34 -0
- package/component-templates/components/segments-circle.tsx +235 -0
- package/component-templates/components/single-select.tsx +248 -0
- package/component-templates/components/stroke-circle.tsx +57 -0
- package/component-templates/components/table/column-table.tsx +15 -0
- package/component-templates/components/table/data-table.tsx +339 -0
- package/component-templates/components/table/readme.tsx +95 -0
- package/component-templates/components/table/table.tsx +60 -0
- package/component-templates/components/text-hover-effect.tsx +120 -0
- package/component-templates/components/timout-loader.tsx +52 -0
- package/component-templates/components/toast.tsx +994 -0
- package/component-templates/configs/config.ts +33 -0
- package/component-templates/configs/feature-config.tsx +432 -0
- package/component-templates/configs/keys.ts +7 -0
- package/component-templates/core/api-service.ts +202 -0
- package/component-templates/core/calculate.ts +18 -0
- package/component-templates/core/idb.ts +166 -0
- package/component-templates/core/storage.ts +213 -0
- package/component-templates/hooks/count-down.ts +38 -0
- package/component-templates/hooks/fade-on-scroll.ts +52 -0
- package/component-templates/hooks/safe-action.ts +59 -0
- package/component-templates/hooks/spam-guard.ts +31 -0
- package/component-templates/lib/utils.ts +6 -0
- package/component-templates/providers/feature-guard.tsx +432 -0
- package/component-templates/queries/query.tsx +775 -0
- package/component-templates/utils/colors/color-by-text.ts +307 -0
- package/component-templates/utils/colors/stripe-effect.ts +100 -0
- package/component-templates/utils/hash/hash-aes.ts +35 -0
- package/components.json +348 -0
- package/package.json +60 -0
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
cva,
|
|
5
|
+
type VariantProps,
|
|
6
|
+
} from 'class-variance-authority';
|
|
7
|
+
import {
|
|
8
|
+
CheckIcon,
|
|
9
|
+
ChevronDown,
|
|
10
|
+
Loader2,
|
|
11
|
+
Plus,
|
|
12
|
+
WandSparkles,
|
|
13
|
+
XIcon,
|
|
14
|
+
} from 'lucide-react';
|
|
15
|
+
|
|
16
|
+
import { cn } from '../lib/utils';
|
|
17
|
+
import { Badge } from '../ui/badge';
|
|
18
|
+
import { Button } from '../ui/button';
|
|
19
|
+
import {
|
|
20
|
+
Command,
|
|
21
|
+
CommandEmpty,
|
|
22
|
+
CommandGroup,
|
|
23
|
+
CommandInput,
|
|
24
|
+
CommandItem,
|
|
25
|
+
CommandList,
|
|
26
|
+
CommandSeparator,
|
|
27
|
+
} from '../ui/command';
|
|
28
|
+
import {
|
|
29
|
+
Popover,
|
|
30
|
+
PopoverContent,
|
|
31
|
+
PopoverTrigger,
|
|
32
|
+
} from '../ui/popover';
|
|
33
|
+
import { Separator } from '../ui/separator';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Variants for the multi-select component to handle different styles.
|
|
37
|
+
* Uses class-variance-authority (cva) to define different styles based on "variant" prop.
|
|
38
|
+
*/
|
|
39
|
+
const multiSelectVariants = cva("m-1 transition ease-in-out delay-150 duration-300 shadow-none", {
|
|
40
|
+
variants: {
|
|
41
|
+
variant: {
|
|
42
|
+
default: "border-foreground/10 text-foreground bg-card hover:bg-card/80",
|
|
43
|
+
secondary: "border-foreground/10 bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
44
|
+
destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
|
45
|
+
inverted: "inverted",
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
defaultVariants: {
|
|
49
|
+
variant: "default",
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Props for MultiSelect component
|
|
55
|
+
*/
|
|
56
|
+
interface MultiSelectProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof multiSelectVariants> {
|
|
57
|
+
/**
|
|
58
|
+
* An array of option objects to be displayed in the multi-select component.
|
|
59
|
+
* Each option object has a label, value, and an optional icon.
|
|
60
|
+
*/
|
|
61
|
+
options: {
|
|
62
|
+
/** The text to display for the option. */
|
|
63
|
+
label: string;
|
|
64
|
+
/** The unique value associated with the option. */
|
|
65
|
+
value: string;
|
|
66
|
+
/** Optional icon component to display alongside the option. */
|
|
67
|
+
icon?: React.ComponentType<{ className?: string }>;
|
|
68
|
+
/** Optional has close button */
|
|
69
|
+
hasClose?: boolean;
|
|
70
|
+
/** className for badge */
|
|
71
|
+
classBadge?: string;
|
|
72
|
+
}[];
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Callback function triggered when the selected values change.
|
|
76
|
+
* Receives an array of the new selected values.
|
|
77
|
+
*/
|
|
78
|
+
onValueChange: (value: string[]) => void;
|
|
79
|
+
|
|
80
|
+
/** The default selected values when the component mounts. */
|
|
81
|
+
defaultValue?: string[];
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Placeholder text to be displayed when no values are selected.
|
|
85
|
+
* Optional, defaults to "Select options".
|
|
86
|
+
*/
|
|
87
|
+
placeholder?: string;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Animation duration in seconds for the visual effects (e.g., bouncing badges).
|
|
91
|
+
* Optional, defaults to 0 (no animation).
|
|
92
|
+
*/
|
|
93
|
+
animation?: number;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Maximum number of items to display. Extra selected items will be summarized.
|
|
97
|
+
* Optional, defaults to 3.
|
|
98
|
+
*/
|
|
99
|
+
maxCount?: number;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The modality of the popover. When set to true, interaction with outside elements
|
|
103
|
+
* will be disabled and only popover content will be visible to screen readers.
|
|
104
|
+
* Optional, defaults to false.
|
|
105
|
+
*/
|
|
106
|
+
modalPopover?: boolean;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* If true, renders the multi-select component as a child of another component.
|
|
110
|
+
* Optional, defaults to false.
|
|
111
|
+
*/
|
|
112
|
+
asChild?: boolean;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Additional class names to apply custom styles to the multi-select component.
|
|
116
|
+
* Optional, can be used to add custom styles.
|
|
117
|
+
*/
|
|
118
|
+
className?: string;
|
|
119
|
+
/**
|
|
120
|
+
* Additional class names to apply custom styles to the multi-select component.
|
|
121
|
+
* Optional, can be used to add custom styles.
|
|
122
|
+
*/
|
|
123
|
+
classNames?: {
|
|
124
|
+
trigger?: string;
|
|
125
|
+
content?: string;
|
|
126
|
+
list?: string;
|
|
127
|
+
item?: string;
|
|
128
|
+
group?: string;
|
|
129
|
+
badge?: string;
|
|
130
|
+
separator?: string;
|
|
131
|
+
input?: string;
|
|
132
|
+
empty?: string;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* If true, allows the user to create a new option when the input field is empty.
|
|
136
|
+
* Optional, defaults to false.
|
|
137
|
+
*/
|
|
138
|
+
createNewWhenEmpty?: boolean;
|
|
139
|
+
/**
|
|
140
|
+
* Callback function triggered when the user creates a new option.
|
|
141
|
+
* Receives the new option value.
|
|
142
|
+
* Optional, only used when createNewWhenEmpty is true.
|
|
143
|
+
*/
|
|
144
|
+
onCreateNewWhenEmpty?: (
|
|
145
|
+
value: string,
|
|
146
|
+
) => Promise<{ label: string; value: string; icon?: React.ComponentType<{ className?: string }> } | void>;
|
|
147
|
+
/**
|
|
148
|
+
* The empty label to display when no options are available.
|
|
149
|
+
* Optional, defaults to "No options available".
|
|
150
|
+
*/
|
|
151
|
+
emptyLabel?: string;
|
|
152
|
+
/**
|
|
153
|
+
* The label to display when creating a new option.
|
|
154
|
+
* Optional, defaults to "Create new option".
|
|
155
|
+
* */
|
|
156
|
+
newLabel?: React.ReactNode;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>(
|
|
160
|
+
(
|
|
161
|
+
{
|
|
162
|
+
options,
|
|
163
|
+
onValueChange,
|
|
164
|
+
variant,
|
|
165
|
+
defaultValue = [],
|
|
166
|
+
placeholder = "Select options",
|
|
167
|
+
emptyLabel = "No options available",
|
|
168
|
+
animation = 0,
|
|
169
|
+
maxCount = 3,
|
|
170
|
+
modalPopover = false,
|
|
171
|
+
asChild = false,
|
|
172
|
+
createNewWhenEmpty = false,
|
|
173
|
+
onCreateNewWhenEmpty,
|
|
174
|
+
newLabel,
|
|
175
|
+
className,
|
|
176
|
+
classNames,
|
|
177
|
+
...props
|
|
178
|
+
},
|
|
179
|
+
ref,
|
|
180
|
+
) => {
|
|
181
|
+
const [selectedValues, setSelectedValues] = React.useState<string[]>(defaultValue);
|
|
182
|
+
const [isPopoverOpen, setIsPopoverOpen] = React.useState(false);
|
|
183
|
+
const [isAnimating, setIsAnimating] = React.useState(false);
|
|
184
|
+
const [searchInput, setSearchInput] = React.useState("");
|
|
185
|
+
const [isCreating, setIsCreating] = React.useState(false);
|
|
186
|
+
|
|
187
|
+
const triggerRef = React.useRef<HTMLButtonElement>(null);
|
|
188
|
+
const [triggerWidth, setTriggerWidth] = React.useState<number | undefined>();
|
|
189
|
+
|
|
190
|
+
React.useImperativeHandle(ref, () => triggerRef.current!, [triggerRef]);
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
React.useEffect(() => {
|
|
194
|
+
const trigger = triggerRef.current;
|
|
195
|
+
if (!trigger) return;
|
|
196
|
+
|
|
197
|
+
const updateWidth = () => {
|
|
198
|
+
setTriggerWidth(trigger.offsetWidth);
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
updateWidth(); // initial set
|
|
202
|
+
|
|
203
|
+
const resizeObserver = new ResizeObserver(updateWidth);
|
|
204
|
+
resizeObserver.observe(trigger);
|
|
205
|
+
|
|
206
|
+
return () => resizeObserver.disconnect();
|
|
207
|
+
}, []);
|
|
208
|
+
|
|
209
|
+
const handleInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
|
210
|
+
if (event.key === "Enter") {
|
|
211
|
+
setIsPopoverOpen(true);
|
|
212
|
+
} else if (event.key === "Backspace" && !event.currentTarget.value) {
|
|
213
|
+
const newSelectedValues = [...selectedValues];
|
|
214
|
+
newSelectedValues.pop();
|
|
215
|
+
setSelectedValues(newSelectedValues);
|
|
216
|
+
onValueChange(newSelectedValues);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const toggleOption = (option: string) => {
|
|
221
|
+
const newSelectedValues = selectedValues.includes(option)
|
|
222
|
+
? selectedValues.filter((value) => value !== option)
|
|
223
|
+
: [...selectedValues, option];
|
|
224
|
+
setSelectedValues(newSelectedValues);
|
|
225
|
+
onValueChange(newSelectedValues);
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const handleClear = () => {
|
|
229
|
+
setSelectedValues([]);
|
|
230
|
+
onValueChange([]);
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const handleTogglePopover = () => {
|
|
234
|
+
setIsPopoverOpen((prev) => !prev);
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const clearExtraOptions = () => {
|
|
238
|
+
const newSelectedValues = selectedValues.slice(0, maxCount);
|
|
239
|
+
setSelectedValues(newSelectedValues);
|
|
240
|
+
onValueChange(newSelectedValues);
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const toggleAll = () => {
|
|
244
|
+
if (selectedValues.length === options.length) {
|
|
245
|
+
handleClear();
|
|
246
|
+
} else {
|
|
247
|
+
const allValues = options.map((option) => option.value);
|
|
248
|
+
setSelectedValues(allValues);
|
|
249
|
+
onValueChange(allValues);
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
function handleInputValueChange(search: string): void {
|
|
254
|
+
setSearchInput(search);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const handleClickCreateNewTagWhenEmpty = async () => {
|
|
258
|
+
try {
|
|
259
|
+
setIsCreating(true);
|
|
260
|
+
const newTag = await onCreateNewWhenEmpty?.(searchInput);
|
|
261
|
+
|
|
262
|
+
if (newTag) {
|
|
263
|
+
const updatedValues = [...selectedValues, newTag.value];
|
|
264
|
+
|
|
265
|
+
setSelectedValues(updatedValues);
|
|
266
|
+
onValueChange(updatedValues);
|
|
267
|
+
setSearchInput("");
|
|
268
|
+
}
|
|
269
|
+
} catch (err) {
|
|
270
|
+
console.error("create error:", err);
|
|
271
|
+
} finally {
|
|
272
|
+
setIsCreating(false);
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
return (
|
|
277
|
+
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen} modal={modalPopover}>
|
|
278
|
+
<PopoverTrigger asChild>
|
|
279
|
+
<Button
|
|
280
|
+
ref={triggerRef}
|
|
281
|
+
{...props}
|
|
282
|
+
onClick={handleTogglePopover}
|
|
283
|
+
className={cn(
|
|
284
|
+
"border-border flex h-auto min-h-10 w-full items-center justify-between rounded-md border bg-inherit p-1 hover:bg-inherit [&_svg]:pointer-events-auto",
|
|
285
|
+
className, classNames?.trigger,
|
|
286
|
+
)}
|
|
287
|
+
>
|
|
288
|
+
{selectedValues.length > 0 ? (
|
|
289
|
+
<div className="flex w-full items-center justify-between">
|
|
290
|
+
<div className="flex flex-wrap items-center">
|
|
291
|
+
{selectedValues.slice(0, maxCount).map((value) => {
|
|
292
|
+
const option = options.find((o) => o.value === value);
|
|
293
|
+
const IconComponent = option?.icon;
|
|
294
|
+
return (
|
|
295
|
+
<Badge
|
|
296
|
+
key={value}
|
|
297
|
+
className={cn(isAnimating ? "" : "", multiSelectVariants({ variant }), option?.classBadge, classNames?.badge)}
|
|
298
|
+
style={{ animationDuration: `${animation}s` }}
|
|
299
|
+
>
|
|
300
|
+
{IconComponent && <IconComponent className="mr-2 h-4 w-4" />}
|
|
301
|
+
{option?.label}
|
|
302
|
+
{option?.hasClose && (
|
|
303
|
+
<XIcon
|
|
304
|
+
className="ml-2 h-4 w-4 cursor-pointer"
|
|
305
|
+
onClick={(event) => {
|
|
306
|
+
event.stopPropagation();
|
|
307
|
+
toggleOption(value);
|
|
308
|
+
}}
|
|
309
|
+
/>
|
|
310
|
+
)}
|
|
311
|
+
</Badge>
|
|
312
|
+
);
|
|
313
|
+
})}
|
|
314
|
+
{selectedValues.length > maxCount && (
|
|
315
|
+
<Badge
|
|
316
|
+
className={cn(
|
|
317
|
+
"text-foreground border-foreground/1 bg-transparent hover:bg-transparent",
|
|
318
|
+
isAnimating ? "" : "",
|
|
319
|
+
multiSelectVariants({ variant }),
|
|
320
|
+
)}
|
|
321
|
+
style={{ animationDuration: `${animation}s` }}
|
|
322
|
+
>
|
|
323
|
+
{`+ ${selectedValues.length - maxCount} more`}
|
|
324
|
+
<XIcon
|
|
325
|
+
className="ml-2 h-4 w-4 cursor-pointer"
|
|
326
|
+
onClick={(event) => {
|
|
327
|
+
event.stopPropagation();
|
|
328
|
+
clearExtraOptions();
|
|
329
|
+
}}
|
|
330
|
+
/>
|
|
331
|
+
</Badge>
|
|
332
|
+
)}
|
|
333
|
+
</div>
|
|
334
|
+
<div className="flex items-center justify-between">
|
|
335
|
+
<XIcon
|
|
336
|
+
className="text-muted-foreground mx-2 h-4 cursor-pointer"
|
|
337
|
+
onClick={(event) => {
|
|
338
|
+
event.stopPropagation();
|
|
339
|
+
handleClear();
|
|
340
|
+
}}
|
|
341
|
+
/>
|
|
342
|
+
<Separator orientation="vertical" className="flex h-full min-h-6" />
|
|
343
|
+
<ChevronDown className="text-muted-foreground mx-2 h-4 cursor-pointer" />
|
|
344
|
+
</div>
|
|
345
|
+
</div>
|
|
346
|
+
) : (
|
|
347
|
+
<div className="mx-auto flex w-full items-center justify-between">
|
|
348
|
+
<span className="text-muted-foreground mx-3 text-sm">{placeholder}</span>
|
|
349
|
+
<ChevronDown className="text-muted-foreground mx-2 h-4 cursor-pointer" />
|
|
350
|
+
</div>
|
|
351
|
+
)}
|
|
352
|
+
</Button>
|
|
353
|
+
</PopoverTrigger>
|
|
354
|
+
<PopoverContent style={{ width: triggerWidth || "auto" }} className={cn("border-border w-full p-0", classNames?.content)} align="start" onEscapeKeyDown={() => setIsPopoverOpen(false)}>
|
|
355
|
+
<Command className="border-border">
|
|
356
|
+
<CommandInput
|
|
357
|
+
placeholder="Search..."
|
|
358
|
+
className={classNames?.input}
|
|
359
|
+
value={searchInput}
|
|
360
|
+
onValueChange={handleInputValueChange}
|
|
361
|
+
onKeyDown={handleInputKeyDown}
|
|
362
|
+
/>
|
|
363
|
+
<CommandList className={cn((createNewWhenEmpty && searchInput.trim() !== "" && "p-1") || "", classNames?.list)}>
|
|
364
|
+
<CommandEmpty className={cn((createNewWhenEmpty && searchInput.trim() !== "" && "py-0") || "", classNames?.empty)}>
|
|
365
|
+
{createNewWhenEmpty && searchInput.trim() !== "" && onCreateNewWhenEmpty ? (
|
|
366
|
+
<div
|
|
367
|
+
className={cn(
|
|
368
|
+
"text-muted-foreground hover:bg-accent flex h-10 cursor-pointer items-center justify-center rounded-md px-4 text-sm",
|
|
369
|
+
isCreating && "pointer-events-none opacity-50",
|
|
370
|
+
)}
|
|
371
|
+
onClick={() => handleClickCreateNewTagWhenEmpty()}
|
|
372
|
+
>
|
|
373
|
+
{isCreating ? (
|
|
374
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
375
|
+
) : (
|
|
376
|
+
<>
|
|
377
|
+
{newLabel ?? <Plus className="mr-2 h-4 w-4" />}
|
|
378
|
+
<span className="font-medium">"{searchInput}"</span>
|
|
379
|
+
</>
|
|
380
|
+
)}
|
|
381
|
+
</div>
|
|
382
|
+
) : (
|
|
383
|
+
<div className="py-2 text-center text-sm text-muted-foreground">{emptyLabel}</div>
|
|
384
|
+
)}
|
|
385
|
+
</CommandEmpty>
|
|
386
|
+
|
|
387
|
+
<CommandGroup className={classNames?.group}>
|
|
388
|
+
<CommandItem key="all" onSelect={toggleAll} className={cn("cursor-pointer", classNames?.item)}>
|
|
389
|
+
<div
|
|
390
|
+
className={cn(
|
|
391
|
+
"border-primary mr-2 flex h-4 w-4 items-center justify-center rounded-sm border",
|
|
392
|
+
selectedValues.length === options.length ? "bg-primary text-primary-foreground" : "opacity-50 [&_svg]:invisible",
|
|
393
|
+
)}
|
|
394
|
+
>
|
|
395
|
+
<CheckIcon className="h-4 w-4" />
|
|
396
|
+
</div>
|
|
397
|
+
<span>(Select All)</span>
|
|
398
|
+
</CommandItem>
|
|
399
|
+
{options.map((option) => {
|
|
400
|
+
const isSelected = selectedValues.includes(option.value);
|
|
401
|
+
return (
|
|
402
|
+
<CommandItem key={option.value} onSelect={() => toggleOption(option.value)} className={cn("cursor-pointer", classNames?.item)}>
|
|
403
|
+
<div
|
|
404
|
+
className={cn(
|
|
405
|
+
"border-border mr-2 flex h-4 w-4 items-center justify-center rounded-sm border",
|
|
406
|
+
isSelected ? "bg-primary text-primary-foreground" : "opacity-50 [&_svg]:invisible",
|
|
407
|
+
)}
|
|
408
|
+
>
|
|
409
|
+
<CheckIcon className="h-4 w-4" />
|
|
410
|
+
</div>
|
|
411
|
+
{option.icon && <option.icon className="text-muted-foreground mr-2 h-4 w-4" />}
|
|
412
|
+
<span>{option.label}</span>
|
|
413
|
+
</CommandItem>
|
|
414
|
+
);
|
|
415
|
+
})}
|
|
416
|
+
</CommandGroup>
|
|
417
|
+
<CommandSeparator className={classNames?.separator} />
|
|
418
|
+
<CommandGroup className={classNames?.group}>
|
|
419
|
+
<div className="flex items-center justify-between">
|
|
420
|
+
{selectedValues.length > 0 && (
|
|
421
|
+
<>
|
|
422
|
+
<CommandItem onSelect={handleClear} className={cn("flex-1 cursor-pointer justify-center", classNames?.item)}>
|
|
423
|
+
Clear
|
|
424
|
+
</CommandItem>
|
|
425
|
+
<Separator orientation="vertical" className={cn("flex h-full min-h-6", classNames?.separator)} />
|
|
426
|
+
</>
|
|
427
|
+
)}
|
|
428
|
+
<CommandItem onSelect={() => setIsPopoverOpen(false)} className={cn("max-w-full flex-1 cursor-pointer justify-center", classNames?.item)}>
|
|
429
|
+
Close
|
|
430
|
+
</CommandItem>
|
|
431
|
+
</div>
|
|
432
|
+
</CommandGroup>
|
|
433
|
+
</CommandList>
|
|
434
|
+
</Command>
|
|
435
|
+
</PopoverContent>
|
|
436
|
+
{animation > 0 && selectedValues.length > 0 && (
|
|
437
|
+
<WandSparkles
|
|
438
|
+
className={cn("text-foreground bg-background my-2 h-3 w-3 cursor-pointer", isAnimating ? "" : "text-muted-foreground")}
|
|
439
|
+
onClick={() => setIsAnimating(!isAnimating)}
|
|
440
|
+
/>
|
|
441
|
+
)}
|
|
442
|
+
</Popover>
|
|
443
|
+
);
|
|
444
|
+
},
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
MultiSelect.displayName = "MultiSelect";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, {
|
|
4
|
+
useEffect,
|
|
5
|
+
useState,
|
|
6
|
+
} from 'react';
|
|
7
|
+
|
|
8
|
+
export type NonHyrationProps = {
|
|
9
|
+
children: React.ReactNode;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* This component is used to prevent hydration errors.
|
|
14
|
+
*/
|
|
15
|
+
const NonHyration = ({ children }: NonHyrationProps) => {
|
|
16
|
+
const [isMounted, setIsMounted] = useState(false);
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
setIsMounted(true);
|
|
20
|
+
}, []);
|
|
21
|
+
|
|
22
|
+
if (!isMounted) return null;
|
|
23
|
+
|
|
24
|
+
return <>{children}</>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export default NonHyration;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// components/PortalDropdown.tsx
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
useEffect,
|
|
6
|
+
useState,
|
|
7
|
+
} from 'react';
|
|
8
|
+
import { createPortal } from 'react-dom';
|
|
9
|
+
|
|
10
|
+
type Props = {
|
|
11
|
+
show: boolean;
|
|
12
|
+
position: { top: number; left: number };
|
|
13
|
+
children: React.ReactNode;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default function PortalShow({ show, position, children }: Props) {
|
|
17
|
+
const [mounted, setMounted] = useState(false);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
setMounted(true);
|
|
21
|
+
}, []);
|
|
22
|
+
|
|
23
|
+
if (!mounted || !show) return null;
|
|
24
|
+
|
|
25
|
+
return createPortal(
|
|
26
|
+
<div
|
|
27
|
+
className="fixed z-[50] translate-y-[calc(100%-60px)] w-max max-w-[260px] rounded-md animate-fade-up bg-background p-2 shadow-xl"
|
|
28
|
+
style={{ top: position.top, left: position.left + 60 }}
|
|
29
|
+
>
|
|
30
|
+
{children}
|
|
31
|
+
</div>,
|
|
32
|
+
document.body,
|
|
33
|
+
);
|
|
34
|
+
}
|