wire up context menus to unit components

This commit is contained in:
Justin Edmund 2025-09-30 03:42:57 -07:00
parent 030c5916c7
commit 0ab2782697
3 changed files with 116 additions and 69 deletions

View file

@ -5,13 +5,14 @@
import { getContext } from 'svelte'
import Icon from '$lib/components/Icon.svelte'
import ContextMenu from '$lib/components/ui/ContextMenu.svelte'
import { ContextMenu as ContextMenuBase } from 'bits-ui'
import { ContextMenu as ContextMenuBase, DropdownMenu as DropdownMenuBase } from 'bits-ui'
import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte'
import { getCharacterImageWithPose } from '$lib/utils/images'
import { openDetailsSidebar } from '$lib/features/details/openDetailsSidebar.svelte'
import { getJobPortraitUrl, Gender } from '$lib/utils/jobUtils'
import perpetuityFilled from '$src/assets/icons/perpetuity/filled.svg'
import perpetuityEmpty from '$src/assets/icons/perpetuity/empty.svg'
import * as m from '$lib/paraglide/messages'
interface Props {
item?: GridCharacter
@ -141,35 +142,42 @@
<div class="unit" class:empty={!item}>
{#if item}
<ContextMenu>
<ContextMenu showGearButton={true}>
{#snippet children()}
{#key item?.id ?? position}
<div
class="frame character cell"
class:protagonist={position === 0}
class:editable={ctx?.canEdit()}
onclick={() => viewDetails()}
>
{#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" />
{#if position !== 0}
{#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
class="perpetuity-icon empty"
src={perpetuityEmpty}
alt="Add Perpetuity Ring"
class="perpetuity static"
src={perpetuityFilled}
alt="Perpetuity Ring"
title="Perpetuity Ring"
/>
</button>
{:else if item.perpetuity}
<img
class="perpetuity static"
src={perpetuityFilled}
alt="Perpetuity Ring"
title="Perpetuity Ring"
/>
{/if}
{/if}
<img
class="image"
@ -182,20 +190,35 @@
{/key}
{/snippet}
{#snippet menu()}
{#snippet contextMenu()}
<ContextMenuBase.Item class="context-menu-item" onclick={viewDetails}>
View Details
{m.context_view_details()}
</ContextMenuBase.Item>
{#if ctx?.canEdit()}
<ContextMenuBase.Item class="context-menu-item" onclick={replace}>
Replace
{m.context_replace()}
</ContextMenuBase.Item>
<ContextMenuBase.Separator class="context-menu-separator" />
<ContextMenuBase.Item class="context-menu-item danger" onclick={remove}>
Remove
{m.context_remove()}
</ContextMenuBase.Item>
{/if}
{/snippet}
{#snippet dropdownMenu()}
<DropdownMenuBase.Item class="dropdown-menu-item" onclick={viewDetails}>
{m.context_view_details()}
</DropdownMenuBase.Item>
{#if ctx?.canEdit()}
<DropdownMenuBase.Item class="dropdown-menu-item" onclick={replace}>
{m.context_replace()}
</DropdownMenuBase.Item>
<DropdownMenuBase.Separator class="dropdown-menu-separator" />
<DropdownMenuBase.Item class="dropdown-menu-item danger" onclick={remove}>
{m.context_remove()}
</DropdownMenuBase.Item>
{/if}
{/snippet}
</ContextMenu>
{:else}
{#key `empty-${position}`}
@ -213,8 +236,8 @@
class="image"
class:placeholder={!isProtagonist || !job}
class:protagonist={isProtagonist}
alt={isProtagonist && job ? job.name.en : ""}
src={isProtagonist ? imageUrl : "/images/placeholders/placeholder-weapon-grid.png"}
alt={isProtagonist && job ? job.name.en : ''}
src={isProtagonist ? imageUrl : '/images/placeholders/placeholder-weapon-grid.png'}
/>
{#if ctx?.canEdit()}
<span class="icon">
@ -318,6 +341,13 @@
.frame.character.cell {
@include rep.aspect(rep.$char-cell-w, rep.$char-cell-h);
&.protagonist {
background-image: url('/images/relief.png'), linear-gradient(to right, #000, #484440, #000);
background-size: cover;
background-position: center -20px;
background-repeat: no-repeat;
}
}
.image {

View file

@ -4,10 +4,11 @@
import { getContext } from 'svelte'
import Icon from '$lib/components/Icon.svelte'
import ContextMenu from '$lib/components/ui/ContextMenu.svelte'
import { ContextMenu as ContextMenuBase } from 'bits-ui'
import { ContextMenu as ContextMenuBase, DropdownMenu as DropdownMenuBase } from 'bits-ui'
import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte'
import { getSummonImage } from '$lib/features/database/detail/image'
import { openDetailsSidebar } from '$lib/features/details/openDetailsSidebar.svelte'
import * as m from '$lib/paraglide/messages'
interface Props {
item?: GridSummon
@ -34,9 +35,10 @@
}
// Use $derived to ensure consistent computation between server and client
let imageUrl = $derived.by(() => {
// Check position first for main/friend summon determination
const isMain = position === -1 || position === 6 || item?.main || item?.friend
const variant = isMain ? 'main' : 'grid'
// Only position -1 (main) and position 6 (friend) use main-sized images
// All other positions (0-5) including subaura (4-5) use grid-sized images
const isMainSized = position === -1 || position === 6 || item?.main || item?.friend
const variant = isMainSized ? 'main' : 'grid'
return getSummonImage(item?.summon?.granblueId, variant)
})
@ -74,7 +76,7 @@
<div class="unit" class:empty={!item}>
{#if item}
<ContextMenu>
<ContextMenu showGearButton={true}>
{#snippet children()}
{#key item?.id ?? position}
<div
@ -91,11 +93,6 @@
alt={displayName(item?.summon)}
src={imageUrl}
/>
{#if ctx?.canEdit() && item?.id}
<div class="actions">
<button class="remove" title="Remove" onclick={(e) => { e.stopPropagation(); remove() }}>×</button>
</div>
{/if}
{#if item?.main || position === -1}
<span class="badge">Main</span>
{/if}
@ -106,20 +103,35 @@
{/key}
{/snippet}
{#snippet menu()}
{#snippet contextMenu()}
<ContextMenuBase.Item class="context-menu-item" onclick={viewDetails}>
View Details
{m.context_view_details()}
</ContextMenuBase.Item>
{#if ctx?.canEdit()}
<ContextMenuBase.Item class="context-menu-item" onclick={replace}>
Replace
{m.context_replace()}
</ContextMenuBase.Item>
<ContextMenuBase.Separator class="context-menu-separator" />
<ContextMenuBase.Item class="context-menu-item danger" onclick={remove}>
Remove
{m.context_remove()}
</ContextMenuBase.Item>
{/if}
{/snippet}
{#snippet dropdownMenu()}
<DropdownMenuBase.Item class="dropdown-menu-item" onclick={viewDetails}>
{m.context_view_details()}
</DropdownMenuBase.Item>
{#if ctx?.canEdit()}
<DropdownMenuBase.Item class="dropdown-menu-item" onclick={replace}>
{m.context_replace()}
</DropdownMenuBase.Item>
<DropdownMenuBase.Separator class="dropdown-menu-separator" />
<DropdownMenuBase.Item class="dropdown-menu-item danger" onclick={remove}>
{m.context_remove()}
</DropdownMenuBase.Item>
{/if}
{/snippet}
</ContextMenu>
{:else}
{#key `empty-${position}`}
@ -263,26 +275,6 @@
color: $grey-50;
}
.actions {
position: absolute;
top: 6px;
right: 6px;
display: flex;
gap: 6px;
z-index: 3;
}
.remove {
background: rgba(0,0,0,.6);
color: white;
border: none;
border-radius: 12px;
width: 24px;
height: 24px;
line-height: 24px;
cursor: pointer;
}
.badge {
position: absolute;
left: 6px;

View file

@ -4,11 +4,12 @@
import { getContext } from 'svelte'
import Icon from '$lib/components/Icon.svelte'
import ContextMenu from '$lib/components/ui/ContextMenu.svelte'
import { ContextMenu as ContextMenuBase } from 'bits-ui'
import { ContextMenu as ContextMenuBase, DropdownMenu as DropdownMenuBase } from 'bits-ui'
import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte'
import { getWeaponImage } from '$lib/features/database/detail/image'
import { openDetailsSidebar } from '$lib/features/details/openDetailsSidebar.svelte'
import { getAwakeningImage, getWeaponKeyImages, getAxSkillImages } from '$lib/utils/modifiers'
import * as m from '$lib/paraglide/messages'
interface Props {
item?: GridWeapon
@ -96,7 +97,7 @@
<div class="unit" class:empty={!item} class:extra={position >= 9}>
{#if item}
<ContextMenu>
<ContextMenu showGearButton={true}>
{#snippet children()}
{#key item?.id ?? position}
<div
@ -134,20 +135,35 @@
{/key}
{/snippet}
{#snippet menu()}
{#snippet contextMenu()}
<ContextMenuBase.Item class="context-menu-item" onclick={viewDetails}>
View Details
{m.context_view_details()}
</ContextMenuBase.Item>
{#if ctx?.canEdit()}
<ContextMenuBase.Item class="context-menu-item" onclick={replace}>
Replace
{m.context_replace()}
</ContextMenuBase.Item>
<ContextMenuBase.Separator class="context-menu-separator" />
<ContextMenuBase.Item class="context-menu-item danger" onclick={remove}>
Remove
{m.context_remove()}
</ContextMenuBase.Item>
{/if}
{/snippet}
{#snippet dropdownMenu()}
<DropdownMenuBase.Item class="dropdown-menu-item" onclick={viewDetails}>
{m.context_view_details()}
</DropdownMenuBase.Item>
{#if ctx?.canEdit()}
<DropdownMenuBase.Item class="dropdown-menu-item" onclick={replace}>
{m.context_replace()}
</DropdownMenuBase.Item>
<DropdownMenuBase.Separator class="dropdown-menu-separator" />
<DropdownMenuBase.Item class="dropdown-menu-item danger" onclick={remove}>
{m.context_remove()}
</DropdownMenuBase.Item>
{/if}
{/snippet}
</ContextMenu>
{:else}
{#key `empty-${position}`}
@ -218,6 +234,7 @@
@use '$src/themes/colors' as *;
@use '$src/themes/typography' as *;
@use '$src/themes/spacing' as *;
@use '$src/themes/layout' as *;
@use '$src/themes/rep' as rep;
.unit {
@ -265,13 +282,21 @@
justify-content: center;
cursor: pointer;
&:hover {
&.editable:hover {
opacity: 0.95;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
transform: $scale-wide;
}
}
.frame.weapon.main {
@include rep.aspect(rep.$weapon-main-w, rep.$weapon-main-h);
&.editable:hover {
transform: $scale-tall;
}
}
.frame.weapon.main { @include rep.aspect(rep.$weapon-main-w, rep.$weapon-main-h); }
.frame.weapon.cell { @include rep.aspect(rep.$weapon-cell-w, rep.$weapon-cell-h); }
.image {