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, .characters {
.protagonist { display: grid;
aspect-ratio: 16/33; grid-template-columns: repeat(4, 1fr);
background: var(--card-bg); gap: $unit-half;
border-radius: 4px; margin: 0;
box-sizing: border-box; padding: 0;
display: grid; list-style: none;
overflow: hidden;
} .character,
.character img { .protagonist {
border-radius: 4px; aspect-ratio: 16/33;
width: 100%; background: var(--placeholder-bg);
} border-radius: 4px;
.characters { box-sizing: border-box;
display: grid; display: grid;
grid-template-columns: repeat(4, 1fr); overflow: hidden;
gap: $unit-half;
} &.empty {
.protagonist { background: var(--placeholder-bg);
border-color: transparent; box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.06);
border-width: 1px; }
border-style: solid; }
@include rep.aspect(32, 66);
} .character img {
.protagonist img { display: block;
position: relative; width: 100%;
width: 100%; height: 100%;
height: 100%; object-fit: cover;
} }
.protagonist.wind { }
background: var(--wind-portrait-bg);
border-color: var(--wind-bg); .protagonist {
} border-color: transparent;
.protagonist.fire { border-width: 1px;
background: var(--fire-portrait-bg); border-style: solid;
border-color: var(--fire-bg); @include rep.aspect(32, 66);
}
.protagonist.water { img {
background: var(--water-portrait-bg); position: relative;
border-color: var(--water-bg); width: 100%;
} height: 100%;
.protagonist.earth { }
background: var(--earth-portrait-bg);
border-color: var(--earth-bg); &.wind {
} background: var(--wind-portrait-bg);
.protagonist.light { border-color: var(--wind-bg);
background: var(--light-portrait-bg); }
border-color: var(--light-bg);
} &.fire {
.protagonist.dark { background: var(--fire-portrait-bg);
background: var(--dark-portrait-bg); border-color: var(--fire-bg);
border-color: var(--dark-bg); }
}
.protagonist.empty { &.water {
background: var(--card-bg); background: var(--water-portrait-bg);
border-color: var(--water-bg);
}
&.earth {
background: var(--earth-portrait-bg);
border-color: var(--earth-bg);
}
&.light {
background: var(--light-portrait-bg);
border-color: var(--light-bg);
}
&.dark {
background: var(--dark-portrait-bg);
border-color: var(--dark-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,60 +64,69 @@
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;
box-sizing: border-box;
}
.summon, .mainSummon,
.mainSummon, .friendSummon {
.friendSummon { @include rep.aspect(56, 97);
background: var(--card-bg); display: grid;
border-radius: 4px; flex: 0 0 auto;
} min-width: 70px;
height: 100%;
}
.mainSummon { .summons {
@include rep.aspect(56, 97); display: grid;
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 */
}
}
.extended .mainSummon, .summon,
.extended .friendSummon { .mainSummon,
@include rep.aspect(56, 97); .friendSummon {
display: grid; background: var(--unit-bg);
flex: 0 0 auto; border-radius: $item-corner-small;
} overflow: hidden;
.summons { &.empty {
@include rep.grid(2, 2, $unit-half); background: var(--placeholder-bg);
} box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.06);
}
.extended .summons { img {
display: grid; display: block;
grid-template-rows: repeat(3, 1fr); width: 100%;
grid-template-columns: repeat(2, 1fr); height: 100%;
gap: $unit-half; }
flex: 1 1 0; }
min-width: 0; /* allow grid to shrink without overflowing */
}
.summon { .mainSummon {
@include rep.aspect(184, 138); @include rep.aspect(56, 97);
display: grid; display: grid;
} }
.summon img, .summons {
.mainSummon img, @include rep.grid(2, 2, $unit-half);
.friendSummon img { margin: 0;
border-radius: 4px; padding: 0;
width: 100%; list-style: none;
height: 100%; }
.summon {
background: var(--unit-bg);
border-radius: $item-corner-small;
overflow: hidden;
min-width: 43px;
display: grid;
@include rep.aspect(184, 138);
}
} }
</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> <li class="weapon" class:empty={!w}>
{#if w}<img alt="Weapon" src={weaponImageUrl(w)} loading="lazy" decoding="async" />{/if}
</li>
{/each}
</ul>
{/each} {/each}
</ul> </div>
</div> </div>
<style lang="scss"> <style lang="scss">
@use '$src/themes/spacing' as *; @use '$src/themes/layout' as *;
@use '$src/themes/rep' as rep; @use '$src/themes/spacing' as *;
@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

@ -2,29 +2,35 @@
// Keep grid and aspect utilities in one place for consistency. // Keep grid and aspect utilities in one place for consistency.
@mixin aspect($w, $h) { @mixin aspect($w, $h) {
// Use calc to avoid unit issues and keep intent clear. // Use calc to avoid unit issues and keep intent clear.
aspect-ratio: calc(#{$w} / #{$h}); aspect-ratio: calc(#{$w} / #{$h});
} }
@mixin grid($rows, $cols, $gap) { @mixin grid($rows, $cols, $gap) {
display: grid; display: grid;
grid-template-rows: repeat(#{$rows}, 1fr); grid-template-rows: repeat(#{$rows}, 1fr);
grid-template-columns: repeat(#{$cols}, 1fr); grid-template-columns: repeat(#{$cols}, 1fr);
gap: $gap; gap: $gap;
} }
// 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;