diff --git a/src/lib/components/units/CharacterUnit.svelte b/src/lib/components/units/CharacterUnit.svelte index 4bd3d492..2018e5ae 100644 --- a/src/lib/components/units/CharacterUnit.svelte +++ b/src/lib/components/units/CharacterUnit.svelte @@ -4,12 +4,13 @@ import type { Job } from '$lib/types/api/entities' import { getContext } from 'svelte' import Icon from '$lib/components/Icon.svelte' - import ContextMenu from '$lib/components/ui/ContextMenu.svelte' - import { ContextMenu as ContextMenuBase, DropdownMenu as DropdownMenuBase } from 'bits-ui' + import UnitMenuContainer from '$lib/components/ui/menu/UnitMenuContainer.svelte' + import MenuItems from '$lib/components/ui/menu/MenuItems.svelte' 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 { sidebar } from '$lib/stores/sidebar.svelte' 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' @@ -70,6 +71,31 @@ // Check if this is the protagonist slot 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() { if (!item?.id) return try { @@ -140,86 +166,88 @@ } -
+
{#if item} - - {#snippet children()} - {#key item?.id ?? position} -
viewDetails()} - > - {#if position !== 0} - {#if ctx?.canEdit()} - + {:else if item.perpetuity} Perpetuity Ring - Add Perpetuity Ring - - {:else if item.perpetuity} - Perpetuity Ring + {/if} {/if} - {/if} - {isProtagonist -
- {/key} + {isProtagonist +
+ {/key} +
{/snippet} {#snippet contextMenu()} - - {m.context_view_details()} - - {#if ctx?.canEdit()} - - {m.context_replace()} - - - - {m.context_remove()} - - {/if} + {/snippet} {#snippet dropdownMenu()} - - {m.context_view_details()} - - {#if ctx?.canEdit()} - - {m.context_replace()} - - - - {m.context_remove()} - - {/if} + {/snippet} - + {:else} {#key `empty-${position}`}
diff --git a/src/lib/components/units/SummonUnit.svelte b/src/lib/components/units/SummonUnit.svelte index 81f1158e..e0f621b1 100644 --- a/src/lib/components/units/SummonUnit.svelte +++ b/src/lib/components/units/SummonUnit.svelte @@ -3,11 +3,12 @@ import type { Party } from '$lib/types/api/party' import { getContext } from 'svelte' import Icon from '$lib/components/Icon.svelte' - import ContextMenu from '$lib/components/ui/ContextMenu.svelte' - import { ContextMenu as ContextMenuBase, DropdownMenu as DropdownMenuBase } from 'bits-ui' + import UnitMenuContainer from '$lib/components/ui/menu/UnitMenuContainer.svelte' + import MenuItems from '$lib/components/ui/menu/MenuItems.svelte' 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 { sidebar } from '$lib/stores/sidebar.svelte' import * as m from '$lib/paraglide/messages' interface Props { @@ -43,6 +44,24 @@ 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() { if (!item?.id) return try { @@ -74,21 +93,22 @@ -
+
{#if item} - - {#snippet children()} - {#key item?.id ?? position} -
viewDetails()} - > + + {#snippet trigger()} +
+ {#key item?.id ?? position} +
viewDetails()} + > {displayName(item?.summon)}Friend {/if}
- {/key} + {/key} +
{/snippet} {#snippet contextMenu()} - - {m.context_view_details()} - - {#if ctx?.canEdit()} - - {m.context_replace()} - - - - {m.context_remove()} - - {/if} + {/snippet} {#snippet dropdownMenu()} - - {m.context_view_details()} - - {#if ctx?.canEdit()} - - {m.context_replace()} - - - - {m.context_remove()} - - {/if} + {/snippet} - +
{:else} {#key `empty-${position}`}
diff --git a/src/lib/components/units/WeaponUnit.svelte b/src/lib/components/units/WeaponUnit.svelte index 13bedb4d..05c1cc59 100644 --- a/src/lib/components/units/WeaponUnit.svelte +++ b/src/lib/components/units/WeaponUnit.svelte @@ -1,390 +1,548 @@ -
= 9}> - {#if item} - - {#snippet children()} - {#key item?.id ?? position} -
= 9} - class:editable={ctx?.canEdit()} - onclick={() => viewDetails()} - > -
- {#if awakeningImage} - {`${item?.awakening?.type?.name?.en - {/if} -
- {#each axSkillImages as skill} - {skill.alt} - {/each} - {#each weaponKeyImages as skill} - {skill.alt} - {/each} -
-
- {displayName(item?.weapon)} -
- {/key} - {/snippet} +
= 9} + class:is-active={isActive} +> + {#if item} + + {#snippet trigger()} +
+ {#key item?.id ?? position} +
= 9} + class:editable={ctx?.canEdit()} + onclick={() => viewDetails()} + > +
+ {#if awakeningImage} + {`${item?.awakening?.type?.name?.en + {/if} +
+ {#each axSkillImages as skill} + {skill.alt} + {/each} + {#each weaponKeyImages as skill} + {skill.alt} + {/each} +
+
+ {displayName(item?.weapon)} +
+ {/key} +
+ {/snippet} - {#snippet contextMenu()} - - {m.context_view_details()} - - {#if ctx?.canEdit()} - - {m.context_replace()} - - - - {m.context_remove()} - - {/if} - {/snippet} + {#snippet contextMenu()} + + {/snippet} - {#snippet dropdownMenu()} - - {m.context_view_details()} - - {#if ctx?.canEdit()} - - {m.context_replace()} - - - - {m.context_remove()} - - {/if} - {/snippet} - - {:else} - {#key `empty-${position}`} -
= 9} - class:editable={ctx?.canEdit()} - onclick={() => ctx?.canEdit() && ctx?.openPicker && ctx.openPicker({ type: 'weapon', position, item })} - > - - {#if ctx?.canEdit()} - - - - {/if} -
- {/key} - {/if} - {#if item} - { - if (!item?.id || !ctx) return - try { - const editKey = ctx.getEditKey() - const updated = await ctx.services.gridService.updateWeaponUncap(item.id, level, undefined, editKey || undefined) - if (updated) { - ctx.updateParty(updated) - } - } catch (err) { - console.error('Failed to update weapon uncap:', err) - // TODO: Show user-friendly error notification - } - }} - updateTranscendence={async (stage) => { - if (!item?.id || !ctx) return - try { - const editKey = ctx.getEditKey() - // When setting transcendence > 0, also set uncap to max (6) - const maxUncap = stage > 0 ? 6 : undefined - const updated = await ctx.services.gridService.updateWeaponUncap(item.id, maxUncap, stage, editKey || undefined) - if (updated) { - ctx.updateParty(updated) - } - } catch (err) { - console.error('Failed to update weapon transcendence:', err) - // TODO: Show user-friendly error notification - } - }} - /> - {/if} -
{item ? displayName(item?.weapon) : ''}
+ {#snippet dropdownMenu()} + + {/snippet} +
+ {:else} + {#key `empty-${position}`} +
= 9} + class:editable={ctx?.canEdit()} + onclick={() => + ctx?.canEdit() && ctx?.openPicker && ctx.openPicker({ type: 'weapon', position, item })} + > + + {#if ctx?.canEdit()} + + + + {/if} +
+ {/key} + {/if} + {#if item} + { + if (!item?.id || !ctx) return + try { + const editKey = ctx.getEditKey() + const updated = await ctx.services.gridService.updateWeaponUncap( + item.id, + level, + undefined, + editKey || undefined + ) + if (updated) { + ctx.updateParty(updated) + } + } catch (err) { + console.error('Failed to update weapon uncap:', err) + // TODO: Show user-friendly error notification + } + }} + updateTranscendence={async (stage) => { + if (!item?.id || !ctx) return + try { + const editKey = ctx.getEditKey() + // When setting transcendence > 0, also set uncap to max (6) + const maxUncap = stage > 0 ? 6 : undefined + const updated = await ctx.services.gridService.updateWeaponUncap( + item.id, + maxUncap, + stage, + editKey || undefined + ) + if (updated) { + ctx.updateParty(updated) + } + } catch (err) { + console.error('Failed to update weapon transcendence:', err) + // TODO: Show user-friendly error notification + } + }} + /> + {/if} +
{item ? displayName(item?.weapon) : ''}
diff --git a/src/lib/stores/sidebar.svelte.ts b/src/lib/stores/sidebar.svelte.ts index 2aafd0f3..39b5ac1f 100644 --- a/src/lib/stores/sidebar.svelte.ts +++ b/src/lib/stores/sidebar.svelte.ts @@ -10,6 +10,7 @@ interface SidebarState { component?: Component componentProps?: Record scrollable?: boolean + activeItemId?: string } class SidebarStore { @@ -31,17 +32,27 @@ class SidebarStore { this.state.scrollable = scrollable } - openWithComponent(title: string, component: Component, props?: Record, scrollable = true) { + openWithComponent( + title: string, + component: Component, + props?: Record, + scrollable = true + ) { this.state.open = true this.state.title = title this.state.component = component this.state.componentProps = props this.state.content = undefined 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() { this.state.open = false + this.state.activeItemId = undefined // Clear content after animation setTimeout(() => { this.state.title = undefined @@ -82,6 +93,10 @@ class SidebarStore { get scrollable() { return this.state.scrollable ?? true } + + get activeItemId() { + return this.state.activeItemId + } } -export const sidebar = new SidebarStore() \ No newline at end of file +export const sidebar = new SidebarStore() diff --git a/src/routes/test/context-menu/+page.svelte b/src/routes/test/context-menu/+page.svelte index 712ce053..c9a79b14 100644 --- a/src/routes/test/context-menu/+page.svelte +++ b/src/routes/test/context-menu/+page.svelte @@ -1,6 +1,6 @@