mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-28 07:37:43 +02:00
feat(theme): add ThemePage components and distinct background colors
- Add unique background colors for each theme variant: - Lume: warm cream/gold tint - Nature: green tint in dark mode - Stone: blue-gray tint in dark mode - Ocean: blue tint in dark mode - Create shared-theme-ui components: - ThemeColorPreview: color circles preview component - ThemeCard: individual theme card with status support - ThemeGrid: responsive grid layout - ThemePage: full page component with mode selector - Integrate theme page in Chat app: - Add /themes route with ThemePage component - Add "🎨 Alle Themes" link to PillNavigation dropdown - Add palette icon to shared-ui icon set - Migrate Presi and Picture apps to shared-theme system - Update semantic color usage across all apps 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
129692812b
commit
54383bf7c2
92 changed files with 1793 additions and 1936 deletions
136
packages/shared-theme-ui/src/components/ThemeCard.svelte
Normal file
136
packages/shared-theme-ui/src/components/ThemeCard.svelte
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
<script lang="ts">
|
||||
import type { ThemeVariant } from '@manacore/shared-theme';
|
||||
import { THEME_DEFINITIONS } from '@manacore/shared-theme';
|
||||
import { Check, Lock, Clock, Star } from '@manacore/shared-icons';
|
||||
import type { ThemeStatus } from '../types';
|
||||
import ThemeColorPreview from './ThemeColorPreview.svelte';
|
||||
|
||||
interface Props {
|
||||
variant: ThemeVariant;
|
||||
isActive: boolean;
|
||||
status?: ThemeStatus;
|
||||
onClick?: () => void;
|
||||
onUnlock?: () => void;
|
||||
translations?: {
|
||||
locked?: string;
|
||||
comingSoon?: string;
|
||||
premium?: string;
|
||||
unlock?: string;
|
||||
lightPreview?: string;
|
||||
darkPreview?: string;
|
||||
};
|
||||
}
|
||||
|
||||
let {
|
||||
variant,
|
||||
isActive,
|
||||
status = 'available',
|
||||
onClick,
|
||||
onUnlock,
|
||||
translations = {},
|
||||
}: Props = $props();
|
||||
|
||||
const t = {
|
||||
locked: translations.locked ?? 'Gesperrt',
|
||||
comingSoon: translations.comingSoon ?? 'Bald verfügbar',
|
||||
premium: translations.premium ?? 'Premium',
|
||||
unlock: translations.unlock ?? 'Freischalten',
|
||||
lightPreview: translations.lightPreview ?? 'Hell',
|
||||
darkPreview: translations.darkPreview ?? 'Dunkel',
|
||||
};
|
||||
|
||||
const definition = $derived(THEME_DEFINITIONS[variant]);
|
||||
const isAvailable = $derived(status === 'available');
|
||||
const isLocked = $derived(status === 'locked');
|
||||
const isComingSoon = $derived(status === 'coming_soon');
|
||||
const isPremium = $derived(status === 'premium');
|
||||
|
||||
function handleClick() {
|
||||
if (isAvailable && onClick) {
|
||||
onClick();
|
||||
}
|
||||
}
|
||||
|
||||
function handleUnlock(e: MouseEvent) {
|
||||
e.stopPropagation();
|
||||
if (onUnlock) {
|
||||
onUnlock();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onclick={handleClick}
|
||||
disabled={!isAvailable}
|
||||
class="relative w-full p-4 rounded-xl border-2 transition-all text-left
|
||||
{isActive
|
||||
? 'border-primary bg-primary/5 ring-2 ring-primary/20'
|
||||
: 'border-border bg-surface hover:border-primary/50'}
|
||||
{!isAvailable ? 'opacity-60 cursor-not-allowed' : 'cursor-pointer'}
|
||||
{isPremium ? 'border-yellow-500/50' : ''}"
|
||||
>
|
||||
<!-- Premium badge -->
|
||||
{#if isPremium}
|
||||
<div
|
||||
class="absolute -top-2 -right-2 flex items-center gap-1 px-2 py-0.5
|
||||
bg-yellow-500 text-yellow-950 text-xs font-medium rounded-full"
|
||||
>
|
||||
<Star size={12} weight="fill" />
|
||||
{t.premium}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Active checkmark -->
|
||||
{#if isActive && isAvailable}
|
||||
<div
|
||||
class="absolute top-3 right-3 w-6 h-6 flex items-center justify-center
|
||||
bg-primary text-primary-foreground rounded-full"
|
||||
>
|
||||
<Check size={14} weight="bold" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<span class="text-xl">{definition.emoji}</span>
|
||||
<span class="font-semibold text-foreground">{definition.label}</span>
|
||||
</div>
|
||||
|
||||
<!-- Color previews -->
|
||||
<div class="space-y-2 mb-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs text-muted-foreground w-10">{t.lightPreview}</span>
|
||||
<ThemeColorPreview {variant} mode="light" size="sm" />
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs text-muted-foreground w-10">{t.darkPreview}</span>
|
||||
<ThemeColorPreview {variant} mode="dark" size="sm" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status badges -->
|
||||
{#if isLocked}
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-1 text-muted-foreground text-sm">
|
||||
<Lock size={14} weight="bold" />
|
||||
{t.locked}
|
||||
</div>
|
||||
{#if onUnlock}
|
||||
<button
|
||||
type="button"
|
||||
onclick={handleUnlock}
|
||||
class="px-2 py-1 text-xs font-medium text-primary bg-primary/10
|
||||
rounded hover:bg-primary/20 transition-colors"
|
||||
>
|
||||
{t.unlock}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if isComingSoon}
|
||||
<div class="flex items-center gap-1 text-muted-foreground text-sm">
|
||||
<Clock size={14} weight="bold" />
|
||||
{t.comingSoon}
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
<script lang="ts">
|
||||
import type { ThemeVariant } from '@manacore/shared-theme';
|
||||
import { THEME_DEFINITIONS } from '@manacore/shared-theme';
|
||||
|
||||
interface Props {
|
||||
variant: ThemeVariant;
|
||||
mode: 'light' | 'dark';
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
}
|
||||
|
||||
let { variant, mode, size = 'md' }: Props = $props();
|
||||
|
||||
const sizeClasses = {
|
||||
sm: 'w-4 h-4',
|
||||
md: 'w-5 h-5',
|
||||
lg: 'w-6 h-6',
|
||||
};
|
||||
|
||||
const gapClasses = {
|
||||
sm: '-ml-1.5',
|
||||
md: '-ml-2',
|
||||
lg: '-ml-2.5',
|
||||
};
|
||||
|
||||
const colors = $derived(() => {
|
||||
const def = THEME_DEFINITIONS[variant];
|
||||
const themeColors = mode === 'dark' ? def.dark : def.light;
|
||||
return [
|
||||
{ name: 'primary', value: themeColors.primary },
|
||||
{ name: 'secondary', value: themeColors.secondary },
|
||||
{ name: 'background', value: themeColors.background },
|
||||
{ name: 'surface', value: themeColors.surface },
|
||||
{ name: 'muted', value: themeColors.muted },
|
||||
];
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex items-center">
|
||||
{#each colors() as color, i}
|
||||
<div
|
||||
class="{sizeClasses[size]} rounded-full border border-white/20 shadow-sm {i > 0
|
||||
? gapClasses[size]
|
||||
: ''}"
|
||||
style="background-color: hsl({color.value}); z-index: {5 - i};"
|
||||
title={color.name}
|
||||
></div>
|
||||
{/each}
|
||||
</div>
|
||||
58
packages/shared-theme-ui/src/components/ThemeGrid.svelte
Normal file
58
packages/shared-theme-ui/src/components/ThemeGrid.svelte
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
<script lang="ts">
|
||||
import type { ThemeVariant } from '@manacore/shared-theme';
|
||||
import { THEME_VARIANTS } from '@manacore/shared-theme';
|
||||
import type { ThemeCardData, ThemePageTranslations } from '../types';
|
||||
import ThemeCard from './ThemeCard.svelte';
|
||||
|
||||
interface Props {
|
||||
currentVariant: ThemeVariant;
|
||||
onSelect: (variant: ThemeVariant) => void;
|
||||
themes?: ThemeCardData[];
|
||||
onUnlock?: (variant: ThemeVariant) => void;
|
||||
showLockedThemes?: boolean;
|
||||
translations?: Partial<ThemePageTranslations>;
|
||||
}
|
||||
|
||||
let {
|
||||
currentVariant,
|
||||
onSelect,
|
||||
themes,
|
||||
onUnlock,
|
||||
showLockedThemes = true,
|
||||
translations = {},
|
||||
}: Props = $props();
|
||||
|
||||
// Build theme data - use provided themes or create defaults from THEME_VARIANTS
|
||||
const themeData = $derived(() => {
|
||||
if (themes) {
|
||||
return showLockedThemes ? themes : themes.filter((t) => t.status === 'available');
|
||||
}
|
||||
// Default: all variants are available
|
||||
return THEME_VARIANTS.map((variant) => ({
|
||||
variant,
|
||||
status: 'available' as const,
|
||||
}));
|
||||
});
|
||||
|
||||
const cardTranslations = $derived({
|
||||
locked: translations.locked,
|
||||
comingSoon: translations.comingSoon,
|
||||
premium: translations.premium,
|
||||
unlock: translations.unlock,
|
||||
lightPreview: translations.lightPreview,
|
||||
darkPreview: translations.darkPreview,
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{#each themeData() as theme (theme.variant)}
|
||||
<ThemeCard
|
||||
variant={theme.variant}
|
||||
isActive={currentVariant === theme.variant}
|
||||
status={theme.status}
|
||||
onClick={() => onSelect(theme.variant)}
|
||||
onUnlock={onUnlock ? () => onUnlock(theme.variant) : undefined}
|
||||
translations={cardTranslations}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
@ -1,4 +1,21 @@
|
|||
// Theme UI Components
|
||||
// Theme UI Components (existing)
|
||||
export { default as ThemeToggle } from './ThemeToggle.svelte';
|
||||
export { default as ThemeSelector } from './ThemeSelector.svelte';
|
||||
export { default as ThemeModeSelector } from './ThemeModeSelector.svelte';
|
||||
|
||||
// New Components
|
||||
export { default as ThemeColorPreview } from './components/ThemeColorPreview.svelte';
|
||||
export { default as ThemeCard } from './components/ThemeCard.svelte';
|
||||
export { default as ThemeGrid } from './components/ThemeGrid.svelte';
|
||||
|
||||
// Pages
|
||||
export { default as ThemePage } from './pages/ThemePage.svelte';
|
||||
|
||||
// Types
|
||||
export type {
|
||||
ThemeStatus,
|
||||
ThemeCardData,
|
||||
ThemePageProps,
|
||||
ThemePageTranslations,
|
||||
} from './types';
|
||||
export { defaultTranslations } from './types';
|
||||
|
|
|
|||
125
packages/shared-theme-ui/src/pages/ThemePage.svelte
Normal file
125
packages/shared-theme-ui/src/pages/ThemePage.svelte
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
<script lang="ts">
|
||||
import type { ThemeVariant, ThemeMode } from '@manacore/shared-theme';
|
||||
import { ArrowLeft, Sun, Moon, Desktop } from '@manacore/shared-icons';
|
||||
import type { ThemeCardData, ThemePageTranslations } from '../types';
|
||||
import { defaultTranslations } from '../types';
|
||||
import ThemeGrid from '../components/ThemeGrid.svelte';
|
||||
|
||||
interface Props {
|
||||
// Theme Store Integration
|
||||
currentVariant: ThemeVariant;
|
||||
onSelectTheme: (variant: ThemeVariant) => void;
|
||||
|
||||
// Theme Data (for store extension)
|
||||
themes?: ThemeCardData[];
|
||||
|
||||
// UI Customization
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
showModeSelector?: boolean;
|
||||
currentMode?: ThemeMode;
|
||||
onModeChange?: (mode: ThemeMode) => void;
|
||||
|
||||
// Back navigation
|
||||
showBackButton?: boolean;
|
||||
onBack?: () => void;
|
||||
|
||||
// Store Features (preparation)
|
||||
showLockedThemes?: boolean;
|
||||
onUnlockTheme?: (variant: ThemeVariant) => void;
|
||||
|
||||
// Translations
|
||||
translations?: Partial<ThemePageTranslations>;
|
||||
}
|
||||
|
||||
let {
|
||||
currentVariant,
|
||||
onSelectTheme,
|
||||
themes,
|
||||
title,
|
||||
subtitle,
|
||||
showModeSelector = false,
|
||||
currentMode = 'system',
|
||||
onModeChange,
|
||||
showBackButton = false,
|
||||
onBack,
|
||||
showLockedThemes = true,
|
||||
onUnlockTheme,
|
||||
translations = {},
|
||||
}: Props = $props();
|
||||
|
||||
// Merge translations with defaults
|
||||
const t = $derived({ ...defaultTranslations, ...translations });
|
||||
|
||||
const modes: { mode: ThemeMode; icon: typeof Sun; label: string }[] = $derived([
|
||||
{ mode: 'light', icon: Sun, label: t.lightMode },
|
||||
{ mode: 'dark', icon: Moon, label: t.darkMode },
|
||||
{ mode: 'system', icon: Desktop, label: t.systemMode },
|
||||
]);
|
||||
</script>
|
||||
|
||||
<div class="min-h-screen bg-background">
|
||||
<div class="max-w-4xl mx-auto px-4 py-8">
|
||||
<!-- Header -->
|
||||
<header class="mb-8">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
{#if showBackButton && onBack}
|
||||
<button
|
||||
type="button"
|
||||
onclick={onBack}
|
||||
class="p-2 -ml-2 text-muted-foreground hover:text-foreground
|
||||
hover:bg-muted rounded-lg transition-colors"
|
||||
aria-label="Zurück"
|
||||
>
|
||||
<ArrowLeft size={20} weight="bold" />
|
||||
</button>
|
||||
{/if}
|
||||
<h1 class="text-2xl font-bold text-foreground">
|
||||
{title ?? t.title}
|
||||
</h1>
|
||||
</div>
|
||||
<p class="text-muted-foreground">
|
||||
{subtitle ?? t.subtitle}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<!-- Mode Selector -->
|
||||
{#if showModeSelector && onModeChange}
|
||||
<section class="mb-8">
|
||||
<h2 class="text-sm font-medium text-muted-foreground mb-3">
|
||||
{t.modeLabel}
|
||||
</h2>
|
||||
<div class="inline-flex rounded-lg bg-muted p-1">
|
||||
{#each modes as { mode, icon: Icon, label }}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => onModeChange(mode)}
|
||||
class="flex items-center gap-2 px-4 py-2 rounded-md text-sm font-medium transition-colors
|
||||
{currentMode === mode
|
||||
? 'bg-surface text-foreground shadow-sm'
|
||||
: 'text-muted-foreground hover:text-foreground'}"
|
||||
>
|
||||
<Icon size={16} weight={currentMode === mode ? 'fill' : 'regular'} />
|
||||
{label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<!-- Theme Grid -->
|
||||
<section>
|
||||
<h2 class="text-sm font-medium text-muted-foreground mb-4">
|
||||
{t.currentTheme}
|
||||
</h2>
|
||||
<ThemeGrid
|
||||
{currentVariant}
|
||||
onSelect={onSelectTheme}
|
||||
{themes}
|
||||
onUnlock={onUnlockTheme}
|
||||
{showLockedThemes}
|
||||
{translations}
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
87
packages/shared-theme-ui/src/types.ts
Normal file
87
packages/shared-theme-ui/src/types.ts
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import type { ThemeVariant, ThemeMode } from '@manacore/shared-theme';
|
||||
|
||||
/**
|
||||
* Theme availability status for store integration
|
||||
*/
|
||||
export type ThemeStatus = 'available' | 'locked' | 'coming_soon' | 'premium';
|
||||
|
||||
/**
|
||||
* Theme card data for displaying in grid/list
|
||||
*/
|
||||
export interface ThemeCardData {
|
||||
variant: ThemeVariant;
|
||||
status: ThemeStatus;
|
||||
isPremium?: boolean;
|
||||
price?: number;
|
||||
releaseDate?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for ThemePage component
|
||||
*/
|
||||
export interface ThemePageProps {
|
||||
// Theme Store Integration
|
||||
currentVariant: ThemeVariant;
|
||||
onSelectTheme: (variant: ThemeVariant) => void;
|
||||
|
||||
// Theme Data (for store extension)
|
||||
themes?: ThemeCardData[];
|
||||
|
||||
// UI Customization
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
showModeSelector?: boolean;
|
||||
currentMode?: ThemeMode;
|
||||
onModeChange?: (mode: ThemeMode) => void;
|
||||
|
||||
// Back navigation
|
||||
showBackButton?: boolean;
|
||||
onBack?: () => void;
|
||||
|
||||
// Store Features (preparation)
|
||||
showLockedThemes?: boolean;
|
||||
onUnlockTheme?: (variant: ThemeVariant) => void;
|
||||
|
||||
// Translations
|
||||
translations?: Partial<ThemePageTranslations>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translations for ThemePage
|
||||
*/
|
||||
export interface ThemePageTranslations {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
modeLabel: string;
|
||||
lightMode: string;
|
||||
darkMode: string;
|
||||
systemMode: string;
|
||||
currentTheme: string;
|
||||
selectTheme: string;
|
||||
locked: string;
|
||||
comingSoon: string;
|
||||
premium: string;
|
||||
unlock: string;
|
||||
lightPreview: string;
|
||||
darkPreview: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default German translations
|
||||
*/
|
||||
export const defaultTranslations: ThemePageTranslations = {
|
||||
title: 'Theme-Einstellungen',
|
||||
subtitle: 'Wähle dein bevorzugtes Farbschema',
|
||||
modeLabel: 'Modus',
|
||||
lightMode: 'Hell',
|
||||
darkMode: 'Dunkel',
|
||||
systemMode: 'System',
|
||||
currentTheme: 'Aktuelles Theme',
|
||||
selectTheme: 'Auswählen',
|
||||
locked: 'Gesperrt',
|
||||
comingSoon: 'Bald verfügbar',
|
||||
premium: 'Premium',
|
||||
unlock: 'Freischalten',
|
||||
lightPreview: 'Hell',
|
||||
darkPreview: 'Dunkel',
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue