sidebar: refactor details components
This commit is contained in:
parent
47885b1429
commit
a858877545
6 changed files with 307 additions and 463 deletions
|
|
@ -1,6 +1,8 @@
|
|||
<script lang="ts">
|
||||
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 ItemHeader from './details/ItemHeader.svelte'
|
||||
import BasicInfoSection from './details/BasicInfoSection.svelte'
|
||||
|
|
@ -13,11 +15,42 @@
|
|||
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))
|
||||
|
||||
// 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
|
||||
function getItemData() {
|
||||
if (type === 'character') {
|
||||
|
|
@ -54,7 +87,7 @@
|
|||
<ItemHeader {type} {item} {itemData} {gridUncapLevel} {gridTranscendence} />
|
||||
|
||||
<DetailsSidebarSegmentedControl
|
||||
hasModifications={modificationStatus.hasModifications}
|
||||
hasModifications={showSegmentedControl}
|
||||
bind:selectedView
|
||||
/>
|
||||
|
||||
|
|
@ -87,6 +120,6 @@
|
|||
display: flex;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
gap: spacing.$unit-2x;
|
||||
gap: spacing.$unit-4x;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
<script lang="ts">
|
||||
import { getElementLabel } from '$lib/utils/element'
|
||||
import { getRarityLabel } from '$lib/utils/rarity'
|
||||
import { getProficiencyLabel } from '$lib/utils/proficiency'
|
||||
import { getRaceLabel } from '$lib/utils/race'
|
||||
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 {
|
||||
type: 'character' | 'weapon' | 'summon'
|
||||
|
|
@ -11,97 +14,69 @@
|
|||
}
|
||||
|
||||
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>
|
||||
|
||||
<div class="details-section">
|
||||
<h3>Basic Information</h3>
|
||||
<div class="detail-row">
|
||||
<span class="label">Rarity</span>
|
||||
<span class="value">{getRarityLabel(itemData?.rarity)}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="label">Element</span>
|
||||
<span class="value">{getElementLabel(itemData?.element)}</span>
|
||||
</div>
|
||||
<DetailsSection title="Basic Information">
|
||||
<DetailRow label="Rarity" value={getRarityLabel(itemData?.rarity)} />
|
||||
<DetailRow label="Element">
|
||||
<ElementLabel element={itemData?.element} size="medium" />
|
||||
</DetailRow>
|
||||
|
||||
{#if type === 'character'}
|
||||
{#if itemData?.race && itemData.race.length > 0}
|
||||
<div class="detail-row">
|
||||
<span class="label">Race</span>
|
||||
<span class="value">
|
||||
{itemData.race
|
||||
.map((r: any) => getRaceLabel(r))
|
||||
.filter(Boolean)
|
||||
.join(', ') || '—'}
|
||||
</span>
|
||||
</div>
|
||||
<DetailRow
|
||||
label="Race"
|
||||
value={itemData.race
|
||||
.map((r: any) => getRaceLabel(r))
|
||||
.filter(Boolean)
|
||||
.join(', ') || '—'}
|
||||
/>
|
||||
{/if}
|
||||
<div class="detail-row">
|
||||
<span class="label">Gender</span>
|
||||
<span class="value">{getGenderLabel(itemData?.gender)}</span>
|
||||
</div>
|
||||
<DetailRow label="Gender" value={getGenderLabel(itemData?.gender)} />
|
||||
{#if itemData?.proficiency && itemData.proficiency.length > 0}
|
||||
<div class="detail-row">
|
||||
<span class="label">Proficiencies</span>
|
||||
<span class="value">
|
||||
{itemData.proficiency
|
||||
.map((p: any) => getProficiencyLabel(p))
|
||||
.filter(Boolean)
|
||||
.join(', ') || '—'}
|
||||
</span>
|
||||
</div>
|
||||
<DetailRow label="Proficiencies">
|
||||
{#each itemData.proficiency as prof}
|
||||
<ProficiencyLabel proficiency={prof} size="medium" />
|
||||
{/each}
|
||||
</DetailRow>
|
||||
{/if}
|
||||
{:else if type === 'weapon'}
|
||||
<div class="detail-row">
|
||||
<span class="label">Proficiency</span>
|
||||
<span class="value">{getProficiencyLabel(itemData?.proficiency)}</span>
|
||||
</div>
|
||||
<DetailRow label="Proficiency">
|
||||
<ProficiencyLabel proficiency={itemData?.proficiency} size="medium" />
|
||||
</DetailRow>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@use '$src/themes/colors' as colors;
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
.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>
|
||||
<DetailRow label="Max Uncap">
|
||||
<UncapIndicator
|
||||
{type}
|
||||
uncapLevel={maxUncapLevel}
|
||||
transcendenceStage={itemData?.uncap?.transcendence ? 5 : 0}
|
||||
flb={itemData?.uncap?.flb ?? false}
|
||||
ulb={itemData?.uncap?.ulb ?? false}
|
||||
transcendence={itemData?.uncap?.transcendence ?? false}
|
||||
{special}
|
||||
/>
|
||||
</DetailRow>
|
||||
</DetailsSection>
|
||||
|
|
|
|||
|
|
@ -62,6 +62,9 @@
|
|||
}
|
||||
return '—'
|
||||
}
|
||||
|
||||
// Special characters have different star counts (SR characters, etc.)
|
||||
const special = $derived(type === 'character' && (itemData?.rarity ?? 3) < 3)
|
||||
</script>
|
||||
|
||||
<div class="item-header-container">
|
||||
|
|
@ -74,6 +77,7 @@
|
|||
flb={itemData?.uncap?.flb}
|
||||
ulb={itemData?.uncap?.ulb}
|
||||
transcendence={itemData?.uncap?.transcendence}
|
||||
{special}
|
||||
editable={false}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
<script lang="ts">
|
||||
import DetailsSection from './DetailsSection.svelte'
|
||||
import DetailRow from './DetailRow.svelte'
|
||||
|
||||
interface Props {
|
||||
itemData: any
|
||||
gridUncapLevel: number | null
|
||||
|
|
@ -8,169 +11,34 @@
|
|||
let { itemData, gridUncapLevel, gridTranscendence }: Props = $props()
|
||||
</script>
|
||||
|
||||
<div class="details-section">
|
||||
{#if itemData?.hp && itemData?.atk}
|
||||
<div class="stats-grid">
|
||||
<div class="grid-header empty"></div>
|
||||
<div class="grid-header">HP</div>
|
||||
<div class="grid-header">ATK</div>
|
||||
{#if itemData?.hp}
|
||||
<DetailsSection title="HP">
|
||||
<DetailRow label="Base" value={itemData.hp.minHp} />
|
||||
<DetailRow label="MLB" value={itemData.hp.maxHp} />
|
||||
{#if itemData.uncap?.flb && itemData.hp.maxHpFlb}
|
||||
<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>
|
||||
<div class="grid-value">{itemData.hp.minHp ?? '—'}</div>
|
||||
<div class="grid-value">{itemData.atk.minAtk ?? '—'}</div>
|
||||
|
||||
<div class="grid-label">MLB</div>
|
||||
<div class="grid-value">{itemData.hp.maxHp ?? '—'}</div>
|
||||
<div class="grid-value">{itemData.atk.maxAtk ?? '—'}</div>
|
||||
|
||||
{#if itemData.uncap?.flb && (itemData.hp.maxHpFlb || itemData.atk.maxAtkFlb)}
|
||||
<div class="grid-label">FLB</div>
|
||||
<div class="grid-value">{itemData.hp.maxHpFlb ?? '—'}</div>
|
||||
<div class="grid-value">{itemData.atk.maxAtkFlb ?? '—'}</div>
|
||||
{/if}
|
||||
|
||||
{#if itemData.uncap?.ulb && (itemData.hp.maxHpUlb || itemData.atk.maxAtkUlb)}
|
||||
<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>
|
||||
{#if itemData?.atk}
|
||||
<DetailsSection title="ATK">
|
||||
<DetailRow label="Base" value={itemData.atk.minAtk} />
|
||||
<DetailRow label="MLB" value={itemData.atk.maxAtk} />
|
||||
{#if itemData.uncap?.flb && itemData.atk.maxAtkFlb}
|
||||
<DetailRow label="FLB" value={itemData.atk.maxAtkFlb} />
|
||||
{/if}
|
||||
{#if itemData.uncap?.ulb && itemData.atk.maxAtkUlb}
|
||||
<DetailRow label="ULB" value={itemData.atk.maxAtkUlb} />
|
||||
{/if}
|
||||
{#if gridTranscendence && gridTranscendence > 0}
|
||||
<DetailRow label="T5" value={itemData.atk.maxAtkXlb} />
|
||||
{/if}
|
||||
</DetailsSection>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,13 @@
|
|||
<script lang="ts">
|
||||
import type { GridCharacter, GridWeapon, GridSummon } from '$lib/types/api/party'
|
||||
import ModificationSection from '../modifications/ModificationSection.svelte'
|
||||
import UncapStatusDisplay from '../modifications/UncapStatusDisplay.svelte'
|
||||
import DetailsSection from './DetailsSection.svelte'
|
||||
import DetailRow from './DetailRow.svelte'
|
||||
import AwakeningDisplay from '../modifications/AwakeningDisplay.svelte'
|
||||
import MasteryDisplay from '../modifications/MasteryDisplay.svelte'
|
||||
import StatModifierItem from '../modifications/StatModifierItem.svelte'
|
||||
import WeaponKeysList from '../modifications/WeaponKeysList.svelte'
|
||||
import { getRarityLabel } from '$lib/utils/rarity'
|
||||
import { getElementLabel } from '$lib/utils/element'
|
||||
import { getRaceLabel } from '$lib/utils/race'
|
||||
import { getGenderLabel } from '$lib/utils/gender'
|
||||
import { getProficiencyLabel } from '$lib/utils/proficiency'
|
||||
import {
|
||||
formatAxSkill,
|
||||
getWeaponKeyTitle,
|
||||
getElementName
|
||||
} from '$lib/utils/modificationFormatters'
|
||||
import { formatAxSkill, getWeaponKeyTitle } from '$lib/utils/modificationFormatters'
|
||||
import ElementLabel from '$lib/components/labels/ElementLabel.svelte'
|
||||
import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte'
|
||||
|
||||
interface Props {
|
||||
type: 'character' | 'weapon' | 'summon'
|
||||
|
|
@ -28,36 +20,54 @@
|
|||
|
||||
let { type, item, itemData, gridUncapLevel, gridTranscendence, modificationStatus }: 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>
|
||||
|
||||
<div class="team-view">
|
||||
<ModificationSection title="Uncap & Transcendence" visible={true}>
|
||||
<UncapStatusDisplay
|
||||
{type}
|
||||
uncapLevel={gridUncapLevel}
|
||||
transcendenceStep={gridTranscendence}
|
||||
special={itemData?.special}
|
||||
flb={itemData?.uncap?.flb}
|
||||
ulb={itemData?.uncap?.ulb}
|
||||
transcendence={itemData?.uncap?.transcendence}
|
||||
/>
|
||||
</ModificationSection>
|
||||
<DetailsSection title="Uncap & Transcendence">
|
||||
<DetailRow label="Uncap Level">
|
||||
<UncapIndicator
|
||||
{type}
|
||||
uncapLevel={gridUncapLevel}
|
||||
transcendenceStage={gridTranscendence}
|
||||
flb={uncapCaps?.flb}
|
||||
ulb={uncapCaps?.ulb}
|
||||
transcendence={uncapCaps?.transcendence}
|
||||
/>
|
||||
</DetailRow>
|
||||
</DetailsSection>
|
||||
|
||||
{#if type === 'character'}
|
||||
{@const char = item as GridCharacter}
|
||||
|
||||
{#if modificationStatus.hasAwakening}
|
||||
<ModificationSection title="Awakening" visible={true}>
|
||||
<DetailsSection title="Awakening">
|
||||
<AwakeningDisplay
|
||||
{...(char.awakening ? { awakening: char.awakening } : {})}
|
||||
size="medium"
|
||||
showLevel={true}
|
||||
/>
|
||||
</ModificationSection>
|
||||
</DetailsSection>
|
||||
{/if}
|
||||
|
||||
{#if modificationStatus.hasRings || modificationStatus.hasEarring}
|
||||
<ModificationSection title="Mastery" visible={true}>
|
||||
<DetailsSection title="Mastery">
|
||||
<MasteryDisplay
|
||||
rings={char.overMastery}
|
||||
earring={char.aetherialMastery}
|
||||
|
|
@ -65,95 +75,69 @@
|
|||
variant="detailed"
|
||||
showIcons={true}
|
||||
/>
|
||||
</ModificationSection>
|
||||
</DetailsSection>
|
||||
{/if}
|
||||
|
||||
{#if modificationStatus.hasPerpetuity}
|
||||
<ModificationSection title="Status" visible={true}>
|
||||
<StatModifierItem label="Perpetuity" value="Active" variant="max" />
|
||||
</ModificationSection>
|
||||
<DetailsSection title="Status">
|
||||
<DetailRow label="Perpetuity Ring" value="Active" />
|
||||
</DetailsSection>
|
||||
{/if}
|
||||
{:else if type === 'weapon'}
|
||||
{@const weapon = item as GridWeapon}
|
||||
|
||||
{#if modificationStatus.hasAwakening && weapon.awakening}
|
||||
<ModificationSection title="Awakening" visible={true}>
|
||||
<DetailsSection title="Awakening">
|
||||
<AwakeningDisplay awakening={weapon.awakening} size="medium" showLevel={true} />
|
||||
</ModificationSection>
|
||||
</DetailsSection>
|
||||
{/if}
|
||||
|
||||
{#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" />
|
||||
</ModificationSection>
|
||||
</DetailsSection>
|
||||
{/if}
|
||||
|
||||
{#if modificationStatus.hasAxSkills && weapon.ax}
|
||||
<ModificationSection title="AX Skills" visible={true}>
|
||||
<DetailsSection title="AX Skills">
|
||||
{#each weapon.ax as axSkill}
|
||||
<StatModifierItem
|
||||
<DetailRow
|
||||
label={formatAxSkill(axSkill).split('+')[0]?.trim() ?? ''}
|
||||
value={`+${axSkill.strength}`}
|
||||
suffix={axSkill.modifier <= 2 ? '' : '%'}
|
||||
variant="enhanced"
|
||||
value={`+${axSkill.strength}${axSkill.modifier <= 2 ? '' : '%'}`}
|
||||
/>
|
||||
{/each}
|
||||
</ModificationSection>
|
||||
</DetailsSection>
|
||||
{/if}
|
||||
|
||||
{#if modificationStatus.hasElement && weapon.element}
|
||||
<ModificationSection title="Element Override" visible={true}>
|
||||
<StatModifierItem label="Instance Element" value={getElementName(weapon.element)} />
|
||||
</ModificationSection>
|
||||
<DetailsSection title="Element Override">
|
||||
<DetailRow label="Weapon Element">
|
||||
<ElementLabel element={weapon.element} size="medium" />
|
||||
</DetailRow>
|
||||
</DetailsSection>
|
||||
{/if}
|
||||
{:else if type === 'summon'}
|
||||
{@const summon = item as GridSummon}
|
||||
|
||||
{#if modificationStatus.hasQuickSummon || modificationStatus.hasFriendSummon}
|
||||
<ModificationSection title="Summon Status" visible={true}>
|
||||
<DetailsSection title="Summon Status">
|
||||
{#if summon.quickSummon}
|
||||
<StatModifierItem label="Quick Summon" value="Enabled" variant="enhanced" />
|
||||
<DetailRow label="Quick Summon" value="Enabled" />
|
||||
{/if}
|
||||
{#if summon.friend}
|
||||
<StatModifierItem label="Friend Summon" value="Yes" />
|
||||
<DetailRow label="Friend Summon" value="Yes" />
|
||||
{/if}
|
||||
</ModificationSection>
|
||||
</DetailsSection>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@use '$src/themes/colors' as colors;
|
||||
@use '$src/themes/spacing' as spacing;
|
||||
@use '$src/themes/typography' as typography;
|
||||
|
||||
.team-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: spacing.$unit-2x;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
gap: spacing.$unit-4x;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,157 +1,137 @@
|
|||
<script lang="ts">
|
||||
import { getWeaponKeyImages } from '$lib/utils/modifiers'
|
||||
import type { WeaponKey } from '$lib/types/api/entities'
|
||||
import type { LocalizedName } from '$lib/types/api/entities'
|
||||
import { getWeaponKeyImages } from '$lib/utils/modifiers'
|
||||
import type { WeaponKey } from '$lib/types/api/entities'
|
||||
import type { LocalizedName } from '$lib/types/api/entities'
|
||||
|
||||
interface Props {
|
||||
weaponKeys?: WeaponKey[]
|
||||
weaponData?: {
|
||||
element?: number
|
||||
proficiency?: number | number[]
|
||||
series?: number
|
||||
name?: LocalizedName
|
||||
}
|
||||
layout?: 'list' | 'grid'
|
||||
}
|
||||
interface Props {
|
||||
weaponKeys?: WeaponKey[]
|
||||
weaponData?: {
|
||||
element?: number
|
||||
proficiency?: number | number[]
|
||||
series?: number
|
||||
name?: LocalizedName
|
||||
}
|
||||
layout?: 'list' | 'grid'
|
||||
}
|
||||
|
||||
let {
|
||||
weaponKeys,
|
||||
weaponData,
|
||||
layout = 'list'
|
||||
}: Props = $props()
|
||||
let { weaponKeys, weaponData, layout = 'list' }: Props = $props()
|
||||
|
||||
let keyImages = $derived(
|
||||
getWeaponKeyImages(
|
||||
weaponKeys,
|
||||
weaponData?.element,
|
||||
Array.isArray(weaponData?.proficiency) ? weaponData?.proficiency[0] : weaponData?.proficiency,
|
||||
weaponData?.series,
|
||||
weaponData?.name
|
||||
)
|
||||
)
|
||||
let keyImages = $derived(
|
||||
getWeaponKeyImages(
|
||||
weaponKeys,
|
||||
weaponData?.element,
|
||||
Array.isArray(weaponData?.proficiency) ? weaponData?.proficiency[0] : weaponData?.proficiency,
|
||||
weaponData?.series,
|
||||
weaponData?.name
|
||||
)
|
||||
)
|
||||
|
||||
function getKeyDescription(key: WeaponKey): string {
|
||||
if (key.name?.en) return key.name.en
|
||||
if (key.name?.ja) return key.name.ja
|
||||
return key.slug || 'Weapon Key'
|
||||
}
|
||||
function getKeyDescription(key: WeaponKey): string {
|
||||
if (key.name?.en) return key.name.en
|
||||
if (key.name?.ja) return key.name.ja
|
||||
return key.slug || 'Weapon Key'
|
||||
}
|
||||
|
||||
function getSlotLabel(slot: number, series?: number): string {
|
||||
if (series === 2) {
|
||||
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}`
|
||||
}
|
||||
function getSlotLabel(slot: number, series?: number): string {
|
||||
return `Skill ${slot + 1}`
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if weaponKeys && weaponKeys.length > 0}
|
||||
<div class="weapon-keys-list {layout}">
|
||||
{#each weaponKeys as key, index}
|
||||
{@const imageData = keyImages[index]}
|
||||
<div class="weapon-key-item">
|
||||
{#if imageData}
|
||||
<img
|
||||
src={imageData.url}
|
||||
alt={imageData.alt}
|
||||
class="weapon-key-icon"
|
||||
/>
|
||||
{/if}
|
||||
<div class="weapon-key-info">
|
||||
<span class="slot-label">
|
||||
{getSlotLabel(key.slot, weaponData?.series)}
|
||||
</span>
|
||||
<span class="key-name">
|
||||
{getKeyDescription(key)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="weapon-keys-list {layout}">
|
||||
{#each weaponKeys as key, index}
|
||||
{@const imageData = keyImages[index]}
|
||||
<div class="weapon-key-item">
|
||||
{#if imageData}
|
||||
<img src={imageData.url} alt={imageData.alt} class="weapon-key-icon" />
|
||||
{/if}
|
||||
<div class="weapon-key-info">
|
||||
<span class="slot-label">
|
||||
{getSlotLabel(key.slot, weaponData?.series)}
|
||||
</span>
|
||||
<span class="key-name">
|
||||
{getKeyDescription(key)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
@use '$src/themes/colors' as colors;
|
||||
@use '$src/themes/spacing' as spacing;
|
||||
@use '$src/themes/typography' as typography;
|
||||
@use '$src/themes/layout' as layout;
|
||||
@use '$src/themes/colors' as colors;
|
||||
@use '$src/themes/spacing' as spacing;
|
||||
@use '$src/themes/typography' as typography;
|
||||
@use '$src/themes/layout' as layout;
|
||||
|
||||
.weapon-keys-list {
|
||||
display: flex;
|
||||
gap: spacing.$unit;
|
||||
.weapon-keys-list {
|
||||
display: flex;
|
||||
gap: spacing.$unit;
|
||||
|
||||
&.list {
|
||||
flex-direction: column;
|
||||
}
|
||||
&.list {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&.grid {
|
||||
flex-wrap: wrap;
|
||||
gap: spacing.$unit-2x;
|
||||
}
|
||||
&.grid {
|
||||
flex-wrap: wrap;
|
||||
gap: spacing.$unit-2x;
|
||||
}
|
||||
|
||||
.weapon-key-item {
|
||||
display: flex;
|
||||
gap: spacing.$unit-2x;
|
||||
align-items: center;
|
||||
padding: spacing.$unit;
|
||||
background: colors.$grey-85;
|
||||
border-radius: layout.$item-corner-small;
|
||||
transition: background 0.2s ease;
|
||||
.weapon-key-item {
|
||||
display: flex;
|
||||
gap: spacing.$unit-2x;
|
||||
align-items: center;
|
||||
padding: spacing.$unit;
|
||||
background: colors.$grey-85;
|
||||
border-radius: layout.$item-corner-small;
|
||||
transition: background 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: colors.$grey-80;
|
||||
}
|
||||
&:hover {
|
||||
background: colors.$grey-80;
|
||||
}
|
||||
|
||||
.weapon-key-icon {
|
||||
width: spacing.$unit-5x;
|
||||
height: spacing.$unit-5x;
|
||||
border-radius: layout.$item-corner-small;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.weapon-key-icon {
|
||||
width: spacing.$unit-5x;
|
||||
height: spacing.$unit-5x;
|
||||
border-radius: layout.$item-corner-small;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.weapon-key-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: spacing.$unit-fourth;
|
||||
.weapon-key-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: spacing.$unit-fourth;
|
||||
|
||||
.slot-label {
|
||||
font-size: typography.$font-tiny;
|
||||
color: var(--text-tertiary, colors.$grey-60);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
.slot-label {
|
||||
font-size: typography.$font-tiny;
|
||||
color: var(--text-tertiary, colors.$grey-60);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.key-name {
|
||||
font-size: typography.$font-small;
|
||||
color: var(--text-primary, colors.$grey-10);
|
||||
font-weight: typography.$medium;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.key-name {
|
||||
font-size: typography.$font-small;
|
||||
color: var(--text-primary, colors.$grey-10);
|
||||
font-weight: typography.$medium;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.grid {
|
||||
.weapon-key-item {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
padding: spacing.$unit-2x;
|
||||
.grid {
|
||||
.weapon-key-item {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
padding: spacing.$unit-2x;
|
||||
|
||||
.weapon-key-icon {
|
||||
width: spacing.$unit-6x;
|
||||
height: spacing.$unit-6x;
|
||||
}
|
||||
.weapon-key-icon {
|
||||
width: spacing.$unit-6x;
|
||||
height: spacing.$unit-6x;
|
||||
}
|
||||
|
||||
.weapon-key-info {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
.weapon-key-info {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Reference in a new issue