mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
Phase 0: Remove ~920 lines of dead code (ThemeBentoPicker, PromoDiscountBadge, AdminLayout, SettingsSidebar, MovingGradient, EmptyState, miniapp API, unused types/functions/transitions/skeleton helpers). Fix API barrel file (add 20 missing exports). Phase 1: Add ErrorBoundary (app/page/widget levels), centralize constants, add axios timeout, fix staleTime:0 in React Query. Phase 2: Consolidate hexToHsl, extract email validation, fix duplicate code. Phase 3: Fix auth race condition (await checkAdminStatus), memoize useCurrency, add WebSocket message validation. Phase 4: Extract useBranding, useFeatureFlags, useScrollRestoration from AppShell. Phase 5: Remove all eslint-disable react-hooks/exhaustive-deps (14 total), simplify logger, remove deprecated hooks (useBackButton, useTelegramDnd, useTelegramWebApp). Dependencies: React 19, react-router 7, zustand 5, i18next 25, react-i18next 16, eslint-plugin-react-refresh 0.5. Remove unused @lottiefiles/dotlottie-react. Convert vite manualChunks to function-based approach for react-router v7 compat. Lint: Fix logger.ts no-unused-expressions, fix react-refresh/only-export-components in 6 Radix primitive files (const re-exports → direct re-exports), fix WebSocketProvider exhaustive-deps. Result: 0 errors, 0 warnings. Add CLAUDE.md to .gitignore.
286 lines
8.4 KiB
TypeScript
286 lines
8.4 KiB
TypeScript
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';
|
|
|
|
export {
|
|
Root as DropdownMenu,
|
|
Trigger as DropdownMenuTrigger,
|
|
Group as DropdownMenuGroup,
|
|
Portal as DropdownMenuPortal,
|
|
Sub as DropdownMenuSub,
|
|
RadioGroup as DropdownMenuRadioGroup,
|
|
} from '@radix-ui/react-dropdown-menu';
|
|
|
|
// 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>
|
|
);
|
|
|
|
// 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';
|