mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
Один инструмент вместо девяти dev-зависимостей. Замеры на этом репо (483 файла, медиана из 3 прогонов, один и тот же компьютер): - eslint . ~20.4s -> biome lint . ~2s - prettier --check . ~8.8s -> biome format . ~0.2s - суммарно линт+формат ~29s -> biome check . ~2-3s (~10x) Паритет с прежними правилами: - форматирование бинарно совместимо с Prettier-конфигом (biome format --write тронул 9 файлов из ~480 — микроразличия); - запрет window.confirm/alert/open/prompt и navigator.clipboard (Telegram WebView) перенесён GritQL-плагином biome-plugins/telegram-webview-guards.grit c теми же сообщениями и исключениями для src/platform/** и clipboard.ts; плагин сразу нашёл window.open в Wheel.tsx под старым eslint-disable — переведён на biome-ignore; - react-hooks-правила: useExhaustiveDependencies/useHookAtTopLevel; - новые для проекта правила (a11y и пр.) не включались или понижены до warning, чтобы миграция не смешивалась с чисткой кода — включать можно отдельными PR. Ограничения (задокументированы): сортировка tailwind-классов (prettier-plugin-tailwindcss) в Biome пока nursery — не включена; CSS исключён из Biome (парсер спотыкается о Tailwind-синтаксис globals.css) — файл один и правится редко. CI не меняется: имена npm-скриптов lint/format:check сохранены. lint-staged переведён на biome check --write.
175 lines
4.4 KiB
TypeScript
175 lines
4.4 KiB
TypeScript
import { forwardRef, type HTMLAttributes, type ReactNode } from 'react';
|
|
import { motion, type HTMLMotionProps } from 'framer-motion';
|
|
import { Slot } from '@radix-ui/react-slot';
|
|
import { cva, type VariantProps } from 'class-variance-authority';
|
|
import { cn } from '@/lib/utils';
|
|
import { usePlatform } from '@/platform';
|
|
import { buttonTap, buttonHover, springTransition } from '@/components/motion/transitions';
|
|
|
|
const cardVariants = cva(
|
|
[
|
|
'relative overflow-hidden',
|
|
'border border-dark-700/40 bg-dark-900/70',
|
|
'rounded-[var(--bento-radius)]',
|
|
'transition-[border-color,background-color,box-shadow,transform,opacity] duration-200',
|
|
// Glass border inset
|
|
'shadow-[inset_0_1px_0_0_rgba(255,255,255,0.05)]',
|
|
],
|
|
{
|
|
variants: {
|
|
size: {
|
|
sm: 'p-3',
|
|
md: 'p-4',
|
|
lg: 'p-5 sm:p-6',
|
|
xl: 'p-6 sm:p-8',
|
|
},
|
|
variant: {
|
|
default: '',
|
|
glass: 'backdrop-blur-linear bg-dark-900/50',
|
|
solid: 'bg-dark-900',
|
|
outline: 'bg-transparent',
|
|
},
|
|
interactive: {
|
|
true: [
|
|
'cursor-pointer',
|
|
'hover:border-dark-600/50 hover:bg-dark-800/60',
|
|
'active:scale-[0.98]',
|
|
],
|
|
false: '',
|
|
},
|
|
glow: {
|
|
true: 'hover:border-accent-500/30 hover:shadow-glow',
|
|
false: '',
|
|
},
|
|
},
|
|
defaultVariants: {
|
|
size: 'lg',
|
|
variant: 'default',
|
|
interactive: false,
|
|
glow: false,
|
|
},
|
|
},
|
|
);
|
|
|
|
export interface CardProps
|
|
extends Omit<HTMLMotionProps<'div'>, 'children'>,
|
|
VariantProps<typeof cardVariants> {
|
|
children: ReactNode;
|
|
asChild?: boolean;
|
|
haptic?: boolean;
|
|
}
|
|
|
|
export const Card = forwardRef<HTMLDivElement, CardProps>(
|
|
(
|
|
{
|
|
children,
|
|
className,
|
|
size,
|
|
variant,
|
|
interactive,
|
|
glow,
|
|
asChild = false,
|
|
haptic: enableHaptic = true,
|
|
onClick,
|
|
...props
|
|
},
|
|
ref,
|
|
) => {
|
|
const { haptic } = usePlatform();
|
|
|
|
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
if (interactive && enableHaptic) {
|
|
haptic.impact('light');
|
|
}
|
|
onClick?.(e);
|
|
};
|
|
|
|
const classes = cn(cardVariants({ size, variant, interactive, glow }), className);
|
|
|
|
if (asChild) {
|
|
return (
|
|
<Slot ref={ref} className={classes}>
|
|
{children}
|
|
</Slot>
|
|
);
|
|
}
|
|
|
|
if (interactive) {
|
|
return (
|
|
<motion.div
|
|
ref={ref}
|
|
className={classes}
|
|
onClick={handleClick}
|
|
whileHover={buttonHover}
|
|
whileTap={buttonTap}
|
|
transition={springTransition}
|
|
{...props}
|
|
>
|
|
{children}
|
|
</motion.div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<motion.div ref={ref} className={classes} {...props}>
|
|
{children}
|
|
</motion.div>
|
|
);
|
|
},
|
|
);
|
|
|
|
Card.displayName = 'Card';
|
|
|
|
// Card Header
|
|
export type CardHeaderProps = HTMLAttributes<HTMLDivElement>;
|
|
|
|
export const CardHeader = forwardRef<HTMLDivElement, CardHeaderProps>(
|
|
({ className, ...props }, ref) => (
|
|
<div ref={ref} className={cn('flex flex-col space-y-1.5', className)} {...props} />
|
|
),
|
|
);
|
|
|
|
CardHeader.displayName = 'CardHeader';
|
|
|
|
// Card Title
|
|
export type CardTitleProps = HTMLAttributes<HTMLHeadingElement>;
|
|
|
|
export const CardTitle = forwardRef<HTMLHeadingElement, CardTitleProps>(
|
|
({ className, ...props }, ref) => (
|
|
<h3 ref={ref} className={cn('text-lg font-semibold text-dark-100', className)} {...props} />
|
|
),
|
|
);
|
|
|
|
CardTitle.displayName = 'CardTitle';
|
|
|
|
// Card Description
|
|
export type CardDescriptionProps = HTMLAttributes<HTMLParagraphElement>;
|
|
|
|
export const CardDescription = forwardRef<HTMLParagraphElement, CardDescriptionProps>(
|
|
({ className, ...props }, ref) => (
|
|
<p ref={ref} className={cn('text-sm text-dark-400', className)} {...props} />
|
|
),
|
|
);
|
|
|
|
CardDescription.displayName = 'CardDescription';
|
|
|
|
// Card Content
|
|
export type CardContentProps = HTMLAttributes<HTMLDivElement>;
|
|
|
|
export const CardContent = forwardRef<HTMLDivElement, CardContentProps>(
|
|
({ className, ...props }, ref) => <div ref={ref} className={cn('pt-4', className)} {...props} />,
|
|
);
|
|
|
|
CardContent.displayName = 'CardContent';
|
|
|
|
// Card Footer
|
|
export type CardFooterProps = HTMLAttributes<HTMLDivElement>;
|
|
|
|
export const CardFooter = forwardRef<HTMLDivElement, CardFooterProps>(
|
|
({ className, ...props }, ref) => (
|
|
<div ref={ref} className={cn('flex items-center pt-4', className)} {...props} />
|
|
),
|
|
);
|
|
|
|
CardFooter.displayName = 'CardFooter';
|