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

View file

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

View file

@ -1,19 +1,21 @@
<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 mainhand: GridWeaponItemView | undefined = weapons.find(
(w: any) => w?.mainhand || w?.position === -1
const mainhand: GridWeapon | undefined = weapons.find(
(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 {
const id = w?.object?.granblueId
function weaponImageUrl(w?: GridWeapon, isMain = false): string {
const id = w?.weapon?.granblueId
if (!id) return ''
const folder = isMain ? 'weapon-main' : 'weapon-grid'
const objElement = w?.object?.element
const objElement = w?.weapon?.element
const instElement = w?.element
if (objElement === 0 && instElement) return `/images/${folder}/${id}_${instElement}.jpg`
return `/images/${folder}/${id}.jpg`
@ -21,28 +23,96 @@
</script>
<div class="rep">
<div class="mainhand">
{#if mainhand}<img alt="Mainhand" src={weaponImageUrl(mainhand, true)} />{/if}
<div class="mainhand" class:empty={!mainhand}>
{#if mainhand}<img
alt="Mainhand"
src={weaponImageUrl(mainhand, true)}
loading="lazy"
decoding="async"
/>{/if}
</div>
<ul class="weapons">
{#each grid as w, i}
<li class="weapon">
{#if w}<img alt="Weapon" src={weaponImageUrl(w)} />{/if}
</li>
<div class="weapons">
{#each Array.from( { length: 3 }, (_, rowIndex) => grid.slice(rowIndex * 3, (rowIndex + 1) * 3) ) as row, rowIndex}
<ul class="weapon-row">
{#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>
{/each}
</ul>
{/each}
</ul>
</div>
</div>
<style lang="scss">
@use '$src/themes/spacing' as *;
@use '$src/themes/rep' as rep;
@use '$src/themes/layout' as *;
@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 img { width: 100%; height: 100%; object-fit: cover; border-radius: 4px; }
.mainhand {
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); }
.weapon { background: var(--unit-bg); border-radius: 4px; overflow: hidden; @include rep.aspect(rep.$weapon-cell-w, rep.$weapon-cell-h); }
.weapon img { width: 100%; height: 100%; object-fit: cover; border-radius: 4px; }
&.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%;
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>

View file

@ -2,29 +2,35 @@
// Keep grid and aspect utilities in one place for consistency.
@mixin aspect($w, $h) {
// Use calc to avoid unit issues and keep intent clear.
aspect-ratio: calc(#{$w} / #{$h});
// Use calc to avoid unit issues and keep intent clear.
aspect-ratio: calc(#{$w} / #{$h});
}
@mixin grid($rows, $cols, $gap) {
display: grid;
grid-template-rows: repeat(#{$rows}, 1fr);
grid-template-columns: repeat(#{$cols}, 1fr);
gap: $gap;
display: grid;
grid-template-rows: repeat(#{$rows}, 1fr);
grid-template-columns: repeat(#{$cols}, 1fr);
gap: $gap;
}
// Common proportions and ratios for reps
// 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
$weapon-cols-proportion: 3.55; // mainhand : grid width proportion (1 : 3.55)
$summon-cols-proportion: 2.25; // main : grid width proportion (1 : 2.25)
// Aspect pairs (w, h) for key cells
$weapon-main-w: 73; $weapon-main-h: 153;
$weapon-cell-w: 280; $weapon-cell-h: 160;
$summon-main-w: 56; $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;
$weapon-main-w: 73;
$weapon-main-h: 153;
$weapon-cell-w: 280;
$weapon-cell-h: 160;
$summon-main-w: 56;
$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;