move notification indicator to menu button

shows mail icon with pulse animation when notifications exist
This commit is contained in:
Justin Edmund 2025-12-17 20:05:34 -08:00
parent 907b4503dd
commit 62dd3f5cd7
2 changed files with 99 additions and 36 deletions

View file

@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M12 10C12 10.5523 11.5523 11 11 11H3C2.44772 11 2 10.5523 2 10V6.42915C2 6.23669 2.20835 6.11641 2.37502 6.21266L6.54395 8.62012C6.64258 8.67706 6.75256 8.69416 6.85742 8.68066C6.9449 8.6822 7.03392 8.66303 7.11523 8.61621L11.625 6.01241C11.7917 5.91619 12 6.03647 12 6.22892V10ZM11 3C11.5523 3 12 3.44772 12 4V4.35196C12 4.53059 11.9047 4.69565 11.75 4.78496L7.07621 7.48356C6.92149 7.5729 6.73086 7.57289 6.57615 7.48354L2.24995 4.98517C2.09528 4.89584 2 4.7308 2 4.55218V4C2 3.44772 2.44772 3 3 3H11Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 647 B

View file

@ -17,7 +17,6 @@
import UserSettingsModal from './UserSettingsModal.svelte' import UserSettingsModal from './UserSettingsModal.svelte'
import InvitationsModal from './crew/InvitationsModal.svelte' import InvitationsModal from './crew/InvitationsModal.svelte'
import { authStore } from '$lib/stores/auth.store' import { authStore } from '$lib/stores/auth.store'
import { crewStore } from '$lib/stores/crew.store.svelte'
// Props from layout data // Props from layout data
const { const {
@ -145,6 +144,15 @@
// Database back button hover state // Database back button hover state
let databaseBackHovered = $state(false) let databaseBackHovered = $state(false)
// Query for the user's crew (to determine if phantom claims should be fetched)
const myCrewQuery = createQuery(() => ({
...crewQueries.myCrew(),
enabled: isAuth
}))
// Derived: whether the user is in a crew (from query, not store)
const isInCrew = $derived(myCrewQuery.data != null)
// Query for pending invitations (only when authenticated) // Query for pending invitations (only when authenticated)
const pendingInvitationsQuery = createQuery(() => ({ const pendingInvitationsQuery = createQuery(() => ({
...crewQueries.pendingInvitations(), ...crewQueries.pendingInvitations(),
@ -154,7 +162,7 @@
// Query for pending phantom claims (only when authenticated and in a crew) // Query for pending phantom claims (only when authenticated and in a crew)
const pendingPhantomClaimsQuery = createQuery(() => ({ const pendingPhantomClaimsQuery = createQuery(() => ({
...crewQueries.pendingPhantomClaims(), ...crewQueries.pendingPhantomClaims(),
enabled: isAuth && crewStore.isInCrew enabled: isAuth && isInCrew
})) }))
// Derived counts // Derived counts
@ -264,23 +272,16 @@
aria-label="Your account" aria-label="Your account"
class="profile-link" class="profile-link"
> >
<span class="avatar-container"> {#if avatarSrc}
{#if avatarSrc} <img
<img src={avatarSrc}
src={avatarSrc} srcset={avatarSrcSet}
srcset={avatarSrcSet} alt={username}
alt={username} class="user-avatar"
class="user-avatar" width="24"
width="24" height="24"
height="24" />
/> {/if}
{/if}
{#if totalNotificationCount > 0}
<span class="avatar-badge">
<NotificationBadge count={totalNotificationCount} />
</span>
{/if}
</span>
<span>{username}</span> <span>{username}</span>
</a> </a>
</li> </li>
@ -298,8 +299,14 @@
<li> <li>
<DropdownMenu.Root> <DropdownMenu.Root>
<DropdownMenu.Trigger class="nav-more-trigger"> <DropdownMenu.Trigger
<Icon name="ellipsis" size={14} /> class="nav-more-trigger {totalNotificationCount > 0 ? `has-notification ${userElement ?? ''}` : ''}"
>
{#if totalNotificationCount > 0}
<Icon name="mail" size={18} />
{:else}
<Icon name="ellipsis" size={14} />
{/if}
</DropdownMenu.Trigger> </DropdownMenu.Trigger>
<DropdownMenu.Portal> <DropdownMenu.Portal>
@ -317,11 +324,13 @@
</DropdownItem> </DropdownItem>
<DropdownMenu.Separator class="dropdown-separator" /> <DropdownMenu.Separator class="dropdown-separator" />
{/if} {/if}
{#if isAuth && totalNotificationCount > 0} {#if isAuth}
<DropdownItem> <DropdownItem>
<button class="dropdown-button-with-badge" onclick={() => (invitationsModalOpen = true)}> <button class="dropdown-button-with-badge" onclick={() => (invitationsModalOpen = true)}>
<span>Notifications</span> <span>Notifications</span>
<NotificationBadge count={totalNotificationCount} showCount /> {#if totalNotificationCount > 0}
<NotificationBadge count={totalNotificationCount} showCount element={userElement} />
{/if}
</button> </button>
</DropdownItem> </DropdownItem>
{/if} {/if}
@ -556,25 +565,12 @@
align-items: center; align-items: center;
gap: spacing.$unit-half; gap: spacing.$unit-half;
.avatar-container {
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.user-avatar { .user-avatar {
width: 24px; width: 24px;
height: 24px; height: 24px;
border-radius: 50%; border-radius: 50%;
object-fit: cover; object-fit: cover;
} }
.avatar-badge {
position: absolute;
top: -2px;
right: -4px;
}
} }
// Dropdown button with badge (for Invitations) // Dropdown button with badge (for Invitations)
@ -630,6 +626,70 @@
} }
} }
// Notification pulse animation for the more trigger
@keyframes notification-pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.6;
}
}
:global(.nav-more-trigger.has-notification) {
animation: notification-pulse 2s ease-in-out infinite;
// Default pulse color (no element selected)
background-color: var(--button-primary-bg);
color: white;
// Compensate for larger mail icon (18px vs 14px ellipsis)
padding: spacing.$unit calc(spacing.$unit + 3px);
&:hover {
animation: none;
background-color: var(--button-primary-bg-hover);
}
}
// Element-specific notification colors
:global(.nav-more-trigger.has-notification.wind) {
background-color: var(--wind-button-bg);
&:hover {
background-color: var(--wind-button-bg-hover);
}
}
:global(.nav-more-trigger.has-notification.fire) {
background-color: var(--fire-button-bg);
&:hover {
background-color: var(--fire-button-bg-hover);
}
}
:global(.nav-more-trigger.has-notification.water) {
background-color: var(--water-button-bg);
&:hover {
background-color: var(--water-button-bg-hover);
}
}
:global(.nav-more-trigger.has-notification.earth) {
background-color: var(--earth-button-bg);
&:hover {
background-color: var(--earth-button-bg-hover);
}
}
:global(.nav-more-trigger.has-notification.light) {
background-color: var(--light-button-bg);
color: black;
&:hover {
background-color: var(--light-button-bg-hover);
}
}
:global(.nav-more-trigger.has-notification.dark) {
background-color: var(--dark-button-bg);
&:hover {
background-color: var(--dark-button-bg-hover);
}
}
// Style the new team button as a prominent circular button // Style the new team button as a prominent circular button
// Remove redundant styles that the Button component already handles // Remove redundant styles that the Button component already handles
:global(.new-team-button) { :global(.new-team-button) {