feat(themes): redesign theme picker with gradient cards + beefy mode selector

Replace the shared ThemePage component inside the Themes workbench
panel with a custom compact layout better suited to the narrow
(~300px) panel context. ThemePage was designed for a full-width
desktop route and reads as noisy/overloaded in a panel.

Mode selector (Hell/Dunkel/System) — primary-fill active state with
white icon+text (was subtle shadow-sm that barely registered in dark
mode), fill-weight icons when active, equal-width pill buttons in a
shared muted container.

Theme cards (Option D — "Farbton-Karte") — swap the 2×5 overlapping
color-dot preview for a large 16:10 gradient (primary → secondary in
the effective mode), theme name overlaid bottom-left with text-shadow,
subtle dark-overlay at the bottom for readability, white check badge
in the corner when active, 2px primary border + glow ring for the
active state. Hover lifts the card 1px. Renders all 8 variants
(default + extended) in a uniform 2-column grid.

Wallpaper tabs (Farben/Bilder/Upload + scope toggle) — restyle via
scoped :global() overrides to match the mode selector: muted pill
container, primary-fill active state, muted-foreground inactive.
Previously these used .bg-surface + .shadow-sm which was nearly
invisible against the panel background.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-14 14:58:10 +02:00
parent b196e7782e
commit 3817111f80

View file

@ -1,29 +1,106 @@
<!--
Themes — Workbench-embedded theme picker with variant selection,
light/dark mode toggle, and wallpaper picker.
Themes — Workbench-embedded theme picker.
Custom layout tuned for the narrow panel (no shared ThemePage):
- Big gradient theme cards (primary → secondary) with overlay label
- Beefy mode selector (Hell / Dunkel / System)
- Wallpaper picker below in a 2-column grid
-->
<script lang="ts">
import { ThemePage } from '@mana/shared-theme-ui';
import { Sun, Moon, Desktop, Check } from '@mana/shared-icons';
import {
THEME_DEFINITIONS,
DEFAULT_THEME_VARIANTS,
EXTENDED_THEME_VARIANTS,
type ThemeVariant,
type ThemeMode,
} from '@mana/shared-theme';
import { theme } from '$lib/stores/theme';
import { wallpaperStore } from '$lib/stores/wallpaper.svelte';
import WallpaperPicker from '$lib/components/wallpaper/WallpaperPicker.svelte';
const modes: { id: ThemeMode; label: string; icon: typeof Sun }[] = [
{ id: 'light', label: 'Hell', icon: Sun },
{ id: 'dark', label: 'Dunkel', icon: Moon },
{ id: 'system', label: 'System', icon: Desktop },
];
/** Pick the light- or dark-mode palette based on the effective mode,
* so the preview card reflects what the app actually renders right
* now instead of a single canonical look. */
function paletteFor(variant: ThemeVariant) {
const def = THEME_DEFINITIONS[variant];
return theme.effectiveMode === 'dark' ? def.dark : def.light;
}
function gradientCss(variant: ThemeVariant): string {
const c = paletteFor(variant);
return `linear-gradient(135deg, hsl(${c.primary}) 0%, hsl(${c.secondary}) 100%)`;
}
let allVariants = $derived<ThemeVariant[]>([
...DEFAULT_THEME_VARIANTS,
...EXTENDED_THEME_VARIANTS,
]);
</script>
<div class="themes-page">
<ThemePage
currentVariant={theme.variant}
onSelectTheme={(v) => theme.setVariant(v)}
showModeSelector={true}
currentMode={theme.mode}
onModeChange={(m) => theme.setMode(m)}
showBackButton={false}
transparent={wallpaperStore.hasWallpaper}
>
<section class="wallpaper-section">
<h2 class="wallpaper-heading">Hintergrund</h2>
<WallpaperPicker />
</section>
</ThemePage>
<!-- ── Mode selector ──────────────────────────────────────────── -->
<section class="mode-section">
<h2 class="section-heading">Modus</h2>
<div class="mode-row" role="tablist">
{#each modes as m}
{@const isActive = theme.mode === m.id}
<button
type="button"
role="tab"
aria-selected={isActive}
class="mode-btn"
class:active={isActive}
onclick={() => theme.setMode(m.id)}
>
<m.icon size={16} weight={isActive ? 'fill' : 'regular'} />
<span>{m.label}</span>
</button>
{/each}
</div>
</section>
<!-- ── Theme cards ────────────────────────────────────────────── -->
<section class="themes-section">
<h2 class="section-heading">Aktuelles Theme</h2>
<div class="themes-grid">
{#each allVariants as variant (variant)}
{@const def = THEME_DEFINITIONS[variant]}
{@const isActive = theme.variant === variant}
<button
type="button"
class="theme-card"
class:active={isActive}
style:background={gradientCss(variant)}
onclick={() => theme.setVariant(variant)}
aria-label={def.label}
aria-pressed={isActive}
>
<!-- Active checkmark -->
{#if isActive}
<span class="active-check">
<Check size={12} weight="bold" />
</span>
{/if}
<!-- Theme name overlay -->
<span class="theme-label">{def.label}</span>
</button>
{/each}
</div>
</section>
<!-- ── Wallpaper section ──────────────────────────────────────── -->
<section class="wallpaper-section">
<h2 class="section-heading">Hintergrund</h2>
<WallpaperPicker />
</section>
</div>
<style>
@ -31,18 +108,172 @@
padding: 0.75rem;
height: 100%;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.section-heading {
font-size: 0.6875rem;
font-weight: 600;
color: hsl(var(--color-muted-foreground));
text-transform: uppercase;
letter-spacing: 0.06em;
margin: 0 0 0.625rem 0;
}
/* ── Mode selector ─────────────────────────────────────────── */
.mode-row {
display: flex;
gap: 0.25rem;
padding: 0.25rem;
background: hsl(var(--color-muted) / 0.3);
border-radius: 0.625rem;
}
.mode-btn {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 0.375rem;
padding: 0.5rem 0.625rem;
border: none;
background: transparent;
border-radius: 0.5rem;
font-size: 0.8125rem;
font-weight: 500;
color: hsl(var(--color-muted-foreground));
cursor: pointer;
transition:
background 0.15s,
color 0.15s,
box-shadow 0.15s;
}
.mode-btn:hover {
color: hsl(var(--color-foreground));
}
.mode-btn.active {
background: hsl(var(--color-primary));
color: hsl(var(--color-primary-foreground, 0 0% 100%));
box-shadow: 0 1px 3px hsl(0 0% 0% / 0.12);
}
.mode-btn.active:hover {
color: hsl(var(--color-primary-foreground, 0 0% 100%));
}
/* ── Theme cards ───────────────────────────────────────────── */
.themes-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.625rem;
}
.theme-card {
position: relative;
aspect-ratio: 16 / 10;
border: 1px solid hsl(var(--color-border));
border-radius: 0.75rem;
cursor: pointer;
overflow: hidden;
padding: 0;
transition:
transform 0.18s ease,
box-shadow 0.18s ease,
border-color 0.15s;
}
.theme-card:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px hsl(0 0% 0% / 0.12);
}
.theme-card.active {
border-color: hsl(var(--color-primary));
box-shadow:
0 0 0 2px hsl(var(--color-primary) / 0.35),
0 4px 12px hsl(0 0% 0% / 0.12);
}
/* Dark overlay at the bottom for label readability */
.theme-card::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(180deg, transparent 50%, hsl(0 0% 0% / 0.35) 100%);
pointer-events: none;
}
.active-check {
position: absolute;
top: 0.5rem;
right: 0.5rem;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border-radius: 50%;
background: hsl(0 0% 100%);
color: hsl(var(--color-primary));
box-shadow: 0 1px 4px hsl(0 0% 0% / 0.2);
}
.theme-label {
position: absolute;
left: 0.625rem;
bottom: 0.5rem;
z-index: 1;
font-size: 0.875rem;
font-weight: 700;
color: hsl(0 0% 100%);
text-shadow: 0 1px 3px hsl(0 0% 0% / 0.5);
letter-spacing: -0.01em;
}
/* ── Wallpaper section ─────────────────────────────────────── */
.wallpaper-section {
margin-top: 2rem;
padding-top: 2rem;
padding-top: 1rem;
border-top: 1px solid hsl(var(--color-border));
}
.wallpaper-heading {
font-size: 0.875rem;
font-weight: 500;
/* Force all WallpaperPicker grids to 2 columns in this panel context
(defaults are 4 and 3 which cram tiles in a narrow panel). */
.wallpaper-section :global(.grid.grid-cols-4),
.wallpaper-section :global(.grid.grid-cols-3) {
grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
gap: 0.625rem !important;
}
/* WallpaperPicker pill-group buttons (scope toggle + Farben/Bilder/
Upload tabs) — restyle to match the mode selector above so active
state reads clearly against the panel background. */
.wallpaper-section :global(.flex.rounded-lg.bg-muted) {
background: hsl(var(--color-muted) / 0.3);
}
.wallpaper-section :global(.flex.rounded-lg.bg-muted > button) {
color: hsl(var(--color-muted-foreground));
margin-bottom: 1rem;
transition:
background 0.15s,
color 0.15s,
box-shadow 0.15s;
}
.wallpaper-section :global(.flex.rounded-lg.bg-muted > button:hover) {
color: hsl(var(--color-foreground));
}
/* Active pill-group button — primary fill with white text, matching
the mode selector. Targets the button that carries .bg-surface
(applied via `class:bg-surface={activeTab === tab.id}`). */
.wallpaper-section :global(.flex.rounded-lg.bg-muted > button.bg-surface) {
background: hsl(var(--color-primary)) !important;
color: hsl(var(--color-primary-foreground, 0 0% 100%)) !important;
box-shadow: 0 1px 3px hsl(0 0% 0% / 0.12) !important;
}
</style>