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,83 @@
import { forwardRef, type ReactNode } from 'react';
import { motion } from 'framer-motion';
import { cn } from '@/lib/utils';
import { Card, type CardProps } from '../Card';
import { slideUp, slideUpTransition } from '@/components/motion/transitions';
export interface StatCardProps extends Omit<CardProps, 'children'> {
label: string;
value: ReactNode;
icon?: ReactNode;
change?: {
value: number;
label?: string;
};
trend?: 'up' | 'down' | 'neutral';
loading?: boolean;
}
export const StatCard = forwardRef<HTMLDivElement, StatCardProps>(
(
{ label, value, icon, change, trend = 'neutral', loading = false, className, ...props },
ref,
) => {
const trendColors = {
up: 'text-success-400',
down: 'text-error-400',
neutral: 'text-dark-400',
};
const trendIcon = {
up: '↑',
down: '↓',
neutral: '→',
};
return (
<Card ref={ref} className={cn('relative', className)} size="md" {...props}>
<div className="flex items-start justify-between">
<div className="min-w-0 flex-1">
{/* Label */}
<p className="truncate text-sm font-medium text-dark-400">{label}</p>
{/* Value */}
{loading ? (
<div className="mt-2 h-8 w-24 animate-pulse rounded bg-dark-800" />
) : (
<motion.p
className="mt-1 text-2xl font-bold text-dark-100 sm:text-3xl"
variants={slideUp}
initial="initial"
animate="animate"
transition={slideUpTransition}
>
{value}
</motion.p>
)}
{/* Change indicator */}
{change && !loading && (
<div className={cn('mt-2 flex items-center gap-1 text-sm', trendColors[trend])}>
<span>{trendIcon[trend]}</span>
<span>
{change.value > 0 ? '+' : ''}
{change.value}%
</span>
{change.label && <span className="text-dark-500">{change.label}</span>}
</div>
)}
</div>
{/* Icon */}
{icon && (
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-linear bg-dark-800/80 text-dark-400">
{icon}
</div>
)}
</div>
</Card>
);
},
);
StatCard.displayName = 'StatCard';

View File

@@ -0,0 +1 @@
export { StatCard, type StatCardProps } from './StatCard';