managarten/packages/shared-ui/src/settings/GlobalSettingsSection.svelte
Till JS 6da317d071 refactor(theme): migrate shared packages to --color-* token naming
Several shared-ui / shared-auth-ui / subscriptions / credits
components used shadcn-style bare CSS variables (--muted, --primary,
--foreground, etc.), but the Mana theme system standardized on
--color-*. The mismatch meant bg-[hsl(var(--muted))] classes
resolved to an invalid color and rendered transparent — most
visible on the Allgemein settings tab where language and week-start
buttons had no background.

Mechanical prefix across ~30 files. Two semantic renames:
- --destructive → --color-error (Mana uses "error" as the token name)
- --popover     → --color-card  (no popover token; card is the closest)

With shared packages on the correct naming, drop the shadcn-compat
alias shim from app.css.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 22:11:42 +02:00

338 lines
12 KiB
Svelte

<script lang="ts">
import type { UserSettingsStore, ThemeMode, WeekStartDay } from '@mana/shared-theme';
import { getAvailableRoutes, getDefaultRoute } from '@mana/shared-theme';
import SettingsSection from './SettingsSection.svelte';
import SettingsCard from './SettingsCard.svelte';
import NavVisibilitySettings from './NavVisibilitySettings.svelte';
import { Gear } from '@mana/shared-icons';
interface NavItem {
href: string;
label: string;
icon?: string;
}
interface Props {
/** User settings store instance */
userSettings: UserSettingsStore;
/** App ID for start page selection */
appId?: string;
/** Navigation items for visibility settings */
navItems?: NavItem[];
/** Items that should always be visible (e.g., home route) */
alwaysVisibleHrefs?: string[];
/** Whether to show nav visibility settings */
showNavVisibility?: boolean;
/** Whether to show theme settings */
showTheme?: boolean;
/** Whether to show language settings */
showLanguage?: boolean;
/** Whether to show general settings (start page, sounds, week start) */
showGeneral?: boolean;
/** Section title */
title?: string;
/** Section description */
description?: string;
/** Translation function (optional, falls back to German) */
t?: (key: string) => string;
}
let {
userSettings,
appId,
navItems = [],
alwaysVisibleHrefs = [],
showNavVisibility = true,
showTheme = true,
showLanguage = true,
showGeneral = true,
title = 'App-Einstellungen',
description = 'Diese Einstellungen gelten für alle Mana Apps',
t = (key: string) => key,
}: Props = $props();
// Available routes for start page selection
const availableRoutes = $derived(appId ? getAvailableRoutes(appId) : []);
const defaultRoute = $derived(appId ? getDefaultRoute(appId) : '/');
const currentStartPage = $derived(
appId ? userSettings.general?.startPages?.[appId] || defaultRoute : '/'
);
// Theme mode handler
async function handleThemeModeChange(mode: ThemeMode) {
await userSettings.updateGlobal({
theme: { ...userSettings.globalSettings.theme, mode },
});
}
// Color scheme handler
async function handleColorSchemeChange(colorScheme: string) {
await userSettings.updateGlobal({
theme: { ...userSettings.globalSettings.theme, colorScheme },
});
}
// Locale handler
async function handleLocaleChange(locale: string) {
await userSettings.updateGlobal({ locale });
}
// Start page handler
async function handleStartPageChange(e: Event) {
const target = e.target as HTMLSelectElement;
if (appId) {
await userSettings.setStartPage(appId, target.value);
}
}
// Week start handler
async function handleWeekStartChange(day: WeekStartDay) {
await userSettings.updateGeneral({ weekStartsOn: day });
}
// Sounds handler
async function handleSoundsChange(enabled: boolean) {
await userSettings.updateGeneral({ soundsEnabled: enabled });
}
const colorSchemes = [
{ id: 'ocean', label: 'Ozean', color: 'bg-blue-500' },
{ id: 'nature', label: 'Natur', color: 'bg-green-500' },
{ id: 'lume', label: 'Lume', color: 'bg-amber-500' },
{ id: 'stone', label: 'Stein', color: 'bg-slate-500' },
];
const languages = [
{ id: 'de', label: 'DE' },
{ id: 'en', label: 'EN' },
{ id: 'fr', label: 'FR' },
{ id: 'es', label: 'ES' },
{ id: 'it', label: 'IT' },
];
</script>
<SettingsSection {title}>
{#snippet icon()}
<Gear size={20} />
{/snippet}
<SettingsCard>
<div class="p-5">
<p class="text-sm text-[hsl(var(--color-muted-foreground))] mb-6">{description}</p>
<div class="space-y-6">
{#if showNavVisibility && appId && navItems.length > 0}
<!-- Navigation Visibility Settings -->
<div class="space-y-4">
<NavVisibilitySettings {userSettings} {appId} {navItems} {alwaysVisibleHrefs} />
</div>
{/if}
{#if showTheme}
<!-- Theme Settings -->
<div
class="space-y-4 {showNavVisibility && appId
? 'pt-4 border-t border-[hsl(var(--color-border))]'
: ''}"
>
<h3
class="text-xs font-semibold text-[hsl(var(--color-muted-foreground))] uppercase tracking-wider"
>
Erscheinungsbild
</h3>
<div class="flex items-center justify-between py-2">
<div>
<p class="font-medium text-[hsl(var(--color-foreground))]">Farbmodus</p>
<p class="text-sm text-[hsl(var(--color-muted-foreground))]">
Hell, Dunkel oder automatisch
</p>
</div>
<div class="flex gap-1">
{#each ['light', 'dark', 'system'] as mode}
<button
class="px-3 py-1.5 text-sm font-medium rounded-lg transition-colors {userSettings
.globalSettings.theme.mode === mode
? 'bg-[hsl(var(--color-primary))] text-[hsl(var(--color-primary-foreground))]'
: 'bg-[hsl(var(--color-muted))] hover:bg-[hsl(var(--color-muted))]/80 text-[hsl(var(--color-foreground))]'}"
onclick={() => handleThemeModeChange(mode as ThemeMode)}
>
{mode === 'light' ? 'Hell' : mode === 'dark' ? 'Dunkel' : 'System'}
</button>
{/each}
</div>
</div>
<div
class="flex items-center justify-between py-2 border-t border-[hsl(var(--color-border))]"
>
<div>
<p class="font-medium text-[hsl(var(--color-foreground))]">Farbschema</p>
<p class="text-sm text-[hsl(var(--color-muted-foreground))]">
Akzentfarbe der Benutzeroberfläche
</p>
</div>
<div class="flex gap-2">
{#each colorSchemes as scheme}
<button
class="w-7 h-7 rounded-full transition-all {scheme.color} {userSettings
.globalSettings.theme.colorScheme === scheme.id
? 'ring-2 ring-offset-2 ring-[hsl(var(--color-primary))]'
: 'hover:scale-110'}"
onclick={() => handleColorSchemeChange(scheme.id)}
title={scheme.label}
></button>
{/each}
</div>
</div>
</div>
{/if}
{#if showLanguage}
<!-- Language Settings -->
<div
class="space-y-4 {showTheme || (showNavVisibility && appId)
? 'pt-4 border-t border-[hsl(var(--color-border))]'
: ''}"
>
<h3
class="text-xs font-semibold text-[hsl(var(--color-muted-foreground))] uppercase tracking-wider"
>
Sprache
</h3>
<div class="flex items-center justify-between py-2">
<div>
<p class="font-medium text-[hsl(var(--color-foreground))]">Anzeigesprache</p>
<p class="text-sm text-[hsl(var(--color-muted-foreground))]">
Sprache der Benutzeroberfläche
</p>
</div>
<div class="flex gap-1">
{#each languages as lang}
<button
class="px-2.5 py-1.5 text-sm font-medium rounded-lg transition-colors {userSettings
.globalSettings.locale === lang.id
? 'bg-[hsl(var(--color-primary))] text-[hsl(var(--color-primary-foreground))]'
: 'bg-[hsl(var(--color-muted))] hover:bg-[hsl(var(--color-muted))]/80 text-[hsl(var(--color-foreground))]'}"
onclick={() => handleLocaleChange(lang.id)}
>
{lang.label}
</button>
{/each}
</div>
</div>
</div>
{/if}
{#if showGeneral}
<!-- General Settings -->
<div
class="space-y-4 {showLanguage || showTheme || (showNavVisibility && appId)
? 'pt-4 border-t border-[hsl(var(--color-border))]'
: ''}"
>
<h3
class="text-xs font-semibold text-[hsl(var(--color-muted-foreground))] uppercase tracking-wider"
>
Allgemein
</h3>
{#if appId && availableRoutes.length > 1}
<!-- Start Page Selector -->
<div class="flex items-center justify-between py-2">
<div>
<p class="font-medium text-[hsl(var(--color-foreground))]">Startseite</p>
<p class="text-sm text-[hsl(var(--color-muted-foreground))]">
Welche Seite beim Öffnen der App angezeigt wird
</p>
</div>
<select
class="px-3 py-1.5 text-sm font-medium rounded-lg bg-[hsl(var(--color-muted))] text-[hsl(var(--color-foreground))] border-none cursor-pointer appearance-none pr-8 bg-[url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22%23666%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpolyline%20points%3D%226%209%2012%2015%2018%209%22%3E%3C%2Fpolyline%3E%3C%2Fsvg%3E')] bg-no-repeat bg-[right_0.5rem_center] bg-[length:1rem]"
value={currentStartPage}
onchange={handleStartPageChange}
>
{#each availableRoutes as route}
<option value={route.path}>
{route.label}
</option>
{/each}
</select>
</div>
{/if}
<!-- Week Start Day -->
<div
class="flex items-center justify-between py-2 {appId && availableRoutes.length > 1
? 'border-t border-[hsl(var(--color-border))]'
: ''}"
>
<div>
<p class="font-medium text-[hsl(var(--color-foreground))]">Wochenstart</p>
<p class="text-sm text-[hsl(var(--color-muted-foreground))]">
Erster Tag der Woche in Kalendern
</p>
</div>
<div class="flex gap-2">
<button
class="px-3 py-1.5 text-sm font-medium rounded-lg transition-colors {userSettings
.general?.weekStartsOn === 'monday'
? 'bg-[hsl(var(--color-primary))] text-[hsl(var(--color-primary-foreground))]'
: 'bg-[hsl(var(--color-muted))] hover:bg-[hsl(var(--color-muted))]/80 text-[hsl(var(--color-foreground))]'}"
onclick={() => handleWeekStartChange('monday')}
>
Montag
</button>
<button
class="px-3 py-1.5 text-sm font-medium rounded-lg transition-colors {userSettings
.general?.weekStartsOn === 'sunday'
? 'bg-[hsl(var(--color-primary))] text-[hsl(var(--color-primary-foreground))]'
: 'bg-[hsl(var(--color-muted))] hover:bg-[hsl(var(--color-muted))]/80 text-[hsl(var(--color-foreground))]'}"
onclick={() => handleWeekStartChange('sunday')}
>
Sonntag
</button>
</div>
</div>
<!-- Sounds Toggle -->
<div
class="flex items-center justify-between py-2 border-t border-[hsl(var(--color-border))]"
>
<div>
<p class="font-medium text-[hsl(var(--color-foreground))]">Sounds</p>
<p class="text-sm text-[hsl(var(--color-muted-foreground))]">
Sound-Effekte in allen Apps
</p>
</div>
<button
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors {(userSettings
.general?.soundsEnabled ?? true)
? 'bg-[hsl(var(--color-primary))]'
: 'bg-gray-200 dark:bg-gray-700'}"
onclick={() => handleSoundsChange(!(userSettings.general?.soundsEnabled ?? true))}
aria-label="Toggle sound effects"
>
<span
class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform {(userSettings
.general?.soundsEnabled ?? true)
? 'translate-x-6'
: 'translate-x-1'}"
></span>
</button>
</div>
</div>
{/if}
</div>
{#if userSettings.syncing}
<div class="mt-4 flex items-center gap-2 text-sm text-[hsl(var(--color-muted-foreground))]">
<div
class="h-4 w-4 animate-spin rounded-full border-2 border-[hsl(var(--color-primary))] border-t-transparent"
></div>
<span>Synchronisiere...</span>
</div>
{/if}
</div>
</SettingsCard>
</SettingsSection>