Skip to content

Commit f6be918

Browse files
committed
Improvements
1 parent 51d12fe commit f6be918

File tree

9 files changed

+363
-15
lines changed

9 files changed

+363
-15
lines changed

packages/dashboard/src/App.vue

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
11
<template>
22
<router-view />
3+
<Toast />
34
</template>
5+
6+
<script setup lang="ts">
7+
import Toast from '@/components/Toast.vue';
8+
</script>

packages/dashboard/src/components/ComposeEmail.vue

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ import api from "@/services/api";
8585
import { useEmailStore } from "@/stores/emails";
8686
import { useMailboxStore } from "@/stores/mailboxes";
8787
import { useUIStore } from "@/stores/ui";
88+
import { useToast } from "@/composables/useToast";
8889
import RichTextEditor from "./RichTextEditor.vue";
8990
9091
const uiStore = useUIStore();
@@ -93,6 +94,7 @@ const emailStore = useEmailStore();
9394
const mailboxStore = useMailboxStore();
9495
const { currentMailbox } = storeToRefs(mailboxStore);
9596
const route = useRoute();
97+
const { success: showSuccessToast, error: showErrorToast } = useToast();
9698
9799
const to = ref("");
98100
const subject = ref("");
@@ -140,7 +142,7 @@ watch(isComposeModalOpen, (isOpen) => {
140142
subject.value = original.subject.startsWith("Re: ")
141143
? original.subject
142144
: `Re: ${original.subject}`;
143-
body.value = `\n\n---\nOn ${original.date}, ${original.sender} wrote:\n${formatQuotedText(stripHtml(original.body))}`;
145+
body.value = `<br><br><blockquote style="border-left: 2px solid #ccc; margin: 0; padding-left: 1em; color: #666;">On ${original.date}, ${original.sender} wrote:<br><br>${original.body}</blockquote>`;
144146
} else if (options.mode === "reply-all" && original) {
145147
// For reply all, include both sender and original recipient
146148
const recipients = new Set([original.sender]);
@@ -154,13 +156,19 @@ watch(isComposeModalOpen, (isOpen) => {
154156
subject.value = original.subject.startsWith("Re: ")
155157
? original.subject
156158
: `Re: ${original.subject}`;
157-
body.value = `\n\n---\nOn ${original.date}, ${original.sender} wrote:\n${formatQuotedText(stripHtml(original.body))}`;
159+
body.value = `<br><br><blockquote style="border-left: 2px solid #ccc; margin: 0; padding-left: 1em; color: #666;">On ${original.date}, ${original.sender} wrote:<br><br>${original.body}</blockquote>`;
158160
} else if (options.mode === "forward" && original) {
159161
to.value = "";
160162
subject.value = original.subject.startsWith("Fwd: ")
161163
? original.subject
162164
: `Fwd: ${original.subject}`;
163-
body.value = `\n\n---\nForwarded message:\nFrom: ${original.sender}\nDate: ${original.date}\nSubject: ${original.subject}\n\n${stripHtml(original.body)}`;
165+
body.value = `<br><br><div style="border: 1px solid #ddd; padding: 1em; background-color: #f9f9f9; margin: 1em 0;">
166+
<strong>Forwarded message:</strong><br>
167+
<strong>From:</strong> ${original.sender}<br>
168+
<strong>Date:</strong> ${original.date}<br>
169+
<strong>Subject:</strong> ${original.subject}<br><br>
170+
${original.body}
171+
</div>`;
164172
} else {
165173
to.value = "";
166174
subject.value = "";
@@ -236,8 +244,11 @@ const send = async () => {
236244
subject.value = "";
237245
body.value = "";
238246
closeModal();
247+
showSuccessToast("Email sent successfully!");
239248
} catch (e: any) {
240-
error.value = e.response?.data?.error || "An unexpected error occurred.";
249+
const errorMessage = e.response?.data?.error || "An unexpected error occurred.";
250+
error.value = errorMessage;
251+
showErrorToast(errorMessage);
241252
} finally {
242253
isLoading.value = false;
243254
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<template>
2+
<div class="fixed top-4 right-4 z-50 space-y-2 pointer-events-none">
3+
<transition-group name="toast" tag="div">
4+
<div
5+
v-for="toast in toasts"
6+
:key="toast.id"
7+
class="pointer-events-auto flex items-center gap-3 px-4 py-3 rounded-lg shadow-lg animate-in fade-in slide-in-from-right-4 duration-200"
8+
:class="getToastClasses(toast.type)"
9+
>
10+
<svg v-if="toast.type === 'success'" class="w-5 h-5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
11+
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
12+
</svg>
13+
<svg v-else-if="toast.type === 'error'" class="w-5 h-5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
14+
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
15+
</svg>
16+
<svg v-else-if="toast.type === 'warning'" class="w-5 h-5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
17+
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
18+
</svg>
19+
<svg v-else class="w-5 h-5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
20+
<path fill-rule="evenodd" d="M18 5v8a2 2 0 01-2 2h-5l-5 4v-4H4a2 2 0 01-2-2V5a2 2 0 012-2h12a2 2 0 012 2zm-11-1a1 1 0 11-2 0 1 1 0 012 0zm3 0a1 1 0 11-2 0 1 1 0 012 0zm3 0a1 1 0 11-2 0 1 1 0 012 0z" clip-rule="evenodd" />
21+
</svg>
22+
<span class="text-sm font-medium">{{ toast.message }}</span>
23+
<button
24+
@click="removeToast(toast.id)"
25+
class="ml-2 text-current opacity-70 hover:opacity-100 transition-opacity"
26+
>
27+
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
28+
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
29+
</svg>
30+
</button>
31+
</div>
32+
</transition-group>
33+
</div>
34+
</template>
35+
36+
<script setup lang="ts">
37+
import { useToast } from '@/composables/useToast';
38+
39+
const { toasts, removeToast } = useToast();
40+
41+
const getToastClasses = (type: string) => {
42+
const baseClasses = 'text-white';
43+
switch (type) {
44+
case 'success':
45+
return `${baseClasses} bg-green-500`;
46+
case 'error':
47+
return `${baseClasses} bg-red-500`;
48+
case 'warning':
49+
return `${baseClasses} bg-yellow-500`;
50+
case 'info':
51+
default:
52+
return `${baseClasses} bg-blue-500`;
53+
}
54+
};
55+
</script>
56+
57+
<style scoped>
58+
.toast-enter-active,
59+
.toast-leave-active {
60+
transition: all 0.3s ease;
61+
}
62+
63+
.toast-enter-from {
64+
opacity: 0;
65+
transform: translateX(30px);
66+
}
67+
68+
.toast-leave-to {
69+
opacity: 0;
70+
transform: translateX(30px);
71+
}
72+
</style>
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { ref } from 'vue';
2+
3+
export interface Toast {
4+
id: string;
5+
message: string;
6+
type: 'success' | 'error' | 'info' | 'warning';
7+
duration?: number;
8+
}
9+
10+
const toasts = ref<Toast[]>([]);
11+
12+
export function useToast() {
13+
const addToast = (message: string, type: 'success' | 'error' | 'info' | 'warning' = 'info', duration = 3000) => {
14+
const id = Date.now().toString();
15+
const toast: Toast = { id, message, type, duration };
16+
17+
toasts.value.push(toast);
18+
19+
if (duration > 0) {
20+
setTimeout(() => {
21+
removeToast(id);
22+
}, duration);
23+
}
24+
25+
return id;
26+
};
27+
28+
const removeToast = (id: string) => {
29+
toasts.value = toasts.value.filter(t => t.id !== id);
30+
};
31+
32+
const success = (message: string, duration?: number) => addToast(message, 'success', duration);
33+
const error = (message: string, duration?: number) => addToast(message, 'error', duration);
34+
const info = (message: string, duration?: number) => addToast(message, 'info', duration);
35+
const warning = (message: string, duration?: number) => addToast(message, 'warning', duration);
36+
37+
return {
38+
toasts,
39+
addToast,
40+
removeToast,
41+
success,
42+
error,
43+
info,
44+
warning,
45+
};
46+
}

packages/dashboard/src/services/api.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ export default {
5858

5959
// Mailboxes
6060
listMailboxes: () => apiClient.get("/api/v1/mailboxes"),
61+
createMailbox: (email: string, name: string, settings?: any) =>
62+
apiClient.post("/api/v1/mailboxes", { email, name, settings }),
6163
getMailbox: (mailboxId: string) =>
6264
apiClient.get(`/api/v1/mailboxes/${mailboxId}`),
6365
updateMailbox: (mailboxId: string, settings: any) =>

packages/dashboard/src/views/EmailDetail.vue

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,31 +3,36 @@
33
<div class="p-6 sm:p-8 border-b border-gray-200 dark:border-gray-700 flex-shrink-0 bg-gradient-to-br from-white to-gray-50 dark:from-gray-800 dark:to-gray-900/50">
44
<div class="flex items-center justify-between mb-6">
55
<div class="flex items-center gap-3">
6-
<button @click="router.back()" class="p-2.5 text-gray-500 hover:text-indigo-600 dark:text-gray-400 dark:hover:text-indigo-400 rounded-xl hover:bg-indigo-50 dark:hover:bg-gray-700/50 transition-all duration-200" title="Back">
6+
<button @click="router.back()" class="p-2.5 text-gray-500 hover:text-indigo-600 dark:text-gray-400 dark:hover:text-indigo-400 rounded-xl hover:bg-indigo-50 dark:hover:bg-gray-700/50 transition-all duration-200 group relative cursor-pointer" title="Back">
7+
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-2 py-1 bg-gray-900 dark:bg-gray-700 text-white text-xs rounded whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none z-20">Back</div>
78
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
89
<path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" />
910
</svg>
1011
</button>
1112
<h1 class="text-xl sm:text-2xl font-bold text-gray-900 dark:text-white">{{ email.subject }}</h1>
1213
</div>
1314
<div class="flex items-center gap-2">
14-
<button @click="handleReply" class="p-2.5 text-gray-500 hover:text-indigo-600 dark:text-gray-400 dark:hover:text-indigo-400 rounded-xl hover:bg-indigo-50 dark:hover:bg-gray-700/50 transition-all duration-200" title="Reply">
15+
<button @click="handleReply" class="p-2.5 text-gray-500 hover:text-indigo-600 dark:text-gray-400 dark:hover:text-indigo-400 rounded-xl hover:bg-indigo-50 dark:hover:bg-gray-700/50 transition-all duration-200 group relative cursor-pointer" title="Reply">
16+
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-2 py-1 bg-gray-900 dark:bg-gray-700 text-white text-xs rounded whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none z-20">Reply</div>
1517
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
1618
<path fill-rule="evenodd" d="M7.707 3.293a1 1 0 010 1.414L5.414 7H11a7 7 0 017 7v2a1 1 0 11-2 0v-2a5 5 0 00-5-5H5.414l2.293 2.293a1 1 0 11-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" />
1719
</svg>
1820
</button>
19-
<button @click="handleReplyAll" class="p-2.5 text-gray-500 hover:text-indigo-600 dark:text-gray-400 dark:hover:text-indigo-400 rounded-xl hover:bg-indigo-50 dark:hover:bg-gray-700/50 transition-all duration-200" title="Reply All">
21+
<button @click="handleReplyAll" class="p-2.5 text-gray-500 hover:text-indigo-600 dark:text-gray-400 dark:hover:text-indigo-400 rounded-xl hover:bg-indigo-50 dark:hover:bg-gray-700/50 transition-all duration-200 group relative cursor-pointer" title="Reply All">
22+
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-2 py-1 bg-gray-900 dark:bg-gray-700 text-white text-xs rounded whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none z-20">Reply All</div>
2023
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
2124
<path d="M2 5a2 2 0 012-2h7a2 2 0 012 2v4a2 2 0 01-2 2H9l-3 3v-3H4a2 2 0 01-2-2V5z" />
2225
<path d="M15 7v2a4 4 0 01-4 4H9.828l-1.766 1.767c.28.149.599.233.938.233h2l3 3v-3h2a2 2 0 002-2V9a2 2 0 00-2-2h-1z" />
2326
</svg>
2427
</button>
25-
<button @click="handleForward" class="p-2.5 text-gray-500 hover:text-indigo-600 dark:text-gray-400 dark:hover:text-indigo-400 rounded-xl hover:bg-indigo-50 dark:hover:bg-gray-700/50 transition-all duration-200" title="Forward">
28+
<button @click="handleForward" class="p-2.5 text-gray-500 hover:text-indigo-600 dark:text-gray-400 dark:hover:text-indigo-400 rounded-xl hover:bg-indigo-50 dark:hover:bg-gray-700/50 transition-all duration-200 group relative cursor-pointer" title="Forward">
29+
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-2 py-1 bg-gray-900 dark:bg-gray-700 text-white text-xs rounded whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none z-20">Forward</div>
2630
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
2731
<path fill-rule="evenodd" d="M12.293 3.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 9H7a5 5 0 00-5 5v2a1 1 0 11-2 0v-2a7 7 0 017-7h7.586l-2.293-2.293a1 1 0 010-1.414z" clip-rule="evenodd" />
2832
</svg>
2933
</button>
30-
<button @click="toggleReadStatus" class="p-2.5 text-gray-500 hover:text-indigo-600 dark:text-gray-400 dark:hover:text-indigo-400 rounded-xl hover:bg-indigo-50 dark:hover:bg-gray-700/50 transition-all duration-200" :title="email.read ? 'Mark as unread' : 'Mark as read'">
34+
<button @click="toggleReadStatus" class="p-2.5 text-gray-500 hover:text-indigo-600 dark:text-gray-400 dark:hover:text-indigo-400 rounded-xl hover:bg-indigo-50 dark:hover:bg-gray-700/50 transition-all duration-200 group relative cursor-pointer" :title="email.read ? 'Mark as unread' : 'Mark as read'">
35+
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-2 py-1 bg-gray-900 dark:bg-gray-700 text-white text-xs rounded whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none z-20">{{ email.read ? 'Mark as unread' : 'Mark as read' }}</div>
3136
<svg v-if="email.read" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
3237
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
3338
</svg>
@@ -36,7 +41,8 @@
3641
<path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z" />
3742
</svg>
3843
</button>
39-
<button @click="toggleStarStatus" class="p-2.5 text-gray-500 hover:text-yellow-500 dark:text-gray-400 dark:hover:text-yellow-400 rounded-xl hover:bg-yellow-50 dark:hover:bg-gray-700/50 transition-all duration-200" :class="{'text-yellow-500 dark:text-yellow-400 bg-yellow-50 dark:bg-yellow-900/20': email.starred}" :title="email.starred ? 'Unstar' : 'Star'">
44+
<button @click="toggleStarStatus" class="p-2.5 text-gray-500 hover:text-yellow-500 dark:text-gray-400 dark:hover:text-yellow-400 rounded-xl hover:bg-yellow-50 dark:hover:bg-gray-700/50 transition-all duration-200 group relative cursor-pointer" :class="{'text-yellow-500 dark:text-yellow-400 bg-yellow-50 dark:bg-yellow-900/20': email.starred}" :title="email.starred ? 'Unstar' : 'Star'">
45+
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-2 py-1 bg-gray-900 dark:bg-gray-700 text-white text-xs rounded whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none z-20">{{ email.starred ? 'Unstar' : 'Star' }}</div>
4046
<svg v-if="email.starred" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
4147
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
4248
</svg>
@@ -45,7 +51,8 @@
4551
</svg>
4652
</button>
4753
<div class="relative" ref="moveMenu">
48-
<button @click="isMoveMenuOpen = !isMoveMenuOpen" class="p-2.5 text-gray-500 hover:text-indigo-600 dark:text-gray-400 dark:hover:text-indigo-400 rounded-xl hover:bg-indigo-50 dark:hover:bg-gray-700/50 transition-all duration-200" title="Move to folder">
54+
<button @click="isMoveMenuOpen = !isMoveMenuOpen" class="p-2.5 text-gray-500 hover:text-indigo-600 dark:text-gray-400 dark:hover:text-indigo-400 rounded-xl hover:bg-indigo-50 dark:hover:bg-gray-700/50 transition-all duration-200 group relative cursor-pointer" title="Move to folder">
55+
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-2 py-1 bg-gray-900 dark:bg-gray-700 text-white text-xs rounded whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none z-20">Move to folder</div>
4956
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
5057
<path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" />
5158
</svg>
@@ -56,7 +63,8 @@
5663
</button>
5764
</div>
5865
</div>
59-
<button @click="handleDelete" class="p-2.5 text-gray-500 hover:text-red-600 dark:text-gray-400 dark:hover:text-red-400 rounded-xl hover:bg-red-50 dark:hover:bg-gray-700/50 transition-all duration-200" title="Delete">
66+
<button @click="handleDelete" class="p-2.5 text-gray-500 hover:text-red-600 dark:text-gray-400 dark:hover:text-red-400 rounded-xl hover:bg-red-50 dark:hover:bg-gray-700/50 transition-all duration-200 group relative cursor-pointer" title="Delete">
67+
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-2 py-1 bg-gray-900 dark:bg-gray-700 text-white text-xs rounded whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none z-20">Delete</div>
6068
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
6169
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm4 0a1 1 0 012 0v6a1 1 0 11-2 0V8z" clip-rule="evenodd" />
6270
</svg>
@@ -92,7 +100,8 @@
92100
<p class="text-sm font-semibold text-gray-900 dark:text-white truncate">{{ attachment.filename }}</p>
93101
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">{{ formatBytes(attachment.size) }}</p>
94102
</div>
95-
<a :href="getAttachmentUrl(attachment.id)" target="_blank" class="flex-shrink-0 p-2.5 text-gray-500 hover:text-indigo-600 dark:text-gray-400 dark:hover:text-indigo-400 rounded-lg hover:bg-indigo-50 dark:hover:bg-gray-700/50 transition-all duration-200" title="Download">
103+
<a :href="getAttachmentUrl(attachment.id)" target="_blank" class="flex-shrink-0 p-2.5 text-gray-500 hover:text-indigo-600 dark:text-gray-400 dark:hover:text-indigo-400 rounded-lg hover:bg-indigo-50 dark:hover:bg-gray-700/50 transition-all duration-200 group relative cursor-pointer" title="Download">
104+
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-2 py-1 bg-gray-900 dark:bg-gray-700 text-white text-xs rounded whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none z-20">Download</div>
96105
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
97106
<path fill-rule="evenodd" d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm3.293-7.707a1 1 0 011.414 0L9 10.586V3a1 1 0 112 0v7.586l1.293-1.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd" />
98107
</svg>

0 commit comments

Comments
 (0)