first commit
linter / quality (push) Canceled after 0s
tests / ci (8.3) (push) Canceled after 0s
tests / ci (8.4) (push) Canceled after 0s
tests / ci (8.5) (push) Canceled after 0s

This commit is contained in:
salam committed 2026-10-06 18:53:03 +02:00
commit e687b4437b
418 files changed
+33577

No files matched your search

@@ -0,0 +1,47 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import { onMount, setContext } from 'svelte';
import { cn } from '@/lib/utils';
import { DROPDOWN_MENU_CONTEXT, type DropdownMenuContext } from './context';
let {
open = $bindable(false),
class: className = '',
children,
}: {
open?: boolean;
class?: string;
children?: Snippet;
} = $props();
let menuElement: HTMLDivElement | null = null;
const context: DropdownMenuContext = {
open: () => open,
setOpen: (value: boolean) => {
open = value;
},
};
setContext(DROPDOWN_MENU_CONTEXT, context);
onMount(() => {
const handlePointerDown = (event: PointerEvent) => {
if (!open || !menuElement) return;
const target = event.target as Node | null;
if (target && !menuElement.contains(target)) {
open = false;
}
};
document.addEventListener('pointerdown', handlePointerDown);
return () => {
document.removeEventListener('pointerdown', handlePointerDown);
};
});
</script>
<div class={cn('relative', className)} bind:this={menuElement}>
{@render children?.()}
</div>
@@ -0,0 +1,67 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import { getContext } from 'svelte';
import { cn } from '@/lib/utils';
import { DROPDOWN_MENU_CONTEXT, type DropdownMenuContext } from './context';
let {
align = 'start',
side = 'bottom',
sideOffset = 0,
class: className = '',
children,
}: {
align?: 'start' | 'center' | 'end';
side?: 'top' | 'right' | 'bottom' | 'left';
sideOffset?: number;
class?: string;
children?: Snippet;
} = $props();
const { open, setOpen } = getContext<DropdownMenuContext>(DROPDOWN_MENU_CONTEXT);
const alignClasses: Record<string, string> = {
start: 'left-0',
center: 'left-1/2 -translate-x-1/2',
end: 'right-0',
};
const sideClasses: Record<string, string> = {
bottom: 'top-full',
top: 'bottom-full',
left: 'right-full',
right: 'left-full',
};
const close = () => setOpen(false);
const offsetStyle = () => {
switch (side) {
case 'top':
return `margin-bottom: ${sideOffset}px;`;
case 'left':
return `margin-right: ${sideOffset}px;`;
case 'right':
return `margin-left: ${sideOffset}px;`;
default:
return `margin-top: ${sideOffset}px;`;
}
};
</script>
{#if open()}
<div
class={cn(
'absolute z-50 min-w-48 rounded-md border bg-popover p-2 text-popover-foreground shadow-md',
alignClasses[align] ?? alignClasses.start,
sideClasses[side] ?? sideClasses.bottom,
className,
)}
style={offsetStyle()}
role="menu"
tabindex="-1"
onkeydown={(event) => event.key === 'Escape' && close()}
>
{@render children?.()}
</div>
{/if}
@@ -0,0 +1,3 @@
<div class="flex flex-col">
<slot />
</div>
@@ -0,0 +1,40 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import { getContext } from 'svelte';
import { cn } from '@/lib/utils';
import { DROPDOWN_MENU_CONTEXT, type DropdownMenuContext } from './context';
type AsChildProps = {
class?: string;
onClick?: () => void;
[key: string]: any;
};
let {
asChild = false,
class: className = '',
children,
}: {
asChild?: boolean;
class?: string;
children?: Snippet<[AsChildProps]>;
} = $props();
const { setOpen } = getContext<DropdownMenuContext>(DROPDOWN_MENU_CONTEXT);
const handleClick = () => setOpen(false);
const classes = () =>
cn(
'flex w-full cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground',
className,
);
</script>
{#if asChild}
{@render children?.({ class: classes(), onClick: handleClick })}
{:else}
<button type="button" class={classes()} onclick={handleClick}>
{@render children?.({})}
</button>
{/if}
@@ -0,0 +1,9 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import { cn } from '@/lib/utils';
let { class: className = '', children }: { class?: string; children?: Snippet } = $props();
</script>
<div class={cn('px-2 py-1.5 text-sm font-semibold', className)}>
{@render children?.()}
</div>
@@ -0,0 +1 @@
<div class="my-1 h-px bg-border"></div>
@@ -0,0 +1,29 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import { getContext } from 'svelte';
import { DROPDOWN_MENU_CONTEXT, type DropdownMenuContext } from './context';
type TriggerProps = {
onclick?: (event: MouseEvent) => void;
'aria-expanded'?: boolean;
'data-state'?: 'open' | 'closed';
[key: string]: any;
};
let {
asChild = false,
children,
}: { asChild?: boolean; children?: Snippet<[TriggerProps]> } = $props();
const { open, setOpen } = getContext<DropdownMenuContext>(DROPDOWN_MENU_CONTEXT);
const handleClick = () => setOpen(!open());
</script>
{#if asChild}
{@render children?.({ onclick: handleClick, 'aria-expanded': open(), 'data-state': open() ? 'open' : 'closed' })}
{:else}
<button type="button" onclick={handleClick} aria-expanded={open()}>
{@render children?.({})}
</button>
{/if}
@@ -0,0 +1,6 @@
export type DropdownMenuContext = {
open: () => boolean;
setOpen: (value: boolean) => void;
};
export const DROPDOWN_MENU_CONTEXT = Symbol('dropdown-menu');
@@ -0,0 +1,7 @@
export { default as DropdownMenu } from './DropdownMenu.svelte';
export { default as DropdownMenuContent } from './DropdownMenuContent.svelte';
export { default as DropdownMenuGroup } from './DropdownMenuGroup.svelte';
export { default as DropdownMenuItem } from './DropdownMenuItem.svelte';
export { default as DropdownMenuLabel } from './DropdownMenuLabel.svelte';
export { default as DropdownMenuSeparator } from './DropdownMenuSeparator.svelte';
export { default as DropdownMenuTrigger } from './DropdownMenuTrigger.svelte';