|
1 | 1 | <script lang="ts"> |
2 | 2 | import MenuItem from './MenuItem.svelte' |
3 | | - import { flip, shift, offset } from 'svelte-floating-ui/dom' |
4 | | - import { createFloatingActions } from 'svelte-floating-ui' |
5 | 3 | import clsx from 'clsx' |
6 | | - import type { MenuItemProps, DrawerOption, AnyProp } from './types.ts' |
| 4 | + import { cn } from './utils.js' |
| 5 | + import type { MenuItemProps } from './types.ts' |
7 | 6 | import { Icon, type IconSource } from '@steeze-ui/svelte-icon' |
8 | 7 | import { ChevronDown, ChevronRight } from '@invopop/ui-icons' |
9 | | - import { FolderL } from '@invopop/ui-icons' |
10 | 8 | import { resolveIcon } from './helpers.js' |
11 | | - import DrawerContext from './DrawerContext.svelte' |
12 | 9 | import TagBeta from './TagBeta.svelte' |
13 | 10 |
|
14 | | - const [floatingRef, floatingContent] = createFloatingActions({ |
15 | | - strategy: 'absolute', |
16 | | - placement: 'bottom-start', |
17 | | - middleware: [offset(-4), flip(), shift()] |
18 | | - }) |
19 | | -
|
20 | 11 | let { |
21 | 12 | label = '', |
22 | 13 | url = '', |
23 | 14 | isFolderItem = false, |
24 | 15 | collapsable = false, |
25 | 16 | open = $bindable(false), |
26 | 17 | active = false, |
27 | | - collapsedSidebar = false, |
28 | 18 | iconTheme = 'default', |
29 | 19 | icon = undefined, |
| 20 | + imageUrl = undefined, |
30 | 21 | beta = false, |
31 | 22 | children = undefined, |
| 23 | + action, |
| 24 | + ref = $bindable(null), |
| 25 | + class: className, |
32 | 26 | onclick |
33 | 27 | }: MenuItemProps = $props() |
34 | 28 |
|
35 | 29 | let resolvedIcon: IconSource | undefined = $state() |
36 | | - let hovered = $state(false) |
37 | | - let highlight = $state(false) |
38 | | - let leaveHoverTimeout: ReturnType<typeof setTimeout> | null = null |
39 | 30 | let itemStyles = $derived( |
40 | | - clsx( |
41 | | - { 'text-foreground-inverse font-medium': !isFolderItem }, |
42 | | - { 'text-foreground-inverse-secondary': isFolderItem && !active }, |
43 | | - { |
44 | | - 'border border-transparent hover:bg-background-selected-inverse group p-[7px] size-8': |
45 | | - collapsedSidebar |
46 | | - }, |
47 | | - { 'w-full px-2 py-1.5 h-8': !collapsedSidebar }, |
48 | | - { |
49 | | - 'bg-background-selected-inverse text-white': active |
50 | | - }, |
51 | | - { 'hover:bg-background-selected-inverse': !active } |
52 | | - ) |
53 | | - ) |
54 | | - let iconStyles = $derived( |
55 | | - clsx({ 'group-hover:text-white text-icon-inverse-bold!': collapsedSidebar }) |
| 31 | + clsx('flex-1 min-w-0 px-2 py-1.5 h-8', { |
| 32 | + 'text-foreground-inverse font-medium': !isFolderItem, |
| 33 | + 'text-foreground-inverse-secondary': isFolderItem && !active, |
| 34 | + 'bg-background-selected-inverse text-white': active, |
| 35 | + 'hover:bg-background-selected-inverse': !active |
| 36 | + }) |
56 | 37 | ) |
57 | 38 | let wrapperStyles = $derived( |
58 | 39 | clsx({ |
59 | | - 'ml-4 border-l border-white-10 pl-2 pt-0.5 relative': isFolderItem |
| 40 | + 'group/menu-item ml-4 border-l border-white-10 pl-2 pt-0.5 relative': isFolderItem |
60 | 41 | }) |
61 | 42 | ) |
62 | | - let items = $derived([ |
63 | | - { label, value: url, selected: active, icon: resolvedIcon }, |
64 | | - ...(children || []).map((c) => ({ |
65 | | - label: c.label || '', |
66 | | - value: c.url || '', |
67 | | - selected: c.active, |
68 | | - icon: FolderL |
69 | | - })) |
70 | | - ] as DrawerOption[]) |
71 | 43 |
|
72 | 44 | $effect(() => { |
73 | 45 | resolveIcon(icon).then((res) => (resolvedIcon = res)) |
|
80 | 52 |
|
81 | 53 | onclick?.(url) |
82 | 54 | } |
83 | | -
|
84 | | - function handleClickChild(value: AnyProp) { |
85 | | - hovered = false |
86 | | - onclick?.(value as string) |
87 | | - } |
88 | | -
|
89 | | - function handleHover() { |
90 | | - highlight = true |
91 | | - if (leaveHoverTimeout) { |
92 | | - clearTimeout(leaveHoverTimeout) |
93 | | - } |
94 | | - hovered = true |
95 | | - } |
96 | | -
|
97 | | - function handleBlur() { |
98 | | - highlight = false |
99 | | - leaveHoverTimeout = setTimeout(() => { |
100 | | - hovered = false |
101 | | - }, 200) |
102 | | - } |
103 | 55 | </script> |
104 | 56 |
|
105 | | -<div class={wrapperStyles}> |
106 | | - {#if isFolderItem && (highlight || active)} |
107 | | - <div class="border-l border-white h-3 w-px absolute top-3.5 left-0 -m-px"></div> |
| 57 | +<div bind:this={ref} class={cn(wrapperStyles, className)} data-menu-item-root> |
| 58 | + {#if isFolderItem} |
| 59 | + <div |
| 60 | + class={clsx('border-l border-white h-3 w-px absolute top-3.5 left-0 -m-px', { |
| 61 | + 'opacity-0 group-hover/menu-item:opacity-100': !active |
| 62 | + })} |
| 63 | + data-menu-item-tree-indicator |
| 64 | + ></div> |
108 | 65 | {/if} |
109 | | - <button |
110 | | - use:floatingRef |
111 | | - onmouseenter={handleHover} |
112 | | - onmouseleave={handleBlur} |
113 | | - onclick={handleClick} |
114 | | - title={label} |
115 | | - class="cursor-pointer {itemStyles} text-base border border-transparent flex items-center justify-between hover:text-white focus:text-white rounded-lg" |
116 | | - > |
117 | | - <span class="flex items-center space-x-1.5"> |
118 | | - {#if resolvedIcon} |
119 | | - <Icon src={resolvedIcon} theme={iconTheme} class="{iconStyles} h-4 w-4 text-icon-inverse" /> |
120 | | - {/if} |
121 | | - {#if !collapsedSidebar} |
122 | | - <span class="whitespace-nowrap tracking-normal">{label}</span> |
| 66 | + <div class={clsx('flex items-center', { 'gap-1.5': action })} data-menu-item-row> |
| 67 | + <button |
| 68 | + onclick={handleClick} |
| 69 | + title={label} |
| 70 | + data-menu-item-button |
| 71 | + class="cursor-pointer {itemStyles} text-base border border-transparent flex items-center justify-between hover:text-white focus:text-white rounded-lg" |
| 72 | + > |
| 73 | + <span class="flex items-center space-x-1.5 min-w-0 flex-1" data-menu-item-content> |
| 74 | + {#if imageUrl} |
| 75 | + <img |
| 76 | + src={imageUrl} |
| 77 | + alt={label} |
| 78 | + class="size-4 shrink-0 rounded bg-white object-contain" |
| 79 | + data-menu-item-image |
| 80 | + /> |
| 81 | + {:else if resolvedIcon} |
| 82 | + <Icon |
| 83 | + src={resolvedIcon} |
| 84 | + theme={iconTheme} |
| 85 | + class="h-4 w-4 text-icon-inverse" |
| 86 | + data-menu-item-icon |
| 87 | + /> |
| 88 | + {/if} |
| 89 | + <span class="truncate tracking-normal" data-menu-item-label>{label}</span> |
123 | 90 | {#if beta} |
124 | 91 | <TagBeta /> |
125 | 92 | {/if} |
126 | | - {/if} |
127 | | - </span> |
128 | | - {#if collapsable && !collapsedSidebar} |
129 | | - <button |
130 | | - class="cursor-pointer" |
131 | | - onclick={(e) => { |
132 | | - e.stopPropagation() |
133 | | - open = !open |
134 | | - }} |
135 | | - > |
136 | | - <Icon src={open ? ChevronDown : ChevronRight} class="h-4 w-4 text-white-40" /> |
137 | | - </button> |
138 | | - {/if} |
139 | | - </button> |
140 | | - {#if children?.length} |
141 | | - {#if collapsedSidebar} |
142 | | - {#if hovered} |
143 | | - <div |
144 | | - use:floatingContent |
145 | | - role="contentinfo" |
146 | | - onmouseenter={handleHover} |
147 | | - onmouseleave={handleBlur} |
148 | | - class="pt-4 z-30" |
| 93 | + </span> |
| 94 | + {#if collapsable} |
| 95 | + <button |
| 96 | + class="cursor-pointer" |
| 97 | + data-menu-item-chevron |
| 98 | + onclick={(e) => { |
| 99 | + e.stopPropagation() |
| 100 | + open = !open |
| 101 | + }} |
149 | 102 | > |
150 | | - <DrawerContext autofocus onclick={handleClickChild} {items} /> |
151 | | - </div> |
| 103 | + <Icon src={open ? ChevronDown : ChevronRight} class="h-4 w-4 text-white-40" /> |
| 104 | + </button> |
152 | 105 | {/if} |
153 | | - {:else if open || !collapsable} |
154 | | - <ul> |
155 | | - {#each children as child} |
156 | | - <li> |
157 | | - <MenuItem {...child} isFolderItem {onclick} /> |
158 | | - </li> |
159 | | - {/each} |
160 | | - </ul> |
| 106 | + </button> |
| 107 | + {#if action} |
| 108 | + <span class="shrink-0" data-menu-item-action> |
| 109 | + {@render action()} |
| 110 | + </span> |
161 | 111 | {/if} |
| 112 | + </div> |
| 113 | + {#if children?.length && (open || !collapsable)} |
| 114 | + <ul data-menu-item-children> |
| 115 | + {#each children as child} |
| 116 | + <li> |
| 117 | + <MenuItem {...child} isFolderItem {onclick} /> |
| 118 | + </li> |
| 119 | + {/each} |
| 120 | + </ul> |
162 | 121 | {/if} |
163 | 122 | </div> |
0 commit comments