mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-22 07:26:42 +02:00
feat: add i18n localization with language switcher to all web apps
- Add svelte-i18n configuration with SSR support to all web apps - Create LanguageSelector component for each app with brand colors - Add German and English locale files - Integrate language switcher into login pages via headerControls snippet - Fix Tailwind v4 @source directives for shared package scanning - Update AppSlider styling to match login container design Apps updated: - Memoro (gold #f8d62b) - Märchenzauber (pink #FF6B9D) - ManaDeck (purple #8b5cf6) - ManaCore (indigo #6366f1) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
bd869dfe09
commit
926ca231b5
147 changed files with 7090 additions and 2276 deletions
|
|
@ -41,6 +41,7 @@
|
|||
const isInteractive = $derived(interactive || !!onclick);
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||
<div
|
||||
class="card card--{variant} card--padding-{padding} {isInteractive ? 'card--interactive' : ''} {fullWidth ? 'card--full-width' : ''} {className}"
|
||||
{onclick}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script lang="ts">
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
type TextVariant = 'body' | 'body-secondary' | 'small' | 'large' | 'muted';
|
||||
type TextAlign = 'left' | 'center' | 'right';
|
||||
|
|
@ -10,7 +11,7 @@
|
|||
align?: TextAlign;
|
||||
weight?: TextWeight;
|
||||
class?: string;
|
||||
children?: any;
|
||||
children?: Snippet;
|
||||
}
|
||||
|
||||
let {
|
||||
|
|
|
|||
|
|
@ -137,6 +137,8 @@
|
|||
|
||||
<!-- Actions -->
|
||||
{#if actions}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div class="data-card__actions flex-shrink-0 flex items-center gap-1" onclick={(e) => e.stopPropagation()}>
|
||||
{@render actions()}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@
|
|||
required?: boolean;
|
||||
autocomplete?: HTMLInputAttributes['autocomplete'];
|
||||
class?: string;
|
||||
id?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
|
|
@ -26,7 +28,9 @@
|
|||
disabled = false,
|
||||
required = false,
|
||||
autocomplete,
|
||||
class: className = ''
|
||||
class: className = '',
|
||||
id = `input-${Math.random().toString(36).slice(2, 9)}`,
|
||||
name
|
||||
}: Props = $props();
|
||||
|
||||
function handleInput(e: Event) {
|
||||
|
|
@ -43,15 +47,17 @@
|
|||
|
||||
<div class="flex flex-col gap-1.5 {className}">
|
||||
{#if label}
|
||||
<label class="text-sm font-medium text-theme">
|
||||
<label for={id} class="text-sm font-medium text-theme">
|
||||
{label}
|
||||
{#if required}
|
||||
<span class="text-red-500">*</span>
|
||||
{/if}
|
||||
</label>
|
||||
{/if}
|
||||
|
||||
|
||||
<input
|
||||
{id}
|
||||
{name}
|
||||
{type}
|
||||
{value}
|
||||
{placeholder}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,5 @@
|
|||
<script lang="ts">
|
||||
export interface SelectOption {
|
||||
value: string;
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
import type { SelectOption } from './Select.types';
|
||||
|
||||
interface Props {
|
||||
/** Current selected value */
|
||||
|
|
@ -24,6 +20,8 @@
|
|||
required?: boolean;
|
||||
/** Additional CSS classes */
|
||||
class?: string;
|
||||
/** Unique ID for accessibility */
|
||||
id?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
|
|
@ -35,7 +33,8 @@
|
|||
error,
|
||||
disabled = false,
|
||||
required = false,
|
||||
class: className = ''
|
||||
class: className = '',
|
||||
id = `select-${Math.random().toString(36).slice(2, 9)}`
|
||||
}: Props = $props();
|
||||
|
||||
function handleChange(e: Event) {
|
||||
|
|
@ -47,7 +46,7 @@
|
|||
|
||||
<div class="select-wrapper {className}">
|
||||
{#if label}
|
||||
<label class="select-label">
|
||||
<label for={id} class="select-label">
|
||||
{label}
|
||||
{#if required}
|
||||
<span class="select-required">*</span>
|
||||
|
|
@ -57,6 +56,7 @@
|
|||
|
||||
<div class="select-container">
|
||||
<select
|
||||
{id}
|
||||
{value}
|
||||
{disabled}
|
||||
{required}
|
||||
|
|
|
|||
5
packages/shared-ui/src/molecules/Select.types.ts
Normal file
5
packages/shared-ui/src/molecules/Select.types.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export interface SelectOption {
|
||||
value: string;
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
|
@ -26,6 +26,8 @@
|
|||
autoResize?: boolean;
|
||||
/** Additional CSS classes */
|
||||
class?: string;
|
||||
/** Unique ID for accessibility */
|
||||
id?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
|
|
@ -41,7 +43,8 @@
|
|||
disabled = false,
|
||||
required = false,
|
||||
autoResize = false,
|
||||
class: className = ''
|
||||
class: className = '',
|
||||
id = `textarea-${Math.random().toString(36).slice(2, 9)}`
|
||||
}: Props = $props();
|
||||
|
||||
let textareaElement: HTMLTextAreaElement | null = $state(null);
|
||||
|
|
@ -68,7 +71,7 @@
|
|||
|
||||
<div class="textarea-wrapper {className}">
|
||||
{#if label}
|
||||
<label class="textarea-label">
|
||||
<label for={id} class="textarea-label">
|
||||
{label}
|
||||
{#if required}
|
||||
<span class="textarea-required">*</span>
|
||||
|
|
@ -77,6 +80,7 @@
|
|||
{/if}
|
||||
|
||||
<textarea
|
||||
{id}
|
||||
bind:this={textareaElement}
|
||||
{value}
|
||||
{placeholder}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
: 'bg-menu'} {disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'}"
|
||||
role="switch"
|
||||
aria-checked={isOn}
|
||||
aria-label="Toggle"
|
||||
{disabled}
|
||||
>
|
||||
<span
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@
|
|||
onclick={handleClick}
|
||||
onkeydown={handleKeyDown}
|
||||
role={clickable ? 'button' : undefined}
|
||||
tabindex={clickable ? 0 : -1}
|
||||
tabindex={clickable ? 0 : undefined}
|
||||
>
|
||||
<!-- Color indicator dot -->
|
||||
<div class="h-2 w-2 rounded-full" style="background-color: {tagColor}"></div>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,5 @@
|
|||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
export interface AppItem {
|
||||
name: string;
|
||||
description: string;
|
||||
longDescription: string;
|
||||
icon?: string;
|
||||
color: string;
|
||||
comingSoon?: boolean;
|
||||
status: 'published' | 'beta' | 'development' | 'planning';
|
||||
}
|
||||
import type { AppItem } from './AppSlider.types';
|
||||
|
||||
interface Props {
|
||||
apps: AppItem[];
|
||||
|
|
@ -120,17 +110,10 @@
|
|||
<div class="flex gap-4 justify-center overflow-x-auto pb-6 scrollbar-hide snap-x snap-mandatory scroll-smooth px-4 py-4" style="perspective: 1000px;">
|
||||
{#each apps as app, index}
|
||||
<button
|
||||
class="group relative flex-shrink-0 rounded-2xl p-5 cursor-pointer snap-center"
|
||||
style="width: 160px; background-color: {hoveredApp === index
|
||||
? isDark
|
||||
? 'rgba(255, 255, 255, 0.08)'
|
||||
: 'rgba(0, 0, 0, 0.08)'
|
||||
: isDark
|
||||
? 'rgba(255, 255, 255, 0.05)'
|
||||
: 'rgba(0, 0, 0, 0.05)'}; border: 1px solid {isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}; box-shadow: 0 4px 20px rgba(0, 0, 0, {isDark ? '0.3' : '0.15'}); transform: perspective(1000px) rotateX({cardRotations[index]?.rotateX || 0}deg) rotateY({cardRotations[index]?.rotateY || 0}deg); transform-style: preserve-3d; transition: transform 0.1s ease-out, background-color 0.2s ease-out;"
|
||||
class="group relative flex-shrink-0 rounded-xl p-5 cursor-pointer snap-center transition-transform hover:scale-105"
|
||||
style="width: 160px; background-color: {isDark ? 'rgba(255, 255, 255, 0.08)' : 'rgba(255, 255, 255, 0.7)'}; backdrop-filter: blur(10px); border: 1px solid {isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'};"
|
||||
onmouseenter={() => hoveredApp = index}
|
||||
onmousemove={(e) => handleCardMouseMove(e, index, e.currentTarget)}
|
||||
onmouseleave={() => { handleCardMouseLeave(index); hoveredApp = null; }}
|
||||
onmouseleave={() => hoveredApp = null}
|
||||
onclick={() => openModal(index)}
|
||||
>
|
||||
<div
|
||||
|
|
|
|||
9
packages/shared-ui/src/organisms/AppSlider.types.ts
Normal file
9
packages/shared-ui/src/organisms/AppSlider.types.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
export interface AppItem {
|
||||
name: string;
|
||||
description: string;
|
||||
longDescription: string;
|
||||
icon?: string;
|
||||
color: string;
|
||||
comingSoon?: boolean;
|
||||
status: 'published' | 'beta' | 'development' | 'planning';
|
||||
}
|
||||
|
|
@ -42,16 +42,21 @@
|
|||
|
||||
{#if visible}
|
||||
<!-- Modal Backdrop -->
|
||||
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||
<div
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4"
|
||||
onclick={handleBackdropClick}
|
||||
onkeydown={(e) => e.key === 'Enter' && handleBackdropClick(e as unknown as MouseEvent)}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
tabindex="-1"
|
||||
>
|
||||
<!-- Modal Content -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="relative flex max-h-[90vh] w-full {maxWidthClasses[maxWidth]} flex-col rounded-xl border border-theme bg-menu shadow-xl"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
onkeydown={(e) => e.stopPropagation()}
|
||||
>
|
||||
{#if showHeader}
|
||||
<!-- Header -->
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue