add collection display components for weapons and summons

- CollectionWeaponCard: grid view with uncap indicator and transcendence
- CollectionWeaponRow: list view with element, uncap, and awakening/keys info
- CollectionSummonCard: grid view with uncap indicator
- CollectionSummonRow: list view with element and uncap info
This commit is contained in:
Justin Edmund 2025-12-03 07:21:53 -08:00
parent 957dd16e5e
commit 033bc1c8f7
4 changed files with 486 additions and 0 deletions

View file

@ -0,0 +1,80 @@
<script lang="ts">
import type { CollectionSummon } from '$lib/types/api/collection'
import { getSummonImage } from '$lib/utils/images'
import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte'
interface Props {
summon: CollectionSummon
onClick?: () => void
}
let { summon, onClick }: Props = $props()
// Get transformation suffix for transcendence
const transformation = $derived(summon.transcendenceStep > 0 ? '02' : undefined)
const imageUrl = $derived(getSummonImage(summon.summon?.granblueId, 'grid', transformation))
const displayName = $derived.by(() => {
const name = summon.summon?.name
if (!name) return '—'
if (typeof name === 'string') return name
return name.en || name.ja || '—'
})
</script>
<button type="button" class="summon-card" onclick={onClick}>
<div class="card-image">
<img class="summon-image" src={imageUrl} alt={displayName} loading="lazy" />
</div>
<UncapIndicator
type="summon"
uncapLevel={summon.uncapLevel}
transcendenceStage={summon.transcendenceStep}
flb={summon.summon?.uncap?.flb}
ulb={summon.summon?.uncap?.ulb}
transcendence={summon.summon?.uncap?.transcendence}
/>
</button>
<style lang="scss">
@use '$src/themes/spacing' as *;
.summon-card {
display: flex;
flex-direction: column;
align-items: center;
gap: $unit-half;
padding: 0;
border: none;
background: transparent;
cursor: pointer;
transition: transform 0.2s ease;
&:hover {
transform: scale(1.05);
}
&:focus-visible {
outline: 2px solid var(--accent-color, #3366ff);
outline-offset: 2px;
border-radius: 8px;
}
}
.card-image {
position: relative;
width: 100%;
aspect-ratio: 1 / 1;
border-radius: 8px;
overflow: hidden;
background: var(--card-bg, #f5f5f5);
}
.summon-image {
width: 100%;
height: 100%;
object-fit: contain;
border-radius: 8px;
}
</style>

View file

@ -0,0 +1,142 @@
<script lang="ts">
import type { CollectionSummon } from '$lib/types/api/collection'
import { getSummonImage } from '$lib/utils/images'
import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte'
import ElementLabel from '$lib/components/labels/ElementLabel.svelte'
interface Props {
summon: CollectionSummon
onClick?: () => void
}
let { summon, onClick }: Props = $props()
// Get transformation suffix for transcendence
const transformation = $derived(summon.transcendenceStep > 0 ? '02' : undefined)
const imageUrl = $derived(getSummonImage(summon.summon?.granblueId, 'grid', transformation))
const displayName = $derived.by(() => {
const name = summon.summon?.name
if (!name) return '—'
if (typeof name === 'string') return name
return name.en || name.ja || '—'
})
// Summon element
const element = $derived(summon.summon?.element)
</script>
<button type="button" class="summon-row" onclick={onClick}>
<div class="thumbnail">
<img src={imageUrl} alt={displayName} loading="lazy" />
</div>
<div class="name-cell">
<span class="name">{displayName}</span>
</div>
<div class="element-cell">
<ElementLabel {element} size="medium" />
</div>
<div class="uncap-cell">
<UncapIndicator
type="summon"
uncapLevel={summon.uncapLevel}
transcendenceStage={summon.transcendenceStep}
flb={summon.summon?.uncap?.flb}
ulb={summon.summon?.uncap?.ulb}
transcendence={summon.summon?.uncap?.transcendence}
/>
</div>
<div class="extra-cell">
<span class="placeholder"></span>
</div>
</button>
<style lang="scss">
@use '$src/themes/spacing' as *;
@use '$src/themes/typography' as *;
.summon-row {
display: flex;
align-items: center;
gap: $unit-2x;
padding: $unit $unit-2x $unit $unit;
border: none;
background: var(--list-cell-bg);
cursor: pointer;
width: 100%;
text-align: left;
border-radius: 12px;
transition:
background 0.15s,
box-shadow 0.15s;
&:hover {
background: var(--list-cell-bg-hover);
box-shadow: 0 0 8px rgba(0, 0, 0, 0.08);
}
&:focus-visible {
outline: 2px solid var(--accent-color, #3366ff);
outline-offset: -2px;
}
}
.thumbnail {
width: 80px;
aspect-ratio: 1 / 1;
border-radius: 6px;
overflow: hidden;
background: var(--card-bg, #f5f5f5);
flex-shrink: 0;
img {
width: 100%;
height: 100%;
object-fit: contain;
}
}
.name-cell {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
gap: $unit;
}
.name {
font-size: $font-regular;
font-weight: $medium;
color: var(--text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.element-cell {
flex-shrink: 0;
}
.uncap-cell {
width: 100px;
display: flex;
justify-content: center;
flex-shrink: 0;
}
.extra-cell {
width: 64px;
display: flex;
justify-content: flex-end;
flex-shrink: 0;
}
.placeholder {
color: var(--text-secondary);
}
</style>

View file

@ -0,0 +1,85 @@
<script lang="ts">
import type { CollectionWeapon } from '$lib/types/api/collection'
import { getWeaponImage } from '$lib/utils/images'
import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte'
interface Props {
weapon: CollectionWeapon
onClick?: () => void
}
let { weapon, onClick }: Props = $props()
// Get transformation suffix for transcendence
const transformation = $derived(weapon.transcendenceStep > 0 ? '02' : undefined)
// Use instance element for element-changeable weapons
const displayElement = $derived(weapon.weapon?.element === 0 ? weapon.element : undefined)
const imageUrl = $derived(
getWeaponImage(weapon.weapon?.granblueId, 'grid', displayElement, transformation)
)
const displayName = $derived.by(() => {
const name = weapon.weapon?.name
if (!name) return '—'
if (typeof name === 'string') return name
return name.en || name.ja || '—'
})
</script>
<button type="button" class="weapon-card" onclick={onClick}>
<div class="card-image">
<img class="weapon-image" src={imageUrl} alt={displayName} loading="lazy" />
</div>
<UncapIndicator
type="weapon"
uncapLevel={weapon.uncapLevel}
transcendenceStage={weapon.transcendenceStep}
flb={weapon.weapon?.uncap?.flb}
ulb={weapon.weapon?.uncap?.ulb}
transcendence={weapon.weapon?.uncap?.transcendence}
/>
</button>
<style lang="scss">
@use '$src/themes/spacing' as *;
.weapon-card {
display: flex;
flex-direction: column;
align-items: center;
gap: $unit-half;
padding: 0;
border: none;
background: transparent;
cursor: pointer;
transition: transform 0.2s ease;
&:hover {
transform: scale(1.05);
}
&:focus-visible {
outline: 2px solid var(--accent-color, #3366ff);
outline-offset: 2px;
border-radius: 8px;
}
}
.card-image {
position: relative;
width: 100%;
aspect-ratio: 1 / 1;
border-radius: 8px;
overflow: hidden;
background: var(--card-bg, #f5f5f5);
}
.weapon-image {
width: 100%;
height: 100%;
object-fit: contain;
border-radius: 8px;
}
</style>

View file

@ -0,0 +1,179 @@
<script lang="ts">
import type { CollectionWeapon } from '$lib/types/api/collection'
import { getWeaponImage } from '$lib/utils/images'
import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte'
import ElementLabel from '$lib/components/labels/ElementLabel.svelte'
interface Props {
weapon: CollectionWeapon
onClick?: () => void
}
let { weapon, onClick }: Props = $props()
// Get transformation suffix for transcendence
const transformation = $derived(weapon.transcendenceStep > 0 ? '02' : undefined)
// Use instance element for element-changeable weapons
const displayElement = $derived(weapon.weapon?.element === 0 ? weapon.element : undefined)
const imageUrl = $derived(
getWeaponImage(weapon.weapon?.granblueId, 'grid', displayElement, transformation)
)
const displayName = $derived.by(() => {
const name = weapon.weapon?.name
if (!name) return '—'
if (typeof name === 'string') return name
return name.en || name.ja || '—'
})
// Show instance element for element-changeable, otherwise show weapon's base element
const element = $derived(weapon.weapon?.element === 0 ? weapon.element : weapon.weapon?.element)
// Awakening display
const awakeningDisplay = $derived.by(() => {
if (!weapon.awakening) return null
const type = weapon.awakening.type?.name?.en || 'Balanced'
const level = weapon.awakening.level || 1
const abbrev =
type === 'Balanced'
? 'BAL'
: type === 'Attack'
? 'ATK'
: type === 'Defense'
? 'DEF'
: type.slice(0, 3).toUpperCase()
return `${abbrev} ${level}`
})
// Weapon keys count (for display)
const keyCount = $derived(weapon.weaponKeys?.length ?? 0)
</script>
<button type="button" class="weapon-row" onclick={onClick}>
<div class="thumbnail">
<img src={imageUrl} alt={displayName} loading="lazy" />
</div>
<div class="name-cell">
<span class="name">{displayName}</span>
</div>
<div class="element-cell">
<ElementLabel {element} size="medium" />
</div>
<div class="uncap-cell">
<UncapIndicator
type="weapon"
uncapLevel={weapon.uncapLevel}
transcendenceStage={weapon.transcendenceStep}
flb={weapon.weapon?.uncap?.flb}
ulb={weapon.weapon?.uncap?.ulb}
transcendence={weapon.weapon?.uncap?.transcendence}
/>
</div>
<div class="extra-cell">
{#if awakeningDisplay}
<span class="awakening">{awakeningDisplay}</span>
{:else if keyCount > 0}
<span class="keys">{keyCount} keys</span>
{:else}
<span class="placeholder"></span>
{/if}
</div>
</button>
<style lang="scss">
@use '$src/themes/spacing' as *;
@use '$src/themes/typography' as *;
.weapon-row {
display: flex;
align-items: center;
gap: $unit-2x;
padding: $unit $unit-2x $unit $unit;
border: none;
background: var(--list-cell-bg);
cursor: pointer;
width: 100%;
text-align: left;
border-radius: 12px;
transition:
background 0.15s,
box-shadow 0.15s;
&:hover {
background: var(--list-cell-bg-hover);
box-shadow: 0 0 8px rgba(0, 0, 0, 0.08);
}
&:focus-visible {
outline: 2px solid var(--accent-color, #3366ff);
outline-offset: -2px;
}
}
.thumbnail {
width: 80px;
aspect-ratio: 1 / 1;
border-radius: 6px;
overflow: hidden;
background: var(--card-bg, #f5f5f5);
flex-shrink: 0;
img {
width: 100%;
height: 100%;
object-fit: contain;
}
}
.name-cell {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
gap: $unit;
}
.name {
font-size: $font-regular;
font-weight: $medium;
color: var(--text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.element-cell {
flex-shrink: 0;
}
.uncap-cell {
width: 100px;
display: flex;
justify-content: center;
flex-shrink: 0;
}
.extra-cell {
width: 64px;
display: flex;
justify-content: flex-end;
flex-shrink: 0;
}
.awakening,
.keys {
font-size: $font-small;
color: var(--text-secondary);
font-weight: $medium;
}
.placeholder {
color: var(--text-secondary);
}
</style>