mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-30 10:27:49 +00:00
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:
104
src/components/primitives/Button/Button.tsx
Normal file
104
src/components/primitives/Button/Button.tsx
Normal 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 };
|
||||
64
src/components/primitives/Button/Button.variants.ts
Normal file
64
src/components/primitives/Button/Button.variants.ts
Normal 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>;
|
||||
1
src/components/primitives/Button/index.ts
Normal file
1
src/components/primitives/Button/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { Button, type ButtonProps, buttonVariants, type ButtonVariants } from './Button';
|
||||
Reference in New Issue
Block a user