|
39 | 39 | depth = 0 |
40 | 40 | }: Props = $props(); |
41 | 41 |
|
| 42 | + let renderActionsDropdown = $state(false); |
42 | 43 | let dropdownOpen = $state(false); |
43 | 44 |
|
44 | 45 | let isLoading = $derived(getAllLoadingChats().includes(conversation.id)); |
|
70 | 71 | } |
71 | 72 | } |
72 | 73 |
|
| 74 | + function handleMouseLeave() { |
| 75 | + if (!dropdownOpen) { |
| 76 | + renderActionsDropdown = false; |
| 77 | + } |
| 78 | + } |
| 79 | +
|
| 80 | + function handleMouseOver() { |
| 81 | + renderActionsDropdown = true; |
| 82 | + } |
| 83 | +
|
73 | 84 | function handleSelect() { |
74 | 85 | onSelect?.(conversation.id); |
75 | 86 | } |
76 | 87 |
|
| 88 | + $effect(() => { |
| 89 | + if (!dropdownOpen) { |
| 90 | + renderActionsDropdown = false; |
| 91 | + } |
| 92 | + }); |
| 93 | +
|
77 | 94 | onMount(() => { |
78 | 95 | document.addEventListener('edit-active-conversation', handleGlobalEditEvent as EventListener); |
79 | 96 |
|
|
86 | 103 | }); |
87 | 104 | </script> |
88 | 105 |
|
89 | | -<div |
90 | | - class="conversation-item group relative flex min-h-9 w-full items-center justify-between space-x-3 rounded-lg py-1.5 transition-colors hover:bg-foreground/10 {isActive |
| 106 | +<!-- svelte-ignore a11y_mouse_events_have_key_events --> |
| 107 | +<button |
| 108 | + class="group flex min-h-9 w-full cursor-pointer items-center justify-between space-x-3 rounded-lg py-1.5 text-left transition-colors hover:bg-foreground/10 {isActive |
91 | 109 | ? 'bg-foreground/5 text-accent-foreground' |
92 | 110 | : ''} px-3" |
| 111 | + onclick={handleSelect} |
| 112 | + onmouseover={handleMouseOver} |
| 113 | + onmouseleave={handleMouseLeave} |
| 114 | + onfocusin={handleMouseOver} |
| 115 | + onfocusout={(e) => { |
| 116 | + if (!e.currentTarget.contains(e.relatedTarget as Node | null)) { |
| 117 | + handleMouseLeave(); |
| 118 | + } |
| 119 | + }} |
93 | 120 | > |
94 | | - <button |
95 | | - class="absolute inset-0 z-0 cursor-pointer rounded-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-ring" |
96 | | - onclick={handleSelect} |
97 | | - aria-label={conversation.name} |
98 | | - > |
99 | | - </button> |
100 | 121 | <div |
101 | | - class="pointer-events-none relative z-10 flex min-w-0 flex-1 items-center gap-2" |
| 122 | + class="flex min-w-0 flex-1 items-center gap-2" |
102 | 123 | style:padding-left="{depth * FORK_TREE_DEPTH_PADDING}px" |
103 | 124 | > |
104 | 125 | {#if depth > 0} |
|
109 | 130 | <a |
110 | 131 | {...props} |
111 | 132 | href={RouterService.chat(conversation.forkedFromConversationId)} |
112 | | - class="pointer-events-auto flex shrink-0 items-center text-muted-foreground transition-colors hover:text-foreground" |
| 133 | + class="flex shrink-0 items-center text-muted-foreground transition-colors hover:text-foreground" |
113 | 134 | > |
114 | 135 | <GitBranch class="h-3.5 w-3.5" /> |
115 | 136 | </a> |
|
125 | 146 | {#if isLoading} |
126 | 147 | <Tooltip.Root> |
127 | 148 | <Tooltip.Trigger> |
128 | | - <button |
129 | | - class="stop-button pointer-events-auto flex h-4 w-4 shrink-0 cursor-pointer items-center justify-center rounded text-muted-foreground transition-colors hover:text-foreground" |
| 149 | + <div |
| 150 | + class="stop-button flex h-4 w-4 shrink-0 cursor-pointer items-center justify-center rounded text-muted-foreground transition-colors hover:text-foreground" |
130 | 151 | onclick={handleStop} |
| 152 | + onkeydown={(e) => e.key === 'Enter' && handleStop(e)} |
| 153 | + role="button" |
| 154 | + tabindex="0" |
131 | 155 | aria-label="Stop generation" |
132 | 156 | > |
133 | 157 | <Loader2 class="loading-icon h-3.5 w-3.5 animate-spin" /> |
134 | 158 |
|
135 | 159 | <Square class="stop-icon hidden h-3 w-3 fill-current text-destructive" /> |
136 | | - </button> |
| 160 | + </div> |
137 | 161 | </Tooltip.Trigger> |
138 | 162 |
|
139 | 163 | <Tooltip.Content> |
|
145 | 169 | <TruncatedText text={conversation.name} class="text-sm font-medium" showTooltip={false} /> |
146 | 170 | </div> |
147 | 171 |
|
148 | | - <div class="actions pointer-events-auto relative z-20 flex items-center"> |
149 | | - <DropdownMenuActions |
150 | | - triggerIcon={MoreHorizontal} |
151 | | - triggerTooltip="More actions" |
152 | | - bind:open={dropdownOpen} |
153 | | - actions={[ |
154 | | - { |
155 | | - icon: conversation.pinned ? PinOff : Pin, |
156 | | - label: conversation.pinned ? 'Unpin' : 'Pin', |
157 | | - onclick: (e: Event) => { |
158 | | - e.stopPropagation(); |
159 | | - handleTogglePin(); |
160 | | - } |
161 | | - }, |
162 | | - { |
163 | | - icon: Pencil, |
164 | | - label: 'Edit', |
165 | | - onclick: handleEdit, |
166 | | - shortcut: ['shift', 'cmd', 'e'] |
167 | | - }, |
168 | | - { |
169 | | - icon: Download, |
170 | | - label: 'Export', |
171 | | - onclick: (e: Event) => { |
172 | | - e.stopPropagation(); |
173 | | - conversationsStore.downloadConversation(conversation.id); |
| 172 | + {#if renderActionsDropdown} |
| 173 | + <div class="actions flex items-center"> |
| 174 | + <DropdownMenuActions |
| 175 | + triggerIcon={MoreHorizontal} |
| 176 | + triggerTooltip="More actions" |
| 177 | + bind:open={dropdownOpen} |
| 178 | + actions={[ |
| 179 | + { |
| 180 | + icon: conversation.pinned ? PinOff : Pin, |
| 181 | + label: conversation.pinned ? 'Unpin' : 'Pin', |
| 182 | + onclick: (e: Event) => { |
| 183 | + e.stopPropagation(); |
| 184 | + handleTogglePin(); |
| 185 | + } |
174 | 186 | }, |
175 | | - shortcut: ['shift', 'cmd', 's'] |
176 | | - }, |
177 | | - { |
178 | | - icon: Trash2, |
179 | | - label: 'Delete', |
180 | | - onclick: handleDelete, |
181 | | - variant: 'destructive', |
182 | | - shortcut: ['shift', 'cmd', 'd'], |
183 | | - separator: true |
184 | | - } |
185 | | - ]} |
186 | | - /> |
187 | | - </div> |
188 | | -</div> |
| 187 | + { |
| 188 | + icon: Pencil, |
| 189 | + label: 'Edit', |
| 190 | + onclick: handleEdit, |
| 191 | + shortcut: ['shift', 'cmd', 'e'] |
| 192 | + }, |
| 193 | + { |
| 194 | + icon: Download, |
| 195 | + label: 'Export', |
| 196 | + onclick: (e: Event) => { |
| 197 | + e.stopPropagation(); |
| 198 | + conversationsStore.downloadConversation(conversation.id); |
| 199 | + }, |
| 200 | + shortcut: ['shift', 'cmd', 's'] |
| 201 | + }, |
| 202 | + { |
| 203 | + icon: Trash2, |
| 204 | + label: 'Delete', |
| 205 | + onclick: handleDelete, |
| 206 | + variant: 'destructive', |
| 207 | + shortcut: ['shift', 'cmd', 'd'], |
| 208 | + separator: true |
| 209 | + } |
| 210 | + ]} |
| 211 | + /> |
| 212 | + </div> |
| 213 | + {/if} |
| 214 | +</button> |
189 | 215 |
|
190 | 216 | <style> |
191 | | - .conversation-item { |
| 217 | + button { |
192 | 218 | :global([data-slot='dropdown-menu-trigger']:not([data-state='open'])) { |
193 | 219 | opacity: 0; |
194 | 220 | } |
|
213 | 239 | } |
214 | 240 | } |
215 | 241 |
|
216 | | - &:is(:hover) .stop-button, |
217 | | - &:focus-within .stop-button { |
| 242 | + &:is(:hover) .stop-button { |
218 | 243 | :global(.stop-icon) { |
219 | 244 | display: block; |
220 | 245 | } |
|
0 commit comments