fix(theme-editor): fix player card widget overflow, add banner variant, and improve widget selection badge

This commit is contained in:
HorizonCode committed 2026-11-01 14:32:56 +01:00
1 parent a025f9b285
commit ad3fe7c586
4 files changed
+159 -42

No files matched your search

@@ -2231,7 +2231,7 @@
element.widget = val as ThemeV2WidgetKind; element.widget = val as ThemeV2WidgetKind;
if (val === 'player-card') { if (val === 'player-card') {
element.width = 380; element.width = 380;
element.height = 180; element.height = 220;
} else if (val === 'avatar') { } else if (val === 'avatar') {
element.width = 72; element.width = 72;
element.height = 72; element.height = 72;
@@ -2296,12 +2296,18 @@
if (val === 'minimal') { if (val === 'minimal') {
element.width = 280; element.width = 280;
element.height = 64; element.height = 64;
} else if (val === 'banner') {
element.width = 340;
element.height = 76;
element.showStats = false;
} else if (val === 'compact') { } else if (val === 'compact') {
element.width = 340; element.width = 340;
element.height = 120; element.height = 140;
element.showStats = true;
} else if (val === 'full') { } else if (val === 'full') {
element.width = 380; element.width = 380;
element.height = 200; element.height = 220;
element.showStats = true;
} }
} else if (element.widget === 'recent-activity') { } else if (element.widget === 'recent-activity') {
if (val === 'minimal') { if (val === 'minimal') {
@@ -2356,6 +2362,14 @@
: element.variant === 'icon-only' : element.variant === 'icon-only'
? 'Icon-Only (Circle)' ? 'Icon-Only (Circle)'
: 'Primary (Capsule)'} : 'Primary (Capsule)'}
{:else if element.widget === 'player-card'}
{element.variant === 'compact'
? 'Compact (2 Stats)'
: element.variant === 'minimal'
? 'Minimal (Pill)'
: element.variant === 'banner'
? 'Banner (Profile & Level Only)'
: 'Full (4 Stats)'}
{:else} {:else}
{element.variant === 'compact' {element.variant === 'compact'
? 'Compact' ? 'Compact'
@@ -2373,6 +2387,11 @@
<Select.Item value="primary" label="Primary (Capsule)" /> <Select.Item value="primary" label="Primary (Capsule)" />
<Select.Item value="compact" label="Compact (Pill)" /> <Select.Item value="compact" label="Compact (Pill)" />
<Select.Item value="icon-only" label="Icon-Only (Circle)" /> <Select.Item value="icon-only" label="Icon-Only (Circle)" />
{:else if element.widget === 'player-card'}
<Select.Item value="full" label="Full (4 Stats)" />
<Select.Item value="compact" label="Compact (2 Stats)" />
<Select.Item value="banner" label="Banner (Profile & Level Only)" />
<Select.Item value="minimal" label="Minimal (Pill)" />
{:else} {:else}
<Select.Item value="full" label="Full" /> <Select.Item value="full" label="Full" />
<Select.Item value="compact" label="Compact" /> <Select.Item value="compact" label="Compact" />
@@ -2382,6 +2401,33 @@
</Select.Root> </Select.Root>
</div> </div>
{#if element.widget === 'player-card' && element.variant !== 'minimal'}
<div class="space-y-2 border-t border-theme-800 pt-2">
<Label class={label}>Player Card Options</Label>
<div class="space-y-2">
<div class="flex items-center gap-2">
<Checkbox
id="element-player-stats"
checked={element.showStats !== false && element.variant !== 'banner'}
onCheckedChange={(checked) => {
element.showStats = checked === true;
if (!checked) {
element.variant = 'banner';
element.height = Math.min(element.height, 80);
} else {
element.variant = 'full';
element.height = Math.max(element.height, 220);
}
}}
/>
<Label for="element-player-stats" class="text-xs text-theme-100 cursor-pointer">
Show stats grid (rank, pp, accuracy, plays)
</Label>
</div>
</div>
</div>
{/if}
{#if element.widget === 'avatar'} {#if element.widget === 'avatar'}
<div class="space-y-2 border-t border-theme-800 pt-2"> <div class="space-y-2 border-t border-theme-800 pt-2">
<Label class={label}>Avatar Display Options</Label> <Label class={label}>Avatar Display Options</Label>
@@ -2027,7 +2027,7 @@
{#if element.variant === 'minimal'} {#if element.variant === 'minimal'}
<div <div
class="flex h-full w-full items-center gap-2.5 rounded-2xl border border-white/10 bg-black/55 px-3 py-2 text-theme-50 shadow-xl backdrop-blur-md" class="flex h-full w-full items-center gap-2.5 overflow-hidden rounded-2xl border border-white/10 bg-black/55 px-3 py-2 text-theme-50 shadow-xl backdrop-blur-md"
> >
<div class="relative shrink-0"> <div class="relative shrink-0">
<Avatar.Root <Avatar.Root
@@ -2067,7 +2067,7 @@
</div> </div>
{:else if element.variant === 'compact'} {:else if element.variant === 'compact'}
<div <div
class="flex h-full w-full flex-col justify-between rounded-2xl border border-white/10 bg-black/55 p-3 text-theme-50 shadow-xl backdrop-blur-md" class="flex h-full w-full flex-col justify-between overflow-hidden rounded-2xl border border-white/10 bg-black/55 p-3 text-theme-50 shadow-xl backdrop-blur-md"
> >
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<div class="relative shrink-0"> <div class="relative shrink-0">
@@ -2125,9 +2125,56 @@
</div> </div>
</div> </div>
</div> </div>
{:else if element.variant === 'banner' || element.showStats === false || element.height < 140}
<div
class="flex h-full w-full items-center gap-3 overflow-hidden rounded-2xl border border-white/10 bg-black/55 p-3.5 text-theme-50 shadow-xl backdrop-blur-md"
>
<div class="relative shrink-0">
<Avatar.Root
class="size-12 rounded-xl border border-white/15 bg-theme-900 shadow-md"
>
<Avatar.Image
src="https://a.ez-pp.farm/{playerId}"
alt={playerName}
class="h-full w-full object-cover"
/>
<Avatar.Fallback
class="flex h-full w-full items-center justify-center bg-theme-900 text-primary"
>
<ThemeIcon name="user" set={iconSet} size={22} />
</Avatar.Fallback>
</Avatar.Root>
{#if country}
<span
class="absolute -bottom-1 -right-1 flex items-center justify-center rounded-md border border-white/20 bg-black/90 px-1 py-0.5 text-[10px] leading-none shadow"
title={country.toUpperCase()}
>
{countryCodeToEmoji(country)}
</span>
{/if}
</div>
<div class="min-w-0 flex flex-col justify-center flex-1">
<span class="block truncate text-sm font-bold text-white">{playerName}</span>
<div class="flex items-center gap-2 mt-1">
<span class="text-[11px] font-semibold text-white/80 shrink-0">
Lv.{levelVal}
</span>
<div class="w-20 rounded-full bg-white/10 h-1.5 overflow-hidden">
<div
class="h-full bg-primary rounded-full transition-all"
style="width: {levelProgress}%"
></div>
</div>
<span class="text-[10px] text-muted-foreground font-mono">
{levelProgress}%
</span>
</div>
</div>
</div>
{:else} {:else}
<div <div
class="flex h-full w-full flex-col justify-between rounded-2xl border border-white/10 bg-black/55 p-3.5 text-theme-50 shadow-xl backdrop-blur-md" class="flex h-full w-full flex-col justify-between overflow-hidden rounded-2xl border border-white/10 bg-black/55 p-3.5 text-theme-50 shadow-xl backdrop-blur-md"
> >
<div class="flex items-center justify-between gap-3"> <div class="flex items-center justify-between gap-3">
<div class="flex items-center gap-3 min-w-0"> <div class="flex items-center gap-3 min-w-0">
@@ -2514,7 +2561,21 @@
<span <span
class="absolute -top-7 left-0 rounded bg-primary px-1.5 py-0.5 text-[10px] font-semibold text-primary-foreground shadow" class="absolute -top-7 left-0 rounded bg-primary px-1.5 py-0.5 text-[10px] font-semibold text-primary-foreground shadow"
> >
{element.type} {element.type === 'widget'
? element.widget === 'player-card'
? 'Player Card'
: element.widget === 'recent-activity'
? 'Recent Activity'
: element.widget === 'launch-button'
? 'Launch Button'
: element.widget === 'telemetry'
? 'Telemetry Bar'
: element.widget === 'header'
? 'Header Title'
: element.widget === 'avatar'
? 'Player Avatar'
: 'Widget'
: element.type}
</span> </span>
{/if} {/if}
+39 -34
View File
@@ -1662,7 +1662,7 @@ const drawWidgetElement = async (context: PreviewContext, element: ThemeV2Widget
ctx.fillText(plays, cell2X + cellW - 8, bottomY + 14); ctx.fillText(plays, cell2X + cellW - 8, bottomY + 14);
ctx.restore(); ctx.restore();
} else { } else {
// full // full or banner
fillRoundedRect( fillRoundedRect(
ctx, ctx,
element.x, element.x,
@@ -1683,9 +1683,12 @@ const drawWidgetElement = async (context: PreviewContext, element: ThemeV2Widget
1 1
); );
const avatarSize = 48; const showStats =
element.variant !== 'banner' && element.showStats !== false && element.height >= 140;
const isBanner = element.variant === 'banner' || !showStats;
const avatarSize = isBanner ? Math.min(48, Math.max(32, element.height - 24)) : 48;
const avatarX = element.x + 16; const avatarX = element.x + 16;
const avatarY = element.y + 16; const avatarY = isBanner ? element.y + (element.height - avatarSize) / 2 : element.y + 16;
fillRoundedRect(ctx, avatarX, avatarY, avatarSize, avatarSize, 12, 'rgba(20, 20, 28, 0.9)'); fillRoundedRect(ctx, avatarX, avatarY, avatarSize, avatarSize, 12, 'rgba(20, 20, 28, 0.9)');
strokeRoundedRect( strokeRoundedRect(
ctx, ctx,
@@ -1730,42 +1733,44 @@ const drawWidgetElement = async (context: PreviewContext, element: ThemeV2Widget
ctx.font = '500 10px monospace'; ctx.font = '500 10px monospace';
ctx.fillText(`${levelProgress}%`, barX + barW + 8, barY - 2); ctx.fillText(`${levelProgress}%`, barX + barW + 8, barY - 2);
const divY = avatarY + avatarSize + 16; if (showStats) {
ctx.strokeStyle = 'rgba(255, 255, 255, 0.06)'; const divY = avatarY + avatarSize + 16;
ctx.beginPath(); ctx.strokeStyle = 'rgba(255, 255, 255, 0.06)';
ctx.moveTo(element.x + 16, divY); ctx.beginPath();
ctx.lineTo(element.x + element.width - 16, divY); ctx.moveTo(element.x + 16, divY);
ctx.stroke(); ctx.lineTo(element.x + element.width - 16, divY);
ctx.stroke();
const gridY = divY + 12; const gridY = divY + 12;
const gap = 8; const gap = 8;
const cellW = (element.width - 32 - gap) / 2; const cellW = (element.width - 32 - gap) / 2;
const cellH = Math.max(34, (element.height - (gridY - element.y) - 14 - gap) / 2); const cellH = Math.max(34, (element.height - (gridY - element.y) - 14 - gap) / 2);
const stats = [ const stats = [
{ label: 'Rank', value: rank, valueColor: '#ffffff' }, { label: 'Rank', value: rank, valueColor: '#ffffff' },
{ label: 'Performance', value: pp, valueColor: accent }, { label: 'Performance', value: pp, valueColor: accent },
{ label: 'Accuracy', value: acc, valueColor: '#ffffff' }, { label: 'Accuracy', value: acc, valueColor: '#ffffff' },
{ label: 'Plays', value: plays, valueColor: '#ffffff' }, { label: 'Plays', value: plays, valueColor: '#ffffff' },
]; ];
stats.forEach((st, idx) => { stats.forEach((st, idx) => {
const col = idx % 2; const col = idx % 2;
const row = Math.floor(idx / 2); const row = Math.floor(idx / 2);
const cx = element.x + 16 + col * (cellW + gap); const cx = element.x + 16 + col * (cellW + gap);
const cy = gridY + row * (cellH + gap); const cy = gridY + row * (cellH + gap);
fillRoundedRect(ctx, cx, cy, cellW, cellH, 10, 'rgba(255, 255, 255, 0.05)'); fillRoundedRect(ctx, cx, cy, cellW, cellH, 10, 'rgba(255, 255, 255, 0.05)');
ctx.fillStyle = 'rgba(255, 255, 255, 0.5)'; ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
ctx.font = canvasFont(context, bodyFont, 9, 500); ctx.font = canvasFont(context, bodyFont, 9, 500);
ctx.textAlign = 'left'; ctx.textAlign = 'left';
ctx.textBaseline = 'top'; ctx.textBaseline = 'top';
ctx.fillText(st.label, cx + 10, cy + 6); ctx.fillText(st.label, cx + 10, cy + 6);
ctx.fillStyle = st.valueColor; ctx.fillStyle = st.valueColor;
ctx.font = canvasFont(context, bodyFont, 12, 700); ctx.font = canvasFont(context, bodyFont, 12, 700);
ctx.fillText(st.value, cx + 10, cy + cellH - 18); ctx.fillText(st.value, cx + 10, cy + cellH - 18);
}); });
}
ctx.restore(); ctx.restore();
} }
} else if (element.widget === 'recent-activity') { } else if (element.widget === 'recent-activity') {
+6 -1
View File
@@ -534,7 +534,8 @@ export type ThemeV2WidgetVariant =
| 'icon-only' | 'icon-only'
| 'circle' | 'circle'
| 'rounded' | 'rounded'
| 'square'; | 'square'
| 'banner';
export type ThemeV2WidgetElement = ThemeV2ElementBase & { export type ThemeV2WidgetElement = ThemeV2ElementBase & {
type: 'widget'; type: 'widget';
@@ -543,6 +544,7 @@ export type ThemeV2WidgetElement = ThemeV2ElementBase & {
showCountry?: boolean; showCountry?: boolean;
showLevel?: boolean; showLevel?: boolean;
showOnlineRing?: boolean; showOnlineRing?: boolean;
showStats?: boolean;
}; };
export type ThemeV2Element = export type ThemeV2Element =
@@ -1262,6 +1264,7 @@ const normalizeElement = (raw: unknown, index: number): ThemeV2Element | undefin
'circle', 'circle',
'rounded', 'rounded',
'square', 'square',
'banner',
] as const, ] as const,
widget === 'launch-button' ? 'primary' : widget === 'avatar' ? 'circle' : 'full' widget === 'launch-button' ? 'primary' : widget === 'avatar' ? 'circle' : 'full'
); );
@@ -1269,6 +1272,7 @@ const normalizeElement = (raw: unknown, index: number): ThemeV2Element | undefin
const showLevel = typeof source.showLevel === 'boolean' ? source.showLevel : false; const showLevel = typeof source.showLevel === 'boolean' ? source.showLevel : false;
const showOnlineRing = const showOnlineRing =
typeof source.showOnlineRing === 'boolean' ? source.showOnlineRing : true; typeof source.showOnlineRing === 'boolean' ? source.showOnlineRing : true;
const showStats = typeof source.showStats === 'boolean' ? source.showStats : true;
return { return {
...base, ...base,
type, type,
@@ -1277,6 +1281,7 @@ const normalizeElement = (raw: unknown, index: number): ThemeV2Element | undefin
showCountry, showCountry,
showLevel, showLevel,
showOnlineRing, showOnlineRing,
showStats,
}; };
} }