Update unit components with editing context
This commit is contained in:
parent
795d9761d7
commit
c9bd155ed6
3 changed files with 455 additions and 177 deletions
|
|
@ -1,16 +1,23 @@
|
|||
<script lang="ts">
|
||||
import type { GridCharacterItemView, PartyView } from '$lib/api/schemas/party'
|
||||
import type { GridCharacter } from '$lib/types/api/party'
|
||||
import type { Party } from '$lib/types/api/party'
|
||||
import { getContext } from 'svelte'
|
||||
import Icon from '$lib/components/Icon.svelte'
|
||||
|
||||
export let item: GridCharacterItemView | undefined
|
||||
export let position: number
|
||||
export let mainWeaponElement: number | null | undefined
|
||||
export let partyElement: number | null | undefined
|
||||
interface Props {
|
||||
item?: GridCharacter
|
||||
position: number
|
||||
mainWeaponElement?: number | null
|
||||
partyElement?: number | null
|
||||
}
|
||||
|
||||
let { item, position, mainWeaponElement, partyElement }: Props = $props()
|
||||
|
||||
type PartyCtx = {
|
||||
getParty: () => PartyView
|
||||
updateParty: (p: PartyView) => void
|
||||
getParty: () => Party
|
||||
updateParty: (p: Party) => void
|
||||
canEdit: () => boolean
|
||||
getEditKey: () => string | null
|
||||
services: { gridService: any; partyService: any }
|
||||
}
|
||||
const ctx = getContext<PartyCtx>('party')
|
||||
|
|
@ -22,11 +29,19 @@
|
|||
if (maybe && typeof maybe === 'object') return maybe.en || maybe.ja || '—'
|
||||
return '—'
|
||||
}
|
||||
function characterImageUrl(c?: GridCharacterItemView): string {
|
||||
const id = (c as any)?.object?.granblueId
|
||||
if (!id) return '/images/placeholders/placeholder-weapon-grid.png'
|
||||
const uncap = (c as any)?.uncapLevel ?? 0
|
||||
const transStep = (c as any)?.transcendenceStep ?? 0
|
||||
// Use $derived to ensure consistent computation between server and client
|
||||
let imageUrl = $derived(() => {
|
||||
// Handle both new structure (item.character) and old structure (item.object) for compatibility
|
||||
const characterData = item?.character || (item as any)?.object
|
||||
|
||||
// If no item or no granblueId, return placeholder
|
||||
if (!item || !characterData?.granblueId) {
|
||||
return '/images/placeholders/placeholder-weapon-grid.png'
|
||||
}
|
||||
|
||||
const id = characterData.granblueId
|
||||
const uncap = item?.uncapLevel ?? 0
|
||||
const transStep = item?.transcendenceStep ?? 0
|
||||
let suffix = '01'
|
||||
if (transStep > 0) suffix = '04'
|
||||
else if (uncap >= 5) suffix = '03'
|
||||
|
|
@ -36,70 +51,135 @@
|
|||
suffix = `${suffix}_0${element}`
|
||||
}
|
||||
return `/images/character-main/${id}_${suffix}.jpg`
|
||||
}
|
||||
})
|
||||
|
||||
async function remove() {
|
||||
if (!item?.id) return
|
||||
const party = ctx.getParty()
|
||||
const editKey = ctx.services.partyService.getEditKey(party.shortcode)
|
||||
const editKey = ctx.getEditKey()
|
||||
const updated = await ctx.services.gridService.removeCharacter(party.id, item.id as any, editKey || undefined)
|
||||
ctx.updateParty(updated)
|
||||
}
|
||||
|
||||
async function add() {
|
||||
const party = ctx.getParty()
|
||||
const editKey = ctx.services.partyService.getEditKey(party.shortcode)
|
||||
const id = window.prompt('Enter character ID to add')
|
||||
if (!id) return
|
||||
const res = await ctx.services.gridService.addCharacter(party.id, id, position, editKey || undefined)
|
||||
if (res.conflicts) {
|
||||
window.alert('Conflict detected. Please resolve via UI in a later step.')
|
||||
return
|
||||
}
|
||||
ctx.updateParty(res.party)
|
||||
}
|
||||
|
||||
async function replaceItem() {
|
||||
if (!item?.id) return add()
|
||||
const party = ctx.getParty()
|
||||
const editKey = ctx.services.partyService.getEditKey(party.shortcode)
|
||||
const id = window.prompt('Enter new character ID')
|
||||
if (!id) return
|
||||
const res = await ctx.services.gridService.replaceCharacter(party.id, item.id as any, id, editKey || undefined)
|
||||
if (res.conflicts) {
|
||||
window.alert('Conflict detected. Please resolve via UI in a later step.')
|
||||
return
|
||||
}
|
||||
ctx.updateParty(res.party)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="unit">
|
||||
<img class="image" alt={item ? displayName(item.object) : ''} src={characterImageUrl(item)} />
|
||||
<div class="name">{item ? displayName(item.object) : '—'}</div>
|
||||
{#if ctx.canEdit() && !item}
|
||||
<button class="add" title="Add" on:click={add}>+</button>
|
||||
{/if}
|
||||
{#if ctx.canEdit() && item?.id}
|
||||
<div class="actions">
|
||||
<button class="replace" title="Replace" on:click={replaceItem}>↺</button>
|
||||
<button class="remove" title="Remove" on:click={remove}>×</button>
|
||||
<div class="unit" class:empty={!item}>
|
||||
{#key (item ? (item as any).id ?? position : `empty-${position}`)}
|
||||
<div
|
||||
class="frame character cell"
|
||||
class:editable={ctx?.canEdit()}
|
||||
on:click={() => ctx?.openPicker && ctx.openPicker({ type: 'character', position, item })}
|
||||
>
|
||||
<img class="image" class:placeholder={!item || !(item?.character?.granblueId || (item as any)?.object?.granblueId)} alt={item ? displayName(item?.character || (item as any)?.object) : ''} src={imageUrl()} />
|
||||
{#if !item && ctx?.canEdit()}
|
||||
<span class="icon">
|
||||
<Icon name="plus" size={24} />
|
||||
</span>
|
||||
{/if}
|
||||
{#if ctx.canEdit() && item?.id}
|
||||
<div class="actions">
|
||||
<button class="remove" title="Remove" on:click|stopPropagation={remove}>×</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if ctx.canEdit() && item?.id}
|
||||
<button class="remove" title="Remove" on:click={remove}>×</button>
|
||||
{/if}
|
||||
{/key}
|
||||
<div class="name">{item ? displayName(item?.character || (item as any)?.object) : ''}</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@use '$src/themes/colors' as *;
|
||||
@use '$src/themes/typography' as *;
|
||||
@use '$src/themes/spacing' as *;
|
||||
@use '$src/themes/rep' as rep;
|
||||
|
||||
.unit { position: relative; width: 100%; display: flex; flex-direction: column; align-items: center; gap: $unit; }
|
||||
.image { width: 100%; height: auto; border: 1px solid $grey-75; border-radius: 8px; display: block; }
|
||||
.name { font-size: $font-small; text-align: center; color: $grey-50; }
|
||||
.actions { position: absolute; top: 6px; right: 6px; display: flex; gap: 6px; }
|
||||
.remove, .replace, .add { background: rgba(0,0,0,.6); color: white; border: none; border-radius: 12px; width: 24px; height: 24px; line-height: 24px; cursor: pointer; }
|
||||
.add { position: absolute; top: 6px; right: 6px; }
|
||||
.unit {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: $unit;
|
||||
|
||||
&.empty .name {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.frame {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
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;
|
||||
|
||||
&.editable {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--primary-color, #0066cc);
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||||
transform: scale(1.01);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.frame.character.cell {
|
||||
@include rep.aspect(rep.$char-cell-w, rep.$char-cell-h);
|
||||
}
|
||||
|
||||
.image {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
z-index: 2;
|
||||
|
||||
&.placeholder {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
color: var(--icon-secondary, #999);
|
||||
transition: color 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.frame.editable:hover .icon {
|
||||
color: var(--icon-secondary-hover, #666);
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: $font-small;
|
||||
text-align: center;
|
||||
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;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,21 @@
|
|||
<script lang="ts">
|
||||
import type { GridSummonItemView, PartyView } from '$lib/api/schemas/party'
|
||||
import type { GridSummon } from '$lib/types/api/party'
|
||||
import type { Party } from '$lib/types/api/party'
|
||||
import { getContext } from 'svelte'
|
||||
import Icon from '$lib/components/Icon.svelte'
|
||||
|
||||
export let item: GridSummonItemView | undefined
|
||||
export let position: number
|
||||
interface Props {
|
||||
item?: GridSummon
|
||||
position: number
|
||||
}
|
||||
|
||||
let { item, position }: Props = $props()
|
||||
|
||||
type PartyCtx = {
|
||||
getParty: () => PartyView
|
||||
updateParty: (p: PartyView) => void
|
||||
getParty: () => Party
|
||||
updateParty: (p: Party) => void
|
||||
canEdit: () => boolean
|
||||
getEditKey: () => string | null
|
||||
services: { gridService: any; partyService: any }
|
||||
}
|
||||
const ctx = getContext<PartyCtx>('party')
|
||||
|
|
@ -20,75 +27,184 @@
|
|||
if (maybe && typeof maybe === 'object') return maybe.en || maybe.ja || '—'
|
||||
return '—'
|
||||
}
|
||||
function summonImageUrl(s?: GridSummonItemView): string {
|
||||
const id = (s as any)?.object?.granblueId
|
||||
const isMain = s?.main || s?.position === -1 || s?.friend || s?.position === 6
|
||||
if (!id) return isMain ? '/images/placeholders/placeholder-summon-main.png' : '/images/placeholders/placeholder-summon-grid.png'
|
||||
// Use $derived to ensure consistent computation between server and client
|
||||
let imageUrl = $derived(() => {
|
||||
// Check position first for main/friend summon determination
|
||||
const isMain = position === -1 || position === 6 || item?.main || item?.friend
|
||||
|
||||
// Handle both new structure (item.summon) and old structure (item.object) for compatibility
|
||||
const summonData = item?.summon || (item as any)?.object
|
||||
|
||||
// If no item or no granblueId, return placeholder
|
||||
if (!item || !summonData?.granblueId) {
|
||||
return isMain
|
||||
? '/images/placeholders/placeholder-summon-main.png'
|
||||
: '/images/placeholders/placeholder-summon-grid.png'
|
||||
}
|
||||
|
||||
const id = summonData.granblueId
|
||||
const folder = isMain ? 'summon-main' : 'summon-grid'
|
||||
return `/images/${folder}/${id}.jpg`
|
||||
}
|
||||
})
|
||||
|
||||
async function remove() {
|
||||
if (!item?.id) return
|
||||
const party = ctx.getParty()
|
||||
const editKey = ctx.services.partyService.getEditKey(party.shortcode)
|
||||
const editKey = ctx.getEditKey()
|
||||
const updated = await ctx.services.gridService.removeSummon(party.id, item.id as any, editKey || undefined)
|
||||
ctx.updateParty(updated)
|
||||
}
|
||||
|
||||
async function add() {
|
||||
const party = ctx.getParty()
|
||||
const editKey = ctx.services.partyService.getEditKey(party.shortcode)
|
||||
const id = window.prompt('Enter summon ID to add')
|
||||
if (!id) return
|
||||
const updated = await ctx.services.gridService.addSummon(party.id, id, position, editKey || undefined)
|
||||
ctx.updateParty(updated)
|
||||
}
|
||||
|
||||
async function replaceItem() {
|
||||
if (!item?.id) return add()
|
||||
const party = ctx.getParty()
|
||||
const editKey = ctx.services.partyService.getEditKey(party.shortcode)
|
||||
const id = window.prompt('Enter new summon ID')
|
||||
if (!id) return
|
||||
const updated = await ctx.services.gridService.replaceSummon(party.id, item.id as any, id, editKey || undefined)
|
||||
ctx.updateParty(updated)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="unit">
|
||||
<img class="image" alt={item ? displayName(item.object) : ''} src={summonImageUrl(item)} />
|
||||
<div class="name">{item ? displayName(item.object) : '—'}</div>
|
||||
{#if ctx.canEdit() && !item}
|
||||
<button class="add" title="Add" on:click={add}>+</button>
|
||||
{/if}
|
||||
{#if ctx.canEdit() && item?.id}
|
||||
<div class="actions">
|
||||
<button class="replace" title="Replace" on:click={replaceItem}>↺</button>
|
||||
<button class="remove" title="Remove" on:click={remove}>×</button>
|
||||
<div class="unit" class:empty={!item}>
|
||||
{#key (item ? (item as any).id ?? position : `empty-${position}`)}
|
||||
<div
|
||||
class="frame summon"
|
||||
class:main={item?.main || position === -1}
|
||||
class:friend={item?.friend || position === 6}
|
||||
class:cell={!((item?.main || position === -1) || (item?.friend || position === 6))}
|
||||
class:editable={ctx?.canEdit()}
|
||||
on:click={() => ctx?.openPicker && ctx.openPicker({ type: 'summon', position, item })}
|
||||
>
|
||||
<img class="image" class:placeholder={!item || !(item?.summon?.granblueId || (item as any)?.object?.granblueId)} alt={item ? displayName(item?.summon || (item as any)?.object) : ''} src={imageUrl()} />
|
||||
{#if !item && ctx?.canEdit()}
|
||||
<span class="icon">
|
||||
<Icon name="plus" size={24} />
|
||||
</span>
|
||||
{/if}
|
||||
{#if ctx.canEdit() && item?.id}
|
||||
<div class="actions">
|
||||
<button class="remove" title="Remove" on:click|stopPropagation={remove}>×</button>
|
||||
</div>
|
||||
{/if}
|
||||
{#if item?.main || position === -1}
|
||||
<span class="badge">Main</span>
|
||||
{/if}
|
||||
{#if item?.friend || position === 6}
|
||||
<span class="badge" style="left:auto; right:6px">Friend</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if ctx.canEdit() && item?.id}
|
||||
<button class="remove" title="Remove" on:click={remove}>×</button>
|
||||
{/if}
|
||||
{#if item?.main || position === -1}
|
||||
<span class="badge">Main</span>
|
||||
{/if}
|
||||
{#if item?.friend || position === 6}
|
||||
<span class="badge" style="left:auto; right:6px">Friend</span>
|
||||
{/if}
|
||||
{/key}
|
||||
<div class="name">{item ? displayName(item?.summon || (item as any)?.object) : ''}</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@use '$src/themes/colors' as *;
|
||||
@use '$src/themes/typography' as *;
|
||||
@use '$src/themes/spacing' as *;
|
||||
@use '$src/themes/rep' as rep;
|
||||
|
||||
.unit { position: relative; width: 100%; display: flex; flex-direction: column; align-items: center; gap: $unit; }
|
||||
.image { width: 100%; height: auto; border: 1px solid $grey-75; border-radius: 8px; display: block; }
|
||||
.name { font-size: $font-small; text-align: center; color: $grey-50; }
|
||||
.actions { position: absolute; top: 6px; right: 6px; display: flex; gap: 6px; }
|
||||
.remove, .replace, .add { background: rgba(0,0,0,.6); color: white; border: none; border-radius: 12px; width: 24px; height: 24px; line-height: 24px; cursor: pointer; }
|
||||
.add { position: absolute; top: 6px; right: 6px; }
|
||||
.badge { position: absolute; left: 6px; top: 6px; background: #333; color: #fff; font-size: 11px; padding: 2px 6px; border-radius: 10px; }
|
||||
.unit {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: $unit;
|
||||
|
||||
&.empty .name {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.frame {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
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;
|
||||
|
||||
&.editable {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--primary-color, #0066cc);
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
}
|
||||
|
||||
&.summon.main.editable:hover,
|
||||
&.summon.friend.editable:hover {
|
||||
transform: scale(1.01);
|
||||
}
|
||||
}
|
||||
|
||||
.frame.summon.main,
|
||||
.frame.summon.friend {
|
||||
@include rep.aspect(rep.$summon-main-w, rep.$summon-main-h);
|
||||
}
|
||||
|
||||
.frame.summon.cell {
|
||||
@include rep.aspect(rep.$summon-cell-w, rep.$summon-cell-h);
|
||||
}
|
||||
|
||||
.image {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
z-index: 2;
|
||||
|
||||
&.placeholder {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
color: var(--icon-secondary, #999);
|
||||
transition: color 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.frame.editable:hover .icon {
|
||||
color: var(--icon-secondary-hover, #666);
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: $font-small;
|
||||
text-align: center;
|
||||
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;
|
||||
top: 6px;
|
||||
background: #333;
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
z-index: 3;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,21 @@
|
|||
<script lang="ts">
|
||||
import type { GridWeaponItemView, PartyView } from '$lib/api/schemas/party'
|
||||
import type { GridWeapon } from '$lib/types/api/party'
|
||||
import type { Party } from '$lib/types/api/party'
|
||||
import { getContext } from 'svelte'
|
||||
import Icon from '$lib/components/Icon.svelte'
|
||||
|
||||
export let item: GridWeaponItemView | undefined
|
||||
export let position: number
|
||||
interface Props {
|
||||
item?: GridWeapon
|
||||
position: number
|
||||
}
|
||||
|
||||
let { item, position }: Props = $props()
|
||||
|
||||
type PartyCtx = {
|
||||
getParty: () => PartyView
|
||||
updateParty: (p: PartyView) => void
|
||||
getParty: () => Party
|
||||
updateParty: (p: Party) => void
|
||||
canEdit: () => boolean
|
||||
getEditKey: () => string | null
|
||||
services: { gridService: any; partyService: any }
|
||||
}
|
||||
|
||||
|
|
@ -22,79 +29,76 @@
|
|||
return '—'
|
||||
}
|
||||
|
||||
function weaponImageUrl(w?: GridWeaponItemView): string {
|
||||
const id = (w as any)?.object?.granblueId
|
||||
const isMain = !!(w && ((w as any).mainhand || (w as any).position === -1))
|
||||
if (!id) return isMain
|
||||
? '/images/placeholders/placeholder-weapon-main.png'
|
||||
: '/images/placeholders/placeholder-weapon-grid.png'
|
||||
// Use $derived to ensure consistent computation between server and client
|
||||
let imageUrl = $derived(() => {
|
||||
// Check position first for main weapon determination
|
||||
const isMain = position === -1 || item?.mainhand
|
||||
|
||||
// Handle both new structure (item.weapon) and old structure (item.object) for compatibility
|
||||
const weaponData = item?.weapon || (item as any)?.object
|
||||
|
||||
// If no item or no granblueId, return placeholder
|
||||
if (!item || !weaponData?.granblueId) {
|
||||
return isMain
|
||||
? '/images/placeholders/placeholder-weapon-main.png'
|
||||
: '/images/placeholders/placeholder-weapon-grid.png'
|
||||
}
|
||||
|
||||
const id = weaponData.granblueId
|
||||
const folder = isMain ? 'weapon-main' : 'weapon-grid'
|
||||
const objElement = (w as any)?.object?.element
|
||||
const instElement = (w as any)?.element
|
||||
const objElement = weaponData?.element
|
||||
const instElement = item?.element
|
||||
|
||||
if (objElement === 0 && instElement) {
|
||||
return `/images/${folder}/${id}_${instElement}.jpg`
|
||||
}
|
||||
return `/images/${folder}/${id}.jpg`
|
||||
}
|
||||
})
|
||||
|
||||
async function remove() {
|
||||
if (!item?.id) return
|
||||
const party = ctx.getParty()
|
||||
const editKey = ctx.services.partyService.getEditKey(party.shortcode)
|
||||
const editKey = ctx.getEditKey()
|
||||
const updated = await ctx.services.gridService.removeWeapon(party.id, item.id as any, editKey || undefined)
|
||||
ctx.updateParty(updated)
|
||||
}
|
||||
|
||||
async function add() {
|
||||
const party = ctx.getParty()
|
||||
const editKey = ctx.services.partyService.getEditKey(party.shortcode)
|
||||
const id = window.prompt('Enter weapon ID to add')
|
||||
if (!id) return
|
||||
const res = await ctx.services.gridService.addWeapon(party.id, id, position, editKey || undefined)
|
||||
if (res.conflicts) {
|
||||
window.alert('Conflict detected. Please resolve via UI in a later step.')
|
||||
return
|
||||
}
|
||||
ctx.updateParty(res.party)
|
||||
}
|
||||
|
||||
async function replaceItem() {
|
||||
if (!item?.id) return add()
|
||||
const party = ctx.getParty()
|
||||
const editKey = ctx.services.partyService.getEditKey(party.shortcode)
|
||||
const id = window.prompt('Enter new weapon ID')
|
||||
if (!id) return
|
||||
const res = await ctx.services.gridService.replaceWeapon(party.id, item.id as any, id, editKey || undefined)
|
||||
if (res.conflicts) {
|
||||
window.alert('Conflict detected. Please resolve via UI in a later step.')
|
||||
return
|
||||
}
|
||||
ctx.updateParty(res.party)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="unit">
|
||||
<img class="image" alt={item ? displayName(item.object) : ''} src={weaponImageUrl(item)} />
|
||||
<div class="name">{item ? displayName(item.object) : '—'}</div>
|
||||
{#if ctx.canEdit() && !item}
|
||||
<button class="add" title="Add" on:click={add}>+</button>
|
||||
{/if}
|
||||
{#if ctx.canEdit() && item?.id}
|
||||
<div class="actions">
|
||||
<button class="replace" title="Replace" on:click={replaceItem}>↺</button>
|
||||
<button class="remove" title="Remove" on:click={remove}>×</button>
|
||||
<div class="unit" class:empty={!item}>
|
||||
{#key (item ? (item as any).id ?? position : `empty-${position}`)}
|
||||
<div
|
||||
class="frame weapon"
|
||||
class:main={item?.mainhand || position === -1}
|
||||
class:cell={!(item?.mainhand || position === -1)}
|
||||
class:editable={ctx?.canEdit()}
|
||||
on:click={() => ctx?.openPicker && ctx.openPicker({ type: 'weapon', position, item })}
|
||||
>
|
||||
<img class="image" class:placeholder={!item || !(item?.weapon?.granblueId || (item as any)?.object?.granblueId)} alt={item ? displayName(item?.weapon || (item as any)?.object) : ''} src={imageUrl()} />
|
||||
{#if !item && ctx?.canEdit()}
|
||||
<span class="icon">
|
||||
<Icon name="plus" size={24} />
|
||||
</span>
|
||||
{/if}
|
||||
{#if ctx.canEdit() && item?.id}
|
||||
<div class="actions">
|
||||
<button class="remove" title="Remove" on:click|stopPropagation={remove}>×</button>
|
||||
</div>
|
||||
{/if}
|
||||
{#if item?.mainhand || position === -1}
|
||||
<span class="badge">Main</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if (item as any)?.mainhand || position === -1}
|
||||
<span class="badge">Main</span>
|
||||
{/if}
|
||||
{/key}
|
||||
<div class="name">{item ? displayName(item?.weapon || (item as any)?.object) : ''}</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@use '$src/themes/colors' as *;
|
||||
@use '$src/themes/typography' as *;
|
||||
@use '$src/themes/spacing' as *;
|
||||
@use '$src/themes/rep' as rep;
|
||||
|
||||
.unit {
|
||||
position: relative;
|
||||
|
|
@ -103,23 +107,101 @@
|
|||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: $unit;
|
||||
|
||||
&.empty .name {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.image { width: 100%; height: auto; border: 1px solid $grey-75; border-radius: 8px; display: block; }
|
||||
.name { font-size: $font-small; text-align: center; color: $grey-50; }
|
||||
.actions { position: absolute; top: 6px; right: 6px; display: flex; gap: 6px; }
|
||||
.remove, .replace, .add {
|
||||
|
||||
.frame {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
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;
|
||||
|
||||
&.editable {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--primary-color, #0066cc);
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
}
|
||||
|
||||
&.weapon.main.editable:hover {
|
||||
transform: scale(1.01);
|
||||
}
|
||||
}
|
||||
|
||||
.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 {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
z-index: 2;
|
||||
|
||||
&.placeholder {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
color: var(--icon-secondary, #999);
|
||||
transition: color 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.frame.editable:hover .icon {
|
||||
color: var(--icon-secondary-hover, #666);
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: $font-small;
|
||||
text-align: center;
|
||||
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;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.add { position: absolute; top: 6px; right: 6px; }
|
||||
|
||||
.badge {
|
||||
position: absolute;
|
||||
left: 6px; top: 6px;
|
||||
background: #333; color: #fff;
|
||||
font-size: 11px; padding: 2px 6px; border-radius: 10px;
|
||||
left: 6px;
|
||||
top: 6px;
|
||||
background: #333;
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
z-index: 3;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Reference in a new issue