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:
parent
9a4a863ccd
commit
0379cff81e
7 changed files with 987 additions and 503 deletions
|
|
@ -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,13 +166,18 @@
|
||||||
}
|
}
|
||||||
</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()}
|
||||||
|
<div
|
||||||
|
class="focus-ring-wrapper {elementClass}"
|
||||||
|
class:is-active={isActive}
|
||||||
|
class:editable={ctx?.canEdit()}
|
||||||
|
>
|
||||||
{#key item?.id ?? position}
|
{#key item?.id ?? position}
|
||||||
<div
|
<div
|
||||||
class="frame character cell"
|
class="frame character cell {elementClass}"
|
||||||
class:protagonist={position === 0}
|
class:protagonist={position === 0}
|
||||||
class:editable={ctx?.canEdit()}
|
class:editable={ctx?.canEdit()}
|
||||||
onclick={() => viewDetails()}
|
onclick={() => viewDetails()}
|
||||||
|
|
@ -180,7 +211,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
<img
|
<img
|
||||||
class="image"
|
class="image {elementClass}"
|
||||||
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)}
|
||||||
|
|
@ -188,38 +219,35 @@
|
||||||
/>
|
/>
|
||||||
</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>
|
||||||
|
|
|
||||||
|
|
@ -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,13 +93,14 @@
|
||||||
|
|
||||||
</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()}
|
||||||
|
<div class="focus-ring-wrapper {elementClass}" class:is-active={isActive} class:editable={ctx?.canEdit()}>
|
||||||
{#key item?.id ?? position}
|
{#key item?.id ?? position}
|
||||||
<div
|
<div
|
||||||
class="frame summon"
|
class="frame summon {elementClass}"
|
||||||
class:main={item?.main || position === -1}
|
class:main={item?.main || position === -1}
|
||||||
class:friend={item?.friend || position === 6}
|
class:friend={item?.friend || position === 6}
|
||||||
class:cell={!((item?.main || position === -1) || (item?.friend || position === 6))}
|
class:cell={!((item?.main || position === -1) || (item?.friend || position === 6))}
|
||||||
|
|
@ -88,7 +108,7 @@
|
||||||
onclick={() => viewDetails()}
|
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}
|
||||||
|
|
@ -101,38 +121,35 @@
|
||||||
{/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>
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,13 @@
|
||||||
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 { sidebar } from '$lib/stores/sidebar.svelte'
|
||||||
import * as m from '$lib/paraglide/messages'
|
import * as m from '$lib/paraglide/messages'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
@ -42,7 +43,7 @@
|
||||||
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)
|
||||||
})
|
})
|
||||||
|
|
@ -64,12 +65,43 @@
|
||||||
// Get AX skill images using utility
|
// Get AX skill images using utility
|
||||||
let axSkillImages = $derived(getAxSkillImages(item?.ax))
|
let axSkillImages = $derived(getAxSkillImages(item?.ax))
|
||||||
|
|
||||||
|
// 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(() => {
|
||||||
|
// For weapons with null element that have an instance element, use it
|
||||||
|
const element =
|
||||||
|
item?.weapon?.element === 0 && item?.element ? item.element : item?.weapon?.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 {
|
||||||
const party = ctx.getParty()
|
const party = ctx.getParty()
|
||||||
const editKey = ctx.getEditKey()
|
const editKey = ctx.getEditKey()
|
||||||
const updated = await ctx.services.gridService.removeWeapon(party.id, item.id as any, editKey || undefined)
|
const updated = await ctx.services.gridService.removeWeapon(
|
||||||
|
party.id,
|
||||||
|
item.id as any,
|
||||||
|
editKey || undefined
|
||||||
|
)
|
||||||
if (updated) {
|
if (updated) {
|
||||||
ctx.updateParty(updated)
|
ctx.updateParty(updated)
|
||||||
}
|
}
|
||||||
|
|
@ -91,17 +123,26 @@
|
||||||
ctx.openPicker({ type: 'weapon', position, item })
|
ctx.openPicker({ type: 'weapon', position, item })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="unit" class:empty={!item} class:extra={position >= 9}>
|
<div
|
||||||
|
class="unit {elementClass}"
|
||||||
|
class:empty={!item}
|
||||||
|
class:extra={position >= 9}
|
||||||
|
class:is-active={isActive}
|
||||||
|
>
|
||||||
{#if item}
|
{#if item}
|
||||||
<ContextMenu showGearButton={true}>
|
<UnitMenuContainer showGearButton={true}>
|
||||||
{#snippet children()}
|
{#snippet trigger()}
|
||||||
|
<div
|
||||||
|
class="focus-ring-wrapper {elementClass}"
|
||||||
|
class:is-active={isActive}
|
||||||
|
class:editable={ctx?.canEdit()}
|
||||||
|
class:main={item?.mainhand || position === -1}
|
||||||
|
>
|
||||||
{#key item?.id ?? position}
|
{#key item?.id ?? position}
|
||||||
<div
|
<div
|
||||||
class="frame weapon"
|
class="frame weapon {elementClass}"
|
||||||
class:main={item?.mainhand || position === -1}
|
class:main={item?.mainhand || position === -1}
|
||||||
class:cell={!(item?.mainhand || position === -1)}
|
class:cell={!(item?.mainhand || position === -1)}
|
||||||
class:extra={position >= 9}
|
class:extra={position >= 9}
|
||||||
|
|
@ -126,45 +167,42 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<img
|
<img
|
||||||
class="image"
|
class="image {elementClass}"
|
||||||
class:placeholder={!item?.weapon?.granblueId}
|
class:placeholder={!item?.weapon?.granblueId}
|
||||||
alt={displayName(item?.weapon)}
|
alt={displayName(item?.weapon)}
|
||||||
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
|
||||||
|
|
@ -173,12 +211,15 @@
|
||||||
class:cell={position !== -1}
|
class:cell={position !== -1}
|
||||||
class:extra={position >= 9}
|
class:extra={position >= 9}
|
||||||
class:editable={ctx?.canEdit()}
|
class:editable={ctx?.canEdit()}
|
||||||
onclick={() => 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 ? '/images/placeholders/placeholder-weapon-main.png' : '/images/placeholders/placeholder-weapon-grid.png'}
|
src={position === -1
|
||||||
|
? '/images/placeholders/placeholder-weapon-main.png'
|
||||||
|
: '/images/placeholders/placeholder-weapon-grid.png'}
|
||||||
/>
|
/>
|
||||||
{#if ctx?.canEdit()}
|
{#if ctx?.canEdit()}
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
|
|
@ -201,7 +242,12 @@
|
||||||
if (!item?.id || !ctx) return
|
if (!item?.id || !ctx) return
|
||||||
try {
|
try {
|
||||||
const editKey = ctx.getEditKey()
|
const editKey = ctx.getEditKey()
|
||||||
const updated = await ctx.services.gridService.updateWeaponUncap(item.id, level, undefined, editKey || undefined)
|
const updated = await ctx.services.gridService.updateWeaponUncap(
|
||||||
|
item.id,
|
||||||
|
level,
|
||||||
|
undefined,
|
||||||
|
editKey || undefined
|
||||||
|
)
|
||||||
if (updated) {
|
if (updated) {
|
||||||
ctx.updateParty(updated)
|
ctx.updateParty(updated)
|
||||||
}
|
}
|
||||||
|
|
@ -216,7 +262,12 @@
|
||||||
const editKey = ctx.getEditKey()
|
const editKey = ctx.getEditKey()
|
||||||
// When setting transcendence > 0, also set uncap to max (6)
|
// When setting transcendence > 0, also set uncap to max (6)
|
||||||
const maxUncap = stage > 0 ? 6 : undefined
|
const maxUncap = stage > 0 ? 6 : undefined
|
||||||
const updated = await ctx.services.gridService.updateWeaponUncap(item.id, maxUncap, stage, editKey || undefined)
|
const updated = await ctx.services.gridService.updateWeaponUncap(
|
||||||
|
item.id,
|
||||||
|
maxUncap,
|
||||||
|
stage,
|
||||||
|
editKey || undefined
|
||||||
|
)
|
||||||
if (updated) {
|
if (updated) {
|
||||||
ctx.updateParty(updated)
|
ctx.updateParty(updated)
|
||||||
}
|
}
|
||||||
|
|
@ -231,10 +282,10 @@
|
||||||
</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 {
|
||||||
|
|
@ -243,7 +294,7 @@
|
||||||
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;
|
||||||
|
|
@ -263,20 +314,42 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.name {
|
.name {
|
||||||
font-weight: $medium;
|
font-weight: typography.$medium;
|
||||||
color: var(--extra-purple-text);
|
color: var(--extra-purple-text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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: layout.$scale-wide;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.editable.main:hover {
|
||||||
|
transform: layout.$scale-tall;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.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;
|
||||||
|
|
@ -284,20 +357,17 @@
|
||||||
|
|
||||||
&.editable: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);
|
||||||
transform: $scale-wide;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.frame.weapon.main {
|
.frame.weapon.main {
|
||||||
@include rep.aspect(rep.$weapon-main-w, rep.$weapon-main-h);
|
@include rep.aspect(rep.$weapon-main-w, rep.$weapon-main-h);
|
||||||
|
|
||||||
&.editable:hover {
|
|
||||||
transform: $scale-tall;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.frame.weapon.cell { @include rep.aspect(rep.$weapon-cell-w, rep.$weapon-cell-h); }
|
.frame.weapon.cell {
|
||||||
|
@include rep.aspect(rep.$weapon-cell-w, rep.$weapon-cell-h);
|
||||||
|
}
|
||||||
|
|
||||||
.image {
|
.image {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
@ -324,9 +394,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modifiers {
|
.modifiers {
|
||||||
|
|
@ -345,8 +415,8 @@
|
||||||
.skills {
|
.skills {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: calc($unit / 4);
|
gap: calc(spacing.$unit / 4);
|
||||||
padding: calc($unit / 2);
|
padding: calc(spacing.$unit / 2);
|
||||||
|
|
||||||
.skill {
|
.skill {
|
||||||
width: 20%;
|
width: 20%;
|
||||||
|
|
@ -387,4 +457,92 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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>
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -41,31 +41,31 @@
|
||||||
{/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>
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue