refactor details sidebar
This commit is contained in:
parent
1b2bee497b
commit
9537c57485
7 changed files with 808 additions and 639 deletions
|
|
@ -1,31 +1,12 @@
|
||||||
<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 { 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 {
|
|
||||||
getCharacterDetailImage,
|
|
||||||
getWeaponBaseImage,
|
|
||||||
getSummonDetailImage,
|
|
||||||
getCharacterPose
|
|
||||||
} from '$lib/utils/images'
|
|
||||||
import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte'
|
|
||||||
import { detectModifications } from '$lib/utils/modificationDetector'
|
import { detectModifications } from '$lib/utils/modificationDetector'
|
||||||
import {
|
|
||||||
formatRingStat,
|
|
||||||
formatEarringStat,
|
|
||||||
formatAxSkill,
|
|
||||||
getWeaponKeyTitle,
|
|
||||||
getElementName
|
|
||||||
} from '$lib/utils/modificationFormatters'
|
|
||||||
import DetailsSidebarSegmentedControl from './modifications/DetailsSidebarSegmentedControl.svelte'
|
import DetailsSidebarSegmentedControl from './modifications/DetailsSidebarSegmentedControl.svelte'
|
||||||
import ModificationSection from './modifications/ModificationSection.svelte'
|
import ItemHeader from './details/ItemHeader.svelte'
|
||||||
import AwakeningDisplay from './modifications/AwakeningDisplay.svelte'
|
import BasicInfoSection from './details/BasicInfoSection.svelte'
|
||||||
import WeaponKeysList from './modifications/WeaponKeysList.svelte'
|
import StatsSection from './details/StatsSection.svelte'
|
||||||
import StatModifierItem from './modifications/StatModifierItem.svelte'
|
import SkillsSection from './details/SkillsSection.svelte'
|
||||||
import UncapStatusDisplay from './modifications/UncapStatusDisplay.svelte'
|
import TeamView from './details/TeamView.svelte'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
type: 'character' | 'weapon' | 'summon'
|
type: 'character' | 'weapon' | 'summon'
|
||||||
|
|
@ -34,7 +15,7 @@
|
||||||
|
|
||||||
let { type, item }: Props = $props()
|
let { type, item }: Props = $props()
|
||||||
|
|
||||||
let selectedView = $state<'canonical' | 'user'>('canonical')
|
let selectedView = $state<'canonical' | 'user'>('user')
|
||||||
let modificationStatus = $derived(detectModifications(type, item))
|
let modificationStatus = $derived(detectModifications(type, item))
|
||||||
|
|
||||||
// Helper to get the actual item data
|
// Helper to get the actual item data
|
||||||
|
|
@ -48,70 +29,29 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper for localized names
|
|
||||||
function displayName(input: any): string {
|
|
||||||
if (!input) return '—'
|
|
||||||
const maybe = input.name ?? input
|
|
||||||
if (typeof maybe === 'string') return maybe
|
|
||||||
if (maybe && typeof maybe === 'object') {
|
|
||||||
return maybe.en || maybe.ja || '—'
|
|
||||||
}
|
|
||||||
return '—'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the item's actual data
|
// Get the item's actual data
|
||||||
const itemData = $derived(getItemData())
|
const itemData = $derived(getItemData())
|
||||||
|
|
||||||
// Grid item info (uncap levels from the grid item itself)
|
// Grid item info (uncap levels from the grid item itself) - convert undefined to null
|
||||||
const gridUncapLevel = $derived(
|
const gridUncapLevel = $derived(
|
||||||
type === 'character'
|
type === 'character'
|
||||||
? (item as GridCharacter).uncapLevel
|
? ((item as GridCharacter).uncapLevel ?? null)
|
||||||
: type === 'weapon'
|
: type === 'weapon'
|
||||||
? (item as GridWeapon).uncapLevel
|
? ((item as GridWeapon).uncapLevel ?? null)
|
||||||
: (item as GridSummon).uncapLevel
|
: ((item as GridSummon).uncapLevel ?? null)
|
||||||
)
|
)
|
||||||
|
|
||||||
const gridTranscendence = $derived(
|
const gridTranscendence = $derived(
|
||||||
type === 'character'
|
type === 'character'
|
||||||
? (item as GridCharacter).transcendenceStep
|
? ((item as GridCharacter).transcendenceStep ?? null)
|
||||||
: type === 'weapon'
|
: type === 'weapon'
|
||||||
? (item as GridWeapon).transcendenceStep
|
? ((item as GridWeapon).transcendenceStep ?? null)
|
||||||
: (item as GridSummon).transcendenceStep
|
: ((item as GridSummon).transcendenceStep ?? null)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Get image URL based on type using detail/base variants
|
|
||||||
function getImageUrl(): string {
|
|
||||||
const id = itemData?.granblueId
|
|
||||||
|
|
||||||
if (type === 'character') {
|
|
||||||
const pose = getCharacterPose(gridUncapLevel, gridTranscendence)
|
|
||||||
return getCharacterDetailImage(id, pose)
|
|
||||||
} else if (type === 'weapon') {
|
|
||||||
return getWeaponBaseImage(id)
|
|
||||||
} else {
|
|
||||||
return getSummonDetailImage(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get element-based background color
|
|
||||||
function getElementBackground(): string {
|
|
||||||
const element = itemData?.element
|
|
||||||
switch (element) {
|
|
||||||
case 1: return 'var(--wind-item-detail-bg)'
|
|
||||||
case 2: return 'var(--fire-item-detail-bg)'
|
|
||||||
case 3: return 'var(--water-item-detail-bg)'
|
|
||||||
case 4: return 'var(--earth-item-detail-bg)'
|
|
||||||
case 5: return 'var(--dark-item-detail-bg)'
|
|
||||||
case 6: return 'var(--light-item-detail-bg)'
|
|
||||||
default: return 'var(--null-item-detail-bg)'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="details-sidebar">
|
<div class="details-sidebar">
|
||||||
<div class="item-header" style="background: {getElementBackground()}">
|
<ItemHeader {type} {item} {itemData} {gridUncapLevel} {gridTranscendence} />
|
||||||
<img src={getImageUrl()} alt={displayName(itemData)} class="item-image {type}" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DetailsSidebarSegmentedControl
|
<DetailsSidebarSegmentedControl
|
||||||
hasModifications={modificationStatus.hasModifications}
|
hasModifications={modificationStatus.hasModifications}
|
||||||
|
|
@ -119,367 +59,13 @@
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{#if selectedView === 'canonical'}
|
{#if selectedView === 'canonical'}
|
||||||
<div class="canonical-view">
|
<div class="canonical-view">
|
||||||
<div class="details-section">
|
<BasicInfoSection {type} {itemData} />
|
||||||
<h3>Basic Information</h3>
|
<StatsSection {itemData} {gridUncapLevel} {gridTranscendence} />
|
||||||
<div class="detail-row">
|
<SkillsSection {type} {itemData} />
|
||||||
<span class="label">Rarity</span>
|
|
||||||
<span class="value">{getRarityLabel(itemData?.rarity)}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-row">
|
|
||||||
<span class="label">Element</span>
|
|
||||||
<span class="value">{getElementLabel(itemData?.element)}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#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) => getRaceLabel(r))
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(', ') || '—'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<div class="detail-row">
|
|
||||||
<span class="label">Gender</span>
|
|
||||||
<span class="value">{getGenderLabel(itemData?.gender)}</span>
|
|
||||||
</div>
|
|
||||||
{#if itemData?.proficiency && itemData.proficiency.length > 0}
|
|
||||||
<div class="detail-row">
|
|
||||||
<span class="label">Proficiencies</span>
|
|
||||||
<span class="value">
|
|
||||||
{itemData.proficiency
|
|
||||||
.map((p) => getProficiencyLabel(p))
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(', ') || '—'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{:else if type === 'weapon'}
|
|
||||||
<div class="detail-row">
|
|
||||||
<span class="label">Proficiency</span>
|
|
||||||
<span class="value">{getProficiencyLabel(itemData?.proficiency?.[0])}</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="details-section">
|
|
||||||
<h3>Uncap Status</h3>
|
|
||||||
<div class="uncap-display">
|
|
||||||
<UncapIndicator
|
|
||||||
{type}
|
|
||||||
uncapLevel={gridUncapLevel}
|
|
||||||
transcendenceStage={gridTranscendence}
|
|
||||||
special={itemData?.special}
|
|
||||||
flb={itemData?.uncap?.flb}
|
|
||||||
ulb={itemData?.uncap?.ulb}
|
|
||||||
transcendence={itemData?.uncap?.transcendence}
|
|
||||||
editable={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="detail-row">
|
|
||||||
<span class="label">Current Uncap</span>
|
|
||||||
<span class="value">{gridUncapLevel ?? 0}★</span>
|
|
||||||
</div>
|
|
||||||
{#if gridTranscendence && gridTranscendence > 0}
|
|
||||||
<div class="detail-row">
|
|
||||||
<span class="label">Transcendence</span>
|
|
||||||
<span class="value">Stage {gridTranscendence}</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if itemData?.uncap}
|
|
||||||
<div class="detail-row">
|
|
||||||
<span class="label">Available Uncaps</span>
|
|
||||||
<span class="value">
|
|
||||||
{[
|
|
||||||
itemData.uncap.flb && 'FLB',
|
|
||||||
itemData.uncap.ulb && 'ULB',
|
|
||||||
itemData.uncap.transcendence && 'Transcendence'
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(', ') || 'Standard'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="details-section">
|
|
||||||
<h3>Stats</h3>
|
|
||||||
{#if itemData?.hp}
|
|
||||||
<div class="stats-group">
|
|
||||||
<h4>HP</h4>
|
|
||||||
<div class="detail-row">
|
|
||||||
<span class="label">Base</span>
|
|
||||||
<span class="value">{itemData.hp.minHp ?? '—'}</span>
|
|
||||||
</div>
|
|
||||||
<div class="detail-row">
|
|
||||||
<span class="label">Max</span>
|
|
||||||
<span class="value">{itemData.hp.maxHp ?? '—'}</span>
|
|
||||||
</div>
|
|
||||||
{#if itemData.uncap?.flb && itemData.hp.maxHpFlb}
|
|
||||||
<div class="detail-row">
|
|
||||||
<span class="label">Max (FLB)</span>
|
|
||||||
<span class="value">{itemData.hp.maxHpFlb}</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if itemData.uncap?.ulb && itemData.hp.maxHpUlb}
|
|
||||||
<div class="detail-row">
|
|
||||||
<span class="label">Max (ULB)</span>
|
|
||||||
<span class="value">{itemData.hp.maxHpUlb}</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if itemData?.atk}
|
|
||||||
<div class="stats-group">
|
|
||||||
<h4>Attack</h4>
|
|
||||||
<div class="detail-row">
|
|
||||||
<span class="label">Base</span>
|
|
||||||
<span class="value">{itemData.atk.minAtk ?? '—'}</span>
|
|
||||||
</div>
|
|
||||||
<div class="detail-row">
|
|
||||||
<span class="label">Max</span>
|
|
||||||
<span class="value">{itemData.atk.maxAtk ?? '—'}</span>
|
|
||||||
</div>
|
|
||||||
{#if itemData.uncap?.flb && itemData.atk.maxAtkFlb}
|
|
||||||
<div class="detail-row">
|
|
||||||
<span class="label">Max (FLB)</span>
|
|
||||||
<span class="value">{itemData.atk.maxAtkFlb}</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if itemData.uncap?.ulb && itemData.atk.maxAtkUlb}
|
|
||||||
<div class="detail-row">
|
|
||||||
<span class="label">Max (ULB)</span>
|
|
||||||
<span class="value">{itemData.atk.maxAtkUlb}</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if type === 'weapon' && itemData?.weaponSkills && itemData.weaponSkills.length > 0}
|
|
||||||
<div class="details-section">
|
|
||||||
<h3>Skills</h3>
|
|
||||||
<div class="skills-list">
|
|
||||||
{#each itemData.weaponSkills as skill}
|
|
||||||
<div class="skill-item">
|
|
||||||
<h4>{displayName(skill) || 'Unknown Skill'}</h4>
|
|
||||||
{#if skill.description}
|
|
||||||
<p>{skill.description}</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if type === 'summon' && itemData?.summonAuras && itemData.summonAuras.length > 0}
|
|
||||||
<div class="details-section">
|
|
||||||
<h3>Auras</h3>
|
|
||||||
<div class="auras-list">
|
|
||||||
{#each itemData.summonAuras as aura}
|
|
||||||
<div class="aura-item">
|
|
||||||
<h4>{displayName(aura) || 'Unknown Aura'}</h4>
|
|
||||||
{#if aura.description}
|
|
||||||
<p>{aura.description}</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if type === 'weapon' && itemData?.weaponKeys && itemData.weaponKeys.length > 0}
|
|
||||||
<div class="details-section">
|
|
||||||
<h3>Weapon Keys</h3>
|
|
||||||
<div class="keys-list">
|
|
||||||
{#each itemData.weaponKeys as key}
|
|
||||||
<div class="key-item">
|
|
||||||
<span class="key-slot">Slot {key.slot}</span>
|
|
||||||
<span class="key-name">{displayName(key.weaponKey1) || '—'}</span>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if type === 'character' && itemData?.special}
|
|
||||||
<div class="details-section">
|
|
||||||
<div class="detail-row special-indicator">
|
|
||||||
<span class="label">Special Character</span>
|
|
||||||
<span class="value">✓</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{:else}
|
{:else}
|
||||||
<div class="user-version-view">
|
<TeamView {type} {item} {itemData} {gridUncapLevel} {gridTranscendence} {modificationStatus} />
|
||||||
<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>
|
|
||||||
|
|
||||||
{#if type === 'character'}
|
|
||||||
{@const char = item as GridCharacter}
|
|
||||||
|
|
||||||
{#if modificationStatus.hasAwakening}
|
|
||||||
<ModificationSection title="Awakening" visible={true}>
|
|
||||||
<AwakeningDisplay
|
|
||||||
awakening={char.awakening}
|
|
||||||
size="medium"
|
|
||||||
showLevel={false}
|
|
||||||
/>
|
|
||||||
</ModificationSection>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if modificationStatus.hasRings}
|
|
||||||
<ModificationSection title="Over Mastery Rings" visible={true}>
|
|
||||||
{#each (char.over_mastery || []) as ring}
|
|
||||||
<StatModifierItem
|
|
||||||
label={formatRingStat(ring.modifier, ring.strength).split('+')[0].trim()}
|
|
||||||
value={`+${ring.strength}`}
|
|
||||||
suffix={ring.modifier <= 2 ? '' : '%'}
|
|
||||||
/>
|
|
||||||
{/each}
|
|
||||||
</ModificationSection>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if modificationStatus.hasEarring}
|
|
||||||
{@const earring = char.aetherial_mastery}
|
|
||||||
{#if earring}
|
|
||||||
<ModificationSection title="Aetherial Mastery" visible={true}>
|
|
||||||
<StatModifierItem
|
|
||||||
label={formatEarringStat(earring.modifier, earring.strength).split('+')[0].trim()}
|
|
||||||
value={`+${earring.strength}`}
|
|
||||||
suffix={earring.modifier <= 3 ? '' : '%'}
|
|
||||||
variant="enhanced"
|
|
||||||
/>
|
|
||||||
</ModificationSection>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if modificationStatus.hasPerpetuity}
|
|
||||||
<ModificationSection title="Status" visible={true}>
|
|
||||||
<StatModifierItem
|
|
||||||
label="Perpetuity"
|
|
||||||
value="Active"
|
|
||||||
variant="max"
|
|
||||||
/>
|
|
||||||
</ModificationSection>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{:else if type === 'weapon'}
|
|
||||||
{@const weapon = item as GridWeapon}
|
|
||||||
|
|
||||||
{#if modificationStatus.hasAwakening && weapon.awakening}
|
|
||||||
<ModificationSection title="Awakening" visible={true}>
|
|
||||||
<AwakeningDisplay
|
|
||||||
awakening={weapon.awakening}
|
|
||||||
size="medium"
|
|
||||||
showLevel={true}
|
|
||||||
/>
|
|
||||||
</ModificationSection>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if modificationStatus.hasWeaponKeys}
|
|
||||||
<ModificationSection title={getWeaponKeyTitle(weapon.weapon?.series)} visible={true}>
|
|
||||||
<WeaponKeysList
|
|
||||||
weaponKeys={weapon.weaponKeys}
|
|
||||||
weaponData={weapon.weapon}
|
|
||||||
layout="list"
|
|
||||||
/>
|
|
||||||
</ModificationSection>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if modificationStatus.hasAxSkills && weapon.ax}
|
|
||||||
<ModificationSection title="AX Skills" visible={true}>
|
|
||||||
{#each weapon.ax as axSkill}
|
|
||||||
<StatModifierItem
|
|
||||||
label={formatAxSkill(axSkill).split('+')[0].trim()}
|
|
||||||
value={`+${axSkill.strength}`}
|
|
||||||
suffix={axSkill.modifier <= 2 ? '' : '%'}
|
|
||||||
variant="enhanced"
|
|
||||||
/>
|
|
||||||
{/each}
|
|
||||||
</ModificationSection>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if modificationStatus.hasElement && weapon.element}
|
|
||||||
<ModificationSection title="Element Override" visible={true}>
|
|
||||||
<StatModifierItem
|
|
||||||
label="Instance Element"
|
|
||||||
value={getElementName(weapon.element)}
|
|
||||||
/>
|
|
||||||
</ModificationSection>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{:else if type === 'summon'}
|
|
||||||
{@const summon = item as GridSummon}
|
|
||||||
|
|
||||||
{#if modificationStatus.hasQuickSummon || modificationStatus.hasFriendSummon}
|
|
||||||
<ModificationSection title="Summon Status" visible={true}>
|
|
||||||
{#if summon.quickSummon}
|
|
||||||
<StatModifierItem
|
|
||||||
label="Quick Summon"
|
|
||||||
value="Enabled"
|
|
||||||
variant="enhanced"
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
{#if summon.friend}
|
|
||||||
<StatModifierItem
|
|
||||||
label="Friend Summon"
|
|
||||||
value="Yes"
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
</ModificationSection>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<ModificationSection title="Basic Information" visible={true}>
|
|
||||||
<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>
|
|
||||||
|
|
||||||
{#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) => getRaceLabel(r))
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(', ') || '—'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<div class="detail-row">
|
|
||||||
<span class="label">Gender</span>
|
|
||||||
<span class="value">{getGenderLabel(itemData?.gender)}</span>
|
|
||||||
</div>
|
|
||||||
{:else if type === 'weapon'}
|
|
||||||
<div class="detail-row">
|
|
||||||
<span class="label">Proficiency</span>
|
|
||||||
<span class="value">{getProficiencyLabel(itemData?.proficiency?.[0])}</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</ModificationSection>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -497,146 +83,9 @@
|
||||||
gap: spacing.$unit-2x;
|
gap: spacing.$unit-2x;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-header {
|
.canonical-view {
|
||||||
display: flex;
|
|
||||||
gap: spacing.$unit-2x;
|
|
||||||
align-items: flex-start;
|
|
||||||
border-radius: layout.$item-corner;
|
|
||||||
border: 1px solid colors.$grey-70;
|
|
||||||
justify-content: center;
|
|
||||||
transition: background 0.3s ease;
|
|
||||||
|
|
||||||
.item-image.weapon {
|
|
||||||
width: 62%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-image.summon,
|
|
||||||
.item-image.character {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.details-section {
|
|
||||||
margin-bottom: spacing.$unit-3x;
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
margin: 0 0 calc(spacing.$unit * 1.5) 0;
|
|
||||||
font-size: typography.$font-regular;
|
|
||||||
font-weight: typography.$medium;
|
|
||||||
color: var(--text-secondary, colors.$grey-40);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h4 {
|
|
||||||
margin: spacing.$unit 0 calc(spacing.$unit * 0.5) 0;
|
|
||||||
font-size: typography.$font-regular;
|
|
||||||
font-weight: typography.$medium;
|
|
||||||
color: var(--text-primary, colors.$grey-20);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.uncap-display {
|
|
||||||
margin-bottom: calc(spacing.$unit * 1.5);
|
|
||||||
padding: spacing.$unit;
|
|
||||||
background: colors.$grey-90;
|
|
||||||
border-radius: calc(layout.$item-corner * 0.5);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stats-group {
|
|
||||||
margin-bottom: spacing.$unit-2x;
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.skills-list,
|
|
||||||
.auras-list {
|
|
||||||
.skill-item,
|
|
||||||
.aura-item {
|
|
||||||
padding: spacing.$unit;
|
|
||||||
background: colors.$grey-90;
|
|
||||||
border-radius: calc(layout.$item-corner * 0.5);
|
|
||||||
margin-bottom: spacing.$unit;
|
|
||||||
|
|
||||||
h4 {
|
|
||||||
margin: 0 0 calc(spacing.$unit * 0.5) 0;
|
|
||||||
font-size: typography.$font-regular;
|
|
||||||
color: var(--text-primary, colors.$grey-10);
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0;
|
|
||||||
font-size: typography.$font-small;
|
|
||||||
color: var(--text-secondary, colors.$grey-50);
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.keys-list {
|
|
||||||
.key-item {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: calc(spacing.$unit * 0.75);
|
|
||||||
background: colors.$grey-90;
|
|
||||||
border-radius: calc(layout.$item-corner * 0.5);
|
|
||||||
margin-bottom: calc(spacing.$unit * 0.5);
|
|
||||||
|
|
||||||
.key-slot {
|
|
||||||
font-size: typography.$font-small;
|
|
||||||
color: var(--text-secondary, colors.$grey-50);
|
|
||||||
}
|
|
||||||
|
|
||||||
.key-name {
|
|
||||||
font-size: typography.$font-small;
|
|
||||||
color: var(--text-primary, colors.$grey-10);
|
|
||||||
font-weight: typography.$medium;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.special-indicator {
|
|
||||||
.value {
|
|
||||||
color: var(--color-success, #4caf50);
|
|
||||||
font-size: typography.$font-large;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.canonical-view,
|
|
||||||
.user-version-view {
|
|
||||||
padding: 0 spacing.$unit-2x spacing.$unit-2x;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-version-view {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
position: relative;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: spacing.$unit-2x;
|
gap: spacing.$unit-2x;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
107
src/lib/components/sidebar/details/BasicInfoSection.svelte
Normal file
107
src/lib/components/sidebar/details/BasicInfoSection.svelte
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
<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'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
type: 'character' | 'weapon' | 'summon'
|
||||||
|
itemData: any
|
||||||
|
}
|
||||||
|
|
||||||
|
let { type, itemData }: Props = $props()
|
||||||
|
</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>
|
||||||
|
|
||||||
|
{#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) => getRaceLabel(r))
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(', ') || '—'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="detail-row">
|
||||||
|
<span class="label">Gender</span>
|
||||||
|
<span class="value">{getGenderLabel(itemData?.gender)}</span>
|
||||||
|
</div>
|
||||||
|
{#if itemData?.proficiency && itemData.proficiency.length > 0}
|
||||||
|
<div class="detail-row">
|
||||||
|
<span class="label">Proficiencies</span>
|
||||||
|
<span class="value">
|
||||||
|
{itemData.proficiency
|
||||||
|
.map((p) => getProficiencyLabel(p))
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(', ') || '—'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{:else if type === 'weapon'}
|
||||||
|
<div class="detail-row">
|
||||||
|
<span class="label">Proficiency</span>
|
||||||
|
<span class="value">{getProficiencyLabel(itemData?.proficiency)}</span>
|
||||||
|
</div>
|
||||||
|
{/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>
|
||||||
128
src/lib/components/sidebar/details/ItemHeader.svelte
Normal file
128
src/lib/components/sidebar/details/ItemHeader.svelte
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { GridCharacter, GridWeapon, GridSummon } from '$lib/types/api/party'
|
||||||
|
import {
|
||||||
|
getCharacterDetailImage,
|
||||||
|
getWeaponBaseImage,
|
||||||
|
getSummonDetailImage,
|
||||||
|
getCharacterPose
|
||||||
|
} from '$lib/utils/images'
|
||||||
|
import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
type: 'character' | 'weapon' | 'summon'
|
||||||
|
item: GridCharacter | GridWeapon | GridSummon
|
||||||
|
itemData: any
|
||||||
|
gridUncapLevel: number | null
|
||||||
|
gridTranscendence: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
let { type, item, itemData, gridUncapLevel, gridTranscendence }: Props = $props()
|
||||||
|
|
||||||
|
// Get image URL based on type using detail/base variants
|
||||||
|
function getImageUrl(): string {
|
||||||
|
const id = itemData?.granblueId
|
||||||
|
|
||||||
|
if (type === 'character') {
|
||||||
|
const pose = getCharacterPose(gridUncapLevel, gridTranscendence)
|
||||||
|
return getCharacterDetailImage(id, pose)
|
||||||
|
} else if (type === 'weapon') {
|
||||||
|
return getWeaponBaseImage(id)
|
||||||
|
} else {
|
||||||
|
return getSummonDetailImage(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get element-based background color
|
||||||
|
function getElementBackground(): string {
|
||||||
|
const element = itemData?.element
|
||||||
|
switch (element) {
|
||||||
|
case 1:
|
||||||
|
return 'var(--wind-item-detail-bg)'
|
||||||
|
case 2:
|
||||||
|
return 'var(--fire-item-detail-bg)'
|
||||||
|
case 3:
|
||||||
|
return 'var(--water-item-detail-bg)'
|
||||||
|
case 4:
|
||||||
|
return 'var(--earth-item-detail-bg)'
|
||||||
|
case 5:
|
||||||
|
return 'var(--dark-item-detail-bg)'
|
||||||
|
case 6:
|
||||||
|
return 'var(--light-item-detail-bg)'
|
||||||
|
default:
|
||||||
|
return 'var(--null-item-detail-bg)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayName(input: any): string {
|
||||||
|
if (!input) return '—'
|
||||||
|
const maybe = input.name ?? input
|
||||||
|
if (typeof maybe === 'string') return maybe
|
||||||
|
if (maybe && typeof maybe === 'object') {
|
||||||
|
return maybe.en || maybe.ja || '—'
|
||||||
|
}
|
||||||
|
return '—'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="item-header-container">
|
||||||
|
<div class="item-header">
|
||||||
|
<div class="uncap-overlay">
|
||||||
|
<UncapIndicator
|
||||||
|
{type}
|
||||||
|
uncapLevel={gridUncapLevel}
|
||||||
|
transcendenceStage={gridTranscendence}
|
||||||
|
flb={itemData?.uncap?.flb}
|
||||||
|
ulb={itemData?.uncap?.ulb}
|
||||||
|
transcendence={itemData?.uncap?.transcendence}
|
||||||
|
editable={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<img src={getImageUrl()} alt={displayName(itemData)} class="item-image {type}" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@use '$src/themes/spacing' as spacing;
|
||||||
|
@use '$src/themes/layout' as layout;
|
||||||
|
|
||||||
|
.item-header-container {
|
||||||
|
padding: 0 spacing.$unit-2x;
|
||||||
|
|
||||||
|
.item-header {
|
||||||
|
display: flex;
|
||||||
|
gap: spacing.$unit-2x;
|
||||||
|
align-items: flex-start;
|
||||||
|
border-radius: layout.$card-corner;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: url('/images/relief.png'), linear-gradient(to right, #000, #484440, #000);
|
||||||
|
background-size: 420px 731px;
|
||||||
|
background-position: -20px -20px;
|
||||||
|
transition: background 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.item-image.weapon {
|
||||||
|
width: 80%;
|
||||||
|
transform: rotate(-15deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-image.summon,
|
||||||
|
.item-image.character {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uncap-overlay {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: layout.$item-corner;
|
||||||
|
padding: spacing.$unit;
|
||||||
|
bottom: spacing.$unit;
|
||||||
|
right: spacing.$unit;
|
||||||
|
background: rgba(0, 0, 0, 0.24);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
169
src/lib/components/sidebar/details/SkillsSection.svelte
Normal file
169
src/lib/components/sidebar/details/SkillsSection.svelte
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
<script lang="ts">
|
||||||
|
interface Props {
|
||||||
|
type: 'character' | 'weapon' | 'summon'
|
||||||
|
itemData: any
|
||||||
|
}
|
||||||
|
|
||||||
|
let { type, itemData }: Props = $props()
|
||||||
|
|
||||||
|
function displayName(input: any): string {
|
||||||
|
if (!input) return '—'
|
||||||
|
const maybe = input.name ?? input
|
||||||
|
if (typeof maybe === 'string') return maybe
|
||||||
|
if (maybe && typeof maybe === 'object') {
|
||||||
|
return maybe.en || maybe.ja || '—'
|
||||||
|
}
|
||||||
|
return '—'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if type === 'weapon' && itemData?.weaponSkills && itemData.weaponSkills.length > 0}
|
||||||
|
<div class="details-section">
|
||||||
|
<h3>Skills</h3>
|
||||||
|
<div class="skills-list">
|
||||||
|
{#each itemData.weaponSkills as skill}
|
||||||
|
<div class="skill-item">
|
||||||
|
<h4>{displayName(skill) || 'Unknown Skill'}</h4>
|
||||||
|
{#if skill.description}
|
||||||
|
<p>{skill.description}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if type === 'summon' && itemData?.summonAuras && itemData.summonAuras.length > 0}
|
||||||
|
<div class="details-section">
|
||||||
|
<h3>Auras</h3>
|
||||||
|
<div class="auras-list">
|
||||||
|
{#each itemData.summonAuras as aura}
|
||||||
|
<div class="aura-item">
|
||||||
|
<h4>{displayName(aura) || 'Unknown Aura'}</h4>
|
||||||
|
{#if aura.description}
|
||||||
|
<p>{aura.description}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if type === 'weapon' && itemData?.weaponKeys && itemData.weaponKeys.length > 0}
|
||||||
|
<div class="details-section">
|
||||||
|
<h3>Weapon Keys</h3>
|
||||||
|
<div class="keys-list">
|
||||||
|
{#each itemData.weaponKeys as key}
|
||||||
|
<div class="key-item">
|
||||||
|
<span class="key-slot">Slot {key.slot}</span>
|
||||||
|
<span class="key-name">{displayName(key.weaponKey1) || '—'}</span>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if type === 'character' && itemData?.special}
|
||||||
|
<div class="details-section">
|
||||||
|
<div class="detail-row special-indicator">
|
||||||
|
<span class="label">Special Character</span>
|
||||||
|
<span class="value">✓</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@use '$src/themes/colors' as colors;
|
||||||
|
@use '$src/themes/spacing' as spacing;
|
||||||
|
@use '$src/themes/typography' as typography;
|
||||||
|
|
||||||
|
.details-section {
|
||||||
|
margin-bottom: spacing.$unit-3x;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0 0 calc(spacing.$unit * 1.5) 0;
|
||||||
|
font-size: typography.$font-regular;
|
||||||
|
font-weight: typography.$medium;
|
||||||
|
color: var(--text-secondary, colors.$grey-40);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-size: typography.$font-regular;
|
||||||
|
font-weight: typography.$medium;
|
||||||
|
color: var(--text-primary, colors.$grey-10);
|
||||||
|
margin: 0 0 calc(spacing.$unit * 0.5) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: typography.$font-small;
|
||||||
|
color: var(--text-secondary, colors.$grey-50);
|
||||||
|
line-height: 1.5;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.skills-list,
|
||||||
|
.auras-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: spacing.$unit-2x;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-item,
|
||||||
|
.aura-item {
|
||||||
|
padding: spacing.$unit;
|
||||||
|
background: var(--page-hover, colors.$grey-80);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keys-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: spacing.$unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: calc(spacing.$unit * 0.75) spacing.$unit;
|
||||||
|
background: var(--page-hover, colors.$grey-80);
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
.key-slot {
|
||||||
|
font-size: typography.$font-small;
|
||||||
|
color: var(--text-secondary, colors.$grey-50);
|
||||||
|
font-weight: typography.$medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-name {
|
||||||
|
font-size: typography.$font-small;
|
||||||
|
color: var(--text-primary, colors.$grey-10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: calc(spacing.$unit * 0.75) 0;
|
||||||
|
|
||||||
|
&.special-indicator {
|
||||||
|
border-top: 1px solid rgba(colors.$grey-70, 0.5);
|
||||||
|
padding-top: spacing.$unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
176
src/lib/components/sidebar/details/StatsSection.svelte
Normal file
176
src/lib/components/sidebar/details/StatsSection.svelte
Normal file
|
|
@ -0,0 +1,176 @@
|
||||||
|
<script lang="ts">
|
||||||
|
interface Props {
|
||||||
|
itemData: any
|
||||||
|
gridUncapLevel: number | null
|
||||||
|
gridTranscendence: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
|
||||||
|
<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>
|
||||||
155
src/lib/components/sidebar/details/TeamView.svelte
Normal file
155
src/lib/components/sidebar/details/TeamView.svelte
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
<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 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'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
type: 'character' | 'weapon' | 'summon'
|
||||||
|
item: GridCharacter | GridWeapon | GridSummon
|
||||||
|
itemData: any
|
||||||
|
gridUncapLevel: number | null
|
||||||
|
gridTranscendence: number | null
|
||||||
|
modificationStatus: any
|
||||||
|
}
|
||||||
|
|
||||||
|
let { type, item, itemData, gridUncapLevel, gridTranscendence, modificationStatus }: Props =
|
||||||
|
$props()
|
||||||
|
</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>
|
||||||
|
|
||||||
|
{#if type === 'character'}
|
||||||
|
{@const char = item as GridCharacter}
|
||||||
|
|
||||||
|
{#if modificationStatus.hasAwakening}
|
||||||
|
<ModificationSection title="Awakening" visible={true}>
|
||||||
|
<AwakeningDisplay awakening={char.awakening} size="medium" showLevel={true} />
|
||||||
|
</ModificationSection>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if modificationStatus.hasRings || modificationStatus.hasEarring}
|
||||||
|
<ModificationSection title="Mastery" visible={true}>
|
||||||
|
<MasteryDisplay
|
||||||
|
rings={char.overMastery}
|
||||||
|
earring={char.aetherialMastery}
|
||||||
|
characterElement={char.character?.element}
|
||||||
|
variant="detailed"
|
||||||
|
showIcons={true}
|
||||||
|
/>
|
||||||
|
</ModificationSection>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if modificationStatus.hasPerpetuity}
|
||||||
|
<ModificationSection title="Status" visible={true}>
|
||||||
|
<StatModifierItem label="Perpetuity" value="Active" variant="max" />
|
||||||
|
</ModificationSection>
|
||||||
|
{/if}
|
||||||
|
{:else if type === 'weapon'}
|
||||||
|
{@const weapon = item as GridWeapon}
|
||||||
|
|
||||||
|
{#if modificationStatus.hasAwakening && weapon.awakening}
|
||||||
|
<ModificationSection title="Awakening" visible={true}>
|
||||||
|
<AwakeningDisplay awakening={weapon.awakening} size="medium" showLevel={true} />
|
||||||
|
</ModificationSection>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if modificationStatus.hasWeaponKeys}
|
||||||
|
<ModificationSection title={getWeaponKeyTitle(weapon.weapon?.series)} visible={true}>
|
||||||
|
<WeaponKeysList weaponKeys={weapon.weaponKeys} weaponData={weapon.weapon} layout="list" />
|
||||||
|
</ModificationSection>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if modificationStatus.hasAxSkills && weapon.ax}
|
||||||
|
<ModificationSection title="AX Skills" visible={true}>
|
||||||
|
{#each weapon.ax as axSkill}
|
||||||
|
<StatModifierItem
|
||||||
|
label={formatAxSkill(axSkill).split('+')[0].trim()}
|
||||||
|
value={`+${axSkill.strength}`}
|
||||||
|
suffix={axSkill.modifier <= 2 ? '' : '%'}
|
||||||
|
variant="enhanced"
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</ModificationSection>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if modificationStatus.hasElement && weapon.element}
|
||||||
|
<ModificationSection title="Element Override" visible={true}>
|
||||||
|
<StatModifierItem label="Instance Element" value={getElementName(weapon.element)} />
|
||||||
|
</ModificationSection>
|
||||||
|
{/if}
|
||||||
|
{:else if type === 'summon'}
|
||||||
|
{@const summon = item as GridSummon}
|
||||||
|
|
||||||
|
{#if modificationStatus.hasQuickSummon || modificationStatus.hasFriendSummon}
|
||||||
|
<ModificationSection title="Summon Status" visible={true}>
|
||||||
|
{#if summon.quickSummon}
|
||||||
|
<StatModifierItem label="Quick Summon" value="Enabled" variant="enhanced" />
|
||||||
|
{/if}
|
||||||
|
{#if summon.friend}
|
||||||
|
<StatModifierItem label="Friend Summon" value="Yes" />
|
||||||
|
{/if}
|
||||||
|
</ModificationSection>
|
||||||
|
{/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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1,79 +1,64 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { SegmentedControl, Segment } from '$lib/components/ui/segmented-control'
|
import SegmentedControl from '$lib/components/ui/segmented-control/SegmentedControl.svelte'
|
||||||
|
import Segment from '$lib/components/ui/segmented-control/Segment.svelte'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
hasModifications: boolean
|
hasModifications: boolean
|
||||||
selectedView: 'canonical' | 'user'
|
selectedView: 'canonical' | 'user'
|
||||||
onViewChange?: (view: 'canonical' | 'user') => void
|
onViewChange?: (view: 'canonical' | 'user') => void
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let { hasModifications, selectedView = $bindable('user'), onViewChange }: Props = $props()
|
||||||
hasModifications,
|
|
||||||
selectedView = $bindable('canonical'),
|
|
||||||
onViewChange
|
|
||||||
}: Props = $props()
|
|
||||||
|
|
||||||
function handleViewChange(value: string) {
|
function handleViewChange(value: string) {
|
||||||
selectedView = value as 'canonical' | 'user'
|
selectedView = value as 'canonical' | 'user'
|
||||||
onViewChange?.(selectedView)
|
onViewChange?.(selectedView)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="details-sidebar-segmented-control">
|
{#if hasModifications}
|
||||||
<SegmentedControl
|
<div class="details-sidebar-segmented-control">
|
||||||
bind:value={selectedView}
|
<SegmentedControl
|
||||||
onValueChange={handleViewChange}
|
bind:value={selectedView}
|
||||||
variant="background"
|
onValueChange={handleViewChange}
|
||||||
grow
|
variant="background"
|
||||||
>
|
grow
|
||||||
<Segment value="canonical">
|
>
|
||||||
<span class="segment-label">Canonical</span>
|
<Segment value="user">This team</Segment>
|
||||||
</Segment>
|
<Segment value="canonical">Stats</Segment>
|
||||||
{#if hasModifications}
|
</SegmentedControl>
|
||||||
<Segment value="user">
|
</div>
|
||||||
<span class="segment-label">
|
{/if}
|
||||||
User Version
|
|
||||||
</span>
|
|
||||||
</Segment>
|
|
||||||
{:else}
|
|
||||||
<div class="disabled-segment">
|
|
||||||
<span class="segment-label disabled">
|
|
||||||
User Version
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</SegmentedControl>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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;
|
||||||
|
|
||||||
.details-sidebar-segmented-control {
|
.details-sidebar-segmented-control {
|
||||||
margin-bottom: spacing.$unit-2x;
|
margin-bottom: spacing.$unit-2x;
|
||||||
padding: 0 spacing.$unit-2x;
|
padding: 0 spacing.$unit-2x;
|
||||||
}
|
}
|
||||||
|
|
||||||
.segment-label {
|
.segment-label {
|
||||||
font-size: typography.$font-regular;
|
font-size: typography.$font-regular;
|
||||||
font-weight: typography.$medium;
|
font-weight: typography.$medium;
|
||||||
|
|
||||||
&.disabled {
|
&.disabled {
|
||||||
color: var(--text-tertiary, colors.$grey-60);
|
color: var(--text-tertiary, colors.$grey-60);
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.disabled-segment {
|
.disabled-segment {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: spacing.$unit;
|
padding: spacing.$unit;
|
||||||
background: colors.$grey-90;
|
background: colors.$grey-90;
|
||||||
border-radius: spacing.$unit-half;
|
border-radius: spacing.$unit-half;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue