first commit
This commit is contained in:
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';
|
||||
Reference in new issue
Block a user