ActionDropdown.svelte 2.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
  1. <script lang="ts">
  2. import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
  3. import * as Tooltip from '$lib/components/ui/tooltip';
  4. import { KeyboardShortcutInfo } from '$lib/components/app';
  5. import { TOOLTIP_DELAY_DURATION } from '$lib/constants/tooltip-config';
  6. import type { Component } from 'svelte';
  7. interface ActionItem {
  8. icon: Component;
  9. label: string;
  10. onclick: (event: Event) => void;
  11. variant?: 'default' | 'destructive';
  12. disabled?: boolean;
  13. shortcut?: string[];
  14. separator?: boolean;
  15. }
  16. interface Props {
  17. triggerIcon: Component;
  18. triggerTooltip?: string;
  19. triggerClass?: string;
  20. actions: ActionItem[];
  21. align?: 'start' | 'center' | 'end';
  22. open?: boolean;
  23. }
  24. let {
  25. triggerIcon,
  26. triggerTooltip,
  27. triggerClass = '',
  28. actions,
  29. align = 'end',
  30. open = $bindable(false)
  31. }: Props = $props();
  32. </script>
  33. <DropdownMenu.Root bind:open>
  34. <DropdownMenu.Trigger
  35. class="flex h-6 w-6 cursor-pointer items-center justify-center rounded-md p-0 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[state=open]:bg-accent data-[state=open]:text-accent-foreground {triggerClass}"
  36. onclick={(e) => e.stopPropagation()}
  37. >
  38. {#if triggerTooltip}
  39. <Tooltip.Root delayDuration={TOOLTIP_DELAY_DURATION}>
  40. <Tooltip.Trigger>
  41. {@render iconComponent(triggerIcon, 'h-3 w-3')}
  42. <span class="sr-only">{triggerTooltip}</span>
  43. </Tooltip.Trigger>
  44. <Tooltip.Content>
  45. <p>{triggerTooltip}</p>
  46. </Tooltip.Content>
  47. </Tooltip.Root>
  48. {:else}
  49. {@render iconComponent(triggerIcon, 'h-3 w-3')}
  50. {/if}
  51. </DropdownMenu.Trigger>
  52. <DropdownMenu.Content {align} class="z-[999999] w-48">
  53. {#each actions as action, index (action.label)}
  54. {#if action.separator && index > 0}
  55. <DropdownMenu.Separator />
  56. {/if}
  57. <DropdownMenu.Item
  58. onclick={action.onclick}
  59. variant={action.variant}
  60. disabled={action.disabled}
  61. class="flex items-center justify-between hover:[&>kbd]:opacity-100"
  62. >
  63. <div class="flex items-center gap-2">
  64. {@render iconComponent(
  65. action.icon,
  66. `h-4 w-4 ${action.variant === 'destructive' ? 'text-destructive' : ''}`
  67. )}
  68. {action.label}
  69. </div>
  70. {#if action.shortcut}
  71. <KeyboardShortcutInfo keys={action.shortcut} variant={action.variant} />
  72. {/if}
  73. </DropdownMenu.Item>
  74. {/each}
  75. </DropdownMenu.Content>
  76. </DropdownMenu.Root>
  77. {#snippet iconComponent(IconComponent: Component, className: string)}
  78. <IconComponent class={className} />
  79. {/snippet}