fix(ci): build shared packages before tests and fix formatting

- Add build:packages step to all test.yml jobs (fixes @manacore/shared-nestjs-auth not found)
- Handle missing coverage artifacts gracefully in test-coverage.yml
- Update .prettierignore to exclude apps-archived/ and problematic files
- Format all source files to pass CI checks

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Wuesteon 2025-12-01 23:15:00 +01:00
parent 5282f5545b
commit 0ebfde0851
163 changed files with 15247 additions and 14677 deletions

View file

@ -1,10 +1,4 @@
import {
Injectable,
CanActivate,
ExecutionContext,
Inject,
Optional,
} from '@nestjs/common';
import { Injectable, CanActivate, ExecutionContext, Inject, Optional } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { MANA_CORE_OPTIONS } from '../mana-core.module';
import { ManaCoreModuleOptions } from '../interfaces/mana-core-options.interface';

View file

@ -47,10 +47,7 @@ export class CreditClientService {
private getAppId(): string {
return (
this.options?.appId ||
this.configService?.get<string>('APP_ID') ||
process.env.APP_ID ||
''
this.options?.appId || this.configService?.get<string>('APP_ID') || process.env.APP_ID || ''
);
}

View file

@ -99,7 +99,9 @@
);
// Effective dark mode based on user preference or system
let isDark = $derived(userThemePreference !== null ? userThemePreference === 'dark' : systemIsDark);
let isDark = $derived(
userThemePreference !== null ? userThemePreference === 'dark' : systemIsDark
);
$effect(() => {
if (typeof window !== 'undefined') {
@ -159,7 +161,13 @@
<button
type="button"
onclick={toggleTheme}
style="position: absolute; top: 1rem; left: 1rem; z-index: 50; display: flex; align-items: center; justify-content: center; width: 2.5rem; height: 2.5rem; border-radius: 0.5rem; border: 1px solid {isDark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'}; background: {isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.05)'}; color: {isDark ? 'rgba(255, 255, 255, 0.7)' : 'rgba(0, 0, 0, 0.7)'}; cursor: pointer; transition: all 0.2s ease;"
style="position: absolute; top: 1rem; left: 1rem; z-index: 50; display: flex; align-items: center; justify-content: center; width: 2.5rem; height: 2.5rem; border-radius: 0.5rem; border: 1px solid {isDark
? 'rgba(255, 255, 255, 0.2)'
: 'rgba(0, 0, 0, 0.2)'}; background: {isDark
? 'rgba(255, 255, 255, 0.1)'
: 'rgba(0, 0, 0, 0.05)'}; color: {isDark
? 'rgba(255, 255, 255, 0.7)'
: 'rgba(0, 0, 0, 0.7)'}; cursor: pointer; transition: all 0.2s ease;"
aria-label={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
>
{#if isDark}
@ -170,7 +178,9 @@
</button>
{#if headerControls}
<div style="position: absolute; top: 1rem; right: 1rem; z-index: 50; opacity: 0.6; display: flex; gap: 0.75rem;">
<div
style="position: absolute; top: 1rem; right: 1rem; z-index: 50; opacity: 0.6; display: flex; gap: 0.75rem;"
>
{@render headerControls()}
</div>
{/if}

View file

@ -120,7 +120,9 @@
);
// Effective dark mode based on user preference or system
let isDark = $derived(userThemePreference !== null ? userThemePreference === 'dark' : systemIsDark);
let isDark = $derived(
userThemePreference !== null ? userThemePreference === 'dark' : systemIsDark
);
$effect(() => {
if (typeof window !== 'undefined') {
@ -293,10 +295,7 @@
<!-- Form Section -->
<div class="form-section">
<div
class="form-card"
class:shake={shakeError}
>
<div class="form-card" class:shake={shakeError}>
<div class="form-header">
<h2 class="form-title">{t.title}</h2>
<p class="form-subtitle">{t.subtitle}</p>
@ -309,7 +308,13 @@
</div>
{/if}
<form onsubmit={(e) => { e.preventDefault(); handleLogin(); }} aria-busy={loading}>
<form
onsubmit={(e) => {
e.preventDefault();
handleLogin();
}}
aria-busy={loading}
>
<!-- Email -->
<div class="input-group">
<label for="email" class="sr-only">{t.emailPlaceholder}</label>
@ -366,7 +371,12 @@
<input type="checkbox" bind:checked={rememberMe} style:accent-color={primaryColor} />
<span>{t.rememberMe}</span>
</label>
<button type="button" onclick={() => goto(forgotPasswordPath)} class="forgot-link" style:color={primaryColor}>
<button
type="button"
onclick={() => goto(forgotPasswordPath)}
class="forgot-link"
style:color={primaryColor}
>
{t.forgotPassword}
</button>
</div>
@ -380,7 +390,13 @@
style:border-color={showSuccess ? '#22c55e' : primaryColor}
>
{#if loading}
<svg class="spinner" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<svg
class="spinner"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="10" stroke-opacity="0.25" />
<path d="M12 2a10 10 0 0 1 10 10" stroke-linecap="round" />
</svg>
@ -624,7 +640,9 @@
border: 1px solid;
border-radius: 0.75rem;
font-size: 1rem;
transition: border-color 0.2s, box-shadow 0.2s;
transition:
border-color 0.2s,
box-shadow 0.2s;
/* Dark mode default */
background-color: rgba(0, 0, 0, 0.2);
border-color: rgba(255, 255, 255, 0.2);
@ -698,7 +716,7 @@
color: rgba(0, 0, 0, 0.7);
}
.remember-label input[type="checkbox"] {
.remember-label input[type='checkbox'] {
width: 1.125rem;
height: 1.125rem;
cursor: pointer;
@ -711,8 +729,8 @@
place-content: center;
}
.remember-label input[type="checkbox"]::before {
content: "";
.remember-label input[type='checkbox']::before {
content: '';
width: 0.65rem;
height: 0.65rem;
transform: scale(0);
@ -721,15 +739,15 @@
clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
}
.remember-label input[type="checkbox"]:checked {
.remember-label input[type='checkbox']:checked {
border-color: var(--primary-color, #fff);
}
.remember-label input[type="checkbox"]:checked::before {
.remember-label input[type='checkbox']:checked::before {
transform: scale(1);
}
.light .remember-label input[type="checkbox"] {
.light .remember-label input[type='checkbox'] {
border-color: rgba(0, 0, 0, 0.3);
}
@ -857,8 +875,12 @@
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.logo-section {
@ -875,9 +897,23 @@
/* Interactive Animations */
@keyframes shake {
0%, 100% { transform: translateX(0); }
10%, 30%, 50%, 70%, 90% { transform: translateX(-4px); }
20%, 40%, 60%, 80% { transform: translateX(4px); }
0%,
100% {
transform: translateX(0);
}
10%,
30%,
50%,
70%,
90% {
transform: translateX(-4px);
}
20%,
40%,
60%,
80% {
transform: translateX(4px);
}
}
.shake {
@ -885,7 +921,9 @@
}
@keyframes spin {
to { transform: rotate(360deg); }
to {
transform: rotate(360deg);
}
}
.spinner {
@ -895,8 +933,13 @@
}
@keyframes success-pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
}
.success-pulse {

View file

@ -117,7 +117,9 @@
);
// Effective dark mode based on user preference or system
let isDark = $derived(userThemePreference !== null ? userThemePreference === 'dark' : systemIsDark);
let isDark = $derived(
userThemePreference !== null ? userThemePreference === 'dark' : systemIsDark
);
$effect(() => {
if (typeof window !== 'undefined') {
@ -241,7 +243,13 @@
<button
type="button"
onclick={toggleTheme}
style="position: absolute; top: 1rem; left: 1rem; z-index: 50; display: flex; align-items: center; justify-content: center; width: 2.5rem; height: 2.5rem; border-radius: 0.5rem; border: 1px solid {isDark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'}; background: {isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.05)'}; color: {isDark ? 'rgba(255, 255, 255, 0.7)' : 'rgba(0, 0, 0, 0.7)'}; cursor: pointer; transition: all 0.2s ease;"
style="position: absolute; top: 1rem; left: 1rem; z-index: 50; display: flex; align-items: center; justify-content: center; width: 2.5rem; height: 2.5rem; border-radius: 0.5rem; border: 1px solid {isDark
? 'rgba(255, 255, 255, 0.2)'
: 'rgba(0, 0, 0, 0.2)'}; background: {isDark
? 'rgba(255, 255, 255, 0.1)'
: 'rgba(0, 0, 0, 0.05)'}; color: {isDark
? 'rgba(255, 255, 255, 0.7)'
: 'rgba(0, 0, 0, 0.7)'}; cursor: pointer; transition: all 0.2s ease;"
aria-label={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
>
{#if isDark}
@ -252,7 +260,9 @@
</button>
{#if headerControls}
<div style="position: absolute; top: 1rem; right: 1rem; z-index: 50; opacity: 0.6; display: flex; gap: 0.75rem;">
<div
style="position: absolute; top: 1rem; right: 1rem; z-index: 50; opacity: 0.6; display: flex; gap: 0.75rem;"
>
{@render headerControls()}
</div>
{/if}

View file

@ -60,4 +60,3 @@ export interface AuthResult {
error?: string;
needsVerification?: boolean;
}

View file

@ -5,9 +5,7 @@
// Helper to convert SVG string to data URL
const svgToDataUrl = (svg: string): string => {
const encoded = encodeURIComponent(svg)
.replace(/'/g, '%27')
.replace(/"/g, '%22');
const encoded = encodeURIComponent(svg).replace(/'/g, '%27').replace(/"/g, '%22');
return `data:image/svg+xml,${encoded}`;
};

View file

@ -286,7 +286,8 @@ export function getPillAppItems(
isDev?: boolean,
customUrls?: Partial<Record<AppIconId, string>>
): PillAppItemConfig[] {
const isDevMode = isDev ?? (typeof window !== 'undefined' && window.location.hostname === 'localhost');
const isDevMode =
isDev ?? (typeof window !== 'undefined' && window.location.hostname === 'localhost');
// Only show active (non-archived) apps
return getActiveManaApps().map((app) => ({

View file

@ -1,7 +1,17 @@
/**
* App identifiers for branding
*/
export type AppId = 'memoro' | 'manacore' | 'manadeck' | 'maerchenzauber' | 'uload' | 'chat' | 'presi' | 'nutriphi' | 'zitare' | 'picture';
export type AppId =
| 'memoro'
| 'manacore'
| 'manadeck'
| 'maerchenzauber'
| 'uload'
| 'chat'
| 'presi'
| 'nutriphi'
| 'zitare'
| 'picture';
/**
* App branding configuration

View file

@ -11,7 +11,9 @@
},
"main": "./src/index.ts",
"types": "./src/index.ts",
"files": ["src"],
"files": [
"src"
],
"scripts": {
"type-check": "tsc --noEmit"
},

View file

@ -39,10 +39,7 @@ export function createFeedbackService(config: FeedbackServiceConfig) {
/**
* Helper to make authenticated requests
*/
async function fetchWithAuth<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
async function fetchWithAuth<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
const token = await getAuthToken();
if (!token) {

View file

@ -104,8 +104,17 @@
<!-- Header -->
<div class="feedback-page__header">
<div class="feedback-page__icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
/>
</svg>
</div>
<h1 class="feedback-page__title">{pageTitle}</h1>

View file

@ -89,8 +89,7 @@ export class JwtAuthGuard implements CanActivate {
* Validate token with Mana Core Auth service
*/
private async validateToken(token: string): Promise<CurrentUserData> {
const authUrl =
this.configService.get<string>('MANA_CORE_AUTH_URL') || 'http://localhost:3001';
const authUrl = this.configService.get<string>('MANA_CORE_AUTH_URL') || 'http://localhost:3001';
const response = await fetch(`${authUrl}/api/v1/auth/validate`, {
method: 'POST',

View file

@ -126,7 +126,12 @@
<div class="profile-page__info-item">
<div class="profile-page__info-icon">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
/>
</svg>
</div>
<div class="profile-page__info-content">
@ -140,7 +145,12 @@
<div class="profile-page__info-item">
<div class="profile-page__info-icon">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
/>
</svg>
</div>
<div class="profile-page__info-content">
@ -155,7 +165,12 @@
<div class="profile-page__info-item">
<div class="profile-page__info-icon">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
/>
</svg>
</div>
<div class="profile-page__info-content">
@ -170,7 +185,12 @@
<div class="profile-page__info-item">
<div class="profile-page__info-icon">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
</div>
<div class="profile-page__info-content">
@ -185,7 +205,12 @@
<div class="profile-page__info-item">
<div class="profile-page__info-icon">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
<div class="profile-page__info-content">
@ -207,7 +232,12 @@
{#if actions.onEditProfile}
<button class="profile-page__action-btn" onclick={actions.onEditProfile}>
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
/>
</svg>
<span>{editProfileLabel}</span>
</button>
@ -216,25 +246,46 @@
{#if actions.onChangePassword}
<button class="profile-page__action-btn" onclick={actions.onChangePassword}>
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"
/>
</svg>
<span>{changePasswordLabel}</span>
</button>
{/if}
{#if actions.onLogout}
<button class="profile-page__action-btn profile-page__action-btn--secondary" onclick={actions.onLogout}>
<button
class="profile-page__action-btn profile-page__action-btn--secondary"
onclick={actions.onLogout}
>
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
/>
</svg>
<span>{logoutLabel}</span>
</button>
{/if}
{#if actions.onDeleteAccount}
<button class="profile-page__action-btn profile-page__action-btn--danger" onclick={actions.onDeleteAccount}>
<button
class="profile-page__action-btn profile-page__action-btn--danger"
onclick={actions.onDeleteAccount}
>
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
<span>{deleteAccountLabel}</span>
</button>
@ -289,7 +340,9 @@
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(0, 0, 0, 0.1);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
overflow: hidden;
}
@ -341,7 +394,9 @@
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(0, 0, 0, 0.1);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
:global(.dark) .profile-page__card {

View file

@ -61,7 +61,9 @@
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(0, 0, 0, 0.1);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
:global(.dark) .cost-card {

View file

@ -108,7 +108,9 @@
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(0, 0, 0, 0.1);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
min-width: 0;
overflow: hidden;
}
@ -120,7 +122,9 @@
.package-card:hover {
transform: translateY(-2px);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
box-shadow:
0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
:global(.dark) .package-card:hover {

View file

@ -139,7 +139,9 @@
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(0, 0, 0, 0.1);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
min-width: 0;
overflow: hidden;
}
@ -151,7 +153,9 @@
.subscription-card:hover {
transform: translateY(-2px);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
box-shadow:
0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
:global(.dark) .subscription-card:hover {

View file

@ -53,10 +53,7 @@
<!-- Progress Bar -->
<div class="usage-card__progress-track">
<div
class="usage-card__progress-fill"
style="width: {availablePercentage}%;"
></div>
<div class="usage-card__progress-fill" style="width: {availablePercentage}%;"></div>
</div>
<!-- Percentage -->
@ -91,7 +88,9 @@
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(0, 0, 0, 0.1);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
:global(.dark) .usage-card {
@ -156,7 +155,7 @@
.usage-card__progress-fill {
height: 100%;
border-radius: 0.5rem;
background: linear-gradient(90deg, #4287f5 0%, #66B2FF 100%);
background: linear-gradient(90deg, #4287f5 0%, #66b2ff 100%);
box-shadow: 0 0 4px rgba(66, 135, 245, 0.5);
}

View file

@ -92,7 +92,9 @@
<div class="subscription-page__header">
<div class="subscription-page__icon">
<svg viewBox="0 0 24 24" fill="currentColor" class="w-10 h-10">
<path d="M12.3 1c.03.05 7.3 9.67 7.3 13.7 0 4.03-3.27 7.3-7.3 7.3S5 18.73 5 14.7C5 10.66 12.3 1 12.3 1zm0 6.4c-.02.03-3.65 4.83-3.65 6.84 0 2.02 1.64 3.65 3.65 3.65s3.65-1.64 3.65-3.65c0-2.01-3.62-6.81-3.65-6.84z" />
<path
d="M12.3 1c.03.05 7.3 9.67 7.3 13.7 0 4.03-3.27 7.3-7.3 7.3S5 18.73 5 14.7C5 10.66 12.3 1 12.3 1zm0 6.4c-.02.03-3.65 4.83-3.65 6.84 0 2.02 1.64 3.65 3.65 3.65s3.65-1.64 3.65-3.65c0-2.01-3.62-6.81-3.65-6.84z"
/>
</svg>
</div>
<h1 class="subscription-page__title">{pageTitle}</h1>
@ -195,7 +197,9 @@
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(0, 0, 0, 0.1);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
color: hsl(var(--primary, 221 83% 53%));
}

View file

@ -12,10 +12,5 @@ export { default as ThemeGrid } from './components/ThemeGrid.svelte';
export { default as ThemePage } from './pages/ThemePage.svelte';
// Types
export type {
ThemeStatus,
ThemeCardData,
ThemePageProps,
ThemePageTranslations,
} from './types';
export type { ThemeStatus, ThemeCardData, ThemePageProps, ThemePageTranslations } from './types';
export { defaultTranslations } from './types';

View file

@ -13,7 +13,15 @@
header?: Snippet;
}
let { items, direction = 'down', label, icon, isOpen = false, onToggle, header }: Props = $props();
let {
items,
direction = 'down',
label,
icon,
isOpen = false,
onToggle,
header,
}: Props = $props();
let internalOpen = $state(false);
let triggerButton: HTMLButtonElement;
@ -90,13 +98,13 @@
sparkle:
'M12 2L13.09 8.26L18 6L15.74 10.91L22 12L15.74 13.09L18 18L13.09 15.74L12 22L10.91 15.74L6 18L8.26 13.09L2 12L8.26 10.91L6 6L10.91 8.26L12 2Z',
leaf: 'M6.5 21.5C3.5 18.5 3.5 12.5 6.5 8.5C9.5 4.5 15 3 20 3C20 8 18.5 13.5 14.5 16.5C10.5 19.5 4.5 19.5 4.5 19.5M6.5 21.5L4.5 19.5M6.5 21.5C6.5 21.5 12 18 14.5 16.5',
hexagon:
'M12 2L21.5 7.5V16.5L12 22L2.5 16.5V7.5L12 2Z',
hexagon: 'M12 2L21.5 7.5V16.5L12 22L2.5 16.5V7.5L12 2Z',
waves:
'M2 12C2 12 5 8 9 8C13 8 15 12 15 12C15 12 17 16 21 16M2 17C2 17 5 13 9 13C13 13 15 17 15 17C15 17 17 21 21 21M2 7C2 7 5 3 9 3C13 3 15 7 15 7C15 7 17 11 21 11',
// User menu icons
user: 'M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z',
settings: 'M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93s.844.083 1.16-.175l.713-.57a1.125 1.125 0 011.578.112l.773.773a1.125 1.125 0 01.112 1.578l-.57.713c-.258.316-.29.756-.175 1.16s.506.71.93.78l.894.15c.542.09.94.56.94 1.109v1.094c0 .55-.398 1.02-.94 1.11l-.894.149c-.424.07-.764.384-.93.78s-.083.844.175 1.16l.57.713a1.125 1.125 0 01-.112 1.578l-.773.773a1.125 1.125 0 01-1.578.112l-.713-.57c-.316-.258-.756-.29-1.16-.175s-.71.506-.78.93l-.15.894c-.09.542-.56.94-1.109.94h-1.094c-.55 0-1.02-.398-1.11-.94l-.149-.894c-.07-.424-.384-.764-.78-.93s-.844-.083-1.16.175l-.713.57a1.125 1.125 0 01-1.578-.112l-.773-.773a1.125 1.125 0 01-.112-1.578l.57-.713c.258-.316.29-.756.175-1.16s-.506-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.764-.384.93-.78s.083-.844-.175-1.16l-.57-.713a1.125 1.125 0 01.112-1.578l.773-.773a1.125 1.125 0 011.578-.112l.713.57c.316.258.756.29 1.16.175s.71-.506.78-.93l.15-.894zM15 12a3 3 0 11-6 0 3 3 0 016 0z',
settings:
'M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93s.844.083 1.16-.175l.713-.57a1.125 1.125 0 011.578.112l.773.773a1.125 1.125 0 01.112 1.578l-.57.713c-.258.316-.29.756-.175 1.16s.506.71.93.78l.894.15c.542.09.94.56.94 1.109v1.094c0 .55-.398 1.02-.94 1.11l-.894.149c-.424.07-.764.384-.93.78s-.083.844.175 1.16l.57.713a1.125 1.125 0 01-.112 1.578l-.773.773a1.125 1.125 0 01-1.578.112l-.713-.57c-.316-.258-.756-.29-1.16-.175s-.71.506-.78.93l-.15.894c-.09.542-.56.94-1.109.94h-1.094c-.55 0-1.02-.398-1.11-.94l-.149-.894c-.07-.424-.384-.764-.78-.93s-.844-.083-1.16.175l-.713.57a1.125 1.125 0 01-1.578-.112l-.773-.773a1.125 1.125 0 01-.112-1.578l.57-.713c.258-.316.29-.756.175-1.16s-.506-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.764-.384.93-.78s.083-.844-.175-1.16l-.57-.713a1.125 1.125 0 01.112-1.578l.773-.773a1.125 1.125 0 011.578-.112l.713.57c.316.258.756.29 1.16.175s.71-.506.78-.93l.15-.894zM15 12a3 3 0 11-6 0 3 3 0 016 0z',
logout:
'M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1',
// App icons
@ -156,7 +164,10 @@
{#each items.filter((i) => !i.disabled) as item, i (item.id)}
{#if item.divider}
<div class="dropdown-divider" style="animation-delay: {(header ? i + 1 : i) * 15}ms"></div>
<div
class="dropdown-divider"
style="animation-delay: {(header ? i + 1 : i) * 15}ms"
></div>
{:else}
<button
onclick={() => handleItemClick(item)}
@ -194,7 +205,13 @@
/>
</svg>
{:else if item.submenu && item.submenu.length > 0}
<svg class="chevron-submenu" class:rotated={openSubmenuId === item.id} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg
class="chevron-submenu"
class:rotated={openSubmenuId === item.id}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"

View file

@ -1,6 +1,12 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import type { PillNavItem, PillDropdownItem, PillNavElement, PillTabGroupConfig, PillAppItem } from './types';
import type {
PillNavItem,
PillDropdownItem,
PillNavElement,
PillTabGroupConfig,
PillAppItem,
} from './types';
import PillDropdown from './PillDropdown.svelte';
import PillTabGroup from './PillTabGroup.svelte';
@ -256,7 +262,8 @@
creditCard:
'M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z',
search: 'M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z',
heart: 'M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z',
heart:
'M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z',
list: 'M4 6h16M4 10h16M4 14h16M4 18h16',
compass:
'M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22ZM16.24 7.76L14.12 14.12L7.76 16.24L9.88 9.88L16.24 7.76Z',
@ -379,7 +386,7 @@
{/if}
{/each}
<!-- Theme Variant Selector -->
<!-- Theme Variant Selector -->
{#if showThemeVariants && themeVariantItems.length > 0}
<PillDropdown
items={themeVariantItems}
@ -397,7 +404,12 @@
title="Light mode"
>
<svg class="mode-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d={getIconPath('sun')} />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d={getIconPath('sun')}
/>
</svg>
</button>
<button
@ -408,7 +420,12 @@
title="Dark mode"
>
<svg class="mode-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d={getIconPath('moon')} />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d={getIconPath('moon')}
/>
</svg>
</button>
<button
@ -420,7 +437,12 @@
>
<svg class="mode-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<rect x="2" y="3" width="20" height="14" rx="2" stroke-width="2" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 21h8M12 17v4" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8 21h8M12 17v4"
/>
</svg>
</button>
</div>
@ -1086,7 +1108,9 @@
border-radius: 9999px !important;
background: rgba(245, 245, 245, 0.95) !important;
border: 1px solid rgba(0, 0, 0, 0.1) !important;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06) !important;
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06) !important;
color: #374151 !important;
}

View file

@ -113,7 +113,9 @@
>
<div
class="status-indicator"
style="background-color: {getStatusColor(app.status)}; box-shadow: 0 0 8px {getStatusColor(app.status)};"
style="background-color: {getStatusColor(
app.status
)}; box-shadow: 0 0 8px {getStatusColor(app.status)};"
></div>
<div class="app-icon-wrapper">
@ -142,26 +144,29 @@
aria-modal="true"
tabindex="-1"
>
<button
onclick={closeModal}
class="modal-close-btn"
aria-label="Close modal"
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<button onclick={closeModal} class="modal-close-btn" aria-label="Close modal">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M18 6L6 18M6 6l12 12" />
</svg>
</button>
<div
bind:this={modalScrollContainer}
class="modal-scroll-container scrollbar-hide"
>
<div bind:this={modalScrollContainer} class="modal-scroll-container scrollbar-hide">
<div class="modal-cards-wrapper">
{#each apps as app, index}
<div
class="modal-card"
class:active={selectedApp === index}
style="transform: perspective(1000px) rotateX({cardRotations[index]?.rotateX || 0}deg) rotateY({cardRotations[index]?.rotateY || 0}deg);"
style="transform: perspective(1000px) rotateX({cardRotations[index]?.rotateX ||
0}deg) rotateY({cardRotations[index]?.rotateY || 0}deg);"
onclick={(e) => {
e.stopPropagation();
selectedApp = index;
@ -274,7 +279,9 @@
border: 1px solid rgba(255, 255, 255, 0.1);
background-color: rgba(255, 255, 255, 0.06);
backdrop-filter: blur(10px);
transition: transform 0.2s ease, background-color 0.2s ease;
transition:
transform 0.2s ease,
background-color 0.2s ease;
/* Staggered entrance animation */
animation: fadeInUp 0.4s ease-out both;
animation-delay: calc(0.3s + var(--index) * 0.08s);
@ -363,7 +370,8 @@
}
@keyframes pulse {
0%, 100% {
0%,
100% {
opacity: 1;
}
50% {
@ -418,7 +426,9 @@
background-color: rgba(255, 255, 255, 0.1);
color: #fff;
cursor: pointer;
transition: background-color 0.2s, transform 0.2s;
transition:
background-color 0.2s,
transform 0.2s;
}
.modal-close-btn:hover {
@ -455,7 +465,9 @@
background-color: rgba(255, 255, 255, 0.08);
backdrop-filter: blur(20px);
transform-style: preserve-3d;
transition: transform 0.1s ease-out, background-color 0.2s ease;
transition:
transform 0.1s ease-out,
background-color 0.2s ease;
animation: modalCardIn 0.3s ease-out both;
}
@ -593,7 +605,9 @@
font-weight: 600;
border: 2px solid;
cursor: pointer;
transition: opacity 0.2s, transform 0.2s;
transition:
opacity 0.2s,
transform 0.2s;
color: #fff;
}
@ -609,8 +623,12 @@
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes modalCardIn {

View file

@ -135,7 +135,14 @@
>
{#if loading}
<svg class="h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
/>
<path
class="opacity-75"
fill="currentColor"

View file

@ -71,7 +71,9 @@
>
{#if showHeader}
<!-- Header -->
<div class="flex items-center justify-between p-6 border-b border-black/10 dark:border-white/10">
<div
class="flex items-center justify-between p-6 border-b border-black/10 dark:border-white/10"
>
<div class="flex items-center gap-3 flex-1">
{#if icon}
{@render icon()}

View file

@ -1,5 +1,11 @@
<script lang="ts">
import { MANA_APPS, APP_URLS, APP_STATUS_LABELS, type ManaApp, type AppIconId } from '@manacore/shared-branding';
import {
MANA_APPS,
APP_URLS,
APP_STATUS_LABELS,
type ManaApp,
type AppIconId,
} from '@manacore/shared-branding';
interface Props {
/** Current app ID to highlight */
@ -12,12 +18,7 @@
onAppClick?: (app: ManaApp) => void;
}
let {
currentAppId,
title = 'Alle Apps',
locale = 'de',
onAppClick,
}: Props = $props();
let { currentAppId, title = 'Alle Apps', locale = 'de', onAppClick }: Props = $props();
// Filter active apps (non-archived)
const apps = $derived(MANA_APPS.filter((app) => !app.archived));
@ -155,13 +156,7 @@
<!-- Modal -->
{#if selectedAppIndex !== null}
<div
class="modal-overlay"
onclick={closeModal}
role="dialog"
aria-modal="true"
tabindex="-1"
>
<div class="modal-overlay" onclick={closeModal} role="dialog" aria-modal="true" tabindex="-1">
<button onclick={closeModal} class="modal-close-btn" aria-label="Close modal">
<svg
width="24"
@ -202,7 +197,10 @@
tabindex="0"
>
<div class="modal-card-status">
<div class="modal-status-dot" style="background-color: {getStatusColor(app.status)};"></div>
<div
class="modal-status-dot"
style="background-color: {getStatusColor(app.status)};"
></div>
<span class="modal-status-label">{statusLabels[app.status]}</span>
</div>
@ -224,7 +222,9 @@
<div class="modal-app-action">
{#if app.comingSoon}
<span class="modal-coming-soon">{locale === 'de' ? 'Demnächst' : 'Coming Soon'}</span>
<span class="modal-coming-soon"
>{locale === 'de' ? 'Demnächst' : 'Coming Soon'}</span
>
{:else}
<button
class="modal-open-btn"
@ -303,7 +303,10 @@
border: 1px solid rgba(0, 0, 0, 0.08);
background-color: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
transition:
transform 0.2s ease,
box-shadow 0.2s ease,
border-color 0.2s ease;
text-align: center;
}
@ -427,7 +430,9 @@
background-color: rgba(255, 255, 255, 0.1);
color: #fff;
cursor: pointer;
transition: background-color 0.2s, transform 0.2s;
transition:
background-color 0.2s,
transform 0.2s;
}
.modal-close-btn:hover {
@ -473,7 +478,9 @@
background-color: rgba(255, 255, 255, 0.08);
backdrop-filter: blur(20px);
transform-style: preserve-3d;
transition: transform 0.1s ease-out, background-color 0.2s ease;
transition:
transform 0.1s ease-out,
background-color 0.2s ease;
animation: modalCardIn 0.3s ease-out both;
}
@ -575,7 +582,9 @@
font-weight: 600;
border: 2px solid;
cursor: pointer;
transition: opacity 0.2s, transform 0.2s;
transition:
opacity 0.2s,
transform 0.2s;
color: #fff;
}

View file

@ -33,7 +33,9 @@
</script>
<div
class="settings-danger-button {border ? 'settings-danger-button--border' : ''} {disabled ? 'settings-danger-button--disabled' : ''} {className}"
class="settings-danger-button {border ? 'settings-danger-button--border' : ''} {disabled
? 'settings-danger-button--disabled'
: ''} {className}"
>
<div class="settings-danger-button__content">
{#if icon}
@ -49,12 +51,7 @@
</div>
</div>
<button
type="button"
{onclick}
class="settings-danger-button__button"
{disabled}
>
<button type="button" {onclick} class="settings-danger-button__button" {disabled}>
{buttonText || label}
</button>
</div>

View file

@ -10,11 +10,7 @@
children: Snippet;
}
let {
title = 'Danger Zone',
class: className = '',
children,
}: Props = $props();
let { title = 'Danger Zone', class: className = '', children }: Props = $props();
</script>
<section class="settings-danger-zone {className}">

View file

@ -14,13 +14,7 @@
children: Snippet;
}
let {
title,
subtitle,
maxWidth = 'md',
class: className = '',
children,
}: Props = $props();
let { title, subtitle, maxWidth = 'md', class: className = '', children }: Props = $props();
const maxWidthClasses = {
sm: 'max-w-lg',

View file

@ -40,7 +40,9 @@
{#if href}
<a
{href}
class="settings-row {border ? 'settings-row--border' : ''} settings-row--clickable {disabled ? 'settings-row--disabled' : ''} {className}"
class="settings-row {border ? 'settings-row--border' : ''} settings-row--clickable {disabled
? 'settings-row--disabled'
: ''} {className}"
>
<div class="settings-row__content">
{#if icon}
@ -69,7 +71,9 @@
<button
type="button"
{onclick}
class="settings-row {border ? 'settings-row--border' : ''} settings-row--clickable {disabled ? 'settings-row--disabled' : ''} {className}"
class="settings-row {border ? 'settings-row--border' : ''} settings-row--clickable {disabled
? 'settings-row--disabled'
: ''} {className}"
{disabled}
>
<div class="settings-row__content">
@ -97,7 +101,9 @@
</button>
{:else}
<div
class="settings-row {border ? 'settings-row--border' : ''} {disabled ? 'settings-row--disabled' : ''} {className}"
class="settings-row {border ? 'settings-row--border' : ''} {disabled
? 'settings-row--disabled'
: ''} {className}"
>
<div class="settings-row__content">
{#if icon}

View file

@ -12,12 +12,7 @@
children: Snippet;
}
let {
title,
icon,
class: className = '',
children,
}: Props = $props();
let { title, icon, class: className = '', children }: Props = $props();
</script>
<section class="settings-section {className}">

View file

@ -39,7 +39,9 @@
</script>
<div
class="settings-toggle {border ? 'settings-toggle--border' : ''} {disabled ? 'settings-toggle--disabled' : ''} {className}"
class="settings-toggle {border ? 'settings-toggle--border' : ''} {disabled
? 'settings-toggle--disabled'
: ''} {className}"
>
<div class="settings-toggle__content">
{#if icon}