sidebar: refactor details components

This commit is contained in:
Justin Edmund 2025-11-30 20:06:31 -08:00
parent 47885b1429
commit a858877545
6 changed files with 307 additions and 463 deletions

View file

@ -1,6 +1,8 @@
<script lang="ts"> <script lang="ts">
import type { GridCharacter, GridWeapon, GridSummon } from '$lib/types/api/party' import type { GridCharacter, GridWeapon, GridSummon } from '$lib/types/api/party'
import { detectModifications } from '$lib/utils/modificationDetector' import { detectModifications, canWeaponBeModified } from '$lib/utils/modificationDetector'
import { partyStore } from '$lib/stores/partyStore.svelte'
import { sidebar } from '$lib/stores/sidebar.svelte'
import DetailsSidebarSegmentedControl from './modifications/DetailsSidebarSegmentedControl.svelte' import DetailsSidebarSegmentedControl from './modifications/DetailsSidebarSegmentedControl.svelte'
import ItemHeader from './details/ItemHeader.svelte' import ItemHeader from './details/ItemHeader.svelte'
import BasicInfoSection from './details/BasicInfoSection.svelte' import BasicInfoSection from './details/BasicInfoSection.svelte'
@ -13,11 +15,42 @@
item: GridCharacter | GridWeapon | GridSummon item: GridCharacter | GridWeapon | GridSummon
} }
let { type, item }: Props = $props() let { type, item: initialItem }: Props = $props()
// Derive item from partyStore for reactivity, fall back to prop if not in store
// This ensures the sidebar updates when party data changes (e.g., uncap level)
let item = $derived.by(() => {
const activeId = sidebar.activeItemId
if (activeId && partyStore.party) {
const storeItem = partyStore.getItem(type, activeId)
if (storeItem) return storeItem
}
return initialItem
})
let selectedView = $state<'canonical' | 'user'>('user')
let modificationStatus = $derived(detectModifications(type, item)) let modificationStatus = $derived(detectModifications(type, item))
// For weapons, only show segmented control if the weapon can be modified
const showSegmentedControl = $derived(
type === 'weapon'
? canWeaponBeModified(item as GridWeapon)
: modificationStatus.hasModifications
)
// Track selected view - updated reactively based on modifiability
let selectedView = $state<'canonical' | 'user'>('user')
// Update view when switching to items with different modifiability
$effect(() => {
if (!showSegmentedControl) {
// Force canonical view for non-modifiable items
selectedView = 'canonical'
} else if (showSegmentedControl && selectedView === 'canonical') {
// Switch to user view when selecting a modifiable item
selectedView = 'user'
}
})
// Helper to get the actual item data // Helper to get the actual item data
function getItemData() { function getItemData() {
if (type === 'character') { if (type === 'character') {
@ -54,7 +87,7 @@
<ItemHeader {type} {item} {itemData} {gridUncapLevel} {gridTranscendence} /> <ItemHeader {type} {item} {itemData} {gridUncapLevel} {gridTranscendence} />
<DetailsSidebarSegmentedControl <DetailsSidebarSegmentedControl
hasModifications={modificationStatus.hasModifications} hasModifications={showSegmentedControl}
bind:selectedView bind:selectedView
/> />
@ -87,6 +120,6 @@
display: flex; display: flex;
position: relative; position: relative;
flex-direction: column; flex-direction: column;
gap: spacing.$unit-2x; gap: spacing.$unit-4x;
} }
</style> </style>

View file

@ -1,9 +1,12 @@
<script lang="ts"> <script lang="ts">
import { getElementLabel } from '$lib/utils/element'
import { getRarityLabel } from '$lib/utils/rarity' import { getRarityLabel } from '$lib/utils/rarity'
import { getProficiencyLabel } from '$lib/utils/proficiency'
import { getRaceLabel } from '$lib/utils/race' import { getRaceLabel } from '$lib/utils/race'
import { getGenderLabel } from '$lib/utils/gender' import { getGenderLabel } from '$lib/utils/gender'
import DetailsSection from './DetailsSection.svelte'
import DetailRow from './DetailRow.svelte'
import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte'
import ElementLabel from '$lib/components/labels/ElementLabel.svelte'
import ProficiencyLabel from '$lib/components/labels/ProficiencyLabel.svelte'
interface Props { interface Props {
type: 'character' | 'weapon' | 'summon' type: 'character' | 'weapon' | 'summon'
@ -11,97 +14,69 @@
} }
let { type, itemData }: Props = $props() let { type, itemData }: Props = $props()
// Calculate max uncap level (all stars filled)
const maxUncapLevel = $derived.by(() => {
const flb = itemData?.uncap?.flb ?? false
const ulb = itemData?.uncap?.ulb ?? false
const transcendence = itemData?.uncap?.transcendence ?? false
const special = type === 'character' && (itemData?.rarity ?? 3) < 3
if (type === 'character') {
if (special) {
return ulb ? 5 : flb ? 4 : 3
} else {
// Regular characters: transcendence star is separate
return flb ? 5 : 4
}
} else {
// Weapons and summons
return transcendence ? 5 : ulb ? 5 : flb ? 4 : 3
}
})
const special = $derived(type === 'character' && (itemData?.rarity ?? 3) < 3)
</script> </script>
<div class="details-section"> <DetailsSection title="Basic Information">
<h3>Basic Information</h3> <DetailRow label="Rarity" value={getRarityLabel(itemData?.rarity)} />
<div class="detail-row"> <DetailRow label="Element">
<span class="label">Rarity</span> <ElementLabel element={itemData?.element} size="medium" />
<span class="value">{getRarityLabel(itemData?.rarity)}</span> </DetailRow>
</div>
<div class="detail-row">
<span class="label">Element</span>
<span class="value">{getElementLabel(itemData?.element)}</span>
</div>
{#if type === 'character'} {#if type === 'character'}
{#if itemData?.race && itemData.race.length > 0} {#if itemData?.race && itemData.race.length > 0}
<div class="detail-row"> <DetailRow
<span class="label">Race</span> label="Race"
<span class="value"> value={itemData.race
{itemData.race .map((r: any) => getRaceLabel(r))
.map((r: any) => getRaceLabel(r)) .filter(Boolean)
.filter(Boolean) .join(', ') || '—'}
.join(', ') || '—'} />
</span>
</div>
{/if} {/if}
<div class="detail-row"> <DetailRow label="Gender" value={getGenderLabel(itemData?.gender)} />
<span class="label">Gender</span>
<span class="value">{getGenderLabel(itemData?.gender)}</span>
</div>
{#if itemData?.proficiency && itemData.proficiency.length > 0} {#if itemData?.proficiency && itemData.proficiency.length > 0}
<div class="detail-row"> <DetailRow label="Proficiencies">
<span class="label">Proficiencies</span> {#each itemData.proficiency as prof}
<span class="value"> <ProficiencyLabel proficiency={prof} size="medium" />
{itemData.proficiency {/each}
.map((p: any) => getProficiencyLabel(p)) </DetailRow>
.filter(Boolean)
.join(', ') || '—'}
</span>
</div>
{/if} {/if}
{:else if type === 'weapon'} {:else if type === 'weapon'}
<div class="detail-row"> <DetailRow label="Proficiency">
<span class="label">Proficiency</span> <ProficiencyLabel proficiency={itemData?.proficiency} size="medium" />
<span class="value">{getProficiencyLabel(itemData?.proficiency)}</span> </DetailRow>
</div>
{/if} {/if}
</div>
<style lang="scss"> <DetailRow label="Max Uncap">
@use '$src/themes/colors' as colors; <UncapIndicator
@use '$src/themes/layout' as layout; {type}
@use '$src/themes/spacing' as spacing; uncapLevel={maxUncapLevel}
@use '$src/themes/typography' as typography; transcendenceStage={itemData?.uncap?.transcendence ? 5 : 0}
flb={itemData?.uncap?.flb ?? false}
.details-section { ulb={itemData?.uncap?.ulb ?? false}
padding: 0 spacing.$unit; transcendence={itemData?.uncap?.transcendence ?? false}
{special}
h3 { />
margin: 0 0 calc(spacing.$unit * 1.5) 0; </DetailRow>
font-size: typography.$font-name; </DetailsSection>
font-weight: typography.$medium;
color: var(--text-primary);
padding: 0 spacing.$unit;
}
}
.detail-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: calc(spacing.$unit * 1.5) spacing.$unit;
&:hover {
background: var(--page-hover);
border-radius: layout.$item-corner;
}
&:last-child {
border-bottom: none;
}
.label {
font-size: typography.$font-regular;
color: var(--text-secondary, colors.$grey-50);
}
.value {
font-size: typography.$font-regular;
color: var(--text-primary, colors.$grey-10);
font-weight: typography.$medium;
text-align: right;
}
}
</style>

View file

@ -62,6 +62,9 @@
} }
return '—' return '—'
} }
// Special characters have different star counts (SR characters, etc.)
const special = $derived(type === 'character' && (itemData?.rarity ?? 3) < 3)
</script> </script>
<div class="item-header-container"> <div class="item-header-container">
@ -74,6 +77,7 @@
flb={itemData?.uncap?.flb} flb={itemData?.uncap?.flb}
ulb={itemData?.uncap?.ulb} ulb={itemData?.uncap?.ulb}
transcendence={itemData?.uncap?.transcendence} transcendence={itemData?.uncap?.transcendence}
{special}
editable={false} editable={false}
/> />
</div> </div>

View file

@ -1,4 +1,7 @@
<script lang="ts"> <script lang="ts">
import DetailsSection from './DetailsSection.svelte'
import DetailRow from './DetailRow.svelte'
interface Props { interface Props {
itemData: any itemData: any
gridUncapLevel: number | null gridUncapLevel: number | null
@ -8,169 +11,34 @@
let { itemData, gridUncapLevel, gridTranscendence }: Props = $props() let { itemData, gridUncapLevel, gridTranscendence }: Props = $props()
</script> </script>
<div class="details-section"> {#if itemData?.hp}
{#if itemData?.hp && itemData?.atk} <DetailsSection title="HP">
<div class="stats-grid"> <DetailRow label="Base" value={itemData.hp.minHp} />
<div class="grid-header empty"></div> <DetailRow label="MLB" value={itemData.hp.maxHp} />
<div class="grid-header">HP</div> {#if itemData.uncap?.flb && itemData.hp.maxHpFlb}
<div class="grid-header">ATK</div> <DetailRow label="FLB" value={itemData.hp.maxHpFlb} />
{/if}
{#if itemData.uncap?.ulb && itemData.hp.maxHpUlb}
<DetailRow label="ULB" value={itemData.hp.maxHpUlb} />
{/if}
{#if gridTranscendence && gridTranscendence > 0}
<DetailRow label="T5" value={itemData.hp.maxHpXlb} />
{/if}
</DetailsSection>
{/if}
<div class="grid-label">Base</div> {#if itemData?.atk}
<div class="grid-value">{itemData.hp.minHp ?? '—'}</div> <DetailsSection title="ATK">
<div class="grid-value">{itemData.atk.minAtk ?? '—'}</div> <DetailRow label="Base" value={itemData.atk.minAtk} />
<DetailRow label="MLB" value={itemData.atk.maxAtk} />
<div class="grid-label">MLB</div> {#if itemData.uncap?.flb && itemData.atk.maxAtkFlb}
<div class="grid-value">{itemData.hp.maxHp ?? '—'}</div> <DetailRow label="FLB" value={itemData.atk.maxAtkFlb} />
<div class="grid-value">{itemData.atk.maxAtk ?? '—'}</div> {/if}
{#if itemData.uncap?.ulb && itemData.atk.maxAtkUlb}
{#if itemData.uncap?.flb && (itemData.hp.maxHpFlb || itemData.atk.maxAtkFlb)} <DetailRow label="ULB" value={itemData.atk.maxAtkUlb} />
<div class="grid-label">FLB</div> {/if}
<div class="grid-value">{itemData.hp.maxHpFlb ?? '—'}</div> {#if gridTranscendence && gridTranscendence > 0}
<div class="grid-value">{itemData.atk.maxAtkFlb ?? '—'}</div> <DetailRow label="T5" value={itemData.atk.maxAtkXlb} />
{/if} {/if}
</DetailsSection>
{#if itemData.uncap?.ulb && (itemData.hp.maxHpUlb || itemData.atk.maxAtkUlb)} {/if}
<div class="grid-label">ULB</div>
<div class="grid-value">{itemData.hp.maxHpUlb ?? '—'}</div>
<div class="grid-value">{itemData.atk.maxAtkUlb ?? '—'}</div>
{/if}
{#if gridTranscendence && gridTranscendence > 0}
<div class="grid-label">T5</div>
<div class="grid-value">{itemData.hp.maxHpXlb ?? '—'}</div>
<div class="grid-value">{itemData.atk.maxAtkXlb ?? '—'}</div>
{/if}
</div>
{/if}
</div>
<style lang="scss">
@use '$src/themes/colors' as colors;
@use '$src/themes/effects' as effects;
@use '$src/themes/layout' as layout;
@use '$src/themes/spacing' as spacing;
@use '$src/themes/typography' as typography;
.details-section {
padding: 0 spacing.$unit;
h3 {
margin: 0 0 calc(spacing.$unit * 1.5) 0;
font-size: typography.$font-name;
font-weight: typography.$medium;
color: var(--text-primary);
padding: 0 spacing.$unit;
}
h4 {
margin: calc(spacing.$unit * 1.5) 0 spacing.$unit 0;
font-size: typography.$font-small;
font-weight: typography.$medium;
color: var(--text-secondary, colors.$grey-50);
}
}
.stats-grid {
display: grid;
grid-template-columns: 0.5fr 1fr 1fr;
background: var(--page-bg);
gap: spacing.$unit-half;
border: 1px solid colors.$grey-80;
border-radius: layout.$card-corner;
box-shadow: effects.$page-elevation;
overflow: hidden;
padding: spacing.$unit;
.grid-header {
padding: spacing.$unit;
background: var(--button-bg-hover);
font-weight: typography.$medium;
color: var(--menu-text);
border-bottom: 1px solid var(--border-primary);
display: flex;
align-items: center;
justify-content: center;
border-radius: layout.$item-corner;
&.empty {
background: transparent;
}
&:not(:first-child) {
border-left: 1px solid var(--border-primary);
}
}
.grid-label {
padding: spacing.$unit;
background: var(--button-bg-hover);
font-weight: typography.$medium;
color: var(--menu-text);
border-radius: layout.$item-corner;
justify-content: center;
display: flex;
align-items: center;
}
.grid-value {
padding: spacing.$unit;
border-radius: layout.$item-corner;
text-align: right;
font-weight: typography.$medium;
font-size: typography.$font-regular;
font-variant-numeric: tabular-nums;
color: var(--menu-text);
border-bottom: 1px solid var(--border-primary);
border-left: 1px solid var(--border-primary);
display: flex;
align-items: center;
justify-content: flex-end;
&:hover {
background: var(--button-bg-hover);
}
}
// Remove border from last row
.grid-label:last-of-type,
.grid-value:last-of-type {
border-bottom: none;
}
}
.detail-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: calc(spacing.$unit * 1.5) spacing.$unit;
&:hover {
background: var(--page-hover);
border-radius: layout.$item-corner;
}
&:last-child {
border-bottom: none;
}
.label {
font-size: typography.$font-regular;
color: var(--text-secondary, colors.$grey-50);
}
.value {
font-size: typography.$font-regular;
color: var(--text-primary, colors.$grey-10);
font-weight: typography.$medium;
text-align: right;
}
}
.stats-group {
margin-bottom: spacing.$unit-2x;
&:last-child {
margin-bottom: 0;
}
}
</style>

View file

@ -1,21 +1,13 @@
<script lang="ts"> <script lang="ts">
import type { GridCharacter, GridWeapon, GridSummon } from '$lib/types/api/party' import type { GridCharacter, GridWeapon, GridSummon } from '$lib/types/api/party'
import ModificationSection from '../modifications/ModificationSection.svelte' import DetailsSection from './DetailsSection.svelte'
import UncapStatusDisplay from '../modifications/UncapStatusDisplay.svelte' import DetailRow from './DetailRow.svelte'
import AwakeningDisplay from '../modifications/AwakeningDisplay.svelte' import AwakeningDisplay from '../modifications/AwakeningDisplay.svelte'
import MasteryDisplay from '../modifications/MasteryDisplay.svelte' import MasteryDisplay from '../modifications/MasteryDisplay.svelte'
import StatModifierItem from '../modifications/StatModifierItem.svelte'
import WeaponKeysList from '../modifications/WeaponKeysList.svelte' import WeaponKeysList from '../modifications/WeaponKeysList.svelte'
import { getRarityLabel } from '$lib/utils/rarity' import { formatAxSkill, getWeaponKeyTitle } from '$lib/utils/modificationFormatters'
import { getElementLabel } from '$lib/utils/element' import ElementLabel from '$lib/components/labels/ElementLabel.svelte'
import { getRaceLabel } from '$lib/utils/race' import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte'
import { getGenderLabel } from '$lib/utils/gender'
import { getProficiencyLabel } from '$lib/utils/proficiency'
import {
formatAxSkill,
getWeaponKeyTitle,
getElementName
} from '$lib/utils/modificationFormatters'
interface Props { interface Props {
type: 'character' | 'weapon' | 'summon' type: 'character' | 'weapon' | 'summon'
@ -28,36 +20,54 @@
let { type, item, itemData, gridUncapLevel, gridTranscendence, modificationStatus }: Props = let { type, item, itemData, gridUncapLevel, gridTranscendence, modificationStatus }: Props =
$props() $props()
// Get uncap capabilities from item data based on type
let uncapCaps = $derived.by(() => {
if (type === 'character') {
const char = item as GridCharacter
const uncap = char.character?.uncap
return { flb: uncap?.flb, ulb: uncap?.ulb, transcendence: false }
} else if (type === 'weapon') {
const weapon = item as GridWeapon
const uncap = weapon.weapon?.uncap
return { flb: uncap?.flb, ulb: uncap?.ulb, transcendence: uncap?.transcendence }
} else {
const summon = item as GridSummon
const uncap = summon.summon?.uncap
return { flb: uncap?.flb, ulb: uncap?.ulb, transcendence: uncap?.transcendence }
}
})
</script> </script>
<div class="team-view"> <div class="team-view">
<ModificationSection title="Uncap & Transcendence" visible={true}> <DetailsSection title="Uncap & Transcendence">
<UncapStatusDisplay <DetailRow label="Uncap Level">
{type} <UncapIndicator
uncapLevel={gridUncapLevel} {type}
transcendenceStep={gridTranscendence} uncapLevel={gridUncapLevel}
special={itemData?.special} transcendenceStage={gridTranscendence}
flb={itemData?.uncap?.flb} flb={uncapCaps?.flb}
ulb={itemData?.uncap?.ulb} ulb={uncapCaps?.ulb}
transcendence={itemData?.uncap?.transcendence} transcendence={uncapCaps?.transcendence}
/> />
</ModificationSection> </DetailRow>
</DetailsSection>
{#if type === 'character'} {#if type === 'character'}
{@const char = item as GridCharacter} {@const char = item as GridCharacter}
{#if modificationStatus.hasAwakening} {#if modificationStatus.hasAwakening}
<ModificationSection title="Awakening" visible={true}> <DetailsSection title="Awakening">
<AwakeningDisplay <AwakeningDisplay
{...(char.awakening ? { awakening: char.awakening } : {})} {...(char.awakening ? { awakening: char.awakening } : {})}
size="medium" size="medium"
showLevel={true} showLevel={true}
/> />
</ModificationSection> </DetailsSection>
{/if} {/if}
{#if modificationStatus.hasRings || modificationStatus.hasEarring} {#if modificationStatus.hasRings || modificationStatus.hasEarring}
<ModificationSection title="Mastery" visible={true}> <DetailsSection title="Mastery">
<MasteryDisplay <MasteryDisplay
rings={char.overMastery} rings={char.overMastery}
earring={char.aetherialMastery} earring={char.aetherialMastery}
@ -65,95 +75,69 @@
variant="detailed" variant="detailed"
showIcons={true} showIcons={true}
/> />
</ModificationSection> </DetailsSection>
{/if} {/if}
{#if modificationStatus.hasPerpetuity} {#if modificationStatus.hasPerpetuity}
<ModificationSection title="Status" visible={true}> <DetailsSection title="Status">
<StatModifierItem label="Perpetuity" value="Active" variant="max" /> <DetailRow label="Perpetuity Ring" value="Active" />
</ModificationSection> </DetailsSection>
{/if} {/if}
{:else if type === 'weapon'} {:else if type === 'weapon'}
{@const weapon = item as GridWeapon} {@const weapon = item as GridWeapon}
{#if modificationStatus.hasAwakening && weapon.awakening} {#if modificationStatus.hasAwakening && weapon.awakening}
<ModificationSection title="Awakening" visible={true}> <DetailsSection title="Awakening">
<AwakeningDisplay awakening={weapon.awakening} size="medium" showLevel={true} /> <AwakeningDisplay awakening={weapon.awakening} size="medium" showLevel={true} />
</ModificationSection> </DetailsSection>
{/if} {/if}
{#if modificationStatus.hasWeaponKeys} {#if modificationStatus.hasWeaponKeys}
<ModificationSection title={getWeaponKeyTitle(weapon.weapon?.series)} visible={true}> <DetailsSection title={getWeaponKeyTitle(weapon.weapon?.series)}>
<WeaponKeysList weaponKeys={weapon.weaponKeys} weaponData={weapon.weapon} layout="list" /> <WeaponKeysList weaponKeys={weapon.weaponKeys} weaponData={weapon.weapon} layout="list" />
</ModificationSection> </DetailsSection>
{/if} {/if}
{#if modificationStatus.hasAxSkills && weapon.ax} {#if modificationStatus.hasAxSkills && weapon.ax}
<ModificationSection title="AX Skills" visible={true}> <DetailsSection title="AX Skills">
{#each weapon.ax as axSkill} {#each weapon.ax as axSkill}
<StatModifierItem <DetailRow
label={formatAxSkill(axSkill).split('+')[0]?.trim() ?? ''} label={formatAxSkill(axSkill).split('+')[0]?.trim() ?? ''}
value={`+${axSkill.strength}`} value={`+${axSkill.strength}${axSkill.modifier <= 2 ? '' : '%'}`}
suffix={axSkill.modifier <= 2 ? '' : '%'}
variant="enhanced"
/> />
{/each} {/each}
</ModificationSection> </DetailsSection>
{/if} {/if}
{#if modificationStatus.hasElement && weapon.element} {#if modificationStatus.hasElement && weapon.element}
<ModificationSection title="Element Override" visible={true}> <DetailsSection title="Element Override">
<StatModifierItem label="Instance Element" value={getElementName(weapon.element)} /> <DetailRow label="Weapon Element">
</ModificationSection> <ElementLabel element={weapon.element} size="medium" />
</DetailRow>
</DetailsSection>
{/if} {/if}
{:else if type === 'summon'} {:else if type === 'summon'}
{@const summon = item as GridSummon} {@const summon = item as GridSummon}
{#if modificationStatus.hasQuickSummon || modificationStatus.hasFriendSummon} {#if modificationStatus.hasQuickSummon || modificationStatus.hasFriendSummon}
<ModificationSection title="Summon Status" visible={true}> <DetailsSection title="Summon Status">
{#if summon.quickSummon} {#if summon.quickSummon}
<StatModifierItem label="Quick Summon" value="Enabled" variant="enhanced" /> <DetailRow label="Quick Summon" value="Enabled" />
{/if} {/if}
{#if summon.friend} {#if summon.friend}
<StatModifierItem label="Friend Summon" value="Yes" /> <DetailRow label="Friend Summon" value="Yes" />
{/if} {/if}
</ModificationSection> </DetailsSection>
{/if} {/if}
{/if} {/if}
</div> </div>
<style lang="scss"> <style lang="scss">
@use '$src/themes/colors' as colors;
@use '$src/themes/spacing' as spacing; @use '$src/themes/spacing' as spacing;
@use '$src/themes/typography' as typography;
.team-view { .team-view {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: spacing.$unit-2x; gap: spacing.$unit-4x;
}
.detail-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: calc(spacing.$unit * 0.75) 0;
border-bottom: 1px solid rgba(colors.$grey-70, 0.5);
&:last-child {
border-bottom: none;
}
.label {
font-size: typography.$font-regular;
color: var(--text-secondary, colors.$grey-50);
}
.value {
font-size: typography.$font-regular;
color: var(--text-primary, colors.$grey-10);
font-weight: typography.$medium;
text-align: right;
}
} }
</style> </style>

View file

@ -1,157 +1,137 @@
<script lang="ts"> <script lang="ts">
import { getWeaponKeyImages } from '$lib/utils/modifiers' import { getWeaponKeyImages } from '$lib/utils/modifiers'
import type { WeaponKey } from '$lib/types/api/entities' import type { WeaponKey } from '$lib/types/api/entities'
import type { LocalizedName } from '$lib/types/api/entities' import type { LocalizedName } from '$lib/types/api/entities'
interface Props { interface Props {
weaponKeys?: WeaponKey[] weaponKeys?: WeaponKey[]
weaponData?: { weaponData?: {
element?: number element?: number
proficiency?: number | number[] proficiency?: number | number[]
series?: number series?: number
name?: LocalizedName name?: LocalizedName
} }
layout?: 'list' | 'grid' layout?: 'list' | 'grid'
} }
let { let { weaponKeys, weaponData, layout = 'list' }: Props = $props()
weaponKeys,
weaponData,
layout = 'list'
}: Props = $props()
let keyImages = $derived( let keyImages = $derived(
getWeaponKeyImages( getWeaponKeyImages(
weaponKeys, weaponKeys,
weaponData?.element, weaponData?.element,
Array.isArray(weaponData?.proficiency) ? weaponData?.proficiency[0] : weaponData?.proficiency, Array.isArray(weaponData?.proficiency) ? weaponData?.proficiency[0] : weaponData?.proficiency,
weaponData?.series, weaponData?.series,
weaponData?.name weaponData?.name
) )
) )
function getKeyDescription(key: WeaponKey): string { function getKeyDescription(key: WeaponKey): string {
if (key.name?.en) return key.name.en if (key.name?.en) return key.name.en
if (key.name?.ja) return key.name.ja if (key.name?.ja) return key.name.ja
return key.slug || 'Weapon Key' return key.slug || 'Weapon Key'
} }
function getSlotLabel(slot: number, series?: number): string { function getSlotLabel(slot: number, series?: number): string {
if (series === 2) { return `Skill ${slot + 1}`
return slot === 0 ? 'Alpha Pendulum' : 'Pendulum' }
}
if (series === 3 || series === 34) {
return `Teluma ${slot + 1}`
}
if (series === 17) {
return slot === 0 ? 'Gauph Key' : `Ultima Key`
}
if (series === 22) {
return `Emblem Slot ${slot + 1}`
}
return `Slot ${slot + 1}`
}
</script> </script>
{#if weaponKeys && weaponKeys.length > 0} {#if weaponKeys && weaponKeys.length > 0}
<div class="weapon-keys-list {layout}"> <div class="weapon-keys-list {layout}">
{#each weaponKeys as key, index} {#each weaponKeys as key, index}
{@const imageData = keyImages[index]} {@const imageData = keyImages[index]}
<div class="weapon-key-item"> <div class="weapon-key-item">
{#if imageData} {#if imageData}
<img <img src={imageData.url} alt={imageData.alt} class="weapon-key-icon" />
src={imageData.url} {/if}
alt={imageData.alt} <div class="weapon-key-info">
class="weapon-key-icon" <span class="slot-label">
/> {getSlotLabel(key.slot, weaponData?.series)}
{/if} </span>
<div class="weapon-key-info"> <span class="key-name">
<span class="slot-label"> {getKeyDescription(key)}
{getSlotLabel(key.slot, weaponData?.series)} </span>
</span> </div>
<span class="key-name"> </div>
{getKeyDescription(key)} {/each}
</span> </div>
</div>
</div>
{/each}
</div>
{/if} {/if}
<style lang="scss"> <style lang="scss">
@use '$src/themes/colors' as colors; @use '$src/themes/colors' as colors;
@use '$src/themes/spacing' as spacing; @use '$src/themes/spacing' as spacing;
@use '$src/themes/typography' as typography; @use '$src/themes/typography' as typography;
@use '$src/themes/layout' as layout; @use '$src/themes/layout' as layout;
.weapon-keys-list { .weapon-keys-list {
display: flex; display: flex;
gap: spacing.$unit; gap: spacing.$unit;
&.list { &.list {
flex-direction: column; flex-direction: column;
} }
&.grid { &.grid {
flex-wrap: wrap; flex-wrap: wrap;
gap: spacing.$unit-2x; gap: spacing.$unit-2x;
} }
.weapon-key-item { .weapon-key-item {
display: flex; display: flex;
gap: spacing.$unit-2x; gap: spacing.$unit-2x;
align-items: center; align-items: center;
padding: spacing.$unit; padding: spacing.$unit;
background: colors.$grey-85; background: colors.$grey-85;
border-radius: layout.$item-corner-small; border-radius: layout.$item-corner-small;
transition: background 0.2s ease; transition: background 0.2s ease;
&:hover { &:hover {
background: colors.$grey-80; background: colors.$grey-80;
} }
.weapon-key-icon { .weapon-key-icon {
width: spacing.$unit-5x; width: spacing.$unit-5x;
height: spacing.$unit-5x; height: spacing.$unit-5x;
border-radius: layout.$item-corner-small; border-radius: layout.$item-corner-small;
flex-shrink: 0; flex-shrink: 0;
} }
.weapon-key-info { .weapon-key-info {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: spacing.$unit-fourth; gap: spacing.$unit-fourth;
.slot-label { .slot-label {
font-size: typography.$font-tiny; font-size: typography.$font-tiny;
color: var(--text-tertiary, colors.$grey-60); color: var(--text-tertiary, colors.$grey-60);
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.3px; letter-spacing: 0.3px;
} }
.key-name { .key-name {
font-size: typography.$font-small; font-size: typography.$font-small;
color: var(--text-primary, colors.$grey-10); color: var(--text-primary, colors.$grey-10);
font-weight: typography.$medium; font-weight: typography.$medium;
} }
} }
} }
} }
.grid { .grid {
.weapon-key-item { .weapon-key-item {
flex-direction: column; flex-direction: column;
text-align: center; text-align: center;
padding: spacing.$unit-2x; padding: spacing.$unit-2x;
.weapon-key-icon { .weapon-key-icon {
width: spacing.$unit-6x; width: spacing.$unit-6x;
height: spacing.$unit-6x; height: spacing.$unit-6x;
} }
.weapon-key-info { .weapon-key-info {
align-items: center; align-items: center;
} }
} }
} }
</style> </style>