add active unit highlighting with element focus rings

- track active item in sidebar store
- add element accent colors and focus ring mixins
- units show pulsing focus ring when selected in sidebar
- refactor units to use shared menu components
- update context menu test page
This commit is contained in:
Justin Edmund 2025-11-28 11:04:04 -08:00
parent 9a4a863ccd
commit 0379cff81e
7 changed files with 987 additions and 503 deletions

View file

@ -4,12 +4,13 @@
import type { Job } from '$lib/types/api/entities' import type { Job } from '$lib/types/api/entities'
import { getContext } from 'svelte' import { getContext } from 'svelte'
import Icon from '$lib/components/Icon.svelte' import Icon from '$lib/components/Icon.svelte'
import ContextMenu from '$lib/components/ui/ContextMenu.svelte' import UnitMenuContainer from '$lib/components/ui/menu/UnitMenuContainer.svelte'
import { ContextMenu as ContextMenuBase, DropdownMenu as DropdownMenuBase } from 'bits-ui' import MenuItems from '$lib/components/ui/menu/MenuItems.svelte'
import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte' import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte'
import { getCharacterImageWithPose } from '$lib/utils/images' import { getCharacterImageWithPose } from '$lib/utils/images'
import { openDetailsSidebar } from '$lib/features/details/openDetailsSidebar.svelte' import { openDetailsSidebar } from '$lib/features/details/openDetailsSidebar.svelte'
import { getJobPortraitUrl, Gender } from '$lib/utils/jobUtils' import { getJobPortraitUrl, Gender } from '$lib/utils/jobUtils'
import { sidebar } from '$lib/stores/sidebar.svelte'
import perpetuityFilled from '$src/assets/icons/perpetuity/filled.svg' import perpetuityFilled from '$src/assets/icons/perpetuity/filled.svg'
import perpetuityEmpty from '$src/assets/icons/perpetuity/empty.svg' import perpetuityEmpty from '$src/assets/icons/perpetuity/empty.svg'
import * as m from '$lib/paraglide/messages' import * as m from '$lib/paraglide/messages'
@ -70,6 +71,31 @@
// Check if this is the protagonist slot // Check if this is the protagonist slot
const isProtagonist = $derived(position === 0) const isProtagonist = $derived(position === 0)
// Check if this item is currently active in the sidebar
let isActive = $derived(item?.id && sidebar.activeItemId === String(item.id))
// Determine element class for focus ring
let elementClass = $derived.by(() => {
const element = item?.character?.element || partyElement
switch (element) {
case 1:
return 'wind'
case 2:
return 'fire'
case 3:
return 'water'
case 4:
return 'earth'
case 5:
return 'dark'
case 6:
return 'light'
default:
return 'neutral'
}
})
async function remove() { async function remove() {
if (!item?.id) return if (!item?.id) return
try { try {
@ -140,86 +166,88 @@
} }
</script> </script>
<div class="unit" class:empty={!item}> <div class="unit {elementClass}" class:empty={!item} class:is-active={isActive}>
{#if item} {#if item}
<ContextMenu showGearButton={true}> <UnitMenuContainer showGearButton={true}>
{#snippet children()} {#snippet trigger()}
{#key item?.id ?? position} <div
<div class="focus-ring-wrapper {elementClass}"
class="frame character cell" class:is-active={isActive}
class:protagonist={position === 0} class:editable={ctx?.canEdit()}
class:editable={ctx?.canEdit()} >
onclick={() => viewDetails()} {#key item?.id ?? position}
> <div
{#if position !== 0} class="frame character cell {elementClass}"
{#if ctx?.canEdit()} class:protagonist={position === 0}
<button class:editable={ctx?.canEdit()}
class="perpetuity" onclick={() => viewDetails()}
class:active={item.perpetuity} >
onclick={togglePerpetuity} {#if position !== 0}
title={item.perpetuity ? 'Remove Perpetuity Ring' : 'Add Perpetuity Ring'} {#if ctx?.canEdit()}
> <button
class="perpetuity"
class:active={item.perpetuity}
onclick={togglePerpetuity}
title={item.perpetuity ? 'Remove Perpetuity Ring' : 'Add Perpetuity Ring'}
>
<img
class="perpetuity-icon filled"
src={perpetuityFilled}
alt="Perpetuity Ring"
/>
<img
class="perpetuity-icon empty"
src={perpetuityEmpty}
alt="Add Perpetuity Ring"
/>
</button>
{:else if item.perpetuity}
<img <img
class="perpetuity-icon filled" class="perpetuity static"
src={perpetuityFilled} src={perpetuityFilled}
alt="Perpetuity Ring" alt="Perpetuity Ring"
title="Perpetuity Ring"
/> />
<img {/if}
class="perpetuity-icon empty"
src={perpetuityEmpty}
alt="Add Perpetuity Ring"
/>
</button>
{:else if item.perpetuity}
<img
class="perpetuity static"
src={perpetuityFilled}
alt="Perpetuity Ring"
title="Perpetuity Ring"
/>
{/if} {/if}
{/if} <img
<img class="image {elementClass}"
class="image" class:placeholder={!item?.character?.granblueId && !isProtagonist}
class:placeholder={!item?.character?.granblueId && !isProtagonist} class:protagonist={isProtagonist}
class:protagonist={isProtagonist} alt={isProtagonist && job ? job.name.en : displayName(item?.character)}
alt={isProtagonist && job ? job.name.en : displayName(item?.character)} src={imageUrl}
src={imageUrl} />
/> </div>
</div> {/key}
{/key} </div>
{/snippet} {/snippet}
{#snippet contextMenu()} {#snippet contextMenu()}
<ContextMenuBase.Item class="context-menu-item" onclick={viewDetails}> <MenuItems
{m.context_view_details()} onViewDetails={viewDetails}
</ContextMenuBase.Item> onReplace={ctx?.canEdit() ? replace : undefined}
{#if ctx?.canEdit()} onRemove={ctx?.canEdit() ? remove : undefined}
<ContextMenuBase.Item class="context-menu-item" onclick={replace}> canEdit={ctx?.canEdit()}
{m.context_replace()} variant="context"
</ContextMenuBase.Item> viewDetailsLabel={m.context_view_details()}
<ContextMenuBase.Separator class="context-menu-separator" /> replaceLabel={m.context_replace()}
<ContextMenuBase.Item class="context-menu-item danger" onclick={remove}> removeLabel={m.context_remove()}
{m.context_remove()} />
</ContextMenuBase.Item>
{/if}
{/snippet} {/snippet}
{#snippet dropdownMenu()} {#snippet dropdownMenu()}
<DropdownMenuBase.Item class="dropdown-menu-item" onclick={viewDetails}> <MenuItems
{m.context_view_details()} onViewDetails={viewDetails}
</DropdownMenuBase.Item> onReplace={ctx?.canEdit() ? replace : undefined}
{#if ctx?.canEdit()} onRemove={ctx?.canEdit() ? remove : undefined}
<DropdownMenuBase.Item class="dropdown-menu-item" onclick={replace}> canEdit={ctx?.canEdit()}
{m.context_replace()} variant="dropdown"
</DropdownMenuBase.Item> viewDetailsLabel={m.context_view_details()}
<DropdownMenuBase.Separator class="dropdown-menu-separator" /> replaceLabel={m.context_replace()}
<DropdownMenuBase.Item class="dropdown-menu-item danger" onclick={remove}> removeLabel={m.context_remove()}
{m.context_remove()} />
</DropdownMenuBase.Item>
{/if}
{/snippet} {/snippet}
</ContextMenu> </UnitMenuContainer>
{:else} {:else}
{#key `empty-${position}`} {#key `empty-${position}`}
<div <div
@ -301,10 +329,9 @@
</div> </div>
<style lang="scss"> <style lang="scss">
@use '$src/themes/colors' as *; @use '$src/themes/colors' as colors;
@use '$src/themes/spacing' as spacing; @use '$src/themes/spacing' as spacing;
@use '$src/themes/typography' as *; @use '$src/themes/typography' as typography;
@use '$src/themes/spacing' as *;
@use '$src/themes/rep' as rep; @use '$src/themes/rep' as rep;
.unit { .unit {
@ -313,27 +340,44 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: $unit; gap: spacing.$unit;
&.empty .name { &.empty .name {
display: none; display: none;
} }
} }
.focus-ring-wrapper {
position: relative;
display: block;
transition: transform 0.2s ease-in-out;
&::before {
content: '';
position: absolute;
inset: 0;
border-radius: 8px;
pointer-events: none;
z-index: 10;
}
&.editable:hover {
transform: scale(1.05);
}
}
.frame { .frame {
position: relative; position: relative;
width: 100%; width: 100%;
overflow: visible;
border-radius: 8px; border-radius: 8px;
background: var(--card-bg, #f5f5f5); background: var(--card-bg, #f5f5f5);
border: 1px solid transparent; transition: opacity 0.2s ease-in-out;
transition: all 0.2s ease-in-out;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
cursor: pointer; cursor: pointer;
&:hover { &.editable:hover {
opacity: 0.95; opacity: 0.95;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
} }
@ -388,14 +432,14 @@
} }
.name { .name {
font-size: $font-small; font-size: typography.$font-small;
text-align: center; text-align: center;
color: $grey-50; color: colors.$grey-50;
} }
.perpetuity { .perpetuity {
position: absolute; position: absolute;
z-index: 20; z-index: 40;
top: calc(spacing.$unit * -1); top: calc(spacing.$unit * -1);
right: spacing.$unit-3x; right: spacing.$unit-3x;
width: spacing.$unit-4x; width: spacing.$unit-4x;
@ -461,4 +505,92 @@
display: none; display: none;
} }
} }
// Pulsing focus ring animation
@keyframes pulse-focus-ring {
0%,
100% {
box-shadow: 0 0 4px 3px currentColor;
}
50% {
box-shadow: 0 0 4px 6px currentColor;
}
}
// Element-specific focus rings
.focus-ring-wrapper.is-active::before {
animation: pulse-focus-ring 2s ease-in-out infinite;
}
.focus-ring-wrapper.is-active {
&.fire::before {
@include colors.focus-ring-fire();
color: rgba(250, 109, 109, 0.2);
}
&.water::before {
@include colors.focus-ring-water();
color: rgba(108, 201, 255, 0.2);
}
&.earth::before {
@include colors.focus-ring-earth();
color: rgba(253, 159, 91, 0.2);
}
&.wind::before {
@include colors.focus-ring-wind();
color: rgba(62, 228, 137, 0.2);
}
&.light::before {
@include colors.focus-ring-light();
color: rgba(232, 214, 51, 0.2);
}
&.dark::before {
@include colors.focus-ring-dark();
color: rgba(222, 123, 255, 0.2);
}
&.neutral::before {
@include colors.focus-ring-neutral();
color: rgba(0, 0, 0, 0.1);
}
}
// Element-specific name colors when active
.unit.is-active {
.name {
font-weight: typography.$bold;
}
&.fire .name {
color: colors.$fire--text--light;
}
&.water .name {
color: colors.$water--text--light;
}
&.earth .name {
color: colors.$earth--text--light;
}
&.wind .name {
color: colors.$wind--text--light;
}
&.light .name {
color: colors.$light--text--light;
}
&.dark .name {
color: colors.$dark--text--light;
}
&.neutral .name {
color: colors.$grey-40;
}
}
</style> </style>

View file

@ -3,11 +3,12 @@
import type { Party } from '$lib/types/api/party' import type { Party } from '$lib/types/api/party'
import { getContext } from 'svelte' import { getContext } from 'svelte'
import Icon from '$lib/components/Icon.svelte' import Icon from '$lib/components/Icon.svelte'
import ContextMenu from '$lib/components/ui/ContextMenu.svelte' import UnitMenuContainer from '$lib/components/ui/menu/UnitMenuContainer.svelte'
import { ContextMenu as ContextMenuBase, DropdownMenu as DropdownMenuBase } from 'bits-ui' import MenuItems from '$lib/components/ui/menu/MenuItems.svelte'
import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte' import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte'
import { getSummonImage } from '$lib/features/database/detail/image' import { getSummonImage } from '$lib/features/database/detail/image'
import { openDetailsSidebar } from '$lib/features/details/openDetailsSidebar.svelte' import { openDetailsSidebar } from '$lib/features/details/openDetailsSidebar.svelte'
import { sidebar } from '$lib/stores/sidebar.svelte'
import * as m from '$lib/paraglide/messages' import * as m from '$lib/paraglide/messages'
interface Props { interface Props {
@ -43,6 +44,24 @@
return getSummonImage(item?.summon?.granblueId, variant) return getSummonImage(item?.summon?.granblueId, variant)
}) })
// Check if this item is currently active in the sidebar
let isActive = $derived(item?.id && sidebar.activeItemId === String(item.id))
// Determine element class for focus ring
let elementClass = $derived.by(() => {
const element = item?.summon?.element
switch(element) {
case 1: return 'wind'
case 2: return 'fire'
case 3: return 'water'
case 4: return 'earth'
case 5: return 'dark'
case 6: return 'light'
default: return 'neutral'
}
})
async function remove() { async function remove() {
if (!item?.id) return if (!item?.id) return
try { try {
@ -74,21 +93,22 @@
</script> </script>
<div class="unit" class:empty={!item}> <div class="unit {elementClass}" class:empty={!item} class:is-active={isActive}>
{#if item} {#if item}
<ContextMenu showGearButton={true}> <UnitMenuContainer showGearButton={true}>
{#snippet children()} {#snippet trigger()}
{#key item?.id ?? position} <div class="focus-ring-wrapper {elementClass}" class:is-active={isActive} class:editable={ctx?.canEdit()}>
<div {#key item?.id ?? position}
class="frame summon" <div
class:main={item?.main || position === -1} class="frame summon {elementClass}"
class:friend={item?.friend || position === 6} class:main={item?.main || position === -1}
class:cell={!((item?.main || position === -1) || (item?.friend || position === 6))} class:friend={item?.friend || position === 6}
class:editable={ctx?.canEdit()} class:cell={!((item?.main || position === -1) || (item?.friend || position === 6))}
onclick={() => viewDetails()} class:editable={ctx?.canEdit()}
> onclick={() => viewDetails()}
>
<img <img
class="image" class="image {elementClass}"
class:placeholder={!item?.summon?.granblueId} class:placeholder={!item?.summon?.granblueId}
alt={displayName(item?.summon)} alt={displayName(item?.summon)}
src={imageUrl} src={imageUrl}
@ -100,39 +120,36 @@
<span class="badge" style="left:auto; right:6px">Friend</span> <span class="badge" style="left:auto; right:6px">Friend</span>
{/if} {/if}
</div> </div>
{/key} {/key}
</div>
{/snippet} {/snippet}
{#snippet contextMenu()} {#snippet contextMenu()}
<ContextMenuBase.Item class="context-menu-item" onclick={viewDetails}> <MenuItems
{m.context_view_details()} onViewDetails={viewDetails}
</ContextMenuBase.Item> onReplace={ctx?.canEdit() ? replace : undefined}
{#if ctx?.canEdit()} onRemove={ctx?.canEdit() ? remove : undefined}
<ContextMenuBase.Item class="context-menu-item" onclick={replace}> canEdit={ctx?.canEdit()}
{m.context_replace()} variant="context"
</ContextMenuBase.Item> viewDetailsLabel={m.context_view_details()}
<ContextMenuBase.Separator class="context-menu-separator" /> replaceLabel={m.context_replace()}
<ContextMenuBase.Item class="context-menu-item danger" onclick={remove}> removeLabel={m.context_remove()}
{m.context_remove()} />
</ContextMenuBase.Item>
{/if}
{/snippet} {/snippet}
{#snippet dropdownMenu()} {#snippet dropdownMenu()}
<DropdownMenuBase.Item class="dropdown-menu-item" onclick={viewDetails}> <MenuItems
{m.context_view_details()} onViewDetails={viewDetails}
</DropdownMenuBase.Item> onReplace={ctx?.canEdit() ? replace : undefined}
{#if ctx?.canEdit()} onRemove={ctx?.canEdit() ? remove : undefined}
<DropdownMenuBase.Item class="dropdown-menu-item" onclick={replace}> canEdit={ctx?.canEdit()}
{m.context_replace()} variant="dropdown"
</DropdownMenuBase.Item> viewDetailsLabel={m.context_view_details()}
<DropdownMenuBase.Separator class="dropdown-menu-separator" /> replaceLabel={m.context_replace()}
<DropdownMenuBase.Item class="dropdown-menu-item danger" onclick={remove}> removeLabel={m.context_remove()}
{m.context_remove()} />
</DropdownMenuBase.Item>
{/if}
{/snippet} {/snippet}
</ContextMenu> </UnitMenuContainer>
{:else} {:else}
{#key `empty-${position}`} {#key `empty-${position}`}
<div <div
@ -199,9 +216,9 @@
</div> </div>
<style lang="scss"> <style lang="scss">
@use '$src/themes/colors' as *; @use '$src/themes/colors' as colors;
@use '$src/themes/typography' as *; @use '$src/themes/typography' as typography;
@use '$src/themes/spacing' as *; @use '$src/themes/spacing' as spacing;
@use '$src/themes/rep' as rep; @use '$src/themes/rep' as rep;
.unit { .unit {
@ -210,27 +227,45 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: $unit; gap: spacing.$unit;
&.empty .name { &.empty .name {
display: none; display: none;
} }
} }
.focus-ring-wrapper {
position: relative;
display: block;
transition: transform 0.2s ease-in-out;
&::before {
content: '';
position: absolute;
inset: 0;
border-radius: 8px;
pointer-events: none;
z-index: 10;
}
&.editable:hover {
transform: scale(1.05);
}
}
.frame { .frame {
position: relative; position: relative;
width: 100%; width: 100%;
overflow: hidden; overflow: hidden;
border-radius: 8px; border-radius: 8px;
background: var(--card-bg, #f5f5f5); background: var(--card-bg, #f5f5f5);
border: 1px solid transparent; transition: opacity 0.2s ease-in-out;
transition: all 0.2s ease-in-out;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
cursor: pointer; cursor: pointer;
&:hover { &.editable:hover {
opacity: 0.95; opacity: 0.95;
box-shadow: 0 2px 4px rgba(0,0,0,0.1); box-shadow: 0 2px 4px rgba(0,0,0,0.1);
} }
@ -270,9 +305,9 @@
} }
.name { .name {
font-size: $font-small; font-size: typography.$font-small;
text-align: center; text-align: center;
color: $grey-50; color: colors.$grey-50;
} }
.badge { .badge {
@ -286,4 +321,91 @@
border-radius: 10px; border-radius: 10px;
z-index: 3; z-index: 3;
} }
// Pulsing focus ring animation
@keyframes pulse-focus-ring {
0%, 100% {
box-shadow: 0 0 4px 3px currentColor;
}
50% {
box-shadow: 0 0 4px 6px currentColor;
}
}
// Element-specific focus rings
.focus-ring-wrapper.is-active::before {
animation: pulse-focus-ring 2s ease-in-out infinite;
}
.focus-ring-wrapper.is-active {
&.fire::before {
@include colors.focus-ring-fire();
color: rgba(250, 109, 109, 0.2);
}
&.water::before {
@include colors.focus-ring-water();
color: rgba(108, 201, 255, 0.2);
}
&.earth::before {
@include colors.focus-ring-earth();
color: rgba(253, 159, 91, 0.2);
}
&.wind::before {
@include colors.focus-ring-wind();
color: rgba(62, 228, 137, 0.2);
}
&.light::before {
@include colors.focus-ring-light();
color: rgba(232, 214, 51, 0.2);
}
&.dark::before {
@include colors.focus-ring-dark();
color: rgba(222, 123, 255, 0.2);
}
&.neutral::before {
@include colors.focus-ring-neutral();
color: rgba(0, 0, 0, 0.1);
}
}
// Element-specific name colors when active
.unit.is-active {
.name {
font-weight: typography.$bold;
}
&.fire .name {
color: colors.$fire--text--light;
}
&.water .name {
color: colors.$water--text--light;
}
&.earth .name {
color: colors.$earth--text--light;
}
&.wind .name {
color: colors.$wind--text--light;
}
&.light .name {
color: colors.$light--text--light;
}
&.dark .name {
color: colors.$dark--text--light;
}
&.neutral .name {
color: colors.$grey-40;
}
}
</style> </style>

View file

@ -1,390 +1,548 @@
<script lang="ts"> <script lang="ts">
import type { GridWeapon } from '$lib/types/api/party' import type { GridWeapon } from '$lib/types/api/party'
import type { Party } from '$lib/types/api/party' import type { Party } from '$lib/types/api/party'
import { getContext } from 'svelte' import { getContext } from 'svelte'
import Icon from '$lib/components/Icon.svelte' import Icon from '$lib/components/Icon.svelte'
import ContextMenu from '$lib/components/ui/ContextMenu.svelte' import UnitMenuContainer from '$lib/components/ui/menu/UnitMenuContainer.svelte'
import { ContextMenu as ContextMenuBase, DropdownMenu as DropdownMenuBase } from 'bits-ui' import MenuItems from '$lib/components/ui/menu/MenuItems.svelte'
import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte' import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte'
import { getWeaponImage } from '$lib/features/database/detail/image' import { getWeaponImage } from '$lib/features/database/detail/image'
import { openDetailsSidebar } from '$lib/features/details/openDetailsSidebar.svelte' import { openDetailsSidebar } from '$lib/features/details/openDetailsSidebar.svelte'
import { getAwakeningImage, getWeaponKeyImages, getAxSkillImages } from '$lib/utils/modifiers' import { getAwakeningImage, getWeaponKeyImages, getAxSkillImages } from '$lib/utils/modifiers'
import * as m from '$lib/paraglide/messages' import { sidebar } from '$lib/stores/sidebar.svelte'
import * as m from '$lib/paraglide/messages'
interface Props { interface Props {
item?: GridWeapon item?: GridWeapon
position: number position: number
} }
let { item, position }: Props = $props() let { item, position }: Props = $props()
type PartyCtx = { type PartyCtx = {
getParty: () => Party getParty: () => Party
updateParty: (p: Party) => void updateParty: (p: Party) => void
canEdit: () => boolean canEdit: () => boolean
getEditKey: () => string | null getEditKey: () => string | null
services: { gridService: any; partyService: any } services: { gridService: any; partyService: any }
} }
const ctx = getContext<PartyCtx>('party') const ctx = getContext<PartyCtx>('party')
function displayName(input: any): string { function displayName(input: any): string {
if (!input) return '—' if (!input) return '—'
const maybe = input.name ?? input const maybe = input.name ?? input
if (typeof maybe === 'string') return maybe if (typeof maybe === 'string') return maybe
if (maybe && typeof maybe === 'object') return maybe.en || maybe.ja || '—' if (maybe && typeof maybe === 'object') return maybe.en || maybe.ja || '—'
return '—' return '—'
} }
// Use $derived to ensure consistent computation between server and client // Use $derived to ensure consistent computation between server and client
let imageUrl = $derived.by(() => { let imageUrl = $derived.by(() => {
const isMain = position === -1 || item?.mainhand const isMain = position === -1 || item?.mainhand
const variant = isMain ? 'main' : 'grid' const variant = isMain ? 'main' : 'grid'
// For weapons with null element that have an instance element, use it // For weapons with null element that have an instance element, use it
const element = (item?.weapon?.element === 0 && item?.element) ? item.element : undefined const element = item?.weapon?.element === 0 && item?.element ? item.element : undefined
return getWeaponImage(item?.weapon?.granblueId, variant, element) return getWeaponImage(item?.weapon?.granblueId, variant, element)
}) })
// Get awakening image URL using utility // Get awakening image URL using utility
let awakeningImage = $derived(getAwakeningImage(item?.awakening)) let awakeningImage = $derived(getAwakeningImage(item?.awakening))
// Get weapon key images using utility // Get weapon key images using utility
let weaponKeyImages = $derived( let weaponKeyImages = $derived(
getWeaponKeyImages( getWeaponKeyImages(
item?.weaponKeys, item?.weaponKeys,
item?.weapon?.element, item?.weapon?.element,
item?.weapon?.proficiency, item?.weapon?.proficiency,
item?.weapon?.series, item?.weapon?.series,
item?.weapon?.name item?.weapon?.name
) )
) )
// Get AX skill images using utility // Get AX skill images using utility
let axSkillImages = $derived(getAxSkillImages(item?.ax)) let axSkillImages = $derived(getAxSkillImages(item?.ax))
async function remove() { // Check if this item is currently active in the sidebar
if (!item?.id) return let isActive = $derived(item?.id && sidebar.activeItemId === String(item.id))
try {
const party = ctx.getParty()
const editKey = ctx.getEditKey()
const updated = await ctx.services.gridService.removeWeapon(party.id, item.id as any, editKey || undefined)
if (updated) {
ctx.updateParty(updated)
}
} catch (err) {
console.error('Error removing weapon:', err)
}
}
function viewDetails() { // Determine element class for focus ring
if (!item) return let elementClass = $derived.by(() => {
openDetailsSidebar({ // For weapons with null element that have an instance element, use it
type: 'weapon', const element =
item item?.weapon?.element === 0 && item?.element ? item.element : item?.weapon?.element
})
}
function replace() { switch (element) {
if (ctx?.openPicker) { case 1:
ctx.openPicker({ type: 'weapon', position, item }) return 'wind'
} case 2:
} return 'fire'
case 3:
return 'water'
case 4:
return 'earth'
case 5:
return 'dark'
case 6:
return 'light'
default:
return 'neutral'
}
})
async function remove() {
if (!item?.id) return
try {
const party = ctx.getParty()
const editKey = ctx.getEditKey()
const updated = await ctx.services.gridService.removeWeapon(
party.id,
item.id as any,
editKey || undefined
)
if (updated) {
ctx.updateParty(updated)
}
} catch (err) {
console.error('Error removing weapon:', err)
}
}
function viewDetails() {
if (!item) return
openDetailsSidebar({
type: 'weapon',
item
})
}
function replace() {
if (ctx?.openPicker) {
ctx.openPicker({ type: 'weapon', position, item })
}
}
</script> </script>
<div class="unit" class:empty={!item} class:extra={position >= 9}> <div
{#if item} class="unit {elementClass}"
<ContextMenu showGearButton={true}> class:empty={!item}
{#snippet children()} class:extra={position >= 9}
{#key item?.id ?? position} class:is-active={isActive}
<div >
class="frame weapon" {#if item}
class:main={item?.mainhand || position === -1} <UnitMenuContainer showGearButton={true}>
class:cell={!(item?.mainhand || position === -1)} {#snippet trigger()}
class:extra={position >= 9} <div
class:editable={ctx?.canEdit()} class="focus-ring-wrapper {elementClass}"
onclick={() => viewDetails()} class:is-active={isActive}
> class:editable={ctx?.canEdit()}
<div class="modifiers"> class:main={item?.mainhand || position === -1}
{#if awakeningImage} >
<img {#key item?.id ?? position}
class="awakening" <div
src={awakeningImage} class="frame weapon {elementClass}"
alt={`${item?.awakening?.type?.name?.en || 'Awakening'} Lv${item?.awakening?.level || 0}`} class:main={item?.mainhand || position === -1}
/> class:cell={!(item?.mainhand || position === -1)}
{/if} class:extra={position >= 9}
<div class="skills"> class:editable={ctx?.canEdit()}
{#each axSkillImages as skill} onclick={() => viewDetails()}
<img class="skill" src={skill.url} alt={skill.alt} /> >
{/each} <div class="modifiers">
{#each weaponKeyImages as skill} {#if awakeningImage}
<img class="skill" src={skill.url} alt={skill.alt} /> <img
{/each} class="awakening"
</div> src={awakeningImage}
</div> alt={`${item?.awakening?.type?.name?.en || 'Awakening'} Lv${item?.awakening?.level || 0}`}
<img />
class="image" {/if}
class:placeholder={!item?.weapon?.granblueId} <div class="skills">
alt={displayName(item?.weapon)} {#each axSkillImages as skill}
src={imageUrl} <img class="skill" src={skill.url} alt={skill.alt} />
/> {/each}
</div> {#each weaponKeyImages as skill}
{/key} <img class="skill" src={skill.url} alt={skill.alt} />
{/snippet} {/each}
</div>
</div>
<img
class="image {elementClass}"
class:placeholder={!item?.weapon?.granblueId}
alt={displayName(item?.weapon)}
src={imageUrl}
/>
</div>
{/key}
</div>
{/snippet}
{#snippet contextMenu()} {#snippet contextMenu()}
<ContextMenuBase.Item class="context-menu-item" onclick={viewDetails}> <MenuItems
{m.context_view_details()} onViewDetails={viewDetails}
</ContextMenuBase.Item> onReplace={ctx?.canEdit() ? replace : undefined}
{#if ctx?.canEdit()} onRemove={ctx?.canEdit() ? remove : undefined}
<ContextMenuBase.Item class="context-menu-item" onclick={replace}> canEdit={ctx?.canEdit()}
{m.context_replace()} variant="context"
</ContextMenuBase.Item> viewDetailsLabel={m.context_view_details()}
<ContextMenuBase.Separator class="context-menu-separator" /> replaceLabel={m.context_replace()}
<ContextMenuBase.Item class="context-menu-item danger" onclick={remove}> removeLabel={m.context_remove()}
{m.context_remove()} />
</ContextMenuBase.Item> {/snippet}
{/if}
{/snippet}
{#snippet dropdownMenu()} {#snippet dropdownMenu()}
<DropdownMenuBase.Item class="dropdown-menu-item" onclick={viewDetails}> <MenuItems
{m.context_view_details()} onViewDetails={viewDetails}
</DropdownMenuBase.Item> onReplace={ctx?.canEdit() ? replace : undefined}
{#if ctx?.canEdit()} onRemove={ctx?.canEdit() ? remove : undefined}
<DropdownMenuBase.Item class="dropdown-menu-item" onclick={replace}> canEdit={ctx?.canEdit()}
{m.context_replace()} variant="dropdown"
</DropdownMenuBase.Item> viewDetailsLabel={m.context_view_details()}
<DropdownMenuBase.Separator class="dropdown-menu-separator" /> replaceLabel={m.context_replace()}
<DropdownMenuBase.Item class="dropdown-menu-item danger" onclick={remove}> removeLabel={m.context_remove()}
{m.context_remove()} />
</DropdownMenuBase.Item> {/snippet}
{/if} </UnitMenuContainer>
{/snippet} {:else}
</ContextMenu> {#key `empty-${position}`}
{:else} <div
{#key `empty-${position}`} class="frame weapon"
<div class:main={position === -1}
class="frame weapon" class:cell={position !== -1}
class:main={position === -1} class:extra={position >= 9}
class:cell={position !== -1} class:editable={ctx?.canEdit()}
class:extra={position >= 9} onclick={() =>
class:editable={ctx?.canEdit()} ctx?.canEdit() && ctx?.openPicker && ctx.openPicker({ type: 'weapon', position, item })}
onclick={() => ctx?.canEdit() && ctx?.openPicker && ctx.openPicker({ type: 'weapon', position, item })} >
> <img
<img class="image placeholder"
class="image placeholder" alt=""
alt="" src={position === -1
src={position === -1 ? '/images/placeholders/placeholder-weapon-main.png' : '/images/placeholders/placeholder-weapon-grid.png'} ? '/images/placeholders/placeholder-weapon-main.png'
/> : '/images/placeholders/placeholder-weapon-grid.png'}
{#if ctx?.canEdit()} />
<span class="icon"> {#if ctx?.canEdit()}
<Icon name="plus" size={24} /> <span class="icon">
</span> <Icon name="plus" size={24} />
{/if} </span>
</div> {/if}
{/key} </div>
{/if} {/key}
{#if item} {/if}
<UncapIndicator {#if item}
type="weapon" <UncapIndicator
uncapLevel={item.uncapLevel} type="weapon"
transcendenceStage={item.transcendenceStep} uncapLevel={item.uncapLevel}
flb={item.weapon?.uncap?.flb} transcendenceStage={item.transcendenceStep}
ulb={item.weapon?.uncap?.ulb} flb={item.weapon?.uncap?.flb}
transcendence={item.weapon?.uncap?.transcendence} ulb={item.weapon?.uncap?.ulb}
editable={ctx?.canEdit()} transcendence={item.weapon?.uncap?.transcendence}
updateUncap={async (level) => { editable={ctx?.canEdit()}
if (!item?.id || !ctx) return updateUncap={async (level) => {
try { if (!item?.id || !ctx) return
const editKey = ctx.getEditKey() try {
const updated = await ctx.services.gridService.updateWeaponUncap(item.id, level, undefined, editKey || undefined) const editKey = ctx.getEditKey()
if (updated) { const updated = await ctx.services.gridService.updateWeaponUncap(
ctx.updateParty(updated) item.id,
} level,
} catch (err) { undefined,
console.error('Failed to update weapon uncap:', err) editKey || undefined
// TODO: Show user-friendly error notification )
} if (updated) {
}} ctx.updateParty(updated)
updateTranscendence={async (stage) => { }
if (!item?.id || !ctx) return } catch (err) {
try { console.error('Failed to update weapon uncap:', err)
const editKey = ctx.getEditKey() // TODO: Show user-friendly error notification
// When setting transcendence > 0, also set uncap to max (6) }
const maxUncap = stage > 0 ? 6 : undefined }}
const updated = await ctx.services.gridService.updateWeaponUncap(item.id, maxUncap, stage, editKey || undefined) updateTranscendence={async (stage) => {
if (updated) { if (!item?.id || !ctx) return
ctx.updateParty(updated) try {
} const editKey = ctx.getEditKey()
} catch (err) { // When setting transcendence > 0, also set uncap to max (6)
console.error('Failed to update weapon transcendence:', err) const maxUncap = stage > 0 ? 6 : undefined
// TODO: Show user-friendly error notification const updated = await ctx.services.gridService.updateWeaponUncap(
} item.id,
}} maxUncap,
/> stage,
{/if} editKey || undefined
<div class="name">{item ? displayName(item?.weapon) : ''}</div> )
if (updated) {
ctx.updateParty(updated)
}
} catch (err) {
console.error('Failed to update weapon transcendence:', err)
// TODO: Show user-friendly error notification
}
}}
/>
{/if}
<div class="name">{item ? displayName(item?.weapon) : ''}</div>
</div> </div>
<style lang="scss"> <style lang="scss">
@use '$src/themes/colors' as *; @use '$src/themes/colors' as colors;
@use '$src/themes/typography' as *; @use '$src/themes/typography' as typography;
@use '$src/themes/spacing' as *; @use '$src/themes/spacing' as spacing;
@use '$src/themes/layout' as *; @use '$src/themes/layout' as layout;
@use '$src/themes/rep' as rep; @use '$src/themes/rep' as rep;
.unit { .unit {
position: relative; position: relative;
width: 100%; width: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: $unit; gap: spacing.$unit;
&.empty .name { &.empty .name {
display: none; display: none;
} }
&.extra { &.extra {
.frame { .frame {
background: var(--extra-purple-card-bg); background: var(--extra-purple-card-bg);
} }
.icon { .icon {
color: var(--extra-purple-secondary); color: var(--extra-purple-secondary);
} }
&:hover .icon { &:hover .icon {
color: var(--extra-purple-primary); color: var(--extra-purple-primary);
} }
.name { .name {
font-weight: $medium; font-weight: typography.$medium;
color: var(--extra-purple-text); color: var(--extra-purple-text);
} }
} }
} }
.frame { .focus-ring-wrapper {
position: relative; position: relative;
width: 100%; display: block;
overflow: hidden; transition: transform 0.2s ease-in-out;
border-radius: 8px;
background: var(--card-bg, #f5f5f5);
border: 1px solid transparent;
transition: all 0.2s ease-in-out;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
&.editable:hover { &::before {
opacity: 0.95; content: '';
box-shadow: 0 2px 4px rgba(0,0,0,0.1); position: absolute;
transform: $scale-wide; inset: 0;
} border-radius: 8px;
} pointer-events: none;
z-index: 10;
}
.frame.weapon.main { &.editable:hover {
@include rep.aspect(rep.$weapon-main-w, rep.$weapon-main-h); transform: layout.$scale-wide;
}
&.editable:hover { &.editable.main:hover {
transform: $scale-tall; transform: layout.$scale-tall;
} }
} }
.frame.weapon.cell { @include rep.aspect(rep.$weapon-cell-w, rep.$weapon-cell-h); } .frame {
position: relative;
width: 100%;
overflow: hidden;
border-radius: 8px;
background: var(--card-bg, #f5f5f5);
transition: opacity 0.2s ease-in-out;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
.image { &.editable:hover {
position: relative; opacity: 0.95;
width: 100%; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
height: 100%; }
object-fit: cover; }
display: block;
z-index: 2;
&.placeholder { .frame.weapon.main {
opacity: 0; @include rep.aspect(rep.$weapon-main-w, rep.$weapon-main-h);
} }
}
.icon { .frame.weapon.cell {
position: absolute; @include rep.aspect(rep.$weapon-cell-w, rep.$weapon-cell-h);
z-index: 1; }
color: var(--icon-secondary, #999);
transition: color 0.2s ease-in-out;
}
.frame.editable:hover .icon { .image {
color: var(--icon-secondary-hover, #666); position: relative;
} width: 100%;
height: 100%;
object-fit: cover;
display: block;
z-index: 2;
.name { &.placeholder {
font-size: $font-small; opacity: 0;
text-align: center; }
color: $grey-50; }
}
.modifiers { .icon {
position: absolute; position: absolute;
width: 100%; z-index: 1;
height: 100%; color: var(--icon-secondary, #999);
z-index: 3; transition: color 0.2s ease-in-out;
pointer-events: none; }
.awakening { .frame.editable:hover .icon {
position: absolute; color: var(--icon-secondary-hover, #666);
width: 30%; }
height: auto;
}
.skills { .name {
position: absolute; font-size: typography.$font-small;
display: flex; text-align: center;
gap: calc($unit / 4); color: colors.$grey-50;
padding: calc($unit / 2); }
.skill { .modifiers {
width: 20%; position: absolute;
height: auto; width: 100%;
} height: 100%;
} z-index: 3;
} pointer-events: none;
// Position modifiers for grid weapons .awakening {
.frame.weapon.cell { position: absolute;
.awakening { width: 30%;
top: 14%; height: auto;
left: -3.5%; }
}
.skills { .skills {
bottom: 2%; position: absolute;
right: 2%; display: flex;
justify-content: flex-end; gap: calc(spacing.$unit / 4);
} padding: calc(spacing.$unit / 2);
}
// Position modifiers for main weapons .skill {
.frame.weapon.main { width: 20%;
.awakening { height: auto;
width: 40%; }
top: 67%; }
left: -3.5%; }
}
.skills { // Position modifiers for grid weapons
bottom: 12%; .frame.weapon.cell {
right: -3.5%; .awakening {
justify-content: flex-end; top: 14%;
left: -3.5%;
}
.skill { .skills {
width: 25%; bottom: 2%;
} right: 2%;
} justify-content: flex-end;
} }
}
// Position modifiers for main weapons
.frame.weapon.main {
.awakening {
width: 40%;
top: 67%;
left: -3.5%;
}
.skills {
bottom: 12%;
right: -3.5%;
justify-content: flex-end;
.skill {
width: 25%;
}
}
}
// Pulsing focus ring animation
@keyframes pulse-focus-ring {
0%,
100% {
box-shadow: 0 0 4px 3px currentColor;
}
50% {
box-shadow: 0 0 4px 6px currentColor;
}
}
// Element-specific focus rings
.focus-ring-wrapper.is-active::before {
animation: pulse-focus-ring 2s ease-in-out infinite;
}
.focus-ring-wrapper.is-active {
&.fire::before {
@include colors.focus-ring-fire();
color: rgba(250, 109, 109, 0.2);
}
&.water::before {
@include colors.focus-ring-water();
color: rgba(108, 201, 255, 0.2);
}
&.earth::before {
@include colors.focus-ring-earth();
color: rgba(253, 159, 91, 0.2);
}
&.wind::before {
@include colors.focus-ring-wind();
color: rgba(62, 228, 137, 0.2);
}
&.light::before {
@include colors.focus-ring-light();
color: rgba(232, 214, 51, 0.2);
}
&.dark::before {
@include colors.focus-ring-dark();
color: rgba(222, 123, 255, 0.2);
}
&.neutral::before {
@include colors.focus-ring-neutral();
color: rgba(0, 0, 0, 0.1);
}
}
// Element-specific name colors when active
.unit.is-active {
.name {
font-weight: typography.$bold;
}
&.fire .name {
color: colors.$fire--text--light;
}
&.water .name {
color: colors.$water--text--light;
}
&.earth .name {
color: colors.$earth--text--light;
}
&.wind .name {
color: colors.$wind--text--light;
}
&.light .name {
color: colors.$light--text--light;
}
&.dark .name {
color: colors.$dark--text--light;
}
&.neutral .name {
color: colors.$grey-40;
}
}
</style> </style>

View file

@ -10,6 +10,7 @@ interface SidebarState {
component?: Component component?: Component
componentProps?: Record<string, any> componentProps?: Record<string, any>
scrollable?: boolean scrollable?: boolean
activeItemId?: string
} }
class SidebarStore { class SidebarStore {
@ -31,17 +32,27 @@ class SidebarStore {
this.state.scrollable = scrollable this.state.scrollable = scrollable
} }
openWithComponent(title: string, component: Component, props?: Record<string, any>, scrollable = true) { openWithComponent(
title: string,
component: Component,
props?: Record<string, any>,
scrollable = true
) {
this.state.open = true this.state.open = true
this.state.title = title this.state.title = title
this.state.component = component this.state.component = component
this.state.componentProps = props this.state.componentProps = props
this.state.content = undefined this.state.content = undefined
this.state.scrollable = scrollable this.state.scrollable = scrollable
// Extract and store the item ID if it's a details sidebar
if (props?.item?.id) {
this.state.activeItemId = String(props.item.id)
}
} }
close() { close() {
this.state.open = false this.state.open = false
this.state.activeItemId = undefined
// Clear content after animation // Clear content after animation
setTimeout(() => { setTimeout(() => {
this.state.title = undefined this.state.title = undefined
@ -82,6 +93,10 @@ class SidebarStore {
get scrollable() { get scrollable() {
return this.state.scrollable ?? true return this.state.scrollable ?? true
} }
get activeItemId() {
return this.state.activeItemId
}
} }
export const sidebar = new SidebarStore() export const sidebar = new SidebarStore()

View file

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import ContextMenu from '$lib/components/ui/ContextMenu.svelte' import UnitMenuContainer from '$lib/components/ui/menu/UnitMenuContainer.svelte'
import { ContextMenu as ContextMenuBase, DropdownMenu as DropdownMenuBase } from 'bits-ui' import MenuItems from '$lib/components/ui/menu/MenuItems.svelte'
let message = $state('No action yet') let message = $state('No action yet')
@ -31,8 +31,8 @@
<div class="test-container"> <div class="test-container">
<div class="test-unit"> <div class="test-unit">
<ContextMenu showGearButton={true}> <UnitMenuContainer showGearButton={true}>
{#snippet children()} {#snippet trigger()}
<img <img
src="/images/placeholders/placeholder-weapon-grid.png" src="/images/placeholders/placeholder-weapon-grid.png"
alt="Test weapon" alt="Test weapon"
@ -40,34 +40,34 @@
/> />
{/snippet} {/snippet}
{#snippet contextMenu()} {#snippet contextMenu()}
<ContextMenuBase.Item class="context-menu-item" onclick={handleViewDetails}> <MenuItems
View Details onViewDetails={handleViewDetails}
</ContextMenuBase.Item> onReplace={handleReplace}
<ContextMenuBase.Item class="context-menu-item" onclick={handleReplace}> onRemove={handleRemove}
Replace canEdit={true}
</ContextMenuBase.Item> variant="context"
<ContextMenuBase.Separator class="context-menu-separator" /> viewDetailsLabel="View Details"
<ContextMenuBase.Item class="context-menu-item danger" onclick={handleRemove}> replaceLabel="Replace"
Remove removeLabel="Remove"
</ContextMenuBase.Item> />
{/snippet} {/snippet}
{#snippet dropdownMenu()} {#snippet dropdownMenu()}
<DropdownMenuBase.Item class="dropdown-menu-item" onclick={handleViewDetails}> <MenuItems
View Details onViewDetails={handleViewDetails}
</DropdownMenuBase.Item> onReplace={handleReplace}
<DropdownMenuBase.Item class="dropdown-menu-item" onclick={handleReplace}> onRemove={handleRemove}
Replace canEdit={true}
</DropdownMenuBase.Item> variant="dropdown"
<DropdownMenuBase.Separator class="dropdown-menu-separator" /> viewDetailsLabel="View Details"
<DropdownMenuBase.Item class="dropdown-menu-item danger" onclick={handleRemove}> replaceLabel="Replace"
Remove removeLabel="Remove"
</DropdownMenuBase.Item> />
{/snippet} {/snippet}
</ContextMenu> </UnitMenuContainer>
<div class="test-label">Hover me or right-click</div> <div class="test-label">Hover me or right-click</div>
</div> </div>
</div> </div>
<div class="result"> <div class="result">

View file

@ -694,3 +694,46 @@ $light--shadow--dark--hover: color.adjust($light-text-00, $alpha: -0.3);
border: $width solid $color; border: $width solid $color;
box-shadow: 0 0 0 2px rgba($color, 0.2); box-shadow: 0 0 0 2px rgba($color, 0.2);
} }
// Element-specific focus ring mixins
@mixin focus-ring-fire($width: 3px) {
outline: none;
border: $width solid var(--fire-accent);
box-shadow: 0 0 4px 6px rgba(250, 109, 109, 0.2);
}
@mixin focus-ring-water($width: 3px) {
outline: none;
border: $width solid var(--water-accent);
box-shadow: 0 0 4px 6px rgba(108, 201, 255, 0.2);
}
@mixin focus-ring-earth($width: 3px) {
outline: none;
border: $width solid var(--earth-accent);
box-shadow: 0 0 4px 6px rgba(253, 159, 91, 0.2);
}
@mixin focus-ring-wind($width: 3px) {
outline: none;
border: $width solid var(--wind-accent);
box-shadow: 0 0 4px 6px rgba(62, 228, 137, 0.2);
}
@mixin focus-ring-light($width: 3px) {
outline: none;
border: $width solid var(--light-accent);
box-shadow: 0 0 4px 6px rgba(232, 214, 51, 0.2);
}
@mixin focus-ring-dark($width: 3px) {
outline: none;
border: $width solid var(--dark-accent);
box-shadow: 0 0 4px 6px rgba(222, 123, 255, 0.2);
}
@mixin focus-ring-neutral($width: 3px) {
outline: none;
border: $width solid var(--null-accent);
box-shadow: 0 0 4px 6px rgba(0, 0, 0, 0.1);
}

View file

@ -257,6 +257,7 @@
--null-text-hover: #{colors.$null--text--hover--light}; --null-text-hover: #{colors.$null--text--hover--light};
--null-shadow: #{colors.$null--shadow--light}; --null-shadow: #{colors.$null--shadow--light};
--null-shadow-hover: #{colors.$null--shadow--light--hover}; --null-shadow-hover: #{colors.$null--shadow--light--hover};
--null-accent: #{colors.$grey-70};
// Light - Element button colors // Light - Element button colors
--null-button-bg: #{colors.$grey-55}; --null-button-bg: #{colors.$grey-55};
@ -302,6 +303,7 @@
--wind-text-hover: #{colors.$wind--text--hover--light}; --wind-text-hover: #{colors.$wind--text--hover--light};
--wind-shadow: #{colors.$wind--shadow--light}; --wind-shadow: #{colors.$wind--shadow--light};
--wind-shadow-hover: #{colors.$wind--shadow--light--hover}; --wind-shadow-hover: #{colors.$wind--shadow--light--hover};
--wind-accent: #{colors.$wind--bg--light};
--fire-bg: #{colors.$fire--bg--light}; --fire-bg: #{colors.$fire--bg--light};
--fire-bg-hover: #{colors.$fire--bg--hover--light}; --fire-bg-hover: #{colors.$fire--bg--hover--light};
@ -312,6 +314,7 @@
--fire-text-hover: #{colors.$fire--text--hover--light}; --fire-text-hover: #{colors.$fire--text--hover--light};
--fire-shadow: #{colors.$fire--shadow--light}; --fire-shadow: #{colors.$fire--shadow--light};
--fire-shadow-hover: #{colors.$fire--shadow--light--hover}; --fire-shadow-hover: #{colors.$fire--shadow--light--hover};
--fire-accent: #{colors.$fire--bg--light};
--water-bg: #{colors.$water--bg--light}; --water-bg: #{colors.$water--bg--light};
--water-bg-hover: #{colors.$water--bg--hover--light}; --water-bg-hover: #{colors.$water--bg--hover--light};
@ -322,6 +325,7 @@
--water-text-hover: #{colors.$water--text--hover--light}; --water-text-hover: #{colors.$water--text--hover--light};
--water-shadow: #{colors.$water--shadow--light}; --water-shadow: #{colors.$water--shadow--light};
--water-shadow-hover: #{colors.$water--shadow--light--hover}; --water-shadow-hover: #{colors.$water--shadow--light--hover};
--water-accent: #{colors.$water--bg--light};
--earth-bg: #{colors.$earth--bg--light}; --earth-bg: #{colors.$earth--bg--light};
--earth-bg-hover: #{colors.$earth--bg--hover--light}; --earth-bg-hover: #{colors.$earth--bg--hover--light};
@ -332,6 +336,7 @@
--earth-text-hover: #{colors.$earth--text--hover--light}; --earth-text-hover: #{colors.$earth--text--hover--light};
--earth-shadow: #{colors.$earth--shadow--light}; --earth-shadow: #{colors.$earth--shadow--light};
--earth-shadow-hover: #{colors.$earth--shadow--light--hover}; --earth-shadow-hover: #{colors.$earth--shadow--light--hover};
--earth-accent: #{colors.$earth--bg--light};
--dark-bg: #{colors.$dark--bg--light}; --dark-bg: #{colors.$dark--bg--light};
--dark-bg-hover: #{colors.$dark--bg--hover--light}; --dark-bg-hover: #{colors.$dark--bg--hover--light};
@ -342,6 +347,7 @@
--dark-text-hover: #{colors.$dark--text--hover--light}; --dark-text-hover: #{colors.$dark--text--hover--light};
--dark-shadow: #{colors.$dark--shadow--light}; --dark-shadow: #{colors.$dark--shadow--light};
--dark-shadow-hover: #{colors.$dark--shadow--light--hover}; --dark-shadow-hover: #{colors.$dark--shadow--light--hover};
--dark-accent: #{colors.$dark--bg--light};
--light-bg: #{colors.$light--bg--light}; --light-bg: #{colors.$light--bg--light};
--light-bg-hover: #{colors.$light--bg--hover--light}; --light-bg-hover: #{colors.$light--bg--hover--light};
@ -352,6 +358,7 @@
--light-text-hover: #{colors.$light--text--hover--light}; --light-text-hover: #{colors.$light--text--hover--light};
--light-shadow: #{colors.$light--shadow--light}; --light-shadow: #{colors.$light--shadow--light};
--light-shadow-hover: #{colors.$light--shadow--light--hover}; --light-shadow-hover: #{colors.$light--shadow--light--hover};
--light-accent: #{colors.$light--bg--light};
// Gradients // Gradients
--hero-gradient: #{effects.$hero--gradient--light}; --hero-gradient: #{effects.$hero--gradient--light};
@ -586,6 +593,7 @@
--null-text-hover: #{colors.$null--text--hover--dark}; --null-text-hover: #{colors.$null--text--hover--dark};
--null-shadow: #{colors.$null--shadow--dark}; --null-shadow: #{colors.$null--shadow--dark};
--null-shadow-hover: #{colors.$null--shadow--dark--hover}; --null-shadow-hover: #{colors.$null--shadow--dark--hover};
--null-accent: #{colors.$grey-70};
// Dark - Element button colors (same as light theme) // Dark - Element button colors (same as light theme)
--null-button-bg: #{colors.$grey-55}; --null-button-bg: #{colors.$grey-55};
@ -632,6 +640,7 @@
--wind-text-hover: #{colors.$wind--text--hover--dark}; --wind-text-hover: #{colors.$wind--text--hover--dark};
--wind-shadow: #{colors.$wind--shadow--dark}; --wind-shadow: #{colors.$wind--shadow--dark};
--wind-shadow-hover: #{colors.$wind--shadow--dark--hover}; --wind-shadow-hover: #{colors.$wind--shadow--dark--hover};
--wind-accent: #{colors.$wind--bg--dark};
--fire-bg: #{colors.$fire--bg--dark}; --fire-bg: #{colors.$fire--bg--dark};
--fire-bg-hover: #{colors.$fire--bg--hover--dark}; --fire-bg-hover: #{colors.$fire--bg--hover--dark};
@ -642,6 +651,7 @@
--fire-text-hover: #{colors.$fire--text--hover--dark}; --fire-text-hover: #{colors.$fire--text--hover--dark};
--fire-shadow: #{colors.$fire--shadow--dark}; --fire-shadow: #{colors.$fire--shadow--dark};
--fire-shadow-hover: #{colors.$fire--shadow--dark--hover}; --fire-shadow-hover: #{colors.$fire--shadow--dark--hover};
--fire-accent: #{colors.$fire--bg--dark};
--water-bg: #{colors.$water--bg--dark}; --water-bg: #{colors.$water--bg--dark};
--water-bg-hover: #{colors.$water--bg--hover--dark}; --water-bg-hover: #{colors.$water--bg--hover--dark};
@ -652,6 +662,7 @@
--water-text-hover: #{colors.$water--text--hover--dark}; --water-text-hover: #{colors.$water--text--hover--dark};
--water-shadow: #{colors.$water--shadow--dark}; --water-shadow: #{colors.$water--shadow--dark};
--water-shadow-hover: #{colors.$water--shadow--dark--hover}; --water-shadow-hover: #{colors.$water--shadow--dark--hover};
--water-accent: #{colors.$water--bg--dark};
--earth-bg: #{colors.$earth--bg--dark}; --earth-bg: #{colors.$earth--bg--dark};
--earth-bg-hover: #{colors.$earth--bg--hover--dark}; --earth-bg-hover: #{colors.$earth--bg--hover--dark};
@ -662,6 +673,7 @@
--earth-text-hover: #{colors.$earth--text--hover--dark}; --earth-text-hover: #{colors.$earth--text--hover--dark};
--earth-shadow: #{colors.$earth--shadow--dark}; --earth-shadow: #{colors.$earth--shadow--dark};
--earth-shadow-hover: #{colors.$earth--shadow--dark--hover}; --earth-shadow-hover: #{colors.$earth--shadow--dark--hover};
--earth-accent: #{colors.$earth--bg--dark};
--dark-bg: #{colors.$dark--bg--dark}; --dark-bg: #{colors.$dark--bg--dark};
--dark-bg-hover: #{colors.$dark--bg--hover--dark}; --dark-bg-hover: #{colors.$dark--bg--hover--dark};
@ -672,6 +684,7 @@
--dark-text-hover: #{colors.$dark--text--hover--dark}; --dark-text-hover: #{colors.$dark--text--hover--dark};
--dark-shadow: #{colors.$dark--shadow--dark}; --dark-shadow: #{colors.$dark--shadow--dark};
--dark-shadow-hover: #{colors.$dark--shadow--dark--hover}; --dark-shadow-hover: #{colors.$dark--shadow--dark--hover};
--dark-accent: #{colors.$dark--bg--dark};
--light-bg: #{colors.$light--bg--dark}; --light-bg: #{colors.$light--bg--dark};
--light-bg-hover: #{colors.$light--bg--hover--dark}; --light-bg-hover: #{colors.$light--bg--hover--dark};
@ -682,6 +695,7 @@
--light-text-hover: #{colors.$light--text--hover--dark}; --light-text-hover: #{colors.$light--text--hover--dark};
--light-shadow: #{colors.$light--shadow--dark}; --light-shadow: #{colors.$light--shadow--dark};
--light-shadow-hover: #{colors.$light--shadow--dark--hover}; --light-shadow-hover: #{colors.$light--shadow--dark--hover};
--light-accent: #{colors.$light--bg--dark};
// Gradients // Gradients
--hero-gradient: #{effects.$hero--gradient--dark}; --hero-gradient: #{effects.$hero--gradient--dark};