Add foundational components and logic for migration, UI design, and input handling
- 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.
This commit is contained in:
89
components/ui/Dropdown.tsx
Normal file
89
components/ui/Dropdown.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user