- Introduced foundational UI components (`Badge`, `LockCard`, `SectionHeader`, `AvatarIcon`, etc.) for flexible layouts and consistent design. - Added migration support with the `MigrationModal` component and backend integration for exporting/importing data between Electron and Tauri. - Extended form components with `TextAreaInput`, `OrderInput`, `ToggleField`, and `ToolbarSelect` for improved input handling. - Updated `ScribeShell` with migration popup logic to prompt users for data migration. - Integrated `AlertStack` for better alert handling and notification management. - Enhanced Rust/Tauri services with migration command implementations. - Added translations and styles for new components.
90 lines
3.5 KiB
TypeScript
90 lines
3.5 KiB
TypeScript
import React, {ReactNode, useEffect, useRef, useState} from "react";
|
|
import {LucideIcon} from "lucide-react";
|
|
|
|
interface DropdownItem {
|
|
label: string;
|
|
icon?: LucideIcon;
|
|
onClick: () => void;
|
|
variant?: 'default' | 'danger';
|
|
}
|
|
|
|
interface DropdownProps {
|
|
trigger: ReactNode;
|
|
items: DropdownItem[];
|
|
align?: 'left' | 'right';
|
|
}
|
|
|
|
export default function Dropdown(
|
|
{
|
|
trigger,
|
|
items,
|
|
align = 'left',
|
|
}: DropdownProps): React.JSX.Element {
|
|
const [isOpen, setIsOpen] = useState<boolean>(false);
|
|
const dropdownRef: React.RefObject<HTMLDivElement | null> = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(function handleClickOutside() {
|
|
function onClickOutside(event: MouseEvent): void {
|
|
if (dropdownRef.current && event.target instanceof Node && !dropdownRef.current.contains(event.target)) {
|
|
setIsOpen(false);
|
|
}
|
|
}
|
|
|
|
document.addEventListener('mousedown', onClickOutside);
|
|
return (): void => document.removeEventListener('mousedown', onClickOutside);
|
|
}, []);
|
|
|
|
function handleItemClick(item: DropdownItem): void {
|
|
item.onClick();
|
|
setIsOpen(false);
|
|
}
|
|
|
|
return (
|
|
<div ref={dropdownRef} className="relative">
|
|
<div onClick={(): void => setIsOpen(!isOpen)}>
|
|
{trigger}
|
|
</div>
|
|
|
|
{isOpen && (
|
|
<div
|
|
className={`
|
|
absolute top-full mt-2 z-50 min-w-48
|
|
bg-tertiary rounded-xl py-2
|
|
border border-secondary backdrop-blur-sm animate-fadeIn
|
|
${align === 'right' ? 'right-0' : 'left-0'}
|
|
`}
|
|
>
|
|
{items.map(function renderDropdownItem(item: DropdownItem, index: number) {
|
|
const isDanger: boolean = item.variant === 'danger';
|
|
const ItemIcon: LucideIcon | undefined = item.icon;
|
|
return (
|
|
<button
|
|
key={index}
|
|
onClick={(): void => handleItemClick(item)}
|
|
className={`
|
|
group w-full flex items-center gap-3 px-4 py-2.5
|
|
transition-all hover:pl-5
|
|
${isDanger
|
|
? 'text-error hover:bg-error/10'
|
|
: 'text-text-primary hover:bg-secondary'
|
|
}
|
|
${index === 0 ? 'rounded-t-xl' : ''}
|
|
${index === items.length - 1 ? 'rounded-b-xl' : ''}
|
|
`}
|
|
>
|
|
{ItemIcon && (
|
|
<ItemIcon
|
|
className={`w-4 h-4 ${isDanger ? 'text-error' : 'text-muted group-hover:text-primary'}`}
|
|
strokeWidth={1.75}
|
|
/>
|
|
)}
|
|
<span className="font-medium text-sm">{item.label}</span>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|