120 lines
3.2 KiB
Svelte
120 lines
3.2 KiB
Svelte
<script lang="ts">
|
|
import { Button } from '$lib/components/ui/button';
|
|
import { scale } from 'svelte/transition';
|
|
import { cubicOut } from 'svelte/easing';
|
|
import type { Snippet } from 'svelte';
|
|
|
|
let {
|
|
items,
|
|
selectedValue,
|
|
onSelect,
|
|
color,
|
|
showCheckmark = true,
|
|
icon,
|
|
ariaLabel
|
|
}: {
|
|
items: Array<{ value: string; label: string }>;
|
|
selectedValue: string;
|
|
onSelect: (value: string) => void;
|
|
color?: string | null;
|
|
showCheckmark?: boolean;
|
|
icon: Snippet;
|
|
ariaLabel: string;
|
|
} = $props();
|
|
|
|
let showMenu = $state(false);
|
|
|
|
const menuClasses = $derived(
|
|
color
|
|
? 'absolute left-0 sm:right-0 sm:left-auto mt-2 w-40 rounded-md border shadow-lg z-50 backdrop-blur-md'
|
|
: 'absolute left-0 sm:right-0 sm:left-auto mt-2 w-40 rounded-md border shadow-lg z-50 backdrop-blur-md border-slate-200 dark:border-slate-800 bg-white/90 dark:bg-slate-950/90'
|
|
);
|
|
|
|
const menuStyle = $derived(
|
|
color
|
|
? `border-color: ${color}; background-color: ${color}20; backdrop-filter: blur(12px);`
|
|
: ''
|
|
);
|
|
|
|
function getItemStyle(itemValue: string): string {
|
|
if (!color) return '';
|
|
return selectedValue === itemValue ? `background-color: ${color}20;` : '';
|
|
}
|
|
|
|
function toggleMenu() {
|
|
showMenu = !showMenu;
|
|
}
|
|
|
|
function handleSelect(value: string) {
|
|
onSelect(value);
|
|
showMenu = false;
|
|
}
|
|
|
|
function handleClickOutside(event: MouseEvent) {
|
|
const target = event.target as HTMLElement;
|
|
if (!target.closest('.dropdown-menu')) {
|
|
showMenu = false;
|
|
}
|
|
}
|
|
|
|
function handleMouseEnter(e: MouseEvent) {
|
|
if (color) {
|
|
(e.currentTarget as HTMLElement).style.backgroundColor = `${color}15`;
|
|
}
|
|
}
|
|
|
|
function handleMouseLeave(e: MouseEvent, itemValue: string) {
|
|
if (color) {
|
|
(e.currentTarget as HTMLElement).style.backgroundColor =
|
|
selectedValue === itemValue ? `${color}20` : 'transparent';
|
|
}
|
|
}
|
|
|
|
$effect(() => {
|
|
if (showMenu) {
|
|
document.addEventListener('click', handleClickOutside);
|
|
return () => document.removeEventListener('click', handleClickOutside);
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<div class="relative dropdown-menu">
|
|
<Button variant="outline" size="icon" onclick={toggleMenu} aria-label={ariaLabel}>
|
|
{@render icon()}
|
|
</Button>
|
|
|
|
{#if showMenu}
|
|
<div
|
|
class={menuClasses}
|
|
style={menuStyle}
|
|
transition:scale={{ duration: 150, start: 0.95, opacity: 0, easing: cubicOut }}
|
|
>
|
|
<div class="py-1">
|
|
{#each items as item}
|
|
<button
|
|
type="button"
|
|
class="w-full text-left px-4 py-2 text-sm transition-colors"
|
|
class:hover:bg-slate-100={!color}
|
|
class:dark:hover:bg-slate-900={!color}
|
|
class:font-bold={selectedValue === item.value}
|
|
class:bg-slate-100={selectedValue === item.value && !color}
|
|
class:dark:bg-slate-900={selectedValue === item.value && !color}
|
|
class:flex={showCheckmark}
|
|
class:items-center={showCheckmark}
|
|
class:justify-between={showCheckmark}
|
|
style={getItemStyle(item.value)}
|
|
onmouseenter={handleMouseEnter}
|
|
onmouseleave={(e) => handleMouseLeave(e, item.value)}
|
|
onclick={() => handleSelect(item.value)}
|
|
>
|
|
<span>{item.label}</span>
|
|
{#if showCheckmark && selectedValue === item.value}
|
|
<span class="ml-2">✓</span>
|
|
{/if}
|
|
</button>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</div>
|