From d51fe039057e4679fc2aac37ef0e46d7059a32eb Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Sun, 30 Nov 2025 02:31:48 -0800 Subject: [PATCH] fix: character slot selection glow and empty protagonist - character grid starts at position 1 (skip protagonist) - add isEmptySelected state for glow on empty slots - empty protagonist shows relief.png background only --- src/lib/components/units/CharacterUnit.svelte | 63 ++++++++++++++++--- src/lib/utils/gridHelpers.ts | 2 +- 2 files changed, 56 insertions(+), 9 deletions(-) diff --git a/src/lib/components/units/CharacterUnit.svelte b/src/lib/components/units/CharacterUnit.svelte index 58a46c35..e898e0a9 100644 --- a/src/lib/components/units/CharacterUnit.svelte +++ b/src/lib/components/units/CharacterUnit.svelte @@ -11,6 +11,7 @@ import { openDetailsSidebar } from '$lib/features/details/openDetailsSidebar.svelte' import { getJobPortraitUrl, Gender } from '$lib/utils/jobUtils' import { sidebar } from '$lib/stores/sidebar.svelte' + import { GridType } from '$lib/types/enums' 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' @@ -30,6 +31,8 @@ updateParty: (p: Party) => void canEdit: () => boolean getEditKey: () => string | null + getSelectedSlot?: () => number + getActiveTab?: () => GridType services: { gridService: any; partyService: any } openPicker?: (opts: { type: 'character' | 'weapon' | 'summon' @@ -53,6 +56,11 @@ return getJobPortraitUrl(job, Gender.Gran) // TODO: Get gender from user preferences } + // For protagonist slot without a job, show nothing (relief.png background shows through) + if (position === 0 && !job) { + return '' + } + // If no item or no character with granblueId, return placeholder if (!item || !item.character?.granblueId) { return getCharacterImageWithPose(null, 'main', 0, 0) @@ -74,6 +82,14 @@ // Check if this item is currently active in the sidebar let isActive = $derived(item?.id && sidebar.activeItemId === String(item.id)) + // Check if this empty slot is currently selected for adding an item + let isEmptySelected = $derived( + !item && + !isProtagonist && + ctx?.getSelectedSlot?.() === position && + ctx?.getActiveTab?.() === GridType.Character + ) + // Determine element class for focus ring let elementClass = $derived.by(() => { const element = item?.character?.element || partyElement @@ -180,6 +196,7 @@ class="frame character cell {elementClass}" class:protagonist={position === 0} class:editable={ctx?.canEdit()} + class:is-active={isActive} onclick={() => viewDetails()} > {#if position !== 0} @@ -210,6 +227,7 @@ /> {/if} {/if} + {#if imageUrl} {isProtagonist + {/if} {/key} @@ -254,20 +273,28 @@ class="frame character cell" class:editable={ctx?.canEdit() && !isProtagonist} class:protagonist={isProtagonist} + class:empty-protagonist={isProtagonist && !job} + class:is-selected={isEmptySelected} onclick={() => !isProtagonist && ctx?.canEdit() && ctx?.openPicker && ctx.openPicker({ type: 'character', position, item })} > - {isProtagonist - {#if ctx?.canEdit()} + {#if !isProtagonist} + + {:else if job && imageUrl} + {job.name.en} + {/if} + {#if ctx?.canEdit() && !isProtagonist} @@ -381,6 +408,22 @@ opacity: 0.95; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } + + // Slot selection - subtle dark pulsing glow (works for both empty and filled) + &.is-selected, + &.is-active { + animation: pulse-slot-shadow 2s ease-in-out infinite; + } + } + + @keyframes pulse-slot-shadow { + 0%, + 100% { + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.12), 0 0 4px 2px rgba(0, 0, 0, 0.06); + } + 50% { + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.24), 0 0 8px 4px rgba(0, 0, 0, 0.12); + } } .frame.character.cell { @@ -391,6 +434,10 @@ background-size: cover; background-position: center -20px; background-repeat: no-repeat; + + &.empty-protagonist { + background-position: center 0; + } } } diff --git a/src/lib/utils/gridHelpers.ts b/src/lib/utils/gridHelpers.ts index 570e2bf9..3898b465 100644 --- a/src/lib/utils/gridHelpers.ts +++ b/src/lib/utils/gridHelpers.ts @@ -19,7 +19,7 @@ export interface SlotRange { const GRID_CONFIGS: Record = { [GridType.Weapon]: { start: 0, end: 8, specialSlots: [-1] }, // mainhand + 9 grid slots [GridType.Summon]: { start: 0, end: 5, specialSlots: [-1, 6] }, // main + 6 grid + friend - [GridType.Character]: { start: 0, end: 4, specialSlots: [] } // 5 slots (0-4) + [GridType.Character]: { start: 1, end: 4, specialSlots: [] } // 4 slots (1-4), position 0 is protagonist (not user-selectable) } /**