add element theming and avatar to navigation

This commit is contained in:
Justin Edmund 2025-09-24 02:52:01 -07:00
parent e6b95f7c12
commit e2c71ad28d
2 changed files with 170 additions and 27 deletions

View file

@ -9,21 +9,31 @@
import Icon from './Icon.svelte' import Icon from './Icon.svelte'
import DropdownItem from './ui/dropdown/DropdownItem.svelte' import DropdownItem from './ui/dropdown/DropdownItem.svelte'
import { DropdownMenu } from 'bits-ui' import { DropdownMenu } from 'bits-ui'
import type { UserCookie } from '$lib/types/UserCookie'
import { getAvatarSrc, getAvatarSrcSet } from '$lib/utils/avatar'
// Props from layout data // Props from layout data
const { const {
username: usernameProp, account,
isAuthenticated: isAuthProp, currentUser,
role: roleProp isAuthenticated: isAuthProp
} = $props<{ } = $props<{
username?: string | null account?: {
userId: string
username: string
role: number
} | null
currentUser?: UserCookie | null
isAuthenticated?: boolean isAuthenticated?: boolean
role?: number | null
}>() }>()
const username = $derived(usernameProp ?? '') const username = $derived(account?.username ?? '')
const isAuth = $derived(Boolean(isAuthProp)) const isAuth = $derived(Boolean(isAuthProp))
const role = $derived(roleProp ?? null) const role = $derived(account?.role ?? null)
// Element from UserCookie is already a string like "fire", "water", etc.
const userElement = $derived(
currentUser?.element as 'wind' | 'fire' | 'water' | 'earth' | 'dark' | 'light' | undefined
)
// Localized links // Localized links
const galleryHref = $derived(localizeHref('/teams/explore')) const galleryHref = $derived(localizeHref('/teams/explore'))
@ -35,6 +45,13 @@
const databaseHref = $derived(localizeHref('/database')) const databaseHref = $derived(localizeHref('/database'))
const newTeamHref = $derived(localizeHref('/teams/new')) const newTeamHref = $derived(localizeHref('/teams/new'))
// Get the element class for styling
const elementClass = $derived(userElement ? `element-${userElement}` : '')
// Get the user's avatar URLs
const avatarSrc = $derived(getAvatarSrc(currentUser?.picture))
const avatarSrcSet = $derived(getAvatarSrcSet(currentUser?.picture))
// Database-specific links // Database-specific links
const databaseCharactersHref = $derived(localizeHref('/database/characters')) const databaseCharactersHref = $derived(localizeHref('/database/characters'))
const databaseWeaponsHref = $derived(localizeHref('/database/weapons')) const databaseWeaponsHref = $derived(localizeHref('/database/weapons'))
@ -43,11 +60,29 @@
// Database route detection // Database route detection
const isDatabaseRoute = $derived($page.url.pathname.startsWith(localizeHref('/database'))) const isDatabaseRoute = $derived($page.url.pathname.startsWith(localizeHref('/database')))
// Function to check if a nav item is selected
function isNavSelected(href: string): boolean {
const path = $page.url.pathname
// For gallery/teams, we need to check for /teams paths
if (href === galleryHref) {
return path === href || path.startsWith(localizeHref('/teams'))
}
// Exact match or starts with href + /
return path === href || path.startsWith(href + '/')
}
// Function to check if a database nav item is selected // Function to check if a database nav item is selected
function isDatabaseNavSelected(href: string): boolean { function isDatabaseNavSelected(href: string): boolean {
return $page.url.pathname === href || $page.url.pathname.startsWith(href + '/') return $page.url.pathname === href || $page.url.pathname.startsWith(href + '/')
} }
// Check if the user profile link is selected
const isProfileSelected = $derived(
isAuth && ($page.url.pathname === meHref || $page.url.pathname === localizeHref(`/${username}`))
)
// Handle logout // Handle logout
async function handleLogout() { async function handleLogout() {
try { try {
@ -66,7 +101,7 @@
} }
</script> </script>
<nav aria-label="Global"> <nav aria-label="Global" class={elementClass}>
{#if isDatabaseRoute} {#if isDatabaseRoute}
<!-- Database navigation mode --> <!-- Database navigation mode -->
<div class="database-nav"> <div class="database-nav">
@ -107,15 +142,40 @@
{:else} {:else}
<!-- Normal navigation mode --> <!-- Normal navigation mode -->
<ul role="list"> <ul role="list">
<li><a href={galleryHref}>{m.nav_gallery()}</a></li> <li>
<li><a href={guidesHref}>Guides</a></li> <a href={galleryHref} class:selected={isNavSelected(galleryHref)}>{m.nav_gallery()}</a>
<li><a href={collectionHref}>{m.nav_collection()}</a></li> </li>
<li><a href={guidesHref} class:selected={isNavSelected(guidesHref)}>Guides</a></li>
<li>
<a href={collectionHref} class:selected={isNavSelected(collectionHref)}
>{m.nav_collection()}</a
>
</li>
<li> <li>
{#if isAuth} {#if isAuth}
<a href={meHref} aria-label="Your account">{username}</a> <a
href={meHref}
class:selected={isProfileSelected}
aria-label="Your account"
class="profile-link"
>
{#if avatarSrc}
<img
src={avatarSrc}
srcset={avatarSrcSet}
alt={username}
class="user-avatar"
width="24"
height="24"
/>
{/if}
<span>{username}</span>
</a>
{:else} {:else}
<a href={loginHref} aria-label="Login">{m.nav_login()}</a> <a href={loginHref} class:selected={isNavSelected(loginHref)} aria-label="Login"
>{m.nav_login()}</a
>
{/if} {/if}
</li> </li>
@ -145,15 +205,17 @@
</li> </li>
</ul> </ul>
{/if} {/if}
<Button <Button
icon="plus" icon="plus"
iconOnly iconOnly
shape="circle" shape="circle"
variant="primary" variant="primary"
class="new-team-button" element={userElement}
aria-label="New team" elementStyle={Boolean(userElement)}
href={newTeamHref} class="new-team-button"
/> aria-label="New team"
href={newTeamHref}
/>
</nav> </nav>
<style lang="scss"> <style lang="scss">
@ -191,9 +253,15 @@
border-radius: layout.$full-corner; border-radius: layout.$full-corner;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: spacing.$unit-quarter;
padding: spacing.$unit-half; padding: spacing.$unit-half;
list-style: none; list-style: none;
li {
display: flex;
align-items: stretch;
}
a { a {
border-radius: layout.$full-corner; border-radius: layout.$full-corner;
color: var(--menu-text); color: var(--menu-text);
@ -274,7 +342,7 @@
background-color: var(--menu-bg); background-color: var(--menu-bg);
border-radius: layout.$full-corner; border-radius: layout.$full-corner;
display: flex; display: flex;
gap: spacing.$unit-half; gap: spacing.$unit-quarter;
flex-direction: row; flex-direction: row;
padding: spacing.$unit-half; padding: spacing.$unit-half;
list-style: none; list-style: none;
@ -307,6 +375,20 @@
} }
} }
// Profile link with avatar
.profile-link {
display: flex !important;
align-items: center;
gap: spacing.$unit-half;
.user-avatar {
width: 24px;
height: 24px;
border-radius: 50%;
object-fit: cover;
}
}
// Style the nav buttons to match link dimensions // Style the nav buttons to match link dimensions
:global(.nav-item-button) { :global(.nav-item-button) {
display: flex; display: flex;
@ -332,7 +414,8 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: spacing.$unit (spacing.$unit * 1.5); align-self: stretch; // Make it stretch to full height
padding: spacing.$unit calc(spacing.$unit * 1.5 + 1px);
border-radius: layout.$full-corner; border-radius: layout.$full-corner;
background-color: transparent; background-color: transparent;
color: var(--menu-text); color: var(--menu-text);
@ -357,6 +440,62 @@
// The Button component already handles size, shape, colors, etc. // The Button component already handles size, shape, colors, etc.
} }
// Element-specific SELECTED states for navigation links
// Hover states remain the default grey (defined in base styles above)
nav.element-wind {
ul a.selected,
.database-nav a.selected {
background-color: var(--wind-nav-selected-bg);
color: var(--wind-nav-selected-text);
font-weight: typography.$bold;
}
}
nav.element-fire {
ul a.selected,
.database-nav a.selected {
background-color: var(--fire-nav-selected-bg);
color: var(--fire-nav-selected-text);
font-weight: typography.$bold;
}
}
nav.element-water {
ul a.selected,
.database-nav a.selected {
background-color: var(--water-nav-selected-bg);
color: var(--water-nav-selected-text);
font-weight: typography.$bold;
}
}
nav.element-earth {
ul a.selected,
.database-nav a.selected {
background-color: var(--earth-nav-selected-bg);
color: var(--earth-nav-selected-text);
font-weight: typography.$bold;
}
}
nav.element-dark {
ul a.selected,
.database-nav a.selected {
background-color: var(--dark-nav-selected-bg);
color: var(--dark-nav-selected-text);
font-weight: typography.$bold;
}
}
nav.element-light {
ul a.selected,
.database-nav a.selected {
background-color: var(--light-nav-selected-bg);
color: var(--light-nav-selected-text);
font-weight: typography.$bold;
}
}
// Dropdown menu styles // Dropdown menu styles
:global(.dropdown-content) { :global(.dropdown-content) {
background-color: var(--menu-bg); background-color: var(--menu-bg);

View file

@ -49,7 +49,11 @@
<div class="main-pane"> <div class="main-pane">
<div class="nav-blur-background"></div> <div class="nav-blur-background"></div>
<div class="main-navigation"> <div class="main-navigation">
<Navigation isAuthenticated={data?.isAuthenticated} username={data?.account?.username} role={data?.account?.role} /> <Navigation
isAuthenticated={data?.isAuthenticated}
account={data?.account}
currentUser={data?.currentUser}
/>
</div> </div>
<main class="main-content"> <main class="main-content">
{@render children?.()} {@render children?.()}
@ -103,7 +107,7 @@
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
height: 80px; // Taller to test the progressive effect height: 81px; // Matches $nav-height
z-index: 1; // Lower z-index so scrollbar appears above z-index: 1; // Lower z-index so scrollbar appears above
pointer-events: none; pointer-events: none;
transition: right $duration-slide ease-in-out; transition: right $duration-slide ease-in-out;
@ -153,7 +157,7 @@
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
position: relative; position: relative;
padding-top: 80px; // Space for fixed navigation (matching test height) padding-top: 81px; // Space for fixed navigation (matches $nav-height)
z-index: 2; // Ensure scrollbar is above blur background z-index: 2; // Ensure scrollbar is above blur background
// Smooth scrolling // Smooth scrolling