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,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';