feat: Linear-style UI redesign with improved mobile experience

Major changes:
- Redesign cabinet with Linear-style components and top navigation
- Replace detail modals with dedicated pages (users, broadcasts, email templates)
- Add email channel support for broadcasts
- Remove pull-to-refresh, improve drag-and-drop on touch devices
- Fix Telegram Mini App: fullscreen, back button, scroll restoration
- Unify admin pages color scheme to design system
- Mobile-first improvements: horizontal tabs for settings, better touch targets
This commit is contained in:
c0mrade
2026-01-31 22:06:36 +03:00
parent 929634aac4
commit b953ee0b8c
108 changed files with 11711 additions and 4542 deletions

View File

@@ -0,0 +1,104 @@
import { Slot } from '@radix-ui/react-slot';
import { motion, type HTMLMotionProps } from 'framer-motion';
import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from 'react';
import { cn } from '@/lib/utils';
import { usePlatform } from '@/platform';
import { buttonTap, buttonHover, springTransition } from '../../motion/transitions';
import { buttonVariants, type ButtonVariants } from './Button.variants';
export interface ButtonProps
extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'disabled'>, ButtonVariants {
children: ReactNode;
asChild?: boolean;
disabled?: boolean;
loading?: boolean;
leftIcon?: ReactNode;
rightIcon?: ReactNode;
haptic?: boolean;
}
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(
{
children,
className,
variant,
size,
fullWidth,
asChild = false,
disabled = false,
loading = false,
leftIcon,
rightIcon,
haptic = true,
onClick,
...props
},
ref,
) => {
const { haptic: platformHaptic } = usePlatform();
const isDisabled = disabled || loading;
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
if (haptic && !isDisabled) {
platformHaptic.impact('light');
}
onClick?.(e);
};
const classes = cn(buttonVariants({ variant, size, fullWidth }), className);
if (asChild) {
return (
<Slot ref={ref} className={classes} {...props}>
{children}
</Slot>
);
}
return (
<motion.button
ref={ref}
className={classes}
disabled={isDisabled}
onClick={handleClick}
whileHover={!isDisabled ? buttonHover : undefined}
whileTap={!isDisabled ? buttonTap : undefined}
transition={springTransition}
{...(props as HTMLMotionProps<'button'>)}
>
{loading ? (
<LoadingSpinner />
) : (
<>
{leftIcon && <span className="shrink-0">{leftIcon}</span>}
{children}
{rightIcon && <span className="shrink-0">{rightIcon}</span>}
</>
)}
</motion.button>
);
},
);
Button.displayName = 'Button';
function LoadingSpinner() {
return (
<svg
className="h-4 w-4 animate-spin"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
);
}
export { buttonVariants, type ButtonVariants };

View File

@@ -0,0 +1,64 @@
import { cva, type VariantProps } from 'class-variance-authority';
export const buttonVariants = cva(
// Base styles
[
'inline-flex items-center justify-center gap-2',
'font-medium transition-all duration-200',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-500/50 focus-visible:ring-offset-2 focus-visible:ring-offset-dark-950',
'disabled:pointer-events-none disabled:opacity-50',
'select-none',
],
{
variants: {
variant: {
primary: [
'bg-accent-500 text-white',
'hover:bg-accent-600',
'active:bg-accent-700',
'shadow-linear-sm hover:shadow-linear',
],
secondary: [
'bg-dark-800/80 text-dark-100',
'border border-dark-700/50',
'hover:bg-dark-700/80 hover:border-dark-600/50',
'active:bg-dark-800',
],
ghost: ['text-dark-300', 'hover:text-dark-100 hover:bg-dark-800/50', 'active:bg-dark-800'],
destructive: [
'bg-error-500/10 text-error-400',
'border border-error-500/20',
'hover:bg-error-500/20 hover:border-error-500/30',
'active:bg-error-500/30',
],
outline: [
'border border-dark-700/50 text-dark-200',
'hover:bg-dark-800/50 hover:border-dark-600/50 hover:text-dark-100',
'active:bg-dark-800',
],
link: [
'text-accent-400',
'hover:text-accent-300 hover:underline',
'active:text-accent-500',
],
},
size: {
sm: 'h-8 px-3 text-sm rounded-linear',
md: 'h-10 px-4 text-sm rounded-linear',
lg: 'h-12 px-6 text-base rounded-linear-lg',
icon: 'h-10 w-10 rounded-linear',
'icon-sm': 'h-8 w-8 rounded-linear',
'icon-lg': 'h-12 w-12 rounded-linear-lg',
},
fullWidth: {
true: 'w-full',
},
},
defaultVariants: {
variant: 'primary',
size: 'md',
},
},
);
export type ButtonVariants = VariantProps<typeof buttonVariants>;

View File

@@ -0,0 +1 @@
export { Button, type ButtonProps, buttonVariants, type ButtonVariants } from './Button';

View File

@@ -0,0 +1,172 @@
import { Command as CommandPrimitive } from 'cmdk';
import { forwardRef, type ComponentPropsWithoutRef, type HTMLAttributes } from 'react';
import { cn } from '@/lib/utils';
// Search icon
const SearchIcon = () => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" className="text-dark-400">
<path
d="M7.333 12.667A5.333 5.333 0 1 0 7.333 2a5.333 5.333 0 0 0 0 10.667ZM14 14l-2.9-2.9"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
// Root Command
export type CommandProps = ComponentPropsWithoutRef<typeof CommandPrimitive>;
export const Command = forwardRef<HTMLDivElement, CommandProps>(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={cn(
'flex h-full w-full flex-col overflow-hidden rounded-linear-lg',
'bg-dark-900/95 text-dark-100 backdrop-blur-linear',
className,
)}
{...props}
/>
));
Command.displayName = 'Command';
// Input
export type CommandInputProps = ComponentPropsWithoutRef<typeof CommandPrimitive.Input>;
export const CommandInput = forwardRef<HTMLInputElement, CommandInputProps>(
({ className, ...props }, ref) => (
<div className="flex items-center border-b border-dark-700/50 px-3" cmdk-input-wrapper="">
<SearchIcon />
<CommandPrimitive.Input
ref={ref}
className={cn(
'flex h-12 w-full bg-transparent py-3 pl-2 text-sm text-dark-100',
'placeholder:text-dark-400',
'focus:outline-none',
'disabled:cursor-not-allowed disabled:opacity-50',
className,
)}
{...props}
/>
</div>
),
);
CommandInput.displayName = 'CommandInput';
// List (scrollable area)
export type CommandListProps = ComponentPropsWithoutRef<typeof CommandPrimitive.List>;
export const CommandList = forwardRef<HTMLDivElement, CommandListProps>(
({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn(
'max-h-[300px] overflow-y-auto overflow-x-hidden',
'scrollbar-thin scrollbar-track-transparent scrollbar-thumb-dark-700',
className,
)}
{...props}
/>
),
);
CommandList.displayName = 'CommandList';
// Empty state
export type CommandEmptyProps = ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>;
export const CommandEmpty = forwardRef<HTMLDivElement, CommandEmptyProps>(
({ className, ...props }, ref) => (
<CommandPrimitive.Empty
ref={ref}
className={cn('py-6 text-center text-sm text-dark-400', className)}
{...props}
/>
),
);
CommandEmpty.displayName = 'CommandEmpty';
// Group
export type CommandGroupProps = ComponentPropsWithoutRef<typeof CommandPrimitive.Group>;
export const CommandGroup = forwardRef<HTMLDivElement, CommandGroupProps>(
({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
'overflow-hidden p-1 text-dark-100',
'[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5',
'[&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-dark-400',
className,
)}
{...props}
/>
),
);
CommandGroup.displayName = 'CommandGroup';
// Separator
export type CommandSeparatorProps = ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>;
export const CommandSeparator = forwardRef<HTMLDivElement, CommandSeparatorProps>(
({ className, ...props }, ref) => (
<CommandPrimitive.Separator
ref={ref}
className={cn('-mx-1 h-px bg-dark-700/50', className)}
{...props}
/>
),
);
CommandSeparator.displayName = 'CommandSeparator';
// Item
export type CommandItemProps = ComponentPropsWithoutRef<typeof CommandPrimitive.Item>;
export const CommandItem = forwardRef<HTMLDivElement, CommandItemProps>(
({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
'relative flex cursor-pointer select-none items-center gap-2 rounded-linear px-2 py-2',
'text-sm text-dark-200 outline-none',
'aria-selected:bg-dark-800/80 aria-selected:text-dark-100',
'data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50',
'transition-colors duration-150',
className,
)}
{...props}
/>
),
);
CommandItem.displayName = 'CommandItem';
// Shortcut display
export type CommandShortcutProps = HTMLAttributes<HTMLSpanElement>;
export const CommandShortcut = ({ className, ...props }: CommandShortcutProps) => (
<span className={cn('ml-auto text-xs tracking-widest text-dark-400', className)} {...props} />
);
CommandShortcut.displayName = 'CommandShortcut';
// Loading state
export type CommandLoadingProps = ComponentPropsWithoutRef<typeof CommandPrimitive.Loading>;
export const CommandLoading = forwardRef<HTMLDivElement, CommandLoadingProps>(
({ className, ...props }, ref) => (
<CommandPrimitive.Loading
ref={ref}
className={cn('py-6 text-center text-sm text-dark-400', className)}
{...props}
/>
),
);
CommandLoading.displayName = 'CommandLoading';

View File

@@ -0,0 +1,20 @@
export {
Command,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandSeparator,
CommandItem,
CommandShortcut,
CommandLoading,
type CommandProps,
type CommandInputProps,
type CommandListProps,
type CommandEmptyProps,
type CommandGroupProps,
type CommandSeparatorProps,
type CommandItemProps,
type CommandShortcutProps,
type CommandLoadingProps,
} from './Command';

View File

@@ -0,0 +1,190 @@
import * as DialogPrimitive from '@radix-ui/react-dialog';
import { motion, AnimatePresence } from 'framer-motion';
import {
forwardRef,
type ComponentPropsWithoutRef,
createContext,
useContext,
useState,
} from 'react';
import { cn } from '@/lib/utils';
import { backdrop, backdropTransition, scale, scaleTransition } from '../../motion/transitions';
// Close icon
const CloseIcon = () => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path
d="M12 4L4 12M4 4l8 8"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
// Context for AnimatePresence
const DialogContext = createContext<{ open: boolean }>({ open: false });
// Root
export type DialogProps = ComponentPropsWithoutRef<typeof DialogPrimitive.Root>;
export const Dialog = ({ children, open, onOpenChange, ...props }: DialogProps) => {
const [internalOpen, setInternalOpen] = useState(false);
const isOpen = open !== undefined ? open : internalOpen;
const handleOpenChange = onOpenChange || setInternalOpen;
return (
<DialogPrimitive.Root open={isOpen} onOpenChange={handleOpenChange} {...props}>
<DialogContext.Provider value={{ open: isOpen }}>{children}</DialogContext.Provider>
</DialogPrimitive.Root>
);
};
// Trigger
export const DialogTrigger = DialogPrimitive.Trigger;
// Portal
export const DialogPortal = DialogPrimitive.Portal;
// Close
export const DialogClose = DialogPrimitive.Close;
// Overlay
export type DialogOverlayProps = ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>;
export const DialogOverlay = forwardRef<HTMLDivElement, DialogOverlayProps>(
({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn('fixed inset-0 z-50 bg-black/60 backdrop-blur-sm', className)}
asChild
{...props}
>
<motion.div
variants={backdrop}
initial="initial"
animate="animate"
exit="exit"
transition={backdropTransition}
/>
</DialogPrimitive.Overlay>
),
);
DialogOverlay.displayName = 'DialogOverlay';
// Content
export interface DialogContentProps extends ComponentPropsWithoutRef<
typeof DialogPrimitive.Content
> {
showCloseButton?: boolean;
}
export const DialogContent = forwardRef<HTMLDivElement, DialogContentProps>(
({ className, children, showCloseButton = true, ...props }, ref) => {
const { open } = useContext(DialogContext);
return (
<DialogPortal forceMount>
<AnimatePresence mode="wait">
{open && (
<>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
'fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2',
'max-h-[85vh] w-full max-w-lg',
'grid gap-4 overflow-auto',
'rounded-linear-lg border border-dark-700/50 bg-dark-900/95 backdrop-blur-linear',
'p-6 shadow-linear-lg',
'focus:outline-none',
className,
)}
asChild
{...props}
>
<motion.div
variants={scale}
initial="initial"
animate="animate"
exit="exit"
transition={scaleTransition}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
className={cn(
'absolute right-4 top-4 rounded-linear p-1.5',
'text-dark-400 opacity-70 transition-all',
'hover:bg-dark-800/80 hover:opacity-100',
'focus:outline-none focus:ring-2 focus:ring-accent-500/50',
)}
>
<CloseIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</motion.div>
</DialogPrimitive.Content>
</>
)}
</AnimatePresence>
</DialogPortal>
);
},
);
DialogContent.displayName = 'DialogContent';
// Header
export type DialogHeaderProps = React.HTMLAttributes<HTMLDivElement>;
export const DialogHeader = ({ className, ...props }: DialogHeaderProps) => (
<div className={cn('flex flex-col space-y-1.5 text-center sm:text-left', className)} {...props} />
);
DialogHeader.displayName = 'DialogHeader';
// Footer
export type DialogFooterProps = React.HTMLAttributes<HTMLDivElement>;
export const DialogFooter = ({ className, ...props }: DialogFooterProps) => (
<div
className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)}
{...props}
/>
);
DialogFooter.displayName = 'DialogFooter';
// Title
export type DialogTitleProps = ComponentPropsWithoutRef<typeof DialogPrimitive.Title>;
export const DialogTitle = forwardRef<HTMLHeadingElement, DialogTitleProps>(
({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn('text-lg font-semibold text-dark-100', className)}
{...props}
/>
),
);
DialogTitle.displayName = 'DialogTitle';
// Description
export type DialogDescriptionProps = ComponentPropsWithoutRef<typeof DialogPrimitive.Description>;
export const DialogDescription = forwardRef<HTMLParagraphElement, DialogDescriptionProps>(
({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn('text-sm text-dark-400', className)}
{...props}
/>
),
);
DialogDescription.displayName = 'DialogDescription';

View File

@@ -0,0 +1,19 @@
export {
Dialog,
DialogTrigger,
DialogPortal,
DialogClose,
DialogOverlay,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
type DialogProps,
type DialogOverlayProps,
type DialogContentProps,
type DialogHeaderProps,
type DialogFooterProps,
type DialogTitleProps,
type DialogDescriptionProps,
} from './Dialog';

View File

@@ -0,0 +1,294 @@
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
import { motion } from 'framer-motion';
import { forwardRef, type ComponentPropsWithoutRef } from 'react';
import { cn } from '@/lib/utils';
import { usePlatform } from '@/platform';
import { dropdown, dropdownTransition } from '../../motion/transitions';
// Icons
const CheckIcon = () => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path
d="M3.5 8.5L6.5 11.5L12.5 4.5"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
const ChevronRightIcon = () => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path
d="M6 4l4 4-4 4"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
const DotIcon = () => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="3" fill="currentColor" />
</svg>
);
// Root
export const DropdownMenu = DropdownMenuPrimitive.Root;
// Trigger
export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
// Group
export const DropdownMenuGroup = DropdownMenuPrimitive.Group;
// Portal
export const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
// Sub
export const DropdownMenuSub = DropdownMenuPrimitive.Sub;
// RadioGroup
export const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
// SubTrigger
export interface DropdownMenuSubTriggerProps extends ComponentPropsWithoutRef<
typeof DropdownMenuPrimitive.SubTrigger
> {
inset?: boolean;
}
export const DropdownMenuSubTrigger = forwardRef<HTMLDivElement, DropdownMenuSubTriggerProps>(
({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
'flex cursor-pointer select-none items-center gap-2 rounded-linear px-2 py-2',
'text-sm text-dark-200 outline-none',
'focus:bg-dark-800/80 focus:text-dark-100',
'data-[state=open]:bg-dark-800/80',
inset && 'pl-8',
className,
)}
{...props}
>
{children}
<ChevronRightIcon />
</DropdownMenuPrimitive.SubTrigger>
),
);
DropdownMenuSubTrigger.displayName = 'DropdownMenuSubTrigger';
// SubContent
export type DropdownMenuSubContentProps = ComponentPropsWithoutRef<
typeof DropdownMenuPrimitive.SubContent
>;
export const DropdownMenuSubContent = forwardRef<HTMLDivElement, DropdownMenuSubContentProps>(
({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
'z-50 min-w-[8rem] overflow-hidden',
'rounded-linear-lg border border-dark-700/50 bg-dark-900/95 backdrop-blur-linear',
'p-1 text-dark-100 shadow-linear-lg',
className,
)}
asChild
{...props}
>
<motion.div
variants={dropdown}
initial="initial"
animate="animate"
exit="exit"
transition={dropdownTransition}
/>
</DropdownMenuPrimitive.SubContent>
),
);
DropdownMenuSubContent.displayName = 'DropdownMenuSubContent';
// Content
export type DropdownMenuContentProps = ComponentPropsWithoutRef<
typeof DropdownMenuPrimitive.Content
>;
export const DropdownMenuContent = forwardRef<HTMLDivElement, DropdownMenuContentProps>(
({ className, sideOffset = 4, ...props }, ref) => {
const { haptic } = usePlatform();
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
'z-50 min-w-[8rem] overflow-hidden',
'rounded-linear-lg border border-dark-700/50 bg-dark-900/95 backdrop-blur-linear',
'p-1 text-dark-100 shadow-linear-lg',
className,
)}
onCloseAutoFocus={() => haptic.impact('light')}
asChild
{...props}
>
<motion.div
variants={dropdown}
initial="initial"
animate="animate"
exit="exit"
transition={dropdownTransition}
/>
</DropdownMenuPrimitive.Content>
</DropdownMenuPrimitive.Portal>
);
},
);
DropdownMenuContent.displayName = 'DropdownMenuContent';
// Item
export interface DropdownMenuItemProps extends ComponentPropsWithoutRef<
typeof DropdownMenuPrimitive.Item
> {
inset?: boolean;
destructive?: boolean;
}
export const DropdownMenuItem = forwardRef<HTMLDivElement, DropdownMenuItemProps>(
({ className, inset, destructive, ...props }, ref) => {
const { haptic } = usePlatform();
return (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
'relative flex cursor-pointer select-none items-center gap-2 rounded-linear px-2 py-2',
'text-sm outline-none transition-colors duration-150',
destructive
? 'text-error-400 focus:bg-error-500/10 focus:text-error-300'
: 'text-dark-200 focus:bg-dark-800/80 focus:text-dark-100',
'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
inset && 'pl-8',
className,
)}
onClick={() => haptic.impact('light')}
{...props}
/>
);
},
);
DropdownMenuItem.displayName = 'DropdownMenuItem';
// CheckboxItem
export type DropdownMenuCheckboxItemProps = ComponentPropsWithoutRef<
typeof DropdownMenuPrimitive.CheckboxItem
>;
export const DropdownMenuCheckboxItem = forwardRef<HTMLDivElement, DropdownMenuCheckboxItemProps>(
({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
'relative flex cursor-pointer select-none items-center rounded-linear py-2 pl-8 pr-2',
'text-sm text-dark-200 outline-none transition-colors duration-150',
'focus:bg-dark-800/80 focus:text-dark-100',
'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className,
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-4 w-4 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
),
);
DropdownMenuCheckboxItem.displayName = 'DropdownMenuCheckboxItem';
// RadioItem
export type DropdownMenuRadioItemProps = ComponentPropsWithoutRef<
typeof DropdownMenuPrimitive.RadioItem
>;
export const DropdownMenuRadioItem = forwardRef<HTMLDivElement, DropdownMenuRadioItemProps>(
({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
'relative flex cursor-pointer select-none items-center rounded-linear py-2 pl-8 pr-2',
'text-sm text-dark-200 outline-none transition-colors duration-150',
'focus:bg-dark-800/80 focus:text-dark-100',
'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className,
)}
{...props}
>
<span className="absolute left-2 flex h-4 w-4 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<DotIcon />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
),
);
DropdownMenuRadioItem.displayName = 'DropdownMenuRadioItem';
// Label
export interface DropdownMenuLabelProps extends ComponentPropsWithoutRef<
typeof DropdownMenuPrimitive.Label
> {
inset?: boolean;
}
export const DropdownMenuLabel = forwardRef<HTMLDivElement, DropdownMenuLabelProps>(
({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn('px-2 py-1.5 text-xs font-medium text-dark-400', inset && 'pl-8', className)}
{...props}
/>
),
);
DropdownMenuLabel.displayName = 'DropdownMenuLabel';
// Separator
export type DropdownMenuSeparatorProps = ComponentPropsWithoutRef<
typeof DropdownMenuPrimitive.Separator
>;
export const DropdownMenuSeparator = forwardRef<HTMLDivElement, DropdownMenuSeparatorProps>(
({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn('-mx-1 my-1 h-px bg-dark-700/50', className)}
{...props}
/>
),
);
DropdownMenuSeparator.displayName = 'DropdownMenuSeparator';
// Shortcut
export type DropdownMenuShortcutProps = React.HTMLAttributes<HTMLSpanElement>;
export const DropdownMenuShortcut = ({ className, ...props }: DropdownMenuShortcutProps) => (
<span className={cn('ml-auto text-xs tracking-widest text-dark-400', className)} {...props} />
);
DropdownMenuShortcut.displayName = 'DropdownMenuShortcut';

View File

@@ -0,0 +1,26 @@
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
type DropdownMenuContentProps,
type DropdownMenuItemProps,
type DropdownMenuCheckboxItemProps,
type DropdownMenuRadioItemProps,
type DropdownMenuLabelProps,
type DropdownMenuSeparatorProps,
type DropdownMenuShortcutProps,
type DropdownMenuSubTriggerProps,
type DropdownMenuSubContentProps,
} from './DropdownMenu';

View File

@@ -0,0 +1,96 @@
import * as PopoverPrimitive from '@radix-ui/react-popover';
import { motion } from 'framer-motion';
import { forwardRef, type ComponentPropsWithoutRef } from 'react';
import { cn } from '@/lib/utils';
import { dropdown, dropdownTransition } from '../../motion/transitions';
// Close icon
const CloseIcon = () => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path
d="M12 4L4 12M4 4l8 8"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
// Root
export const Popover = PopoverPrimitive.Root;
// Trigger
export const PopoverTrigger = PopoverPrimitive.Trigger;
// Anchor
export const PopoverAnchor = PopoverPrimitive.Anchor;
// Close
export const PopoverClose = PopoverPrimitive.Close;
// Content
export interface PopoverContentProps extends ComponentPropsWithoutRef<
typeof PopoverPrimitive.Content
> {
showCloseButton?: boolean;
}
export const PopoverContent = forwardRef<HTMLDivElement, PopoverContentProps>(
(
{ className, children, align = 'center', sideOffset = 4, showCloseButton = false, ...props },
ref,
) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
'z-50 w-72 overflow-hidden',
'rounded-linear-lg border border-dark-700/50 bg-dark-900/95 backdrop-blur-linear',
'p-4 text-dark-100 shadow-linear-lg outline-none',
className,
)}
asChild
{...props}
>
<motion.div
variants={dropdown}
initial="initial"
animate="animate"
exit="exit"
transition={dropdownTransition}
>
{children}
{showCloseButton && (
<PopoverPrimitive.Close
className={cn(
'absolute right-2 top-2 rounded-linear p-1.5',
'text-dark-400 opacity-70 transition-all',
'hover:bg-dark-800/80 hover:opacity-100',
'focus:outline-none focus:ring-2 focus:ring-accent-500/50',
)}
>
<CloseIcon />
<span className="sr-only">Close</span>
</PopoverPrimitive.Close>
)}
</motion.div>
</PopoverPrimitive.Content>
</PopoverPrimitive.Portal>
),
);
PopoverContent.displayName = 'PopoverContent';
// Arrow
export type PopoverArrowProps = ComponentPropsWithoutRef<typeof PopoverPrimitive.Arrow>;
export const PopoverArrow = forwardRef<SVGSVGElement, PopoverArrowProps>(
({ className, ...props }, ref) => (
<PopoverPrimitive.Arrow ref={ref} className={cn('fill-dark-800', className)} {...props} />
),
);
PopoverArrow.displayName = 'PopoverArrow';

View File

@@ -0,0 +1,10 @@
export {
Popover,
PopoverTrigger,
PopoverAnchor,
PopoverClose,
PopoverContent,
PopoverArrow,
type PopoverContentProps,
type PopoverArrowProps,
} from './Popover';

View File

@@ -0,0 +1,183 @@
import * as SelectPrimitive from '@radix-ui/react-select';
import { motion } from 'framer-motion';
import { forwardRef, type ComponentPropsWithoutRef, type ReactNode } from 'react';
import { cn } from '@/lib/utils';
import { usePlatform } from '@/platform';
import { dropdown, dropdownTransition } from '../../motion/transitions';
// Icons
const ChevronDownIcon = () => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" className="text-dark-400">
<path
d="M4 6L8 10L12 6"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
const CheckIcon = () => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" className="text-accent-400">
<path
d="M3.5 8.5L6.5 11.5L12.5 4.5"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
// Root
export const Select = SelectPrimitive.Root;
// Trigger
export interface SelectTriggerProps extends ComponentPropsWithoutRef<
typeof SelectPrimitive.Trigger
> {
placeholder?: string;
}
export const SelectTrigger = forwardRef<HTMLButtonElement, SelectTriggerProps>(
({ className, placeholder, ...props }, ref) => {
const { haptic } = usePlatform();
return (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
'flex h-10 w-full items-center justify-between gap-2 rounded-linear px-3',
'border border-dark-700/50 bg-dark-800/80',
'text-sm text-dark-100 placeholder:text-dark-400',
'hover:border-dark-600/50 hover:bg-dark-700/80',
'focus:outline-none focus:ring-2 focus:ring-accent-500/50 focus:ring-offset-2 focus:ring-offset-dark-950',
'disabled:cursor-not-allowed disabled:opacity-50',
'transition-all duration-200',
'[&>span]:line-clamp-1',
className,
)}
onClick={() => haptic.impact('light')}
{...props}
>
<SelectPrimitive.Value placeholder={placeholder} />
<SelectPrimitive.Icon>
<ChevronDownIcon />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
);
},
);
SelectTrigger.displayName = 'SelectTrigger';
// Content
export type SelectContentProps = ComponentPropsWithoutRef<typeof SelectPrimitive.Content>;
export const SelectContent = forwardRef<HTMLDivElement, SelectContentProps>(
({ className, children, position = 'popper', ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
'relative z-50 max-h-80 min-w-[8rem] overflow-hidden',
'rounded-linear-lg border border-dark-700/50 bg-dark-900/95 backdrop-blur-linear',
'text-dark-100 shadow-linear-lg',
position === 'popper' &&
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
className,
)}
position={position}
asChild
{...props}
>
<motion.div
variants={dropdown}
initial="initial"
animate="animate"
exit="exit"
transition={dropdownTransition}
>
<SelectPrimitive.Viewport
className={cn(
'p-1',
position === 'popper' &&
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]',
)}
>
{children}
</SelectPrimitive.Viewport>
</motion.div>
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
),
);
SelectContent.displayName = 'SelectContent';
// Item
export interface SelectItemProps extends ComponentPropsWithoutRef<typeof SelectPrimitive.Item> {
children: ReactNode;
}
export const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(
({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
'relative flex w-full cursor-pointer select-none items-center rounded-linear py-2 pl-3 pr-8',
'text-sm text-dark-200 outline-none',
'hover:bg-dark-800/80 hover:text-dark-100',
'focus:bg-dark-800/80 focus:text-dark-100',
'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
'data-[state=checked]:text-accent-400',
'transition-colors duration-150',
className,
)}
{...props}
>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
<span className="absolute right-2 flex h-4 w-4 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon />
</SelectPrimitive.ItemIndicator>
</span>
</SelectPrimitive.Item>
),
);
SelectItem.displayName = 'SelectItem';
// Group
export const SelectGroup = SelectPrimitive.Group;
// Label
export type SelectLabelProps = ComponentPropsWithoutRef<typeof SelectPrimitive.Label>;
export const SelectLabel = forwardRef<HTMLDivElement, SelectLabelProps>(
({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn('px-3 py-1.5 text-xs font-medium text-dark-400', className)}
{...props}
/>
),
);
SelectLabel.displayName = 'SelectLabel';
// Separator
export type SelectSeparatorProps = ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>;
export const SelectSeparator = forwardRef<HTMLDivElement, SelectSeparatorProps>(
({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn('-mx-1 my-1 h-px bg-dark-700/50', className)}
{...props}
/>
),
);
SelectSeparator.displayName = 'SelectSeparator';

View File

@@ -0,0 +1,14 @@
export {
Select,
SelectTrigger,
SelectContent,
SelectItem,
SelectGroup,
SelectLabel,
SelectSeparator,
type SelectTriggerProps,
type SelectContentProps,
type SelectItemProps,
type SelectLabelProps,
type SelectSeparatorProps,
} from './Select';

View File

@@ -0,0 +1,284 @@
import * as DialogPrimitive from '@radix-ui/react-dialog';
import { motion, AnimatePresence, useDragControls, PanInfo } from 'framer-motion';
import {
forwardRef,
type ComponentPropsWithoutRef,
createContext,
useContext,
useState,
useCallback,
} from 'react';
import { cn } from '@/lib/utils';
import { usePlatform } from '@/platform';
import { useBackButton } from '@/platform';
import {
backdrop,
backdropTransition,
sheetSlideUp,
sheetTransition,
} from '../../motion/transitions';
// Close icon
const CloseIcon = () => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path
d="M12 4L4 12M4 4l8 8"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
// Context for AnimatePresence and control
interface SheetContextValue {
open: boolean;
onClose: () => void;
}
const SheetContext = createContext<SheetContextValue>({
open: false,
onClose: () => {},
});
// Root
export interface SheetProps extends ComponentPropsWithoutRef<typeof DialogPrimitive.Root> {
onClose?: () => void;
}
export const Sheet = ({ children, open, onOpenChange, onClose, ...props }: SheetProps) => {
const [internalOpen, setInternalOpen] = useState(false);
const isOpen = open !== undefined ? open : internalOpen;
const handleOpenChange = useCallback(
(newOpen: boolean) => {
if (onOpenChange) {
onOpenChange(newOpen);
} else {
setInternalOpen(newOpen);
}
if (!newOpen && onClose) {
onClose();
}
},
[onOpenChange, onClose],
);
const handleClose = useCallback(() => {
handleOpenChange(false);
}, [handleOpenChange]);
return (
<DialogPrimitive.Root open={isOpen} onOpenChange={handleOpenChange} {...props}>
<SheetContext.Provider value={{ open: isOpen, onClose: handleClose }}>
{children}
</SheetContext.Provider>
</DialogPrimitive.Root>
);
};
// Trigger
export const SheetTrigger = DialogPrimitive.Trigger;
// Portal
export const SheetPortal = DialogPrimitive.Portal;
// Close
export const SheetClose = DialogPrimitive.Close;
// Overlay
export type SheetOverlayProps = ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>;
export const SheetOverlay = forwardRef<HTMLDivElement, SheetOverlayProps>(
({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn('fixed inset-0 z-50 bg-black/60 backdrop-blur-sm', className)}
asChild
{...props}
>
<motion.div
variants={backdrop}
initial="initial"
animate="animate"
exit="exit"
transition={backdropTransition}
/>
</DialogPrimitive.Overlay>
),
);
SheetOverlay.displayName = 'SheetOverlay';
// Content
export interface SheetContentProps extends ComponentPropsWithoutRef<
typeof DialogPrimitive.Content
> {
showDragHandle?: boolean;
showCloseButton?: boolean;
enableDragToClose?: boolean;
closeThreshold?: number;
}
export const SheetContent = forwardRef<HTMLDivElement, SheetContentProps>(
(
{
className,
children,
showDragHandle = true,
showCloseButton = false,
enableDragToClose = true,
closeThreshold = 0.3,
...props
},
ref,
) => {
const { open, onClose } = useContext(SheetContext);
const { haptic } = usePlatform();
const dragControls = useDragControls();
// Back button integration
useBackButton(open ? onClose : null);
const handleDragEnd = useCallback(
(_: MouseEvent | TouchEvent | PointerEvent, info: PanInfo) => {
const velocity = info.velocity.y;
const offset = info.offset.y;
const height = window.innerHeight;
// Close if dragged down fast enough or past threshold
if (velocity > 500 || offset > height * closeThreshold) {
haptic.impact('light');
onClose();
}
},
[closeThreshold, haptic, onClose],
);
return (
<SheetPortal forceMount>
<AnimatePresence mode="wait">
{open && (
<>
<SheetOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
'fixed inset-x-0 bottom-0 z-50',
'flex flex-col',
'max-h-[85vh]',
'rounded-t-2xl border-t border-dark-700/50 bg-dark-900/95 backdrop-blur-linear',
'shadow-linear-lg',
'focus:outline-none',
'pb-[env(safe-area-inset-bottom,0px)]',
className,
)}
asChild
{...props}
>
<motion.div
variants={sheetSlideUp}
initial="initial"
animate="animate"
exit="exit"
transition={sheetTransition}
drag={enableDragToClose ? 'y' : false}
dragControls={dragControls}
dragConstraints={{ top: 0, bottom: 0 }}
dragElastic={{ top: 0, bottom: 0.6 }}
onDragEnd={handleDragEnd}
>
{/* Drag handle */}
{showDragHandle && (
<div
className="flex cursor-grab justify-center pb-2 pt-3 active:cursor-grabbing"
onPointerDown={(e) => dragControls.start(e)}
>
<div className="h-1 w-10 rounded-full bg-dark-600" />
</div>
)}
{/* Close button */}
{showCloseButton && (
<DialogPrimitive.Close
className={cn(
'absolute right-4 top-4 rounded-linear p-1.5',
'text-dark-400 opacity-70 transition-all',
'hover:bg-dark-800/80 hover:opacity-100',
'focus:outline-none focus:ring-2 focus:ring-accent-500/50',
)}
>
<CloseIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
{/* Content */}
<div className="flex-1 overflow-y-auto px-4 pb-4">{children}</div>
</motion.div>
</DialogPrimitive.Content>
</>
)}
</AnimatePresence>
</SheetPortal>
);
},
);
SheetContent.displayName = 'SheetContent';
// Header
export type SheetHeaderProps = React.HTMLAttributes<HTMLDivElement>;
export const SheetHeader = ({ className, ...props }: SheetHeaderProps) => (
<div
className={cn('flex flex-col space-y-1.5 px-2 pt-2 text-center sm:text-left', className)}
{...props}
/>
);
SheetHeader.displayName = 'SheetHeader';
// Footer
export type SheetFooterProps = React.HTMLAttributes<HTMLDivElement>;
export const SheetFooter = ({ className, ...props }: SheetFooterProps) => (
<div
className={cn('flex flex-col gap-2 px-2 pt-4 sm:flex-row sm:justify-end', className)}
{...props}
/>
);
SheetFooter.displayName = 'SheetFooter';
// Title
export type SheetTitleProps = ComponentPropsWithoutRef<typeof DialogPrimitive.Title>;
export const SheetTitle = forwardRef<HTMLHeadingElement, SheetTitleProps>(
({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn('text-lg font-semibold text-dark-100', className)}
{...props}
/>
),
);
SheetTitle.displayName = 'SheetTitle';
// Description
export type SheetDescriptionProps = ComponentPropsWithoutRef<typeof DialogPrimitive.Description>;
export const SheetDescription = forwardRef<HTMLParagraphElement, SheetDescriptionProps>(
({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn('text-sm text-dark-400', className)}
{...props}
/>
),
);
SheetDescription.displayName = 'SheetDescription';

View File

@@ -0,0 +1,19 @@
export {
Sheet,
SheetTrigger,
SheetPortal,
SheetClose,
SheetOverlay,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
type SheetProps,
type SheetOverlayProps,
type SheetContentProps,
type SheetHeaderProps,
type SheetFooterProps,
type SheetTitleProps,
type SheetDescriptionProps,
} from './Sheet';

View File

@@ -0,0 +1,73 @@
import * as SwitchPrimitive from '@radix-ui/react-switch';
import { motion } from 'framer-motion';
import { forwardRef, type ComponentPropsWithoutRef } from 'react';
import { cn } from '@/lib/utils';
import { usePlatform } from '@/platform';
import { springTransition } from '../../motion/transitions';
export interface SwitchProps extends Omit<
ComponentPropsWithoutRef<typeof SwitchPrimitive.Root>,
'onChange'
> {
label?: string;
description?: string;
onChange?: (checked: boolean) => void;
haptic?: boolean;
}
export const Switch = forwardRef<HTMLButtonElement, SwitchProps>(
({ className, label, description, onChange, onCheckedChange, haptic = true, ...props }, ref) => {
const { haptic: platformHaptic } = usePlatform();
const handleCheckedChange = (checked: boolean) => {
if (haptic) {
platformHaptic.impact('light');
}
onCheckedChange?.(checked);
onChange?.(checked);
};
const switchElement = (
<SwitchPrimitive.Root
ref={ref}
className={cn(
'peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full',
'border-2 border-transparent transition-colors duration-200',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-500/50 focus-visible:ring-offset-2 focus-visible:ring-offset-dark-950',
'disabled:cursor-not-allowed disabled:opacity-50',
'data-[state=checked]:bg-accent-500 data-[state=unchecked]:bg-dark-700',
className,
)}
onCheckedChange={handleCheckedChange}
{...props}
>
<SwitchPrimitive.Thumb asChild>
<motion.span
className={cn(
'pointer-events-none block h-5 w-5 rounded-full bg-white shadow-lg ring-0',
'data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0',
)}
layout
transition={springTransition}
/>
</SwitchPrimitive.Thumb>
</SwitchPrimitive.Root>
);
if (!label) {
return switchElement;
}
return (
<label className="flex cursor-pointer items-center justify-between gap-4">
<div className="min-w-0 flex-1">
<span className="block text-sm font-medium text-dark-100">{label}</span>
{description && <span className="mt-0.5 block text-sm text-dark-400">{description}</span>}
</div>
{switchElement}
</label>
);
},
);
Switch.displayName = 'Switch';

View File

@@ -0,0 +1 @@
export { Switch, type SwitchProps } from './Switch';

View File

@@ -0,0 +1,75 @@
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
import { motion } from 'framer-motion';
import { forwardRef, type ComponentPropsWithoutRef, type ReactNode } from 'react';
import { cn } from '@/lib/utils';
import { tooltip, tooltipTransition } from '../../motion/transitions';
// Provider - wrap your app with this
export const TooltipProvider = TooltipPrimitive.Provider;
// Root
export const Tooltip = TooltipPrimitive.Root;
// Trigger
export const TooltipTrigger = TooltipPrimitive.Trigger;
// Content
export type TooltipContentProps = ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>;
export const TooltipContent = forwardRef<HTMLDivElement, TooltipContentProps>(
({ className, sideOffset = 4, children, ...props }, ref) => (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
'z-50 overflow-hidden',
'rounded-linear px-3 py-1.5',
'border border-dark-700/50 bg-dark-800',
'text-xs text-dark-100 shadow-linear',
className,
)}
asChild
{...props}
>
<motion.div
variants={tooltip}
initial="initial"
animate="animate"
exit="exit"
transition={tooltipTransition}
>
{children}
</motion.div>
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
),
);
TooltipContent.displayName = 'TooltipContent';
// Convenience wrapper component
export interface SimpleTooltipProps {
content: ReactNode;
children: ReactNode;
side?: 'top' | 'right' | 'bottom' | 'left';
align?: 'start' | 'center' | 'end';
delayDuration?: number;
className?: string;
}
export const SimpleTooltip = ({
content,
children,
side = 'top',
align = 'center',
delayDuration = 200,
className,
}: SimpleTooltipProps) => (
<Tooltip delayDuration={delayDuration}>
<TooltipTrigger asChild>{children}</TooltipTrigger>
<TooltipContent side={side} align={align} className={className}>
{content}
</TooltipContent>
</Tooltip>
);

View File

@@ -0,0 +1,9 @@
export {
TooltipProvider,
Tooltip,
TooltipTrigger,
TooltipContent,
SimpleTooltip,
type TooltipContentProps,
type SimpleTooltipProps,
} from './Tooltip';

View File

@@ -0,0 +1,10 @@
// Re-export all primitives from a single entry point
export * from './Button';
export * from './Command';
export * from './Dialog';
export * from './DropdownMenu';
export * from './Popover';
export * from './Select';
export * from './Sheet';
export * from './Switch';
export * from './Tooltip';