update: shadcn latest component

This commit is contained in:
mbaharip
2025-01-21 22:01:41 +07:00
parent c25f163e62
commit c0efc5043f
17 changed files with 715 additions and 46 deletions
+4 -2
View File
@@ -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 };
+36
View File
@@ -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 }
+2 -2
View File
@@ -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>
+30
View File
@@ -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 }
+292
View File
@@ -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 };
+241
View File
@@ -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>
);
}
+1 -1
View File
@@ -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} />;
};
+7 -7
View File
@@ -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,
};
+1 -1
View File
@@ -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
View File
@@ -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 };
+2 -2
View File
@@ -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>
+2 -1
View File
@@ -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}
+1 -1
View File
@@ -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>
));
+1 -1
View File
@@ -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;
+29
View File
@@ -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 }
+2 -1
View File
@@ -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 };