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">
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>

View file

@ -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>

View file

@ -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>

View file

@ -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}

View file

@ -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>

View file

@ -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>