|
|
@@ -1,8 +1,6 @@
|
|
|
<script lang="ts">
|
|
|
import { Trash2, Pencil, MoreHorizontal } from '@lucide/svelte';
|
|
|
- import { ActionDropdown, ConfirmationDialog } from '$lib/components/app';
|
|
|
- import * as AlertDialog from '$lib/components/ui/alert-dialog';
|
|
|
- import Input from '$lib/components/ui/input/input.svelte';
|
|
|
+ import { ActionDropdown } from '$lib/components/app';
|
|
|
import { onMount } from 'svelte';
|
|
|
|
|
|
interface Props {
|
|
|
@@ -10,9 +8,8 @@
|
|
|
conversation: DatabaseConversation;
|
|
|
handleMobileSidebarItemClick?: () => void;
|
|
|
onDelete?: (id: string) => void;
|
|
|
- onEdit?: (id: string, name: string) => void;
|
|
|
+ onEdit?: (id: string) => void;
|
|
|
onSelect?: (id: string) => void;
|
|
|
- showLastModified?: boolean;
|
|
|
}
|
|
|
|
|
|
let {
|
|
|
@@ -21,54 +18,48 @@
|
|
|
onDelete,
|
|
|
onEdit,
|
|
|
onSelect,
|
|
|
- isActive = false,
|
|
|
- showLastModified = false
|
|
|
+ isActive = false
|
|
|
}: Props = $props();
|
|
|
|
|
|
- let editedName = $state('');
|
|
|
- let showDeleteDialog = $state(false);
|
|
|
- let showDropdown = $state(false);
|
|
|
- let showEditDialog = $state(false);
|
|
|
-
|
|
|
- function formatLastModified(timestamp: number) {
|
|
|
- const now = Date.now();
|
|
|
- const diff = now - timestamp;
|
|
|
- const minutes = Math.floor(diff / (1000 * 60));
|
|
|
- const hours = Math.floor(diff / (1000 * 60 * 60));
|
|
|
- const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
|
|
-
|
|
|
- if (minutes < 1) return 'Just now';
|
|
|
- if (minutes < 60) return `${minutes}m ago`;
|
|
|
- if (hours < 24) return `${hours}h ago`;
|
|
|
- return `${days}d ago`;
|
|
|
+ let renderActionsDropdown = $state(false);
|
|
|
+ let dropdownOpen = $state(false);
|
|
|
+
|
|
|
+ function handleEdit(event: Event) {
|
|
|
+ event.stopPropagation();
|
|
|
+ onEdit?.(conversation.id);
|
|
|
}
|
|
|
|
|
|
- function handleConfirmDelete() {
|
|
|
+ function handleDelete(event: Event) {
|
|
|
+ event.stopPropagation();
|
|
|
onDelete?.(conversation.id);
|
|
|
}
|
|
|
|
|
|
- function handleConfirmEdit() {
|
|
|
- if (!editedName.trim()) return;
|
|
|
- showEditDialog = false;
|
|
|
- onEdit?.(conversation.id, editedName);
|
|
|
+ function handleGlobalEditEvent(event: Event) {
|
|
|
+ const customEvent = event as CustomEvent<{ conversationId: string }>;
|
|
|
+ if (customEvent.detail.conversationId === conversation.id && isActive) {
|
|
|
+ handleEdit(event);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- function handleEdit(event: Event) {
|
|
|
- event.stopPropagation();
|
|
|
- editedName = conversation.name;
|
|
|
- showEditDialog = true;
|
|
|
+ function handleMouseLeave() {
|
|
|
+ if (!dropdownOpen) {
|
|
|
+ renderActionsDropdown = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function handleMouseOver() {
|
|
|
+ renderActionsDropdown = true;
|
|
|
}
|
|
|
|
|
|
function handleSelect() {
|
|
|
onSelect?.(conversation.id);
|
|
|
}
|
|
|
|
|
|
- function handleGlobalEditEvent(event: Event) {
|
|
|
- const customEvent = event as CustomEvent<{ conversationId: string }>;
|
|
|
- if (customEvent.detail.conversationId === conversation.id && isActive) {
|
|
|
- handleEdit(event);
|
|
|
+ $effect(() => {
|
|
|
+ if (!dropdownOpen) {
|
|
|
+ renderActionsDropdown = false;
|
|
|
}
|
|
|
- }
|
|
|
+ });
|
|
|
|
|
|
onMount(() => {
|
|
|
document.addEventListener('edit-active-conversation', handleGlobalEditEvent as EventListener);
|
|
|
@@ -82,99 +73,46 @@
|
|
|
});
|
|
|
</script>
|
|
|
|
|
|
+<!-- svelte-ignore a11y_mouse_events_have_key_events -->
|
|
|
<button
|
|
|
- class="group flex w-full cursor-pointer items-center justify-between space-x-3 rounded-lg px-3 py-1.5 text-left transition-colors hover:bg-foreground/10 {isActive
|
|
|
+ class="group flex min-h-9 w-full cursor-pointer items-center justify-between space-x-3 rounded-lg px-3 py-1.5 text-left transition-colors hover:bg-foreground/10 {isActive
|
|
|
? 'bg-foreground/5 text-accent-foreground'
|
|
|
: ''}"
|
|
|
onclick={handleSelect}
|
|
|
+ onmouseover={handleMouseOver}
|
|
|
+ onmouseleave={handleMouseLeave}
|
|
|
>
|
|
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
|
- <div
|
|
|
- class="text flex min-w-0 flex-1 items-center space-x-3"
|
|
|
- onclick={handleMobileSidebarItemClick}
|
|
|
- >
|
|
|
- <div class="min-w-0 flex-1">
|
|
|
- <p class="truncate text-sm font-medium">{conversation.name}</p>
|
|
|
-
|
|
|
- {#if showLastModified}
|
|
|
- <div class="mt-2 flex flex-wrap items-center space-y-2 space-x-2">
|
|
|
- <span class="w-full text-xs text-muted-foreground">
|
|
|
- {formatLastModified(conversation.lastModified)}
|
|
|
- </span>
|
|
|
- </div>
|
|
|
- {/if}
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <div class="actions flex items-center">
|
|
|
- <ActionDropdown
|
|
|
- triggerIcon={MoreHorizontal}
|
|
|
- triggerTooltip="More actions"
|
|
|
- bind:open={showDropdown}
|
|
|
- actions={[
|
|
|
- {
|
|
|
- icon: Pencil,
|
|
|
- label: 'Edit',
|
|
|
- onclick: handleEdit,
|
|
|
- shortcut: ['shift', 'cmd', 'e']
|
|
|
- },
|
|
|
- {
|
|
|
- icon: Trash2,
|
|
|
- label: 'Delete',
|
|
|
- onclick: (e) => {
|
|
|
- e.stopPropagation();
|
|
|
- showDeleteDialog = true;
|
|
|
+ <span class="truncate text-sm font-medium" onclick={handleMobileSidebarItemClick}>
|
|
|
+ {conversation.name}
|
|
|
+ </span>
|
|
|
+
|
|
|
+ {#if renderActionsDropdown}
|
|
|
+ <div class="actions flex items-center">
|
|
|
+ <ActionDropdown
|
|
|
+ triggerIcon={MoreHorizontal}
|
|
|
+ triggerTooltip="More actions"
|
|
|
+ bind:open={dropdownOpen}
|
|
|
+ actions={[
|
|
|
+ {
|
|
|
+ icon: Pencil,
|
|
|
+ label: 'Edit',
|
|
|
+ onclick: handleEdit,
|
|
|
+ shortcut: ['shift', 'cmd', 'e']
|
|
|
},
|
|
|
- variant: 'destructive',
|
|
|
- shortcut: ['shift', 'cmd', 'd'],
|
|
|
- separator: true
|
|
|
- }
|
|
|
- ]}
|
|
|
- />
|
|
|
-
|
|
|
- <ConfirmationDialog
|
|
|
- bind:open={showDeleteDialog}
|
|
|
- title="Delete Conversation"
|
|
|
- description={`Are you sure you want to delete "${conversation.name}"? This action cannot be undone and will permanently remove all messages in this conversation.`}
|
|
|
- confirmText="Delete"
|
|
|
- cancelText="Cancel"
|
|
|
- variant="destructive"
|
|
|
- icon={Trash2}
|
|
|
- onConfirm={handleConfirmDelete}
|
|
|
- onCancel={() => (showDeleteDialog = false)}
|
|
|
- />
|
|
|
-
|
|
|
- <AlertDialog.Root bind:open={showEditDialog}>
|
|
|
- <AlertDialog.Content>
|
|
|
- <AlertDialog.Header>
|
|
|
- <AlertDialog.Title>Edit Conversation Name</AlertDialog.Title>
|
|
|
-
|
|
|
- <AlertDialog.Description>
|
|
|
- <Input
|
|
|
- class="mt-4 text-foreground"
|
|
|
- onkeydown={(e) => {
|
|
|
- if (e.key === 'Enter') {
|
|
|
- e.preventDefault();
|
|
|
- handleConfirmEdit();
|
|
|
- showEditDialog = false;
|
|
|
- }
|
|
|
- }}
|
|
|
- placeholder="Enter a new name"
|
|
|
- type="text"
|
|
|
- bind:value={editedName}
|
|
|
- />
|
|
|
- </AlertDialog.Description>
|
|
|
- </AlertDialog.Header>
|
|
|
-
|
|
|
- <AlertDialog.Footer>
|
|
|
- <AlertDialog.Cancel>Cancel</AlertDialog.Cancel>
|
|
|
-
|
|
|
- <AlertDialog.Action onclick={handleConfirmEdit}>Save</AlertDialog.Action>
|
|
|
- </AlertDialog.Footer>
|
|
|
- </AlertDialog.Content>
|
|
|
- </AlertDialog.Root>
|
|
|
- </div>
|
|
|
+ {
|
|
|
+ icon: Trash2,
|
|
|
+ label: 'Delete',
|
|
|
+ onclick: handleDelete,
|
|
|
+ variant: 'destructive',
|
|
|
+ shortcut: ['shift', 'cmd', 'd'],
|
|
|
+ separator: true
|
|
|
+ }
|
|
|
+ ]}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ {/if}
|
|
|
</button>
|
|
|
|
|
|
<style>
|