mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-24 01:36:42 +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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue