feat(theme-v2): add detachable header widget, fix preview thumbnail rendering, and improve wineskin launch
- Support detaching title header to canvas with full, compact, and minimal variants - Render detached widgets and respect chrome layout configuration in theme preview generator - Fix macOS Wineskin launching by adding Frameworks to DYLD_FALLBACK_LIBRARY_PATH - Fix launch button interactive handling, scaling margins, and icon-only states - Improve player widget styling and variant display
This commit is contained in:
1 parent
386c829455
commit
7648d1fd4c
10 files changed
+4190
-488
No files matched your search
@@ -18,6 +18,8 @@
|
||||
import { getThemeDataSampleValue, resolveThemeDataValue, themeGlobalData } from '$lib/themeData';
|
||||
import type { ThemeFontChoice } from '$lib/themeFonts';
|
||||
import { DEFAULT_THEME_ICON_SET, type ThemeIconSet } from '$lib/themeIcons';
|
||||
import { openURL } from '$lib/utils';
|
||||
import { sileo } from 'sileo';
|
||||
import {
|
||||
THEME_DATA_FIELDS,
|
||||
THEME_PARTICLE_PRESETS,
|
||||
@@ -27,15 +29,20 @@
|
||||
type ThemeV2Element,
|
||||
type ThemeV2Group,
|
||||
type ThemeV2MaskShape,
|
||||
type ThemeV2WidgetKind,
|
||||
type ThemeV2WidgetVariant,
|
||||
} from '$lib/themeV2';
|
||||
import {
|
||||
CircleUser,
|
||||
Clock,
|
||||
Component,
|
||||
Film,
|
||||
Gauge,
|
||||
Image as ImageIcon,
|
||||
LayoutGrid,
|
||||
Megaphone,
|
||||
MousePointerClick,
|
||||
Play,
|
||||
RefreshCw,
|
||||
Repeat,
|
||||
Shapes,
|
||||
@@ -198,6 +205,12 @@
|
||||
<Clock class="size-4" />
|
||||
{:else if element.type === 'countdown'}
|
||||
<Timer class="size-4" />
|
||||
{:else if element.type === 'widget'}
|
||||
{#if element.widget === 'avatar'}
|
||||
<CircleUser class="size-4" />
|
||||
{:else}
|
||||
<LayoutGrid class="size-4" />
|
||||
{/if}
|
||||
{:else}
|
||||
<ImageIcon class="size-4" />
|
||||
{/if}
|
||||
@@ -1593,6 +1606,46 @@
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="space-y-1.5 pt-1">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="w-full gap-2 border-theme-700 bg-theme-900/60 text-xs text-theme-100 hover:bg-theme-800"
|
||||
onclick={() => {
|
||||
if (element.action.type === 'navigate') {
|
||||
sileo.info({
|
||||
title: 'Button Action Test',
|
||||
description: `Navigates to screen "${element.action.screen}"`,
|
||||
});
|
||||
} else if (element.action.type === 'launch') {
|
||||
sileo.info({
|
||||
title: 'Button Action Test',
|
||||
description: 'Launches osu!',
|
||||
});
|
||||
} else if (element.action.type === 'url') {
|
||||
if (element.action.url) {
|
||||
sileo.info({
|
||||
title: 'Button Action Test',
|
||||
description: `Opens ${element.action.url}`,
|
||||
});
|
||||
void openURL(element.action.url);
|
||||
} else {
|
||||
sileo.warning({
|
||||
title: 'Button Action Test',
|
||||
description: 'No URL specified',
|
||||
});
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Play class="size-3.5 text-primary" />
|
||||
Test Button Action
|
||||
</Button>
|
||||
<p class="text-[9px] leading-relaxed text-muted-foreground">
|
||||
In editor mode, canvas clicks select layers for dragging. Test actions using this button, or Shift+click on canvas.
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div class="space-y-1">
|
||||
<Label for="element-button-icon" class={label}>Icon</Label>
|
||||
@@ -2165,6 +2218,296 @@
|
||||
</div>
|
||||
</div>
|
||||
</ThemeSection>
|
||||
{:else if element.type === 'widget'}
|
||||
<ThemeSection title="Widget" contentClass="space-y-3 pt-2.5">
|
||||
<div class="space-y-1.5">
|
||||
<Label for="element-widget-type" class={label}>Widget Type</Label>
|
||||
<Select.Root
|
||||
type="single"
|
||||
value={element.widget}
|
||||
onValueChange={(val) => {
|
||||
if (val) {
|
||||
element.widget = val as ThemeV2WidgetKind;
|
||||
if (val === 'player-card') {
|
||||
element.width = 380;
|
||||
element.height = 180;
|
||||
} else if (val === 'avatar') {
|
||||
element.width = 72;
|
||||
element.height = 72;
|
||||
if (!element.variant) element.variant = 'circle';
|
||||
} else if (val === 'recent-activity') {
|
||||
element.width = 320;
|
||||
element.height = 180;
|
||||
} else if (val === 'telemetry') {
|
||||
element.width = 320;
|
||||
element.height = 42;
|
||||
} else if (val === 'header') {
|
||||
element.width = 320;
|
||||
element.height = 64;
|
||||
} else if (val === 'launch-button') {
|
||||
element.width = 200;
|
||||
element.height = 52;
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Select.Trigger
|
||||
id="element-widget-type"
|
||||
class="h-8 border-theme-800 bg-theme-950 text-xs text-theme-50"
|
||||
>
|
||||
{element.widget === 'launch-button'
|
||||
? 'Launch Button'
|
||||
: element.widget === 'player-card'
|
||||
? 'Player Card'
|
||||
: element.widget === 'avatar'
|
||||
? 'Player Avatar'
|
||||
: element.widget === 'recent-activity'
|
||||
? 'Recent Activity'
|
||||
: element.widget === 'telemetry'
|
||||
? 'Telemetry Bar'
|
||||
: 'Header Title'}
|
||||
</Select.Trigger>
|
||||
<Select.Content class="border-theme-800 bg-theme-950">
|
||||
<Select.Item value="launch-button" label="Launch Button" />
|
||||
<Select.Item value="player-card" label="Player Card" />
|
||||
<Select.Item value="avatar" label="Player Avatar" />
|
||||
<Select.Item value="recent-activity" label="Recent Activity" />
|
||||
<Select.Item value="telemetry" label="Telemetry Bar" />
|
||||
<Select.Item value="header" label="Header Title" />
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<Label for="element-widget-variant" class={label}>Variant</Label>
|
||||
<Select.Root
|
||||
type="single"
|
||||
value={element.variant ??
|
||||
(element.widget === 'avatar'
|
||||
? 'circle'
|
||||
: element.widget === 'launch-button'
|
||||
? 'primary'
|
||||
: 'full')}
|
||||
onValueChange={(val) => {
|
||||
if (val) {
|
||||
element.variant = val as ThemeV2WidgetVariant;
|
||||
if (element.widget === 'player-card') {
|
||||
if (val === 'minimal') {
|
||||
element.width = 280;
|
||||
element.height = 64;
|
||||
} else if (val === 'compact') {
|
||||
element.width = 340;
|
||||
element.height = 120;
|
||||
} else if (val === 'full') {
|
||||
element.width = 380;
|
||||
element.height = 200;
|
||||
}
|
||||
} else if (element.widget === 'recent-activity') {
|
||||
if (val === 'minimal') {
|
||||
element.width = 280;
|
||||
element.height = 64;
|
||||
} else if (val === 'compact') {
|
||||
element.width = 340;
|
||||
element.height = 120;
|
||||
} else if (val === 'full') {
|
||||
element.width = 340;
|
||||
element.height = 200;
|
||||
}
|
||||
} else if (element.widget === 'launch-button') {
|
||||
if (val === 'icon-only') {
|
||||
element.width = 48;
|
||||
element.height = 48;
|
||||
} else if (val === 'compact') {
|
||||
element.width = 140;
|
||||
element.height = 42;
|
||||
} else if (val === 'primary') {
|
||||
element.width = 210;
|
||||
element.height = 48;
|
||||
}
|
||||
} else if (element.widget === 'header') {
|
||||
if (val === 'minimal') {
|
||||
element.width = 240;
|
||||
element.height = 42;
|
||||
} else if (val === 'compact') {
|
||||
element.width = 380;
|
||||
element.height = 48;
|
||||
} else if (val === 'full') {
|
||||
element.width = 320;
|
||||
element.height = 64;
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Select.Trigger
|
||||
id="element-widget-variant"
|
||||
class="h-8 border-theme-800 bg-theme-950 text-xs text-theme-50"
|
||||
>
|
||||
{#if element.widget === 'avatar'}
|
||||
{element.variant === 'rounded'
|
||||
? 'Rounded Square'
|
||||
: element.variant === 'square'
|
||||
? 'Square'
|
||||
: 'Circle'}
|
||||
{:else if element.widget === 'launch-button'}
|
||||
{element.variant === 'compact'
|
||||
? 'Compact (Pill)'
|
||||
: element.variant === 'icon-only'
|
||||
? 'Icon-Only (Circle)'
|
||||
: 'Primary (Capsule)'}
|
||||
{:else}
|
||||
{element.variant === 'compact'
|
||||
? 'Compact'
|
||||
: element.variant === 'minimal' || element.variant === 'icon-only'
|
||||
? 'Minimal'
|
||||
: 'Full'}
|
||||
{/if}
|
||||
</Select.Trigger>
|
||||
<Select.Content class="border-theme-800 bg-theme-950">
|
||||
{#if element.widget === 'avatar'}
|
||||
<Select.Item value="circle" label="Circle" />
|
||||
<Select.Item value="rounded" label="Rounded Square" />
|
||||
<Select.Item value="square" label="Square" />
|
||||
{:else if element.widget === 'launch-button'}
|
||||
<Select.Item value="primary" label="Primary (Capsule)" />
|
||||
<Select.Item value="compact" label="Compact (Pill)" />
|
||||
<Select.Item value="icon-only" label="Icon-Only (Circle)" />
|
||||
{:else}
|
||||
<Select.Item value="full" label="Full" />
|
||||
<Select.Item value="compact" label="Compact" />
|
||||
<Select.Item value="minimal" label="Minimal" />
|
||||
{/if}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
|
||||
{#if element.widget === 'avatar'}
|
||||
<div class="space-y-2 border-t border-theme-800 pt-2">
|
||||
<Label class={label}>Avatar Display Options</Label>
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="element-avatar-country"
|
||||
checked={element.showCountry !== false}
|
||||
onCheckedChange={(checked) => (element.showCountry = checked === true)}
|
||||
/>
|
||||
<Label
|
||||
for="element-avatar-country"
|
||||
class="text-[11px] font-normal text-muted-foreground"
|
||||
>
|
||||
Show Country Flag Badge
|
||||
</Label>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="element-avatar-level"
|
||||
checked={element.showLevel !== false}
|
||||
onCheckedChange={(checked) => (element.showLevel = checked === true)}
|
||||
/>
|
||||
<Label
|
||||
for="element-avatar-level"
|
||||
class="text-[11px] font-normal text-muted-foreground"
|
||||
>
|
||||
Show Level Badge
|
||||
</Label>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="element-avatar-ring"
|
||||
checked={element.showOnlineRing !== false}
|
||||
onCheckedChange={(checked) => (element.showOnlineRing = checked === true)}
|
||||
/>
|
||||
<Label
|
||||
for="element-avatar-ring"
|
||||
class="text-[11px] font-normal text-muted-foreground"
|
||||
>
|
||||
Online Status Glow Ring
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Quick Snap Position Presets for Widgets -->
|
||||
<div class="space-y-1.5 border-t border-theme-800 pt-2">
|
||||
<Label class={label}>Quick Position Snaps</Label>
|
||||
<div class="grid grid-cols-2 gap-1">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="h-6 border-theme-800 bg-theme-900/40 text-[10px] hover:bg-theme-800"
|
||||
onclick={() => {
|
||||
element.x = 1000 - element.width - 24;
|
||||
element.y = 650 - element.height - 24;
|
||||
}}
|
||||
>
|
||||
↘ Bottom-Right
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="h-6 border-theme-800 bg-theme-900/40 text-[10px] hover:bg-theme-800"
|
||||
onclick={() => {
|
||||
element.x = Math.round((1000 - element.width) / 2);
|
||||
element.y = 650 - element.height - 24;
|
||||
}}
|
||||
>
|
||||
↓ Bottom-Center
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="h-6 border-theme-800 bg-theme-900/40 text-[10px] hover:bg-theme-800"
|
||||
onclick={() => {
|
||||
element.x = 24;
|
||||
element.y = 650 - element.height - 24;
|
||||
}}
|
||||
>
|
||||
↙ Bottom-Left
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="h-6 border-theme-800 bg-theme-900/40 text-[10px] hover:bg-theme-800"
|
||||
onclick={() => {
|
||||
element.x = 1000 - element.width - 24;
|
||||
element.y = 24;
|
||||
}}
|
||||
>
|
||||
↗ Top-Right
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="col-span-2 h-6 border-theme-800 bg-theme-900/40 text-[10px] hover:bg-theme-800"
|
||||
onclick={() => {
|
||||
element.x = Math.round((1000 - element.width) / 2);
|
||||
element.y = Math.round((650 - element.height) / 2);
|
||||
}}
|
||||
>
|
||||
⊙ Center on Canvas
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-[9px] leading-relaxed text-muted-foreground">
|
||||
{#if element.widget === 'launch-button'}
|
||||
Interactive game launch button. Satisfies the required Launch button condition.
|
||||
{:else if element.widget === 'player-card'}
|
||||
Live stats card displaying rank, pp, accuracy, level, and playcount.
|
||||
{:else if element.widget === 'avatar'}
|
||||
Player avatar with live profile image, shape styles, country badge, and online ring.
|
||||
{:else if element.widget === 'recent-activity'}
|
||||
Highlights the player's latest play, beatmap cover art, and grade badge.
|
||||
{:else if element.widget === 'telemetry'}
|
||||
Pill bar showing beatmap counts, skin counts, ping, and osu! build.
|
||||
{:else if element.widget === 'header'}
|
||||
Launcher title and dynamic greeting. Full shows title and subtitle, Compact shows an inline pill greeting, and Minimal displays title only.
|
||||
{:else}
|
||||
Launcher title and dynamic greeting.
|
||||
{/if}
|
||||
</p>
|
||||
</ThemeSection>
|
||||
{/if}
|
||||
|
||||
<ThemeSection title="Mask" contentClass="space-y-3 pt-2.5">
|
||||
|
||||
Reference in new issue
Block a user