Update rep components with improved styling

This commit is contained in:
Justin Edmund 2025-09-15 04:10:02 -07:00
parent 7e862ed56a
commit ff6074675b
4 changed files with 300 additions and 167 deletions

View file

@ -1,13 +1,17 @@
<script lang="ts"> <script lang="ts">
import type { PartyView } from '$lib/api/schemas/party' import type { Party, GridWeapon, GridCharacter } from '$lib/types/api/party'
export let party: PartyView export let party: Party
const characters = party.characters || [] const characters = party.characters || []
const grid = Array.from({ length: 3 }, (_, i) => characters.find((c: any) => c?.position === i)) const grid = Array.from({ length: 3 }, (_, i) =>
characters.find((c: GridCharacter) => c?.position === i)
)
function protagonistClass(): string { function protagonistClass(): string {
const main = (party.weapons || []).find((w: any) => w?.mainhand || w?.position === -1) const main: GridWeapon | undefined = (party.weapons || []).find(
const el = (main as any)?.element || (main as any)?.object?.element (w: GridWeapon) => w?.mainhand || w?.position === -1
)
const el = main?.element ?? main?.weapon?.element
switch (el) { switch (el) {
case 1: case 1:
return 'wind' return 'wind'
@ -26,8 +30,8 @@
} }
} }
function characterImageUrl(c?: any): string { function characterImageUrl(c?: GridCharacter): string {
const id = c?.object?.granblueId const id = c?.character?.granblueId
if (!id) return '' if (!id) return ''
const uncap = c?.uncapLevel ?? 0 const uncap = c?.uncapLevel ?? 0
const trans = c?.transcendenceStep ?? 0 const trans = c?.transcendenceStep ?? 0
@ -36,8 +40,10 @@
else if (uncap >= 5) suffix = '03' else if (uncap >= 5) suffix = '03'
else if (uncap > 2) suffix = '02' else if (uncap > 2) suffix = '02'
if (String(id) === '3030182000') { if (String(id) === '3030182000') {
const main = (party.weapons || []).find((w: any) => w?.mainhand || w?.position === -1) const main: GridWeapon | undefined = (party.weapons || []).find(
const el = (main as any)?.element || (main as any)?.object?.element || 1 (w: GridWeapon) => w?.mainhand || w?.position === -1
)
const el = main?.element ?? main?.weapon?.element ?? 1
suffix = `${suffix}_0${el}` suffix = `${suffix}_0${el}`
} }
return `/images/character-main/${id}_${suffix}.jpg` return `/images/character-main/${id}_${suffix}.jpg`
@ -46,79 +52,108 @@
<div class="rep"> <div class="rep">
<ul class="characters"> <ul class="characters">
<li class={`protagonist ${protagonistClass()}`}></li> <li class={`protagonist ${protagonistClass()}`} class:empty={!protagonistClass()}></li>
{#each grid as c, i} {#each grid as c, i}
<li class="character"> <li class="character" class:empty={!c}>
{#if c}<img alt="Character" src={characterImageUrl(c)} />{/if} {#if c}<img
alt="Character"
src={characterImageUrl(c)}
loading="lazy"
decoding="async"
/>{/if}
</li> </li>
{/each} {/each}
</ul> </ul>
</div> </div>
<style lang="scss"> <style lang="scss">
@use '$src/themes/layout' as *;
@use '$src/themes/spacing' as *; @use '$src/themes/spacing' as *;
@use '$src/themes/rep' as rep; @use '$src/themes/rep' as rep;
.rep { .rep {
width: 100%; width: 100%;
height: 100%; height: 100%;
border-radius: 10px; border-radius: $item-corner-small;
grid-gap: $unit-half; grid-gap: $unit-half;
}
.character,
.protagonist {
aspect-ratio: 16/33;
background: var(--card-bg);
border-radius: 4px;
box-sizing: border-box;
display: grid;
overflow: hidden;
}
.character img {
border-radius: 4px;
width: 100%;
}
.characters { .characters {
display: grid; display: grid;
grid-template-columns: repeat(4, 1fr); grid-template-columns: repeat(4, 1fr);
gap: $unit-half; gap: $unit-half;
margin: 0;
padding: 0;
list-style: none;
.character,
.protagonist {
aspect-ratio: 16/33;
background: var(--placeholder-bg);
border-radius: 4px;
box-sizing: border-box;
display: grid;
overflow: hidden;
&.empty {
background: var(--placeholder-bg);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.06);
} }
}
.character img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
}
.protagonist { .protagonist {
border-color: transparent; border-color: transparent;
border-width: 1px; border-width: 1px;
border-style: solid; border-style: solid;
@include rep.aspect(32, 66); @include rep.aspect(32, 66);
}
.protagonist img { img {
position: relative; position: relative;
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.protagonist.wind {
&.wind {
background: var(--wind-portrait-bg); background: var(--wind-portrait-bg);
border-color: var(--wind-bg); border-color: var(--wind-bg);
} }
.protagonist.fire {
&.fire {
background: var(--fire-portrait-bg); background: var(--fire-portrait-bg);
border-color: var(--fire-bg); border-color: var(--fire-bg);
} }
.protagonist.water {
&.water {
background: var(--water-portrait-bg); background: var(--water-portrait-bg);
border-color: var(--water-bg); border-color: var(--water-bg);
} }
.protagonist.earth {
&.earth {
background: var(--earth-portrait-bg); background: var(--earth-portrait-bg);
border-color: var(--earth-bg); border-color: var(--earth-bg);
} }
.protagonist.light {
&.light {
background: var(--light-portrait-bg); background: var(--light-portrait-bg);
border-color: var(--light-bg); border-color: var(--light-bg);
} }
.protagonist.dark {
&.dark {
background: var(--dark-portrait-bg); background: var(--dark-portrait-bg);
border-color: var(--dark-bg); border-color: var(--dark-bg);
} }
.protagonist.empty {
background: var(--card-bg); &.empty {
background: var(--placeholder-bg);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.06);
}
}
} }
</style> </style>

View file

@ -1,24 +1,26 @@
<script lang="ts"> <script lang="ts">
import type { PartyView, GridSummonItemView } from '$lib/api/schemas/party' import type { Party, GridSummon } from '$lib/types/api/party'
export let party: PartyView export let party: Party
export let extendedView = false export let extendedView = false
const summons = party.summons || [] const summons = party.summons || []
const main = summons.find((s: any) => s?.main || s?.position === -1) const main: GridSummon | undefined = summons.find(
const friend = extendedView (s: GridSummon) => s?.main || s?.position === -1
? summons.find((s: any) => s?.friend || s?.position === -2) )
const friend: GridSummon | undefined = extendedView
? summons.find((s: GridSummon) => s?.friend || s?.position === -2)
: undefined : undefined
// In standard view: show positions 0-3 (4 summons) // In standard view: show positions 0-3 (4 summons)
// In extended view: show positions 0-5 (6 summons including subauras) // In extended view: show positions 0-5 (6 summons including subauras)
const gridLength = extendedView ? 6 : 4 const gridLength = extendedView ? 6 : 4
const grid = Array.from({ length: gridLength }, (_, i) => const grid = Array.from({ length: gridLength }, (_, i) =>
summons.find((s: any) => s?.position === i) summons.find((s: GridSummon) => s?.position === i)
) )
function summonImageUrl(s?: any, isMain = false): string { function summonImageUrl(s?: GridSummon, isMain = false): string {
const id = s?.object?.granblueId const id = s?.summon?.granblueId
if (!id) return '' if (!id) return ''
const folder = isMain ? 'summon-main' : 'summon-grid' const folder = isMain ? 'summon-main' : 'summon-grid'
return `/images/${folder}/${id}.jpg` return `/images/${folder}/${id}.jpg`
@ -26,24 +28,35 @@
</script> </script>
<div class="rep" class:extended={extendedView}> <div class="rep" class:extended={extendedView}>
<div class="mainSummon"> <div class="mainSummon" class:empty={!main}>
{#if main}<img alt="Main Summon" src={summonImageUrl(main, true)} />{/if} {#if main}<img
alt="Main Summon"
src={summonImageUrl(main, true)}
loading="lazy"
decoding="async"
/>{/if}
</div> </div>
<ul class="summons"> <ul class="summons">
{#each grid as s, i} {#each grid as s, i}
<li class="summon"> <li class="summon" class:empty={!s}>
{#if s}<img alt="Summon" src={summonImageUrl(s)} />{/if} {#if s}<img alt="Summon" src={summonImageUrl(s)} loading="lazy" decoding="async" />{/if}
</li> </li>
{/each} {/each}
</ul> </ul>
{#if extendedView} {#if extendedView}
<div class="friendSummon"> <div class="friendSummon" class:empty={!friend}>
{#if friend}<img alt="Friend Summon" src={summonImageUrl(friend, true)} />{/if} {#if friend}<img
alt="Friend Summon"
src={summonImageUrl(friend, true)}
loading="lazy"
decoding="async"
/>{/if}
</div> </div>
{/if} {/if}
</div> </div>
<style lang="scss"> <style lang="scss">
@use '$src/themes/layout' as *;
@use '$src/themes/spacing' as *; @use '$src/themes/spacing' as *;
@use '$src/themes/rep' as rep; @use '$src/themes/rep' as rep;
@ -51,23 +64,48 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
display: grid; display: grid;
grid-template-columns: 1fr #{rep.$summon-cols-proportion}fr; gap: calc($unit-half + 1px);
grid-gap: $unit-half;
}
// Extended view layout: main summon | 6 grid summons | friend summon // Extended view layout: main summon | 6 grid summons | friend summon
.rep.extended { &.extended {
display: flex; grid-template-columns: auto 1fr auto;
gap: $unit-half;
grid-template-columns: none; .mainSummon,
box-sizing: border-box; .friendSummon {
@include rep.aspect(56, 97);
display: grid;
flex: 0 0 auto;
min-width: 70px;
height: 100%;
}
.summons {
display: grid;
grid-template-rows: repeat(3, 1fr);
grid-template-columns: repeat(2, 1fr);
column-gap: calc($unit-half + 1px);
row-gap: calc($unit * 1.5 - 2px);
min-width: 0; /* allow grid to shrink without overflowing */
}
} }
.summon, .summon,
.mainSummon, .mainSummon,
.friendSummon { .friendSummon {
background: var(--card-bg); background: var(--unit-bg);
border-radius: 4px; border-radius: $item-corner-small;
overflow: hidden;
&.empty {
background: var(--placeholder-bg);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.06);
}
img {
display: block;
width: 100%;
height: 100%;
}
} }
.mainSummon { .mainSummon {
@ -75,36 +113,20 @@
display: grid; display: grid;
} }
.extended .mainSummon,
.extended .friendSummon {
@include rep.aspect(56, 97);
display: grid;
flex: 0 0 auto;
}
.summons { .summons {
@include rep.grid(2, 2, $unit-half); @include rep.grid(2, 2, $unit-half);
} margin: 0;
padding: 0;
.extended .summons { list-style: none;
display: grid;
grid-template-rows: repeat(3, 1fr);
grid-template-columns: repeat(2, 1fr);
gap: $unit-half;
flex: 1 1 0;
min-width: 0; /* allow grid to shrink without overflowing */
} }
.summon { .summon {
@include rep.aspect(184, 138); background: var(--unit-bg);
border-radius: $item-corner-small;
overflow: hidden;
min-width: 43px;
display: grid; display: grid;
@include rep.aspect(184, 138);
} }
.summon img,
.mainSummon img,
.friendSummon img {
border-radius: 4px;
width: 100%;
height: 100%;
} }
</style> </style>

View file

@ -1,19 +1,21 @@
<script lang="ts"> <script lang="ts">
import type { PartyView, GridWeaponItemView } from '$lib/api/schemas/party' import type { Party, GridWeapon } from '$lib/types/api/party'
export let party: PartyView export let party: Party
const weapons = party.weapons || [] const weapons = party.weapons || []
const mainhand: GridWeaponItemView | undefined = weapons.find( const mainhand: GridWeapon | undefined = weapons.find(
(w: any) => w?.mainhand || w?.position === -1 (w: GridWeapon) => w?.mainhand || w?.position === -1
)
const grid = Array.from({ length: 9 }, (_, i) =>
weapons.find((w: GridWeapon) => w?.position === i)
) )
const grid = Array.from({ length: 9 }, (_, i) => weapons.find((w: any) => w?.position === i))
function weaponImageUrl(w?: any, isMain = false): string { function weaponImageUrl(w?: GridWeapon, isMain = false): string {
const id = w?.object?.granblueId const id = w?.weapon?.granblueId
if (!id) return '' if (!id) return ''
const folder = isMain ? 'weapon-main' : 'weapon-grid' const folder = isMain ? 'weapon-main' : 'weapon-grid'
const objElement = w?.object?.element const objElement = w?.weapon?.element
const instElement = w?.element const instElement = w?.element
if (objElement === 0 && instElement) return `/images/${folder}/${id}_${instElement}.jpg` if (objElement === 0 && instElement) return `/images/${folder}/${id}_${instElement}.jpg`
return `/images/${folder}/${id}.jpg` return `/images/${folder}/${id}.jpg`
@ -21,28 +23,96 @@
</script> </script>
<div class="rep"> <div class="rep">
<div class="mainhand"> <div class="mainhand" class:empty={!mainhand}>
{#if mainhand}<img alt="Mainhand" src={weaponImageUrl(mainhand, true)} />{/if} {#if mainhand}<img
alt="Mainhand"
src={weaponImageUrl(mainhand, true)}
loading="lazy"
decoding="async"
/>{/if}
</div> </div>
<ul class="weapons"> <div class="weapons">
{#each grid as w, i} {#each Array.from( { length: 3 }, (_, rowIndex) => grid.slice(rowIndex * 3, (rowIndex + 1) * 3) ) as row, rowIndex}
<li class="weapon"> <ul class="weapon-row">
{#if w}<img alt="Weapon" src={weaponImageUrl(w)} />{/if} {#each row as w, colIndex}
<li class="weapon" class:empty={!w}>
{#if w}<img alt="Weapon" src={weaponImageUrl(w)} loading="lazy" decoding="async" />{/if}
</li> </li>
{/each} {/each}
</ul> </ul>
{/each}
</div>
</div> </div>
<style lang="scss"> <style lang="scss">
@use '$src/themes/layout' as *;
@use '$src/themes/spacing' as *; @use '$src/themes/spacing' as *;
@use '$src/themes/rep' as rep; @use '$src/themes/rep' as rep;
.rep { width: 100%; height: 100%; display: grid; grid-template-columns: 1fr #{rep.$weapon-cols-proportion}fr; gap: $unit-half; } .rep {
width: 100%;
height: 100%;
display: grid;
grid-template-columns: 1.11fr #{rep.$weapon-cols-proportion}fr;
gap: $unit-half;
.mainhand { background: var(--unit-bg); border-radius: 4px; @include rep.aspect(rep.$weapon-main-w, rep.$weapon-main-h); overflow: hidden; } .mainhand {
.mainhand img { width: 100%; height: 100%; object-fit: cover; border-radius: 4px; } background: var(--placeholder-bg);
border-radius: $item-corner-small;
@include rep.aspect(rep.$weapon-main-w, rep.$weapon-main-h);
overflow: hidden;
min-height: 115px;
.weapons { margin: 0; padding: 0; list-style: none; height: 100%; @include rep.grid(3, 3, $unit-half); } &.empty {
.weapon { background: var(--unit-bg); border-radius: 4px; overflow: hidden; @include rep.aspect(rep.$weapon-cell-w, rep.$weapon-cell-h); } background: var(--placeholder-bg);
.weapon img { width: 100%; height: 100%; object-fit: cover; border-radius: 4px; } box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.06);
}
img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
}
.weapons {
display: flex;
flex-direction: column;
gap: $unit-half;
height: 100%;
.weapon-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: $unit-half;
margin: 0;
padding: 0;
list-style: none;
flex: 1;
.weapon {
background: var(--unit-bg);
border-radius: $item-corner-small;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
&.empty {
background: var(--placeholder-bg);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.06);
}
img {
border-radius: $item-corner-small;
display: block;
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
}
}
}
}
</style> </style>

View file

@ -15,16 +15,22 @@
// Common proportions and ratios for reps // Common proportions and ratios for reps
// Chosen to match legacy visual geometry without redesign. // Chosen to match legacy visual geometry without redesign.
$rep-body-ratio: 1.95; // width / height for GridRep body; ensures no content cut-off $rep-body-ratio: 2.03; // width / height for GridRep body; ensures no content cut-off
// Column proportions for weapon/summon layouts // Column proportions for weapon/summon layouts
$weapon-cols-proportion: 3.55; // mainhand : grid width proportion (1 : 3.55) $weapon-cols-proportion: 3.55; // mainhand : grid width proportion (1 : 3.55)
$summon-cols-proportion: 2.25; // main : grid width proportion (1 : 2.25) $summon-cols-proportion: 2.25; // main : grid width proportion (1 : 2.25)
// Aspect pairs (w, h) for key cells // Aspect pairs (w, h) for key cells
$weapon-main-w: 73; $weapon-main-h: 153; $weapon-main-w: 73;
$weapon-cell-w: 280; $weapon-cell-h: 160; $weapon-main-h: 153;
$summon-main-w: 56; $summon-main-h: 97; $weapon-cell-w: 280;
$summon-cell-w: 184; $summon-cell-h: 138; $weapon-cell-h: 160;
$char-protag-w: 32; $char-protag-h: 66; $summon-main-w: 56;
$char-cell-w: 16; $char-cell-h: 33; $summon-main-h: 97;
$summon-cell-w: 184;
$summon-cell-h: 138;
$char-protag-w: 32;
$char-protag-h: 66;
$char-cell-w: 16;
$char-cell-h: 33;