add context menu to job components

This commit is contained in:
Justin Edmund 2025-09-30 03:43:41 -07:00
parent 0ab2782697
commit a88411eb46
3 changed files with 123 additions and 70 deletions

View file

@ -1,6 +1,11 @@
<script lang="ts"> <script lang="ts">
import type { Job } from '$lib/types/api/entities' import type { Job } from '$lib/types/api/entities'
import { getJobIconUrl, formatJobProficiency } from '$lib/utils/jobUtils' import {
getJobIconUrl,
getJobWideImageUrl,
formatJobProficiency,
Gender
} from '$lib/utils/jobUtils'
import ProficiencyLabel from '../labels/ProficiencyLabel.svelte' import ProficiencyLabel from '../labels/ProficiencyLabel.svelte'
interface Props { interface Props {
@ -21,26 +26,34 @@
aria-pressed={selected} aria-pressed={selected}
aria-label="{job.name.en} - {selected ? 'Currently selected' : 'Click to select'}" aria-label="{job.name.en} - {selected ? 'Currently selected' : 'Click to select'}"
> >
<img src={getJobIconUrl(job.granblueId)} alt={job.name.en} class="job-icon" loading="lazy" /> <div class="job-image-container">
<img
src={getJobWideImageUrl(job, Gender.Gran)}
alt={job.name.en}
class="job-wide"
loading="lazy"
/>
</div>
<div class="job-info"> <div class="job-info">
<span class="job-name">{job.name.en}</span> <span class="job-name">{job.name.en}</span>
<div class="job-details"> {#if proficiencies.length > 0}
{#if job.ultimateMastery} <div class="proficiencies">
<span class="badge ultimate">UM</span> {#each job.proficiency as prof}
{/if} {#if prof > 0}
<ProficiencyLabel proficiency={prof} size="small" />
{/if}
{/each}
</div>
{/if}
</div>
{#if proficiencies.length > 0} <div class="job-right">
<div class="proficiencies"> {#if job.ultimateMastery}
{#each job.proficiency as prof} <span class="badge ultimate">UM</span>
{#if prof > 0} {/if}
<ProficiencyLabel proficiency={prof} size="small" /> <img src={getJobIconUrl(job.granblueId)} alt="" class="job-icon" loading="lazy" />
{/if}
{/each}
</div>
{/if}
</div>
</div> </div>
</button> </button>
@ -53,7 +66,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: spacing.$unit; gap: spacing.$unit;
padding: spacing.$unit-2x spacing.$unit; padding: spacing.$unit;
background: var(--card-bg); background: var(--card-bg);
border-radius: layout.$card-corner; border-radius: layout.$card-corner;
border: none; border: none;
@ -83,24 +96,26 @@
position: relative; position: relative;
.job-icon { .job-image-container {
// Display at native size (job icons are typically 48x48px) position: relative;
width: auto; width: 120px;
height: 24px; border-radius: layout.$item-corner;
max-width: 48px; overflow: hidden;
max-height: 48px;
border-radius: 4px;
flex-shrink: 0; flex-shrink: 0;
object-fit: contain;
.job-wide {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
} }
.job-info { .job-info {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: row; flex-direction: column;
justify-content: space-between; gap: spacing.$unit-half;
align-items: center;
gap: 4px;
min-width: 0; min-width: 0;
.job-name { .job-name {
@ -112,39 +127,46 @@
white-space: nowrap; white-space: nowrap;
} }
.job-details { .proficiencies {
display: flex; display: flex;
align-items: center;
gap: spacing.$unit-half; gap: spacing.$unit-half;
align-items: center;
overflow: hidden;
}
}
.badge { .job-right {
display: inline-block; display: flex;
padding: 2px 6px; align-items: center;
border-radius: 8px; gap: spacing.$unit;
font-size: typography.$font-small; flex-shrink: 0;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
flex-shrink: 0;
&.master { .badge {
background: var(--badge-master-bg, #ffd700); display: inline-block;
color: var(--badge-master-text, #000); padding: 2px 6px;
} border-radius: 8px;
font-size: typography.$font-small;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
&.ultimate { &.master {
background: var(--badge-ultimate-bg, #9b59b6); background: var(--badge-master-bg, #ffd700);
color: var(--badge-ultimate-text, #fff); color: var(--badge-master-text, #000);
}
} }
.proficiencies { &.ultimate {
display: flex; background: var(--badge-ultimate-bg, #9b59b6);
gap: spacing.$unit-half; color: var(--badge-ultimate-text, #fff);
align-items: center;
overflow: hidden;
} }
} }
.job-icon {
width: 28px;
height: 28px;
border-radius: layout.$item-corner;
object-fit: contain;
}
} }
} }
</style> </style>

View file

@ -86,23 +86,36 @@
<div class="job-header"> <div class="job-header">
{#if canEdit} {#if canEdit}
<button class="job-name clickable" on:click={onSelectJob}> <button class="job-name clickable" on:click={onSelectJob}>
<img src={jobIconUrl} alt="{job.name.en} icon" class="job-icon" /> <div class="job-name-row">
<h3>{job.name.en}</h3> <img src={jobIconUrl} alt="{job.name.en} icon" class="job-icon" />
<h3>{job.name.en}</h3>
</div>
{#if job.masterLevel || job.ultimateMastery}
<div class="job-badges">
{#if job.masterLevel}
<span class="badge master">ML{job.masterLevel}</span>
{/if}
{#if job.ultimateMastery}
<span class="badge ultimate">UM</span>
{/if}
</div>
{/if}
</button> </button>
{:else} {:else}
<div class="job-name"> <div class="job-name">
<img src={jobIconUrl} alt="{job.name.en} icon" class="job-icon" /> <div class="job-name-row">
<h3>{job.name.en}</h3> <img src={jobIconUrl} alt="{job.name.en} icon" class="job-icon" />
</div> <h3>{job.name.en}</h3>
{/if} </div>
{#if job.masterLevel || job.ultimateMastery}
{#if job.masterLevel || job.ultimateMastery} <div class="job-badges">
<div class="job-badges"> {#if job.masterLevel}
{#if job.masterLevel} <span class="badge master">ML{job.masterLevel}</span>
<span class="badge master">Master Lv.{job.masterLevel}</span> {/if}
{/if} {#if job.ultimateMastery}
{#if job.ultimateMastery} <span class="badge ultimate">UM</span>
<span class="badge ultimate">Ultimate</span> {/if}
</div>
{/if} {/if}
</div> </div>
{/if} {/if}
@ -297,6 +310,7 @@
.job-name { .job-name {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between;
gap: spacing.$unit; gap: spacing.$unit;
padding: spacing.$unit; padding: spacing.$unit;
border-radius: layout.$card-corner; border-radius: layout.$card-corner;
@ -316,6 +330,12 @@
} }
} }
.job-name-row {
display: flex;
align-items: center;
gap: spacing.$unit-half;
}
.job-icon { .job-icon {
width: 32px; width: 32px;
height: 32px; height: 32px;

View file

@ -41,11 +41,9 @@ export function getJobFullImageUrl(job: Job | undefined, gender: Gender = Gender
return '/images/placeholders/placeholder-weapon-grid.png' return '/images/placeholders/placeholder-weapon-grid.png'
} }
// Convert job name to slug format (lowercase, spaces to hyphens)
const slug = job.name.en.toLowerCase().replace(/\s+/g, '-')
const genderSuffix = gender === Gender.Djeeta ? 'b' : 'a' const genderSuffix = gender === Gender.Djeeta ? 'b' : 'a'
return `/images/jobs/${slug}_${genderSuffix}.png` return `/images/job-zoom/${job.granblueId}_${genderSuffix}.png`
} }
/** /**
@ -61,6 +59,19 @@ export function getJobIconUrl(granblueId: string | undefined): string {
return `/images/job-icons/${granblueId}.png` return `/images/job-icons/${granblueId}.png`
} }
/**
* Generate job wide banner image URL for JobItem component
* These are wider banner-style images stored in /static/images/job-wide/
*/
export function getJobWideImageUrl(job: Job | undefined, gender: Gender = Gender.Gran): string {
if (!job) {
return '/images/placeholders/placeholder-weapon-grid.png'
}
const genderSuffix = gender === Gender.Djeeta ? 'b' : 'a'
return `/images/job-wide/${job.granblueId}_${genderSuffix}.jpg`
}
/** /**
* Get job tier display name * Get job tier display name
* Converts internal row codes to user-friendly names * Converts internal row codes to user-friendly names