fix(theme-editor): fix player card widget overflow, add banner variant, and improve widget selection badge
This commit is contained in:
1 parent
a025f9b285
commit
ad3fe7c586
4 files changed
+159
-42
No files matched your search
@@ -2231,7 +2231,7 @@
|
||||
element.widget = val as ThemeV2WidgetKind;
|
||||
if (val === 'player-card') {
|
||||
element.width = 380;
|
||||
element.height = 180;
|
||||
element.height = 220;
|
||||
} else if (val === 'avatar') {
|
||||
element.width = 72;
|
||||
element.height = 72;
|
||||
@@ -2296,12 +2296,18 @@
|
||||
if (val === 'minimal') {
|
||||
element.width = 280;
|
||||
element.height = 64;
|
||||
} else if (val === 'banner') {
|
||||
element.width = 340;
|
||||
element.height = 76;
|
||||
element.showStats = false;
|
||||
} else if (val === 'compact') {
|
||||
element.width = 340;
|
||||
element.height = 120;
|
||||
element.height = 140;
|
||||
element.showStats = true;
|
||||
} else if (val === 'full') {
|
||||
element.width = 380;
|
||||
element.height = 200;
|
||||
element.height = 220;
|
||||
element.showStats = true;
|
||||
}
|
||||
} else if (element.widget === 'recent-activity') {
|
||||
if (val === 'minimal') {
|
||||
@@ -2356,6 +2362,14 @@
|
||||
: element.variant === 'icon-only'
|
||||
? 'Icon-Only (Circle)'
|
||||
: '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}
|
||||
{element.variant === 'compact'
|
||||
? 'Compact'
|
||||
@@ -2373,6 +2387,11 @@
|
||||
<Select.Item value="primary" label="Primary (Capsule)" />
|
||||
<Select.Item value="compact" label="Compact (Pill)" />
|
||||
<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}
|
||||
<Select.Item value="full" label="Full" />
|
||||
<Select.Item value="compact" label="Compact" />
|
||||
@@ -2382,6 +2401,33 @@
|
||||
</Select.Root>
|
||||
</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'}
|
||||
<div class="space-y-2 border-t border-theme-800 pt-2">
|
||||
<Label class={label}>Avatar Display Options</Label>
|
||||
|
||||
@@ -2027,7 +2027,7 @@
|
||||
|
||||
{#if element.variant === 'minimal'}
|
||||
<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">
|
||||
<Avatar.Root
|
||||
@@ -2067,7 +2067,7 @@
|
||||
</div>
|
||||
{:else if element.variant === 'compact'}
|
||||
<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="relative shrink-0">
|
||||
@@ -2125,9 +2125,56 @@
|
||||
</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}
|
||||
<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 gap-3 min-w-0">
|
||||
@@ -2514,7 +2561,21 @@
|
||||
<span
|
||||
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>
|
||||
{/if}
|
||||
|
||||
|
||||
+39
-34
@@ -1662,7 +1662,7 @@ const drawWidgetElement = async (context: PreviewContext, element: ThemeV2Widget
|
||||
ctx.fillText(plays, cell2X + cellW - 8, bottomY + 14);
|
||||
ctx.restore();
|
||||
} else {
|
||||
// full
|
||||
// full or banner
|
||||
fillRoundedRect(
|
||||
ctx,
|
||||
element.x,
|
||||
@@ -1683,9 +1683,12 @@ const drawWidgetElement = async (context: PreviewContext, element: ThemeV2Widget
|
||||
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 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)');
|
||||
strokeRoundedRect(
|
||||
ctx,
|
||||
@@ -1730,42 +1733,44 @@ const drawWidgetElement = async (context: PreviewContext, element: ThemeV2Widget
|
||||
ctx.font = '500 10px monospace';
|
||||
ctx.fillText(`${levelProgress}%`, barX + barW + 8, barY - 2);
|
||||
|
||||
const divY = avatarY + avatarSize + 16;
|
||||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.06)';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(element.x + 16, divY);
|
||||
ctx.lineTo(element.x + element.width - 16, divY);
|
||||
ctx.stroke();
|
||||
if (showStats) {
|
||||
const divY = avatarY + avatarSize + 16;
|
||||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.06)';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(element.x + 16, divY);
|
||||
ctx.lineTo(element.x + element.width - 16, divY);
|
||||
ctx.stroke();
|
||||
|
||||
const gridY = divY + 12;
|
||||
const gap = 8;
|
||||
const cellW = (element.width - 32 - gap) / 2;
|
||||
const cellH = Math.max(34, (element.height - (gridY - element.y) - 14 - gap) / 2);
|
||||
const gridY = divY + 12;
|
||||
const gap = 8;
|
||||
const cellW = (element.width - 32 - gap) / 2;
|
||||
const cellH = Math.max(34, (element.height - (gridY - element.y) - 14 - gap) / 2);
|
||||
|
||||
const stats = [
|
||||
{ label: 'Rank', value: rank, valueColor: '#ffffff' },
|
||||
{ label: 'Performance', value: pp, valueColor: accent },
|
||||
{ label: 'Accuracy', value: acc, valueColor: '#ffffff' },
|
||||
{ label: 'Plays', value: plays, valueColor: '#ffffff' },
|
||||
];
|
||||
const stats = [
|
||||
{ label: 'Rank', value: rank, valueColor: '#ffffff' },
|
||||
{ label: 'Performance', value: pp, valueColor: accent },
|
||||
{ label: 'Accuracy', value: acc, valueColor: '#ffffff' },
|
||||
{ label: 'Plays', value: plays, valueColor: '#ffffff' },
|
||||
];
|
||||
|
||||
stats.forEach((st, idx) => {
|
||||
const col = idx % 2;
|
||||
const row = Math.floor(idx / 2);
|
||||
const cx = element.x + 16 + col * (cellW + gap);
|
||||
const cy = gridY + row * (cellH + gap);
|
||||
stats.forEach((st, idx) => {
|
||||
const col = idx % 2;
|
||||
const row = Math.floor(idx / 2);
|
||||
const cx = element.x + 16 + col * (cellW + gap);
|
||||
const cy = gridY + row * (cellH + gap);
|
||||
|
||||
fillRoundedRect(ctx, cx, cy, cellW, cellH, 10, 'rgba(255, 255, 255, 0.05)');
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
|
||||
ctx.font = canvasFont(context, bodyFont, 9, 500);
|
||||
ctx.textAlign = 'left';
|
||||
ctx.textBaseline = 'top';
|
||||
ctx.fillText(st.label, cx + 10, cy + 6);
|
||||
fillRoundedRect(ctx, cx, cy, cellW, cellH, 10, 'rgba(255, 255, 255, 0.05)');
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
|
||||
ctx.font = canvasFont(context, bodyFont, 9, 500);
|
||||
ctx.textAlign = 'left';
|
||||
ctx.textBaseline = 'top';
|
||||
ctx.fillText(st.label, cx + 10, cy + 6);
|
||||
|
||||
ctx.fillStyle = st.valueColor;
|
||||
ctx.font = canvasFont(context, bodyFont, 12, 700);
|
||||
ctx.fillText(st.value, cx + 10, cy + cellH - 18);
|
||||
});
|
||||
ctx.fillStyle = st.valueColor;
|
||||
ctx.font = canvasFont(context, bodyFont, 12, 700);
|
||||
ctx.fillText(st.value, cx + 10, cy + cellH - 18);
|
||||
});
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
} else if (element.widget === 'recent-activity') {
|
||||
|
||||
+6
-1
@@ -534,7 +534,8 @@ export type ThemeV2WidgetVariant =
|
||||
| 'icon-only'
|
||||
| 'circle'
|
||||
| 'rounded'
|
||||
| 'square';
|
||||
| 'square'
|
||||
| 'banner';
|
||||
|
||||
export type ThemeV2WidgetElement = ThemeV2ElementBase & {
|
||||
type: 'widget';
|
||||
@@ -543,6 +544,7 @@ export type ThemeV2WidgetElement = ThemeV2ElementBase & {
|
||||
showCountry?: boolean;
|
||||
showLevel?: boolean;
|
||||
showOnlineRing?: boolean;
|
||||
showStats?: boolean;
|
||||
};
|
||||
|
||||
export type ThemeV2Element =
|
||||
@@ -1262,6 +1264,7 @@ const normalizeElement = (raw: unknown, index: number): ThemeV2Element | undefin
|
||||
'circle',
|
||||
'rounded',
|
||||
'square',
|
||||
'banner',
|
||||
] as const,
|
||||
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 showOnlineRing =
|
||||
typeof source.showOnlineRing === 'boolean' ? source.showOnlineRing : true;
|
||||
const showStats = typeof source.showStats === 'boolean' ? source.showStats : true;
|
||||
return {
|
||||
...base,
|
||||
type,
|
||||
@@ -1277,6 +1281,7 @@ const normalizeElement = (raw: unknown, index: number): ThemeV2Element | undefin
|
||||
showCountry,
|
||||
showLevel,
|
||||
showOnlineRing,
|
||||
showStats,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user