add sidebar modification display components

This commit is contained in:
Justin Edmund 2025-09-25 00:24:36 -07:00
parent f090d2fe41
commit 249877efe6
6 changed files with 648 additions and 0 deletions

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

View file

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

View file

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

View file

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

View file

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

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