mirror of
https://github.com/Nezumi-2711/next-gdrive-index.git
synced 2026-09-22 13:38:38 +00:00
update: shadcn latest component
This commit is contained in:
@@ -9,6 +9,8 @@ const alertVariants = cva(
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-background text-foreground",
|
||||
primary: "bg-primary/25 border-primary/50",
|
||||
warning: "bg-amber-600/25 dark:bg-amber-400/25 border-amber-600/50 dark:border-amber-400/50",
|
||||
destructive: "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
||||
},
|
||||
},
|
||||
@@ -35,7 +37,7 @@ const AlertTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<H
|
||||
({ className, ...props }, ref) => (
|
||||
<h5
|
||||
ref={ref}
|
||||
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
||||
className={cn("mb-1 text-base font-semibold leading-none tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
@@ -53,4 +55,4 @@ const AlertDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttrib
|
||||
);
|
||||
AlertDescription.displayName = "AlertDescription";
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription };
|
||||
export { Alert, AlertDescription, AlertTitle };
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "~/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
|
||||
outline: "text-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
@@ -18,7 +18,7 @@ const buttonVariants = cva(
|
||||
"outline-destructive":
|
||||
"border border-input bg-background text-destructive stroke-destructive shadow-sm hover:bg-destructive hover:text-destructive-foreground hover:stroke-destructive-foreground",
|
||||
"ghost-destructive":
|
||||
"stroke-destructive hover:bg-destructive hover:text-destructive-foreground hover:stroke-destructive-foreground",
|
||||
"stroke-destructive text-destructive hover:bg-destructive dark:hover:bg-destructive/20 hover:text-destructive-foreground hover:stroke-destructive-foreground",
|
||||
"outline":
|
||||
"border border-input bg-background stroke-foreground shadow-sm hover:bg-accent hover:text-accent-foreground hover:stroke-accent-foreground",
|
||||
"secondary":
|
||||
@@ -108,7 +108,7 @@ const LoadingButton = React.forwardRef<HTMLButtonElement, LoadingButtonProps>(
|
||||
duration: 0.3,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
className={cn("relative inline-flex items-center justify-center", className)}
|
||||
className={cn("relative inline-flex items-center justify-center gap-2", className)}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||
import { Check } from "lucide-react"
|
||||
|
||||
import { cn } from "~/lib/utils"
|
||||
|
||||
const Checkbox = React.forwardRef<
|
||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
className={cn("flex items-center justify-center text-current")}
|
||||
>
|
||||
<Check className="h-4 w-4" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
))
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
||||
|
||||
export { Checkbox }
|
||||
@@ -0,0 +1,292 @@
|
||||
"use client";
|
||||
|
||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||
import * as React from "react";
|
||||
|
||||
import { useResponsive } from "~/context/responsiveContext";
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
import { Button, ButtonProps } from "./button";
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "./command";
|
||||
import { Drawer, DrawerContent, DrawerTrigger } from "./drawer";
|
||||
import Icon from "./icon";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "./popover";
|
||||
|
||||
interface ResponsiveComboboxRootProps extends React.PropsWithChildren {
|
||||
modal?: boolean;
|
||||
}
|
||||
interface ResponsiveComboboxTriggerProps extends React.PropsWithChildren {
|
||||
asChild?: true;
|
||||
className?: string;
|
||||
placeholder?: string;
|
||||
variant?: ButtonProps["variant"];
|
||||
size?: ButtonProps["size"];
|
||||
}
|
||||
interface ResponsiveComboboxContentProps extends React.PropsWithChildren {
|
||||
cmdk?: {
|
||||
placeholder?: string;
|
||||
emptyMessage?: React.ReactNode;
|
||||
};
|
||||
}
|
||||
interface ResponsiveComboboxContextProps {
|
||||
items: {
|
||||
label: React.ReactNode;
|
||||
value: string;
|
||||
}[];
|
||||
selected?: string;
|
||||
onSelect: (value: string) => void;
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}
|
||||
interface ResponsiveComboboxProviderProps extends React.PropsWithChildren {
|
||||
items: {
|
||||
label: React.ReactNode;
|
||||
value: string;
|
||||
}[];
|
||||
defaultSelected?: string;
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}
|
||||
|
||||
const ResponsiveComboboxContext = React.createContext<ResponsiveComboboxContextProps>({
|
||||
items: [],
|
||||
selected: "",
|
||||
onSelect: () => {
|
||||
void 0;
|
||||
},
|
||||
open: false,
|
||||
onOpenChange: () => {
|
||||
void false;
|
||||
},
|
||||
});
|
||||
const ResponsiveCombobox = (
|
||||
props: React.PropsWithChildren<
|
||||
ResponsiveComboboxProviderProps & {
|
||||
comboboxProps?: ResponsiveComboboxRootProps;
|
||||
}
|
||||
>,
|
||||
) => {
|
||||
const { isDesktop } = useResponsive();
|
||||
const Component = React.useMemo(() => (isDesktop ? Popover : Drawer), [isDesktop]);
|
||||
const items = React.useMemo<typeof props.items>(() => props.items, [props.items]);
|
||||
const [selected, setSelected] = React.useState<string | undefined>(props.defaultSelected ?? undefined);
|
||||
const [open, setOpen] = React.useState<boolean>(props.open ?? false);
|
||||
|
||||
const onSelect = React.useCallback((value: string) => {
|
||||
setSelected(selected === value ? undefined : value);
|
||||
setOpen(false);
|
||||
}, []);
|
||||
const onOpenChange = React.useCallback((open: boolean) => {
|
||||
setOpen(open);
|
||||
props.onOpenChange?.(open);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ResponsiveComboboxContext.Provider value={{ items, selected, onSelect }}>
|
||||
<Component
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
modal={props.comboboxProps?.modal}
|
||||
>
|
||||
{props.children}
|
||||
</Component>
|
||||
</ResponsiveComboboxContext.Provider>
|
||||
);
|
||||
};
|
||||
const useProvider = () => {
|
||||
const context = React.useContext(ResponsiveComboboxContext);
|
||||
if (!context) {
|
||||
throw new Error("Combobox Provider is not found.");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
const ResponsiveComboboxTrigger = (props: ResponsiveComboboxTriggerProps) => {
|
||||
const { children: _children, className, placeholder, variant = "outline", size, ...rest } = props;
|
||||
const { selected, items, open } = useProvider();
|
||||
const { isDesktop } = useResponsive();
|
||||
const Component = React.useMemo(() => (isDesktop ? PopoverTrigger : DrawerTrigger), [isDesktop]);
|
||||
|
||||
return (
|
||||
<Component asChild>
|
||||
<Button
|
||||
variant={variant}
|
||||
size={size}
|
||||
aria-expanded={open}
|
||||
className={cn("w-[200px] justify-between", className)}
|
||||
{...rest}
|
||||
>
|
||||
<div className='inline-flex items-center gap-2'>
|
||||
{selected ? items.find((item) => item.value === selected)?.label : placeholder ?? "Select item..."}
|
||||
</div>
|
||||
<Icon
|
||||
name='ChevronsUpDown'
|
||||
className='ml-2 h-4 w-4 shrink-0 opacity-50'
|
||||
hideWrapper
|
||||
/>
|
||||
</Button>
|
||||
</Component>
|
||||
);
|
||||
};
|
||||
const ResponsiveComboboxContent = (props: ResponsiveComboboxContentProps) => {
|
||||
const { cmdk, children: _children, ...rest } = props;
|
||||
const { items, selected, onSelect } = useProvider();
|
||||
const { isDesktop } = useResponsive();
|
||||
const Component = React.useMemo(() => (isDesktop ? PopoverContent : DrawerContent), [isDesktop]);
|
||||
|
||||
/**
|
||||
* https://github.com/oaarnikoivu/shadcn-virtualized-combobox
|
||||
*/
|
||||
|
||||
const [filteredOptions, setFilteredOptions] = React.useState<typeof items>(items);
|
||||
const [focusedIndex, setFocusedIndex] = React.useState<number>(0);
|
||||
const [isKeyboardNavActive, setIsKeyboardNavActive] = React.useState<boolean>(false);
|
||||
|
||||
const parentRef = React.useRef(null);
|
||||
|
||||
const virtualizer = useVirtualizer({
|
||||
count: filteredOptions.length,
|
||||
getScrollElement: () => parentRef.current,
|
||||
estimateSize: () => 35,
|
||||
});
|
||||
const virtualOptions = virtualizer.getVirtualItems();
|
||||
|
||||
const scrollToIndex = (index: number) => {
|
||||
virtualizer.scrollToIndex(index, {
|
||||
align: "center",
|
||||
});
|
||||
};
|
||||
const handleSearch = (search: string) => {
|
||||
setIsKeyboardNavActive(false);
|
||||
setFilteredOptions(items.filter((item) => item.value.toLowerCase().includes(search.toLowerCase() ?? [])));
|
||||
};
|
||||
const handleKeyDown = (event: React.KeyboardEvent) => {
|
||||
switch (event.key) {
|
||||
case "ArrowDown":
|
||||
event.preventDefault();
|
||||
setIsKeyboardNavActive(true);
|
||||
setFocusedIndex((prev) => {
|
||||
const newIndex = prev === -1 ? 0 : Math.min(prev + 1, filteredOptions.length - 1);
|
||||
scrollToIndex(newIndex);
|
||||
return newIndex;
|
||||
});
|
||||
break;
|
||||
case "ArrowUp":
|
||||
event.preventDefault();
|
||||
setIsKeyboardNavActive(true);
|
||||
setFocusedIndex((prev) => {
|
||||
const newIndex = prev === -1 ? 0 : Math.max(prev - 1, 0);
|
||||
scrollToIndex(newIndex);
|
||||
return newIndex;
|
||||
});
|
||||
break;
|
||||
case "Enter":
|
||||
event.preventDefault();
|
||||
if (filteredOptions[focusedIndex]) {
|
||||
onSelect(filteredOptions[focusedIndex].value);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (selected) {
|
||||
const option = filteredOptions.find((item) => item.value === selected);
|
||||
if (option) {
|
||||
const index = filteredOptions.indexOf(option);
|
||||
setFocusedIndex(index);
|
||||
virtualizer.scrollToIndex(index, {
|
||||
align: "center",
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [selected, filteredOptions, virtualizer]);
|
||||
|
||||
return (
|
||||
<Component
|
||||
{...rest}
|
||||
className='tablet:w-fit tablet:min-w-[200px] tablet:p-0'
|
||||
>
|
||||
<div className='px-4 tablet:px-0'>
|
||||
<Command
|
||||
shouldFilter={false}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<CommandInput
|
||||
onValueChange={handleSearch}
|
||||
placeholder={cmdk?.placeholder ?? "Search..."}
|
||||
/>
|
||||
<CommandList
|
||||
ref={parentRef}
|
||||
style={{
|
||||
height: "400px",
|
||||
width: "100%",
|
||||
overflow: "auto",
|
||||
}}
|
||||
onMouseDown={() => setIsKeyboardNavActive(false)}
|
||||
onMouseMove={() => setIsKeyboardNavActive(false)}
|
||||
>
|
||||
<CommandEmpty>{cmdk?.emptyMessage ?? "No item found."}</CommandEmpty>
|
||||
<CommandGroup>
|
||||
<div
|
||||
style={{
|
||||
height: `${virtualizer.getTotalSize()}px`,
|
||||
width: "100%",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
{virtualOptions.map((virtualOption) => (
|
||||
<CommandItem
|
||||
key={filteredOptions[virtualOption.index]?.value}
|
||||
disabled={isKeyboardNavActive}
|
||||
className={cn(
|
||||
"absolute left-0 top-0 w-full bg-transparent",
|
||||
focusedIndex === virtualOption.index && "bg-accent text-accent-foreground",
|
||||
isKeyboardNavActive &&
|
||||
focusedIndex !== virtualOption.index &&
|
||||
"aria-selected:bg-transparent aria-selected:text-primary",
|
||||
)}
|
||||
style={{
|
||||
height: `${virtualOption.size}px`,
|
||||
transform: `translateY(${virtualOption.start}px)`,
|
||||
}}
|
||||
value={filteredOptions[virtualOption.index]?.value}
|
||||
onMouseEnter={() => !isKeyboardNavActive && setFocusedIndex(virtualOption.index)}
|
||||
onMouseLeave={() => !isKeyboardNavActive && setFocusedIndex(-1)}
|
||||
onSelect={onSelect}
|
||||
>
|
||||
<Icon
|
||||
name='Check'
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
selected === filteredOptions[virtualOption.index]?.value ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
{filteredOptions[virtualOption.index]?.label}
|
||||
</CommandItem>
|
||||
))}
|
||||
{/* {items.map((item) => (
|
||||
<CommandItem
|
||||
key={item.value}
|
||||
value={item.value}
|
||||
onSelect={onSelect}
|
||||
>
|
||||
<Icon
|
||||
name='Check'
|
||||
className={cn("mr-2 h-4 w-4", selected === item.value ? "opacity-100" : "opacity-0")}
|
||||
/>
|
||||
{item.label}
|
||||
</CommandItem>
|
||||
))} */}
|
||||
</div>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</div>
|
||||
</Component>
|
||||
);
|
||||
};
|
||||
|
||||
export { ResponsiveCombobox, ResponsiveComboboxContent, ResponsiveComboboxTrigger };
|
||||
@@ -0,0 +1,241 @@
|
||||
"use client";
|
||||
|
||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||
import { Check, ChevronsUpDown } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "~/components/ui/command";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "~/components/ui/popover";
|
||||
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
type Option = {
|
||||
value: string;
|
||||
label: React.ReactNode;
|
||||
};
|
||||
|
||||
interface VirtualizedCommandProps {
|
||||
width: string;
|
||||
maxWidth: string;
|
||||
height: string;
|
||||
options: Option[];
|
||||
placeholder: string;
|
||||
selectedOption: string;
|
||||
onSelectOption?: (option: string) => void;
|
||||
}
|
||||
|
||||
const VirtualizedCommand = ({
|
||||
width,
|
||||
maxWidth,
|
||||
height,
|
||||
options,
|
||||
placeholder,
|
||||
selectedOption,
|
||||
onSelectOption,
|
||||
}: VirtualizedCommandProps) => {
|
||||
const [filteredOptions, setFilteredOptions] = React.useState<Option[]>(options);
|
||||
const [focusedIndex, setFocusedIndex] = React.useState(0);
|
||||
const [isKeyboardNavActive, setIsKeyboardNavActive] = React.useState(false);
|
||||
|
||||
const parentRef = React.useRef(null);
|
||||
|
||||
const virtualizer = useVirtualizer({
|
||||
count: filteredOptions.length,
|
||||
getScrollElement: () => parentRef.current,
|
||||
estimateSize: () => 35,
|
||||
});
|
||||
|
||||
const virtualOptions = virtualizer.getVirtualItems();
|
||||
|
||||
const scrollToIndex = (index: number) => {
|
||||
virtualizer.scrollToIndex(index, {
|
||||
align: "center",
|
||||
});
|
||||
};
|
||||
|
||||
const handleSearch = (search: string) => {
|
||||
setIsKeyboardNavActive(false);
|
||||
setFilteredOptions(options.filter((option) => option.value.toLowerCase().includes(search.toLowerCase() ?? [])));
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent) => {
|
||||
switch (event.key) {
|
||||
case "ArrowDown": {
|
||||
event.preventDefault();
|
||||
setIsKeyboardNavActive(true);
|
||||
setFocusedIndex((prev) => {
|
||||
const newIndex = prev === -1 ? 0 : Math.min(prev + 1, filteredOptions.length - 1);
|
||||
scrollToIndex(newIndex);
|
||||
return newIndex;
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "ArrowUp": {
|
||||
event.preventDefault();
|
||||
setIsKeyboardNavActive(true);
|
||||
setFocusedIndex((prev) => {
|
||||
const newIndex = prev === -1 ? filteredOptions.length - 1 : Math.max(prev - 1, 0);
|
||||
scrollToIndex(newIndex);
|
||||
return newIndex;
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "Enter": {
|
||||
event.preventDefault();
|
||||
if (filteredOptions[focusedIndex]) {
|
||||
onSelectOption?.(filteredOptions[focusedIndex].value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (selectedOption) {
|
||||
const option = filteredOptions.find((option) => option.value === selectedOption);
|
||||
if (option) {
|
||||
const index = filteredOptions.indexOf(option);
|
||||
setFocusedIndex(index);
|
||||
virtualizer.scrollToIndex(index, {
|
||||
align: "center",
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [selectedOption, filteredOptions, virtualizer]);
|
||||
|
||||
return (
|
||||
<Command
|
||||
shouldFilter={false}
|
||||
onKeyDown={handleKeyDown}
|
||||
style={{
|
||||
width: width,
|
||||
maxWidth: maxWidth,
|
||||
}}
|
||||
>
|
||||
<CommandInput
|
||||
onValueChange={handleSearch}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
<CommandList
|
||||
ref={parentRef}
|
||||
style={{
|
||||
height: height,
|
||||
width: "100%",
|
||||
overflow: "auto",
|
||||
}}
|
||||
onMouseDown={() => setIsKeyboardNavActive(false)}
|
||||
onMouseMove={() => setIsKeyboardNavActive(false)}
|
||||
>
|
||||
<CommandEmpty>No item found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
<div
|
||||
style={{
|
||||
height: `${virtualizer.getTotalSize()}px`,
|
||||
width: "100%",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
{virtualOptions.map((virtualOption) => (
|
||||
<CommandItem
|
||||
key={filteredOptions[virtualOption.index]?.value}
|
||||
disabled={isKeyboardNavActive}
|
||||
className={cn(
|
||||
"absolute left-0 top-0 w-full bg-transparent",
|
||||
focusedIndex === virtualOption.index && "bg-accent text-accent-foreground",
|
||||
isKeyboardNavActive &&
|
||||
focusedIndex !== virtualOption.index &&
|
||||
"aria-selected:bg-transparent aria-selected:text-primary",
|
||||
)}
|
||||
style={{
|
||||
height: `${virtualOption.size}px`,
|
||||
transform: `translateY(${virtualOption.start}px)`,
|
||||
}}
|
||||
value={filteredOptions[virtualOption.index]?.value}
|
||||
onMouseEnter={() => !isKeyboardNavActive && setFocusedIndex(virtualOption.index)}
|
||||
onMouseLeave={() => !isKeyboardNavActive && setFocusedIndex(-1)}
|
||||
onSelect={onSelectOption}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
selectedOption === filteredOptions[virtualOption.index]?.value ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
{filteredOptions[virtualOption.index]?.label}
|
||||
</CommandItem>
|
||||
))}
|
||||
</div>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
);
|
||||
};
|
||||
|
||||
interface VirtualizedComboboxProps {
|
||||
options: Option[];
|
||||
searchPlaceholder?: string;
|
||||
width?: string;
|
||||
minWidth?: string;
|
||||
maxWidth?: string;
|
||||
height?: string;
|
||||
selectedOption?: string;
|
||||
onSelectOption?: (option: string) => void;
|
||||
}
|
||||
|
||||
export function VirtualizedCombobox({
|
||||
options,
|
||||
searchPlaceholder = "Search items...",
|
||||
width = "100%",
|
||||
minWidth = "200px",
|
||||
maxWidth = "400px",
|
||||
height = "400px",
|
||||
selectedOption = "",
|
||||
onSelectOption,
|
||||
}: VirtualizedComboboxProps) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
// const [selectedOption, setSelectedOption] = React.useState("");
|
||||
|
||||
return (
|
||||
<Popover
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant='outline'
|
||||
role='combobox'
|
||||
aria-expanded={open}
|
||||
className='justify-between'
|
||||
style={{
|
||||
width: width,
|
||||
minWidth: minWidth,
|
||||
maxWidth: maxWidth,
|
||||
}}
|
||||
>
|
||||
{selectedOption ? options.find((option) => option.value === selectedOption)?.label : searchPlaceholder}
|
||||
<ChevronsUpDown className='ml-2 h-4 w-4 shrink-0 opacity-50' />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className='p-0'
|
||||
style={{ width: width, minWidth: minWidth, maxWidth: maxWidth }}
|
||||
>
|
||||
<VirtualizedCommand
|
||||
width={width}
|
||||
maxWidth={maxWidth}
|
||||
height={height}
|
||||
options={options}
|
||||
placeholder={searchPlaceholder}
|
||||
selectedOption={selectedOption}
|
||||
onSelectOption={(currentValue) => {
|
||||
onSelectOption?.(currentValue);
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
interface ResponsiveDialogRootProps extends React.PropsWithChildren {
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
modal?: boolean;
|
||||
}
|
||||
interface ResponsiveDialogProps extends React.PropsWithChildren {
|
||||
asChild?: true;
|
||||
@@ -47,7 +48,6 @@ const ResponsiveDialog = (props: ResponsiveDialogRootProps) => {
|
||||
const { ...rest } = props;
|
||||
const { isDesktop } = useResponsive();
|
||||
const Component = React.useMemo(() => (isDesktop ? Dialog : Drawer), [isDesktop]);
|
||||
|
||||
return <Component {...rest} />;
|
||||
};
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ const DialogContent = React.forwardRef<
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-screen-md translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -95,13 +95,13 @@ DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogTrigger,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Drawer as DrawerPrimitive } from "vaul";
|
||||
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
const Drawer = ({ shouldScaleBackground = true, ...props }: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
|
||||
const Drawer = ({ shouldScaleBackground = false, ...props }: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
|
||||
<DrawerPrimitive.Root
|
||||
shouldScaleBackground={shouldScaleBackground}
|
||||
{...props}
|
||||
|
||||
@@ -32,6 +32,7 @@ import { Separator } from "./separator";
|
||||
interface ResponsiveDropdownMenuRootProps extends React.PropsWithChildren {
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
modal?: boolean;
|
||||
}
|
||||
interface ResponsiveDropdownTriggerProps extends React.PropsWithChildren {
|
||||
asChild?: true;
|
||||
@@ -54,6 +55,7 @@ interface ResponsiveDropdownMenuItemProps extends React.PropsWithChildren {
|
||||
selected?: boolean;
|
||||
onSelect?: () => void;
|
||||
closeOnSelect?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
interface ResponsiveDropdownSeparatorProps extends React.PropsWithChildren {
|
||||
asChild?: true;
|
||||
@@ -113,7 +115,7 @@ const ResponsiveDropdownMenuContent = (props: ResponsiveDropdownContentProps) =>
|
||||
return Component;
|
||||
};
|
||||
const ResponsiveDropdownMenuItem = (props: ResponsiveDropdownMenuItemProps) => {
|
||||
const { selected, onSelect, closeOnSelect, ...rest } = props;
|
||||
const { selected, onSelect, closeOnSelect, disabled, ...rest } = props;
|
||||
const { isDesktop } = useResponsive();
|
||||
const loading = useLoading();
|
||||
const Wrapper = React.useCallback<React.FC<{ children: React.ReactNode }>>(
|
||||
@@ -127,6 +129,7 @@ const ResponsiveDropdownMenuItem = (props: ResponsiveDropdownMenuItemProps) => {
|
||||
isDesktop ? (
|
||||
<DropdownMenuItem
|
||||
onSelect={onSelect}
|
||||
disabled={disabled}
|
||||
{...rest}
|
||||
/>
|
||||
) : (
|
||||
@@ -134,13 +137,13 @@ const ResponsiveDropdownMenuItem = (props: ResponsiveDropdownMenuItemProps) => {
|
||||
<Button
|
||||
variant={selected ? "secondary" : "outline"}
|
||||
className='w-full'
|
||||
disabled={selected}
|
||||
disabled={selected ?? disabled}
|
||||
onClick={onSelect}
|
||||
{...rest}
|
||||
/>
|
||||
</Wrapper>
|
||||
),
|
||||
[isDesktop, onSelect, rest, Wrapper, selected],
|
||||
[isDesktop, onSelect, rest, Wrapper, selected, disabled],
|
||||
);
|
||||
|
||||
if (loading) return null;
|
||||
|
||||
+58
-24
@@ -9,6 +9,9 @@ import { Label } from "~/components/ui/label";
|
||||
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
import { Button } from "./button";
|
||||
import Icon from "./icon";
|
||||
|
||||
const Form = FormProvider;
|
||||
|
||||
type FormFieldContextValue<
|
||||
@@ -62,36 +65,67 @@ type FormItemContextValue = {
|
||||
|
||||
const FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue);
|
||||
|
||||
const FormItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => {
|
||||
const id = React.useId();
|
||||
const FormItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & {
|
||||
disableBorder?: true;
|
||||
}
|
||||
>(({ className, disableBorder, ...props }, ref) => {
|
||||
const id = React.useId();
|
||||
|
||||
return (
|
||||
<FormItemContext.Provider value={{ id }}>
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("space-y-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
</FormItemContext.Provider>
|
||||
);
|
||||
},
|
||||
);
|
||||
return (
|
||||
<FormItemContext.Provider value={{ id }}>
|
||||
<div
|
||||
ref={ref}
|
||||
data-use-border={!disableBorder}
|
||||
className={cn(
|
||||
"space-y-2 rounded-lg data-[use-border=true]:border data-[use-border=true]:px-4 data-[use-border=true]:py-2 data-[use-border=true]:shadow",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</FormItemContext.Provider>
|
||||
);
|
||||
});
|
||||
FormItem.displayName = "FormItem";
|
||||
|
||||
const FormLabel = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => {
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & {
|
||||
onFieldReset?: () => void;
|
||||
resetDisabled?: boolean;
|
||||
wrapperClassname?: string;
|
||||
}
|
||||
>(({ className, onFieldReset, resetDisabled, wrapperClassname, ...props }, ref) => {
|
||||
const { error, formItemId } = useFormField();
|
||||
|
||||
return (
|
||||
<Label
|
||||
ref={ref}
|
||||
className={cn(error && "text-destructive", className)}
|
||||
htmlFor={formItemId}
|
||||
{...props}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
"flex w-full items-center justify-between gap-2 tablet:w-fit tablet:justify-start",
|
||||
wrapperClassname,
|
||||
)}
|
||||
>
|
||||
<Label
|
||||
ref={ref}
|
||||
className={cn(error && "text-destructive", className)}
|
||||
htmlFor={formItemId}
|
||||
{...props}
|
||||
/>
|
||||
{onFieldReset && (
|
||||
<Button
|
||||
variant={"ghost"}
|
||||
onClick={onFieldReset}
|
||||
disabled={resetDisabled}
|
||||
size={"icon"}
|
||||
>
|
||||
<Icon
|
||||
name='RefreshCcw'
|
||||
className='stroke-inherit'
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
FormLabel.displayName = "FormLabel";
|
||||
@@ -121,7 +155,7 @@ const FormDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttribu
|
||||
<p
|
||||
ref={ref}
|
||||
id={formDescriptionId}
|
||||
className={cn("text-[0.8rem] text-muted-foreground", className)}
|
||||
className={cn("text-[0.8rem] leading-snug text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -152,4 +186,4 @@ const FormMessage = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<
|
||||
);
|
||||
FormMessage.displayName = "FormMessage";
|
||||
|
||||
export { useFormField, Form, FormItem, FormLabel, FormControl, FormDescription, FormMessage, FormField };
|
||||
export { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, useFormField };
|
||||
|
||||
@@ -24,7 +24,7 @@ export default function Icon({ name, className, hideWrapper, wrapperProps, ...pr
|
||||
hideWrapper ? (
|
||||
<Fragment>
|
||||
<LucideIcon
|
||||
className={cn("size-4 stroke-inherit", className)}
|
||||
className={cn("size-4 stroke-current", className)}
|
||||
{...props}
|
||||
/>
|
||||
</Fragment>
|
||||
@@ -34,7 +34,7 @@ export default function Icon({ name, className, hideWrapper, wrapperProps, ...pr
|
||||
{...restWrapperProps}
|
||||
>
|
||||
<LucideIcon
|
||||
className={cn("size-4 stroke-inherit", className)}
|
||||
className={cn("size-4 stroke-current", className)}
|
||||
{...props}
|
||||
/>
|
||||
</span>
|
||||
|
||||
@@ -7,8 +7,9 @@ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
data-type={type}
|
||||
className={cn(
|
||||
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-xs placeholder:md:text-sm",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
|
||||
@@ -16,7 +16,7 @@ const Progress = React.forwardRef<
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
className='h-full w-full flex-1 bg-primary transition-all'
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
style={{ transform: `translateX(-${100 - (value ?? 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
));
|
||||
|
||||
@@ -36,7 +36,7 @@ const ScrollBar = React.forwardRef<
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb className='relative flex-1 rounded-full bg-border' />
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb className='relative flex-1 rounded-full bg-muted-foreground' />
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
));
|
||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
||||
|
||||
import { cn } from "~/lib/utils"
|
||||
|
||||
const Switch = React.forwardRef<
|
||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
))
|
||||
Switch.displayName = SwitchPrimitives.Root.displayName
|
||||
|
||||
export { Switch }
|
||||
@@ -15,6 +15,7 @@ type ToasterToast = ToastProps & {
|
||||
action?: ToastActionElement;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const actionTypes = {
|
||||
ADD_TOAST: "ADD_TOAST",
|
||||
UPDATE_TOAST: "UPDATE_TOAST",
|
||||
@@ -186,4 +187,4 @@ function useToast() {
|
||||
};
|
||||
}
|
||||
|
||||
export { useToast, toast };
|
||||
export { toast, useToast };
|
||||
|
||||
Reference in New Issue
Block a user