refactor(themes): align installed cards with the workshop layout

Installed theme cards now use the same visual hierarchy as workshop cards: status and API badges at the top, version on the preview, an author strip over the artwork, a compact title and description, and a footer with status plus compact actions. This removes the oversized primary button and duplicate version/author rows.
This commit is contained in:
HorizonCode committed 2026-11-03 23:13:44 +01:00
1 parent 47c022642f
commit 760bd9a283
1 file changed
+203 -173
+203 -173
View File
@@ -4012,17 +4012,37 @@
<div <div
class="pointer-events-none absolute inset-0 bg-gradient-to-t from-theme-950/75 via-transparent to-transparent" class="pointer-events-none absolute inset-0 bg-gradient-to-t from-theme-950/75 via-transparent to-transparent"
></div> ></div>
{#if isActive} <div
<Badge class="absolute top-1.5 left-1.5 text-[9px]">In use</Badge> class="absolute top-1.5 left-1.5 flex max-w-[calc(100%-4.5rem)] flex-wrap items-center gap-1"
{/if} >
{#if theme.name !== 'Default' && theme.folder_name !== 'default' && theme.apiVersion} {#if isActive}
<Badge <Badge class="text-[9px]">In use</Badge>
variant="secondary" {/if}
class="absolute top-1.5 right-1.5 bg-black/50 px-1.5 py-0 text-[9px] backdrop-blur-sm" {#if theme.name !== 'Default' && theme.folder_name !== 'default' && theme.apiVersion}
<Badge
variant="secondary"
class="bg-black/50 px-1.5 py-0 text-[9px] backdrop-blur-sm"
>
API {theme.apiVersion.startsWith('2') ? '2.0' : '1.0'}
</Badge>
{/if}
</div>
<div class="absolute top-1.5 right-1.5 flex items-center gap-1">
{#if theme.nsfw}
<Badge
variant="destructive"
class="px-1.5 py-0 text-[9px]"
title="Adult content"
>
18+
</Badge>
{/if}
<span
class="rounded-md bg-black/60 px-1.5 py-0.5 text-[9px] font-medium text-white/90 backdrop-blur-sm"
> >
API {theme.apiVersion.startsWith('2') ? '2.0' : '1.0'} v{theme.version}
</Badge> </span>
{/if} </div>
{#if theme.nsfw && !revealedNsfw.includes(theme.name)} {#if theme.nsfw && !revealedNsfw.includes(theme.name)}
<button <button
type="button" type="button"
@@ -4038,194 +4058,204 @@
{:else if theme.nsfw} {:else if theme.nsfw}
<button <button
type="button" type="button"
class="absolute bottom-1.5 left-1.5 z-10 flex items-center gap-1 rounded-full bg-black/60 px-1.5 py-0.5 text-[8px] font-medium text-white/80 backdrop-blur-sm hover:bg-black/75 hover:text-white" class="absolute right-1.5 bottom-10 z-10 flex items-center gap-1 rounded-full bg-black/60 px-1.5 py-0.5 text-[8px] font-medium text-white/80 backdrop-blur-sm hover:bg-black/75 hover:text-white"
onclick={() => toggleNsfwPreview(theme.name)} onclick={() => toggleNsfwPreview(theme.name)}
> >
<ThemeIcon name="eye-off" set={iconSet} class="size-3" /> <ThemeIcon name="eye-off" set={iconSet} class="size-3" />
Hide preview Hide preview
</button> </button>
{/if} {/if}
{#if theme.nsfw} <div
<Badge class="absolute right-2 bottom-2 left-2 flex items-center justify-between gap-2"
variant="destructive" >
class="absolute right-1.5 bottom-1.5 px-1.5 py-0 text-[9px]" <div class="flex min-w-0 items-center gap-1.5">
title="Adult content"
>
18+
</Badge>
{/if}
</div>
<div class="flex flex-1 flex-col gap-1.5 p-2.5">
<div class="flex items-start justify-between gap-1.5">
<div class="min-w-0">
<span <span
class="block truncate text-[13px] leading-tight font-semibold" class="flex size-5 shrink-0 items-center justify-center rounded-full border border-white/20 bg-theme-800 text-[9px] font-bold text-white/80 uppercase"
> >
{theme.name} {theme.author.trim().charAt(0) || '?'}
</span> </span>
<span <span
class="mt-0.5 block truncate text-[10px] text-muted-foreground" class="truncate text-[11px] font-semibold text-white/95 drop-shadow-sm"
> >
by {theme.author} {theme.author || 'Unknown author'}
</span> </span>
</div> </div>
<span <span
class="shrink-0 rounded-full bg-theme-800/50 px-1.5 py-0.5 text-[9px] whitespace-nowrap tabular-nums text-muted-foreground" class="shrink-0 text-[9px] {theme.updateAvailable
? 'text-primary'
: 'text-muted-foreground/90'}"
> >
{#if theme.updateAvailable && theme.availableVersion} {theme.updateAvailable ? 'Update available' : 'Installed'}
v{theme.version} <span class="text-primary">→</span>
v{theme.availableVersion}
{:else}
v{theme.version}
{/if}
</span> </span>
</div> </div>
<p </div>
class="min-h-8 line-clamp-2 text-[10px] leading-snug text-muted-foreground" <div class="flex flex-1 flex-col gap-2 p-3">
> <h3 class="truncate text-[13px] font-bold text-theme-50">
{theme.description} {theme.name}
</p> </h3>
<div class="mt-auto flex items-center gap-1 pt-0.5"> {#if theme.description}
<Button <p
class="relative h-8 min-w-0 flex-1 overflow-hidden px-2 text-xs {theme.status === class="line-clamp-2 text-[10px] leading-relaxed text-muted-foreground"
'downloading' || theme.status === 'extracting'
? 'disabled:bg-primary disabled:opacity-100'
: ''}"
size="sm"
aria-busy={theme.status === 'downloading' ||
theme.status === 'extracting'}
disabled={isActive ||
theme.status === 'downloading' ||
theme.status === 'extracting' ||
theme.status === 'deleting'}
onclick={() => requestThemeApply(theme)}
> >
{#if theme.status === 'downloading' || theme.status === 'extracting'} {theme.description}
<span </p>
class="absolute inset-y-0 left-0 bg-white/20 transition-[width] duration-300 ease-out" {/if}
style={`width: ${Math.min(100, Math.max(0, theme.progress * 100))}%`} <div
></span> class="mt-auto flex items-center justify-between gap-2 border-t border-theme-800/40 pt-2.5"
{#if !$reduceAnimations} >
<span <span class="min-w-0 truncate text-[9px] text-muted-foreground">
class="theme-download-shimmer absolute inset-y-0 left-0 w-1/2 bg-gradient-to-r from-transparent via-white/25 to-transparent" {#if theme.updateAvailable && theme.availableVersion}
></span> <span class="text-primary"
{/if} >v{theme.availableVersion} available</span
{/if}
<span
class="relative z-10 flex items-center justify-center gap-2"
>
{#if theme.status === 'extracting'}
<ThemeIcon
name="loader"
set={iconSet}
class="size-4 animate-spin"
/>
{/if}
<TextMorph
text={themeButtonLabel(
theme,
$active_custom_theme?.folder_name
)}
duration={260}
ease="cubic-bezier(0.19, 1, 0.22, 1)"
disabled={$reduceAnimations}
class="whitespace-nowrap"
/>
</span>
</Button>
{#if hasThemeMenu}
<DropdownMenu.Root>
<DropdownMenu.Trigger>
{#snippet child({ props })}
<Button
{...props}
variant="secondary"
size="icon"
class="size-7 shrink-0 text-muted-foreground hover:text-white"
title="More theme actions"
aria-label={`More actions for ${theme.name}`}
>
<Ellipsis class="size-3.5" />
</Button>
{/snippet}
</DropdownMenu.Trigger>
<DropdownMenu.Content
align="end"
class="w-48 border-theme-700 bg-theme-950 text-theme-50"
> >
{#if canEditTheme} {:else if isActive}
<DropdownMenu.Item Active theme
class="gap-2 text-[11px]" {:else}
onclick={() => openThemeEditor(theme)} Installed
> {/if}
<ThemeIcon name="edit" set={iconSet} class="size-3.5" /> </span>
Edit theme <div class="flex shrink-0 items-center gap-1">
</DropdownMenu.Item> <Button
class="relative h-7 min-w-0 max-w-44 overflow-hidden px-2.5 text-xs {theme.status ===
'downloading' || theme.status === 'extracting'
? 'disabled:bg-primary disabled:opacity-100'
: ''}"
size="sm"
aria-busy={theme.status === 'downloading' ||
theme.status === 'extracting'}
disabled={isActive ||
theme.status === 'downloading' ||
theme.status === 'extracting' ||
theme.status === 'deleting'}
onclick={() => requestThemeApply(theme)}
>
{#if theme.status === 'downloading' || theme.status === 'extracting'}
<span
class="absolute inset-y-0 left-0 bg-white/20 transition-[width] duration-300 ease-out"
style={`width: ${Math.min(100, Math.max(0, theme.progress * 100))}%`}
></span>
{#if !$reduceAnimations}
<span
class="theme-download-shimmer absolute inset-y-0 left-0 w-1/2 bg-gradient-to-r from-transparent via-white/25 to-transparent"
></span>
{/if} {/if}
{#if canRestoreTheme} {/if}
<DropdownMenu.Item <span
class="gap-2 text-[11px]" class="relative z-10 flex items-center justify-center gap-2"
onclick={() => >
requestThemeRestore(theme, theme.backups![0])} {#if theme.status === 'extracting'}
> <ThemeIcon
<ThemeIcon name="loader"
name="history" set={iconSet}
set={iconSet} class="size-4 animate-spin"
class="size-3.5" />
/>
Restore previous version
</DropdownMenu.Item>
{/if} {/if}
{#if canUpdateTheme} <TextMorph
<DropdownMenu.Item text={themeButtonLabel(
class="gap-2 text-[11px]" theme,
onclick={() => updateInstalledTheme(theme)} $active_custom_theme?.folder_name
> )}
<ThemeIcon duration={260}
name="download" ease="cubic-bezier(0.19, 1, 0.22, 1)"
set={iconSet} disabled={$reduceAnimations}
class="size-3.5" class="whitespace-nowrap"
/> />
Update theme </span>
</DropdownMenu.Item> </Button>
{/if} {#if hasThemeMenu}
{#if canReportTheme} <DropdownMenu.Root>
<DropdownMenu.Item <DropdownMenu.Trigger>
class="gap-2 text-[11px]" {#snippet child({ props })}
disabled={installedReportLoading} <Button
onclick={() => void openInstalledReport(theme)} {...props}
> variant="secondary"
<Flag class="size-3.5" /> size="icon"
Report theme class="size-7 shrink-0 text-muted-foreground hover:text-white"
</DropdownMenu.Item> title="More theme actions"
{/if} aria-label={`More actions for ${theme.name}`}
{#if canUninstallTheme} >
<DropdownMenu.Separator class="my-1 bg-theme-800" /> <Ellipsis class="size-3.5" />
<DropdownMenu.Item </Button>
class="gap-2 text-[11px] text-red-300 data-[highlighted]:bg-red-500/15 data-[highlighted]:text-red-200" {/snippet}
disabled={theme.status !== 'installed'} </DropdownMenu.Trigger>
onclick={() => void uninstallInstalledTheme(theme)} <DropdownMenu.Content
> align="end"
{#if theme.status === 'deleting'} class="w-48 border-theme-700 bg-theme-950 text-theme-50"
>
{#if canEditTheme}
<DropdownMenu.Item
class="gap-2 text-[11px]"
onclick={() => openThemeEditor(theme)}
>
<ThemeIcon name="edit" set={iconSet} class="size-3.5" />
Edit theme
</DropdownMenu.Item>
{/if}
{#if canRestoreTheme}
<DropdownMenu.Item
class="gap-2 text-[11px]"
onclick={() =>
requestThemeRestore(theme, theme.backups![0])}
>
<ThemeIcon <ThemeIcon
name="loader" name="history"
set={iconSet}
class="size-3.5 animate-spin"
/>
Uninstalling...
{:else}
<ThemeIcon
name="trash"
set={iconSet} set={iconSet}
class="size-3.5" class="size-3.5"
/> />
Uninstall theme Restore previous version
{/if} </DropdownMenu.Item>
</DropdownMenu.Item> {/if}
{/if} {#if canUpdateTheme}
</DropdownMenu.Content> <DropdownMenu.Item
</DropdownMenu.Root> class="gap-2 text-[11px]"
{/if} onclick={() => updateInstalledTheme(theme)}
>
<ThemeIcon
name="download"
set={iconSet}
class="size-3.5"
/>
Update theme
</DropdownMenu.Item>
{/if}
{#if canReportTheme}
<DropdownMenu.Item
class="gap-2 text-[11px]"
disabled={installedReportLoading}
onclick={() => void openInstalledReport(theme)}
>
<Flag class="size-3.5" />
Report theme
</DropdownMenu.Item>
{/if}
{#if canUninstallTheme}
<DropdownMenu.Separator class="my-1 bg-theme-800" />
<DropdownMenu.Item
class="gap-2 text-[11px] text-red-300 data-[highlighted]:bg-red-500/15 data-[highlighted]:text-red-200"
disabled={theme.status !== 'installed'}
onclick={() => void uninstallInstalledTheme(theme)}
>
{#if theme.status === 'deleting'}
<ThemeIcon
name="loader"
set={iconSet}
class="size-3.5 animate-spin"
/>
Uninstalling...
{:else}
<ThemeIcon
name="trash"
set={iconSet}
class="size-3.5"
/>
Uninstall theme
{/if}
</DropdownMenu.Item>
{/if}
</DropdownMenu.Content>
</DropdownMenu.Root>
{/if}
</div>
</div> </div>
</div> </div>
</div> </div>