add sidebar modification display components
This commit is contained in:
parent
f090d2fe41
commit
249877efe6
6 changed files with 648 additions and 0 deletions
108
src/lib/components/sidebar/modifications/AwakeningDisplay.svelte
Normal file
108
src/lib/components/sidebar/modifications/AwakeningDisplay.svelte
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
<script lang="ts">
|
||||
import { getAwakeningImage } from '$lib/utils/modifiers'
|
||||
import type { Awakening } from '$lib/types/api/entities'
|
||||
|
||||
interface Props {
|
||||
awakening?: {
|
||||
type?: Awakening
|
||||
level?: number
|
||||
} | Awakening
|
||||
size?: 'small' | 'medium' | 'large'
|
||||
showLevel?: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
awakening,
|
||||
size = 'medium',
|
||||
showLevel = true
|
||||
}: Props = $props()
|
||||
|
||||
function getAwakeningData() {
|
||||
if (!awakening) return null
|
||||
|
||||
if ('type' in awakening && awakening.type) {
|
||||
return {
|
||||
type: awakening.type,
|
||||
level: awakening.level || 0
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: awakening as Awakening,
|
||||
level: 0
|
||||
}
|
||||
}
|
||||
|
||||
let awakeningData = $derived(getAwakeningData())
|
||||
let imageUrl = $derived(getAwakeningImage(awakeningData))
|
||||
let displayName = $derived(awakeningData?.type?.name?.en || awakeningData?.type?.name?.ja || 'Awakening')
|
||||
</script>
|
||||
|
||||
{#if awakeningData && imageUrl}
|
||||
<div class="awakening-display {size}">
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt={displayName}
|
||||
class="awakening-icon"
|
||||
/>
|
||||
<div class="awakening-info">
|
||||
<span class="awakening-name">{displayName}</span>
|
||||
{#if showLevel && awakeningData.level !== undefined}
|
||||
<span class="awakening-level">Lv{awakeningData.level}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</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;
|
||||
|
||||
.awakening-display {
|
||||
display: flex;
|
||||
gap: spacing.$unit-2x;
|
||||
align-items: center;
|
||||
padding: spacing.$unit;
|
||||
background: colors.$grey-85;
|
||||
border-radius: layout.$item-corner-small;
|
||||
|
||||
&.small .awakening-icon {
|
||||
width: spacing.$unit-4x;
|
||||
height: spacing.$unit-4x;
|
||||
}
|
||||
|
||||
&.medium .awakening-icon {
|
||||
width: spacing.$unit-6x;
|
||||
height: spacing.$unit-6x;
|
||||
}
|
||||
|
||||
&.large .awakening-icon {
|
||||
width: spacing.$unit-8x;
|
||||
height: spacing.$unit-8x;
|
||||
}
|
||||
|
||||
.awakening-icon {
|
||||
border-radius: layout.$item-corner-small;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.awakening-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: spacing.$unit-half;
|
||||
|
||||
.awakening-name {
|
||||
font-size: typography.$font-regular;
|
||||
color: var(--text-primary, colors.$grey-10);
|
||||
font-weight: typography.$medium;
|
||||
}
|
||||
|
||||
.awakening-level {
|
||||
font-size: typography.$font-small;
|
||||
color: var(--text-secondary, colors.$grey-50);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
<script lang="ts">
|
||||
import { SegmentedControl, Segment } from '$lib/components/ui/segmented-control'
|
||||
|
||||
interface Props {
|
||||
hasModifications: boolean
|
||||
selectedView: 'canonical' | 'user'
|
||||
onViewChange?: (view: 'canonical' | 'user') => void
|
||||
}
|
||||
|
||||
let {
|
||||
hasModifications,
|
||||
selectedView = $bindable('canonical'),
|
||||
onViewChange
|
||||
}: Props = $props()
|
||||
|
||||
function handleViewChange(value: string) {
|
||||
selectedView = value as 'canonical' | 'user'
|
||||
onViewChange?.(selectedView)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="details-sidebar-segmented-control">
|
||||
<SegmentedControl
|
||||
bind:value={selectedView}
|
||||
onValueChange={handleViewChange}
|
||||
variant="background"
|
||||
grow
|
||||
>
|
||||
<Segment value="canonical">
|
||||
<span class="segment-label">Canonical</span>
|
||||
</Segment>
|
||||
{#if hasModifications}
|
||||
<Segment value="user">
|
||||
<span class="segment-label">
|
||||
User Version
|
||||
</span>
|
||||
</Segment>
|
||||
{:else}
|
||||
<div class="disabled-segment">
|
||||
<span class="segment-label disabled">
|
||||
User Version
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
</SegmentedControl>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@use '$src/themes/colors' as colors;
|
||||
@use '$src/themes/spacing' as spacing;
|
||||
@use '$src/themes/typography' as typography;
|
||||
|
||||
.details-sidebar-segmented-control {
|
||||
margin-bottom: spacing.$unit-2x;
|
||||
padding: 0 spacing.$unit-2x;
|
||||
}
|
||||
|
||||
.segment-label {
|
||||
font-size: typography.$font-regular;
|
||||
font-weight: typography.$medium;
|
||||
|
||||
&.disabled {
|
||||
color: var(--text-tertiary, colors.$grey-60);
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.disabled-segment {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: spacing.$unit;
|
||||
background: colors.$grey-90;
|
||||
border-radius: spacing.$unit-half;
|
||||
cursor: not-allowed;
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte'
|
||||
|
||||
interface Props {
|
||||
title: string
|
||||
visible?: boolean
|
||||
children: Snippet
|
||||
}
|
||||
|
||||
let { title, visible = true, children }: Props = $props()
|
||||
</script>
|
||||
|
||||
{#if visible}
|
||||
<div class="modification-section">
|
||||
<h3>{title}</h3>
|
||||
<div class="modification-content">
|
||||
{@render children()}
|
||||
</div>
|
||||
</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;
|
||||
|
||||
.modification-section {
|
||||
margin-bottom: spacing.$unit-3x;
|
||||
padding: spacing.$unit-2x;
|
||||
background: colors.$grey-90;
|
||||
border-radius: layout.$item-corner;
|
||||
|
||||
h3 {
|
||||
font-size: typography.$font-regular;
|
||||
font-weight: typography.$medium;
|
||||
color: var(--text-secondary, colors.$grey-40);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin: 0 0 spacing.$unit-2x 0;
|
||||
}
|
||||
|
||||
.modification-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: spacing.$unit;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
<script lang="ts">
|
||||
interface Props {
|
||||
label: string
|
||||
value: string | number
|
||||
suffix?: string
|
||||
icon?: string
|
||||
variant?: 'default' | 'enhanced' | 'max'
|
||||
class?: string
|
||||
}
|
||||
|
||||
let {
|
||||
label,
|
||||
value,
|
||||
suffix = '',
|
||||
icon,
|
||||
variant = 'default',
|
||||
class: className = ''
|
||||
}: Props = $props()
|
||||
</script>
|
||||
|
||||
<div class="stat-modifier {variant} {className}">
|
||||
{#if icon}
|
||||
<img src={icon} alt="" class="stat-icon" />
|
||||
{/if}
|
||||
<span class="label">{label}</span>
|
||||
<span class="value">{value}{suffix}</span>
|
||||
</div>
|
||||
|
||||
<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/effects' as effects;
|
||||
|
||||
.stat-modifier {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: spacing.$unit;
|
||||
background: colors.$grey-90;
|
||||
border-radius: layout.$item-corner-small;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
.stat-icon {
|
||||
width: spacing.$unit-2x;
|
||||
height: spacing.$unit-2x;
|
||||
margin-right: spacing.$unit;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: typography.$font-small;
|
||||
color: var(--text-secondary, colors.$grey-50);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: typography.$font-regular;
|
||||
font-weight: typography.$medium;
|
||||
color: var(--text-primary, colors.$grey-10);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
&.enhanced {
|
||||
background: colors.$grey-85;
|
||||
box-shadow: effects.$hover-shadow;
|
||||
|
||||
.value {
|
||||
color: var(--color-success, #4caf50);
|
||||
}
|
||||
}
|
||||
|
||||
&.max {
|
||||
background: linear-gradient(135deg, colors.$grey-85, colors.$grey-80);
|
||||
box-shadow: effects.$hover-shadow;
|
||||
|
||||
.value {
|
||||
color: #ffd700;
|
||||
font-weight: typography.$bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
<script lang="ts">
|
||||
import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte'
|
||||
import { formatUncapLevel, formatTranscendenceStep } from '$lib/utils/modificationFormatters'
|
||||
import StatModifierItem from './StatModifierItem.svelte'
|
||||
|
||||
interface Props {
|
||||
type: 'character' | 'weapon' | 'summon'
|
||||
uncapLevel?: number | null
|
||||
transcendenceStep?: number | null
|
||||
maxUncap?: number
|
||||
special?: boolean
|
||||
flb?: boolean
|
||||
ulb?: boolean
|
||||
transcendence?: boolean
|
||||
showIndicator?: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
type,
|
||||
uncapLevel = 0,
|
||||
transcendenceStep = 0,
|
||||
maxUncap = 3,
|
||||
special = false,
|
||||
flb = false,
|
||||
ulb = false,
|
||||
transcendence = false,
|
||||
showIndicator = true
|
||||
}: Props = $props()
|
||||
|
||||
function getMaxPossibleUncap(): number {
|
||||
if (transcendence) return 6
|
||||
if (ulb) return 5
|
||||
if (flb) return 4
|
||||
return maxUncap || 3
|
||||
}
|
||||
|
||||
function getUncapStatus(): string {
|
||||
const current = uncapLevel || 0
|
||||
const max = getMaxPossibleUncap()
|
||||
|
||||
if (current >= max) return 'Max'
|
||||
return `${current} / ${max}`
|
||||
}
|
||||
|
||||
function isMaxUncap(): boolean {
|
||||
return (uncapLevel || 0) >= getMaxPossibleUncap()
|
||||
}
|
||||
|
||||
function isMaxTranscendence(): boolean {
|
||||
return transcendenceStep === 5
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="uncap-status-display">
|
||||
{#if showIndicator}
|
||||
<div class="uncap-indicator-wrapper">
|
||||
<UncapIndicator
|
||||
{type}
|
||||
{uncapLevel}
|
||||
transcendenceStage={transcendenceStep}
|
||||
{special}
|
||||
{flb}
|
||||
{ulb}
|
||||
{transcendence}
|
||||
editable={false}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="uncap-details">
|
||||
<StatModifierItem
|
||||
label="Uncap Level"
|
||||
value={formatUncapLevel(uncapLevel)}
|
||||
variant={isMaxUncap() ? 'max' : 'default'}
|
||||
/>
|
||||
|
||||
{#if transcendence && transcendenceStep && transcendenceStep > 0}
|
||||
<StatModifierItem
|
||||
label="Transcendence"
|
||||
value={formatTranscendenceStep(transcendenceStep)}
|
||||
variant={isMaxTranscendence() ? 'max' : 'enhanced'}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<div class="available-uncaps">
|
||||
<span class="label">Available Upgrades</span>
|
||||
<div class="uncap-badges">
|
||||
{#if flb}
|
||||
<span class="badge" class:active={uncapLevel >= 4}>FLB</span>
|
||||
{/if}
|
||||
{#if ulb}
|
||||
<span class="badge" class:active={uncapLevel >= 5}>ULB</span>
|
||||
{/if}
|
||||
{#if transcendence}
|
||||
<span class="badge" class:active={transcendenceStep > 0}>Trans</span>
|
||||
{/if}
|
||||
{#if !flb && !ulb && !transcendence}
|
||||
<span class="badge standard">Standard</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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;
|
||||
|
||||
.uncap-status-display {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: spacing.$unit-2x;
|
||||
|
||||
.uncap-indicator-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: spacing.$unit-2x;
|
||||
background: colors.$grey-90;
|
||||
border-radius: layout.$item-corner;
|
||||
}
|
||||
|
||||
.uncap-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: spacing.$unit;
|
||||
}
|
||||
|
||||
.available-uncaps {
|
||||
padding: spacing.$unit;
|
||||
background: colors.$grey-90;
|
||||
border-radius: layout.$item-corner-small;
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
font-size: typography.$font-small;
|
||||
color: var(--text-secondary, colors.$grey-50);
|
||||
margin-bottom: spacing.$unit;
|
||||
}
|
||||
|
||||
.uncap-badges {
|
||||
display: flex;
|
||||
gap: spacing.$unit;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.badge {
|
||||
padding: spacing.$unit-half spacing.$unit;
|
||||
background: colors.$grey-85;
|
||||
border-radius: layout.$item-corner-small;
|
||||
font-size: typography.$font-tiny;
|
||||
color: var(--text-tertiary, colors.$grey-60);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
font-weight: typography.$medium;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&.active {
|
||||
background: var(--color-success-bg, rgba(76, 175, 80, 0.2));
|
||||
color: var(--color-success, #4caf50);
|
||||
border: 1px solid var(--color-success, #4caf50);
|
||||
}
|
||||
|
||||
&.standard {
|
||||
background: colors.$grey-85;
|
||||
color: var(--text-secondary, colors.$grey-50);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
157
src/lib/components/sidebar/modifications/WeaponKeysList.svelte
Normal file
157
src/lib/components/sidebar/modifications/WeaponKeysList.svelte
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
<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'
|
||||
|
||||
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 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 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}`
|
||||
}
|
||||
</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>
|
||||
{/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;
|
||||
|
||||
.weapon-keys-list {
|
||||
display: flex;
|
||||
gap: spacing.$unit;
|
||||
|
||||
&.list {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&.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;
|
||||
|
||||
&: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-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;
|
||||
}
|
||||
|
||||
.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;
|
||||
|
||||
.weapon-key-icon {
|
||||
width: spacing.$unit-6x;
|
||||
height: spacing.$unit-6x;
|
||||
}
|
||||
|
||||
.weapon-key-info {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in a new issue