mirror of
https://github.com/abusoww/tuxmate.git
synced 2026-04-17 19:53:11 +02:00
132 lines
5.7 KiB
TypeScript
132 lines
5.7 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect, useRef } from 'react';
|
|
import { createPortal } from 'react-dom';
|
|
import { Check, ChevronDown } from 'lucide-react';
|
|
import { distros, type DistroId } from '@/lib/data';
|
|
import { DistroIcon } from './DistroIcon';
|
|
|
|
/**
|
|
* Distro picker dropdown. Uses portal rendering so the dropdown isn't
|
|
* clipped by parent overflow. Learned that lesson the hard way.
|
|
*/
|
|
export function DistroSelector({
|
|
selectedDistro,
|
|
onSelect
|
|
}: {
|
|
selectedDistro: DistroId;
|
|
onSelect: (id: DistroId) => void;
|
|
}) {
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const [mounted, setMounted] = useState(false);
|
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
|
const [dropdownPos, setDropdownPos] = useState({ top: 0, right: 0 });
|
|
const currentDistro = distros.find(d => d.id === selectedDistro);
|
|
|
|
useEffect(() => {
|
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
setMounted(true);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (isOpen && buttonRef.current) {
|
|
const rect = buttonRef.current.getBoundingClientRect();
|
|
setDropdownPos({
|
|
top: rect.bottom + 8,
|
|
right: window.innerWidth - rect.right,
|
|
});
|
|
}
|
|
}, [isOpen]);
|
|
|
|
const handleOpen = () => {
|
|
setIsOpen(!isOpen);
|
|
};
|
|
|
|
// Portal the dropdown to body so it's not affected by parent styles.
|
|
// The positioning math looks scary but it's just "anchor to button bottom-right".
|
|
const dropdown = isOpen && mounted ? (
|
|
<>
|
|
{/* Backdrop with subtle blur */}
|
|
<div
|
|
onClick={() => setIsOpen(false)}
|
|
className="backdrop-blur-[2px]"
|
|
style={{
|
|
position: 'fixed',
|
|
inset: 0,
|
|
zIndex: 99998,
|
|
background: 'rgba(0,0,0,0.05)',
|
|
}}
|
|
/>
|
|
{/* Dropdown - AccessGuide style: rectangular with left border */}
|
|
<div
|
|
className="distro-dropdown bg-[var(--bg-secondary)] border-l-4 rounded-md"
|
|
style={{
|
|
position: 'fixed',
|
|
top: dropdownPos.top,
|
|
right: dropdownPos.right,
|
|
zIndex: 99999,
|
|
borderLeftColor: currentDistro?.color || 'var(--accent)',
|
|
padding: '8px 0',
|
|
minWidth: '220px',
|
|
boxShadow: '0 8px 32px rgba(0,0,0,0.2)',
|
|
transformOrigin: 'top right',
|
|
animation: 'distroDropdownOpen 0.25s ease-out',
|
|
}}
|
|
>
|
|
{/* Distro List */}
|
|
<div>
|
|
{distros.map((distro, i) => (
|
|
<button
|
|
key={distro.id}
|
|
onClick={() => { onSelect(distro.id); setIsOpen(false); analytics.distroSelected(distro.name); }}
|
|
className={`group w-full flex items-center gap-3 py-3 px-4 cursor-pointer text-left transition-colors duration-100 ${selectedDistro === distro.id
|
|
? 'border-l-2 -ml-[2px] pl-[18px]'
|
|
: 'hover:bg-[var(--bg-hover)]'
|
|
}`}
|
|
style={{
|
|
animation: `distroItemSlide 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) ${i * 0.03}s both`,
|
|
backgroundColor: selectedDistro === distro.id ? `color-mix(in srgb, ${distro.color}, transparent 85%)` : undefined,
|
|
borderColor: selectedDistro === distro.id ? distro.color : undefined
|
|
}}
|
|
>
|
|
<div className="w-6 h-6 flex items-center justify-center">
|
|
<DistroIcon url={distro.iconUrl} name={distro.name} size={22} />
|
|
</div>
|
|
<span className={`flex-1 text-[15px] ${selectedDistro === distro.id
|
|
? 'text-[var(--text-primary)] font-medium'
|
|
: 'text-[var(--text-secondary)]'
|
|
}`}>{distro.name}</span>
|
|
{selectedDistro === distro.id && (
|
|
<Check className="w-5 h-5" style={{ color: distro.color }} strokeWidth={2.5} />
|
|
)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</>
|
|
) : null;
|
|
|
|
return (
|
|
<>
|
|
<button
|
|
ref={buttonRef}
|
|
onClick={handleOpen}
|
|
aria-haspopup="listbox"
|
|
aria-expanded={isOpen}
|
|
aria-label={`Select distribution, current: ${currentDistro?.name}`}
|
|
className={`group flex items-center gap-2.5 h-10 pl-3 pr-3.5 border-l-4 bg-[var(--bg-secondary)] rounded-md transition-all duration-150 ${isOpen ? 'bg-[var(--bg-tertiary)]' : 'hover:bg-[var(--bg-hover)]'}`}
|
|
style={{
|
|
borderColor: currentDistro?.color || 'var(--accent)'
|
|
}}
|
|
>
|
|
<div className="w-6 h-6 flex items-center justify-center">
|
|
<DistroIcon url={currentDistro?.iconUrl || ''} name={currentDistro?.name || ''} size={20} />
|
|
</div>
|
|
<span className="text-[15px] font-medium text-[var(--text-primary)]">{currentDistro?.name}</span>
|
|
<ChevronDown className={`w-4 h-4 text-[var(--text-muted)] transition-transform duration-150 ${isOpen ? 'rotate-180' : ''}`} />
|
|
</button>
|
|
{mounted && typeof document !== 'undefined' && createPortal(dropdown, document.body)}
|
|
</>
|
|
);
|
|
}
|