mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 22:01:09 +02:00
feat(apps): add unified Apps page and PillNavigation to all web apps
- Add AppsPage component to shared-ui for displaying all Mana apps - Add allAppsHref prop to PillNavigation with "Alle Apps" link in dropdown - Integrate PillNavigation in archived apps (maerchenzauber, news, uload, wisekeep) - Add /apps route to all web apps (active and archived) - Replace custom sidebars/headers with unified PillNavigation Apps updated: - Active: chat, manacore, manadeck, picture, presi, zitare - Archived: maerchenzauber, memoro, news, nutriphi, uload, wisekeep 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
1f2d21e005
commit
4e4db4612c
25 changed files with 1354 additions and 538 deletions
|
|
@ -25,122 +25,51 @@
|
|||
|
||||
let { children } = $props();
|
||||
let loading = $state(true);
|
||||
let isSidebarCollapsed = $state(false);
|
||||
let isMobileMenuOpen = $state(false);
|
||||
let showKeyboardShortcuts = $state(false);
|
||||
let isSidebarMode = $state(false);
|
||||
let isCollapsed = $state(false);
|
||||
let isDark = $state(false);
|
||||
|
||||
// Keyboard shortcuts configuration
|
||||
const navRoutes: Record<string, string> = {
|
||||
'1': '/dashboard', // Dashboard
|
||||
'2': '/stories', // Stories
|
||||
'3': '/characters', // Characters
|
||||
'4': '/discover', // Discover
|
||||
'5': '/settings', // Settings
|
||||
};
|
||||
|
||||
const actionRoutes: Record<string, string> = {
|
||||
n: '/stories/create', // New Story
|
||||
s: '/stories/create', // New Story (alternative)
|
||||
c: '/characters/create', // New Character
|
||||
};
|
||||
|
||||
// Shortcut descriptions for help modal
|
||||
const shortcutGroups = [
|
||||
{
|
||||
title: 'Navigation',
|
||||
shortcuts: [
|
||||
{ keys: ['Cmd/Ctrl', '1'], description: 'Dashboard' },
|
||||
{ keys: ['Cmd/Ctrl', '2'], description: 'Geschichten' },
|
||||
{ keys: ['Cmd/Ctrl', '3'], description: 'Charaktere' },
|
||||
{ keys: ['Cmd/Ctrl', '4'], description: 'Entdecken' },
|
||||
{ keys: ['Cmd/Ctrl', '5'], description: 'Einstellungen' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Aktionen',
|
||||
shortcuts: [
|
||||
{ keys: ['Cmd/Ctrl', 'N'], description: 'Neue Geschichte' },
|
||||
{ keys: ['Cmd/Ctrl', 'Shift', 'C'], description: 'Neuer Charakter' },
|
||||
{ keys: ['?'], description: 'Tastaturkürzel anzeigen' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Allgemein',
|
||||
shortcuts: [
|
||||
{ keys: ['Esc'], description: 'Menü/Modal schließen' },
|
||||
{ keys: ['B'], description: 'Seitenleiste ein-/ausblenden' },
|
||||
],
|
||||
},
|
||||
];
|
||||
// Navigation shortcuts (Ctrl+1-5)
|
||||
const navRoutes = ['/dashboard', '/stories', '/characters', '/discover', '/settings'];
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
// Don't handle if user is typing in an input
|
||||
const target = event.target as HTMLElement;
|
||||
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ? to show keyboard shortcuts
|
||||
if (event.key === '?' && !event.ctrlKey && !event.metaKey) {
|
||||
event.preventDefault();
|
||||
showKeyboardShortcuts = !showKeyboardShortcuts;
|
||||
return;
|
||||
}
|
||||
|
||||
// ESC to close modals/menus
|
||||
if (event.key === 'Escape') {
|
||||
if (showKeyboardShortcuts) {
|
||||
showKeyboardShortcuts = false;
|
||||
return;
|
||||
}
|
||||
if (isMobileMenuOpen) {
|
||||
isMobileMenuOpen = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// B to toggle sidebar (without modifiers)
|
||||
if (event.key === 'b' && !event.ctrlKey && !event.metaKey && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
handleSidebarToggle();
|
||||
return;
|
||||
}
|
||||
|
||||
// Ctrl/Cmd + key shortcuts
|
||||
if ((event.ctrlKey || event.metaKey) && !event.altKey) {
|
||||
// Ctrl/Cmd + number for navigation
|
||||
const route = navRoutes[event.key];
|
||||
if (route) {
|
||||
if ((event.ctrlKey || event.metaKey) && !event.shiftKey && !event.altKey) {
|
||||
const num = parseInt(event.key);
|
||||
if (num >= 1 && num <= 5) {
|
||||
event.preventDefault();
|
||||
goto(route);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ctrl/Cmd + Shift + C for new character
|
||||
if (event.shiftKey && event.key.toLowerCase() === 'c') {
|
||||
event.preventDefault();
|
||||
goto('/characters/create');
|
||||
return;
|
||||
}
|
||||
|
||||
// Ctrl/Cmd + N for new story
|
||||
if (!event.shiftKey && event.key.toLowerCase() === 'n') {
|
||||
event.preventDefault();
|
||||
goto('/stories/create');
|
||||
return;
|
||||
const route = navRoutes[num - 1];
|
||||
if (route) {
|
||||
goto(route);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleSidebarToggle() {
|
||||
isSidebarCollapsed = !isSidebarCollapsed;
|
||||
function handleModeChange(isSidebar: boolean) {
|
||||
isSidebarMode = isSidebar;
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem('maerchenzauber-sidebar-collapsed', String(isSidebarCollapsed));
|
||||
localStorage.setItem('maerchenzauber-nav-sidebar', String(isSidebar));
|
||||
}
|
||||
}
|
||||
|
||||
function handleMobileMenuToggle() {
|
||||
isMobileMenuOpen = !isMobileMenuOpen;
|
||||
function handleCollapsedChange(collapsed: boolean) {
|
||||
isCollapsed = collapsed;
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem('maerchenzauber-nav-collapsed', String(collapsed));
|
||||
}
|
||||
}
|
||||
|
||||
function handleToggleTheme() {
|
||||
isDark = !isDark;
|
||||
document.documentElement.classList.toggle('dark', isDark);
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem('maerchenzauber-dark-mode', String(isDark));
|
||||
}
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
|
|
@ -148,7 +77,6 @@
|
|||
goto('/login');
|
||||
}
|
||||
|
||||
// Client-side auth guard
|
||||
onMount(async () => {
|
||||
await authStore.initialize();
|
||||
|
||||
|
|
@ -157,11 +85,20 @@
|
|||
return;
|
||||
}
|
||||
|
||||
// Restore sidebar state from localStorage
|
||||
// Restore nav mode from localStorage
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
const savedCollapsed = localStorage.getItem('maerchenzauber-sidebar-collapsed');
|
||||
const savedSidebar = localStorage.getItem('maerchenzauber-nav-sidebar');
|
||||
if (savedSidebar === 'true') {
|
||||
isSidebarMode = true;
|
||||
}
|
||||
const savedCollapsed = localStorage.getItem('maerchenzauber-nav-collapsed');
|
||||
if (savedCollapsed === 'true') {
|
||||
isSidebarCollapsed = true;
|
||||
isCollapsed = true;
|
||||
}
|
||||
const savedDark = localStorage.getItem('maerchenzauber-dark-mode');
|
||||
if (savedDark === 'true') {
|
||||
isDark = true;
|
||||
document.documentElement.classList.add('dark');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -172,10 +109,7 @@
|
|||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
{#if loading}
|
||||
<!-- Loading State -->
|
||||
<div
|
||||
class="flex min-h-screen items-center justify-center bg-gradient-to-br from-pink-50 to-purple-50 dark:from-gray-900 dark:to-gray-800"
|
||||
>
|
||||
<div class="flex min-h-screen items-center justify-center">
|
||||
<div class="text-center">
|
||||
<div
|
||||
class="mb-4 inline-block h-12 w-12 animate-spin rounded-full border-4 border-solid border-pink-500 border-r-transparent"
|
||||
|
|
@ -184,127 +118,47 @@
|
|||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Main Layout -->
|
||||
<div
|
||||
class="flex min-h-screen bg-gradient-to-br from-pink-50/50 to-purple-50/50 dark:from-gray-900 dark:to-gray-800"
|
||||
>
|
||||
<!-- Sidebar (Desktop) -->
|
||||
<Sidebar
|
||||
isCollapsed={isSidebarCollapsed}
|
||||
<div class="flex min-h-screen flex-col">
|
||||
<PillNavigation
|
||||
items={navItems}
|
||||
currentPath={$page.url.pathname}
|
||||
onToggle={handleSidebarToggle}
|
||||
appName="Märchenzauber"
|
||||
homeRoute="/dashboard"
|
||||
onLogout={handleLogout}
|
||||
/>
|
||||
|
||||
<!-- Mobile Menu Overlay -->
|
||||
{#if isMobileMenuOpen}
|
||||
<div
|
||||
class="fixed inset-0 z-40 bg-black/50 lg:hidden"
|
||||
onclick={() => (isMobileMenuOpen = false)}
|
||||
onkeydown={(e) => e.key === 'Escape' && (isMobileMenuOpen = false)}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
></div>
|
||||
<div class="fixed inset-y-0 left-0 z-50 w-64 lg:hidden">
|
||||
<Sidebar
|
||||
isCollapsed={false}
|
||||
currentPath={$page.url.pathname}
|
||||
onToggle={() => (isMobileMenuOpen = false)}
|
||||
onLogout={handleLogout}
|
||||
isMobile={true}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<div
|
||||
class="flex flex-1 flex-col transition-all duration-300"
|
||||
class:lg:ml-64={!isSidebarCollapsed}
|
||||
class:lg:ml-20={isSidebarCollapsed}
|
||||
onToggleTheme={handleToggleTheme}
|
||||
{isDark}
|
||||
{isSidebarMode}
|
||||
onModeChange={handleModeChange}
|
||||
{isCollapsed}
|
||||
onCollapsedChange={handleCollapsedChange}
|
||||
showThemeToggle={true}
|
||||
primaryColor="#ec4899"
|
||||
showAppSwitcher={true}
|
||||
{appItems}
|
||||
{userEmail}
|
||||
settingsHref="/settings"
|
||||
manaHref="/subscription"
|
||||
profileHref="/profile"
|
||||
allAppsHref="/apps"
|
||||
>
|
||||
<!-- Header -->
|
||||
<Header onMenuClick={handleMobileMenuToggle} onLogout={handleLogout} />
|
||||
{#snippet logo()}
|
||||
<span class="text-xl">✨</span>
|
||||
<span class="pill-label font-bold">Märchenzauber</span>
|
||||
{/snippet}
|
||||
</PillNavigation>
|
||||
|
||||
<!-- Page Content -->
|
||||
<main class="flex-1 overflow-auto p-4 lg:p-6">
|
||||
<main
|
||||
class="main-content flex-1 transition-all duration-300 {isCollapsed
|
||||
? ''
|
||||
: isSidebarMode
|
||||
? 'pl-[180px]'
|
||||
: 'pt-20'}"
|
||||
>
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
{@render children()}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Toast Notifications -->
|
||||
<ToastContainer />
|
||||
|
||||
<!-- Keyboard Shortcuts Modal -->
|
||||
{#if showKeyboardShortcuts}
|
||||
<div
|
||||
class="fixed inset-0 z-[200] flex items-center justify-center bg-black/50 backdrop-blur-sm"
|
||||
onclick={() => (showKeyboardShortcuts = false)}
|
||||
onkeydown={(e) => e.key === 'Escape' && (showKeyboardShortcuts = false)}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx-4 max-h-[80vh] w-full max-w-lg overflow-y-auto rounded-2xl bg-white p-6 shadow-2xl dark:bg-gray-800"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
onkeydown={(e) => e.stopPropagation()}
|
||||
role="dialog"
|
||||
>
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<h2 class="text-xl font-bold text-gray-800 dark:text-gray-200">Tastaturkürzel</h2>
|
||||
<button
|
||||
onclick={() => (showKeyboardShortcuts = false)}
|
||||
class="rounded-lg p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-gray-700 dark:hover:text-gray-300"
|
||||
aria-label="Schließen"
|
||||
>
|
||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
{#each shortcutGroups as group}
|
||||
<div>
|
||||
<h3
|
||||
class="mb-3 text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
{group.title}
|
||||
</h3>
|
||||
<div class="space-y-2">
|
||||
{#each group.shortcuts as shortcut}
|
||||
<div
|
||||
class="flex items-center justify-between rounded-lg bg-gray-50 px-3 py-2 dark:bg-gray-700/50"
|
||||
>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300"
|
||||
>{shortcut.description}</span
|
||||
>
|
||||
<div class="flex gap-1">
|
||||
{#each shortcut.keys as key}
|
||||
<kbd
|
||||
class="rounded bg-gray-200 px-2 py-1 text-xs font-medium text-gray-700 dark:bg-gray-600 dark:text-gray-300"
|
||||
>
|
||||
{key}
|
||||
</kbd>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<p class="mt-6 text-center text-xs text-gray-500 dark:text-gray-400">
|
||||
Drücke <kbd class="rounded bg-gray-200 px-1.5 py-0.5 text-xs dark:bg-gray-600">?</kbd> um dieses
|
||||
Menü zu öffnen
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
<script lang="ts">
|
||||
import { AppsPage } from '@manacore/shared-ui';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Alle Apps - Märchenzauber</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="apps-page-wrapper">
|
||||
<AppsPage currentAppId="maerchenzauber" locale="de" title="Alle Apps" />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.apps-page-wrapper {
|
||||
min-height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -2,135 +2,153 @@
|
|||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { PillNavigation } from '@manacore/shared-ui';
|
||||
import type { PillNavItem } from '@manacore/shared-ui';
|
||||
import { getPillAppItems } from '@manacore/shared-branding';
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
const navItems = [
|
||||
{ href: '/feed', label: 'Feed', icon: 'feed' },
|
||||
{ href: '/summaries', label: 'Zusammenfassungen', icon: 'summaries' },
|
||||
{ href: '/in-depth', label: 'In-Depth', icon: 'indepth' },
|
||||
{ href: '/saved', label: 'Gespeichert', icon: 'saved' },
|
||||
// App switcher items
|
||||
const appItems = getPillAppItems('news');
|
||||
|
||||
// User email for dropdown
|
||||
let userEmail = $derived(authStore.user?.email);
|
||||
|
||||
// Navigation items for News
|
||||
const navItems: PillNavItem[] = [
|
||||
{ href: '/feed', label: 'Feed', icon: 'rss' },
|
||||
{ href: '/summaries', label: 'Zusammenfassungen', icon: 'document' },
|
||||
{ href: '/in-depth', label: 'In-Depth', icon: 'book' },
|
||||
{ href: '/saved', label: 'Gespeichert', icon: 'bookmark' },
|
||||
{ href: '/settings', label: 'Einstellungen', icon: 'settings' },
|
||||
];
|
||||
|
||||
let loading = $state(true);
|
||||
let isSidebarMode = $state(false);
|
||||
let isCollapsed = $state(false);
|
||||
let isDark = $state(false);
|
||||
|
||||
// Navigation shortcuts (Ctrl+1-5)
|
||||
const navRoutes = ['/feed', '/summaries', '/in-depth', '/saved', '/settings'];
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
const target = event.target as HTMLElement;
|
||||
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((event.ctrlKey || event.metaKey) && !event.shiftKey && !event.altKey) {
|
||||
const num = parseInt(event.key);
|
||||
if (num >= 1 && num <= 5) {
|
||||
event.preventDefault();
|
||||
const route = navRoutes[num - 1];
|
||||
if (route) {
|
||||
goto(route);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleModeChange(isSidebar: boolean) {
|
||||
isSidebarMode = isSidebar;
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem('news-nav-sidebar', String(isSidebar));
|
||||
}
|
||||
}
|
||||
|
||||
function handleCollapsedChange(collapsed: boolean) {
|
||||
isCollapsed = collapsed;
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem('news-nav-collapsed', String(collapsed));
|
||||
}
|
||||
}
|
||||
|
||||
function handleToggleTheme() {
|
||||
isDark = !isDark;
|
||||
document.documentElement.classList.toggle('dark', isDark);
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem('news-dark-mode', String(isDark));
|
||||
}
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
await authStore.logout();
|
||||
goto('/auth/login');
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
// Restore nav mode from localStorage
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
const savedSidebar = localStorage.getItem('news-nav-sidebar');
|
||||
if (savedSidebar === 'true') {
|
||||
isSidebarMode = true;
|
||||
}
|
||||
const savedCollapsed = localStorage.getItem('news-nav-collapsed');
|
||||
if (savedCollapsed === 'true') {
|
||||
isCollapsed = true;
|
||||
}
|
||||
const savedDark = localStorage.getItem('news-dark-mode');
|
||||
if (savedDark === 'true') {
|
||||
isDark = true;
|
||||
document.documentElement.classList.add('dark');
|
||||
}
|
||||
}
|
||||
loading = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="min-h-screen flex">
|
||||
<!-- Sidebar -->
|
||||
<aside class="w-64 bg-background-card border-r border-border flex flex-col">
|
||||
<!-- Logo -->
|
||||
<div class="p-4 border-b border-border">
|
||||
<a href="/feed" class="flex items-center gap-2">
|
||||
<svg
|
||||
class="w-8 h-8 text-primary"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M12 7.5h1.5m-1.5 3h1.5m-7.5 3h7.5m-7.5 3h7.5m3-9h3.375c.621 0 1.125.504 1.125 1.125V18a2.25 2.25 0 01-2.25 2.25M16.5 7.5V18a2.25 2.25 0 002.25 2.25M16.5 7.5V4.875c0-.621-.504-1.125-1.125-1.125H4.125C3.504 3.75 3 4.254 3 4.875V18a2.25 2.25 0 002.25 2.25h13.5M6 7.5h3v3H6v-3z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="font-bold text-lg">News Hub</span>
|
||||
</a>
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
{#if loading}
|
||||
<div class="flex min-h-screen items-center justify-center">
|
||||
<div class="text-center">
|
||||
<div
|
||||
class="mb-4 inline-block h-12 w-12 animate-spin rounded-full border-4 border-solid border-blue-500 border-r-transparent"
|
||||
></div>
|
||||
<p class="text-gray-600 dark:text-gray-400">Laden...</p>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex min-h-screen flex-col">
|
||||
<PillNavigation
|
||||
items={navItems}
|
||||
currentPath={$page.url.pathname}
|
||||
appName="News"
|
||||
homeRoute="/feed"
|
||||
onLogout={handleLogout}
|
||||
onToggleTheme={handleToggleTheme}
|
||||
{isDark}
|
||||
{isSidebarMode}
|
||||
onModeChange={handleModeChange}
|
||||
{isCollapsed}
|
||||
onCollapsedChange={handleCollapsedChange}
|
||||
showThemeToggle={true}
|
||||
primaryColor="#3b82f6"
|
||||
showAppSwitcher={true}
|
||||
{appItems}
|
||||
{userEmail}
|
||||
settingsHref="/settings"
|
||||
manaHref="/subscription"
|
||||
profileHref="/profile"
|
||||
allAppsHref="/apps"
|
||||
>
|
||||
{#snippet logo()}
|
||||
<span class="text-xl">📰</span>
|
||||
<span class="pill-label font-bold">News</span>
|
||||
{/snippet}
|
||||
</PillNavigation>
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="flex-1 p-4 space-y-1">
|
||||
{#each navItems as item}
|
||||
<a
|
||||
href={item.href}
|
||||
class="flex items-center gap-3 px-3 py-2 rounded-lg transition-colors {$page.url.pathname.startsWith(
|
||||
item.href
|
||||
)
|
||||
? 'bg-primary/10 text-primary'
|
||||
: 'text-text-secondary hover:bg-background-card-hover hover:text-text-primary'}"
|
||||
>
|
||||
{#if item.icon === 'feed'}
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.5"
|
||||
d="M6 5c7.18 0 13 5.82 13 13M6 11a7 7 0 017 7m-6 0a1 1 0 11-2 0 1 1 0 012 0z"
|
||||
/>
|
||||
</svg>
|
||||
{:else if item.icon === 'summaries'}
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.5"
|
||||
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"
|
||||
/>
|
||||
</svg>
|
||||
{:else if item.icon === 'indepth'}
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.5"
|
||||
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"
|
||||
/>
|
||||
</svg>
|
||||
{:else if item.icon === 'saved'}
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.5"
|
||||
d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
<span>{item.label}</span>
|
||||
</a>
|
||||
{/each}
|
||||
</nav>
|
||||
|
||||
<!-- User Menu -->
|
||||
<div class="p-4 border-t border-border">
|
||||
<a
|
||||
href="/profile"
|
||||
class="flex items-center gap-3 px-3 py-2 rounded-lg text-text-secondary hover:bg-background-card-hover hover:text-text-primary transition-colors"
|
||||
>
|
||||
<div class="w-8 h-8 bg-primary/20 rounded-full flex items-center justify-center">
|
||||
<span class="text-primary text-sm font-medium">
|
||||
{authStore.user?.name?.[0]?.toUpperCase() ||
|
||||
authStore.user?.email?.[0]?.toUpperCase() ||
|
||||
'?'}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-medium truncate">{authStore.user?.name || 'User'}</p>
|
||||
<p class="text-xs text-text-muted truncate">{authStore.user?.email}</p>
|
||||
</div>
|
||||
</a>
|
||||
<button
|
||||
onclick={handleLogout}
|
||||
class="w-full mt-2 flex items-center gap-3 px-3 py-2 rounded-lg text-text-secondary hover:bg-red-500/10 hover:text-red-400 transition-colors"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.5"
|
||||
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>Abmelden</span>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="flex-1 overflow-auto">
|
||||
{@render children()}
|
||||
</main>
|
||||
</div>
|
||||
<main
|
||||
class="main-content flex-1 transition-all duration-300 {isCollapsed
|
||||
? ''
|
||||
: isSidebarMode
|
||||
? 'pl-[180px]'
|
||||
: 'pt-20'}"
|
||||
>
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
{@render children()}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
<script lang="ts">
|
||||
import { AppsPage } from '@manacore/shared-ui';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Alle Apps - News</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="apps-page-wrapper">
|
||||
<AppsPage currentAppId="news" locale="de" title="Alle Apps" />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.apps-page-wrapper {
|
||||
min-height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,39 +1,100 @@
|
|||
<script lang="ts">
|
||||
import Navigation from '$lib/components/Navigation.svelte';
|
||||
import FloatingSidebar from '$lib/components/FloatingSidebar.svelte';
|
||||
import MobileSidebar from '$lib/components/MobileSidebar.svelte';
|
||||
import AccountSwitcher from '$lib/components/AccountSwitcher.svelte';
|
||||
import WorkspaceSwitcher from '$lib/components/WorkspaceSwitcher.svelte';
|
||||
import NotificationBell from '$lib/components/NotificationBell.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import { accountsStore } from '$lib/stores/accounts';
|
||||
import { workspacesStore } from '$lib/stores/workspaces';
|
||||
import { activeWorkspace } from '$lib/stores/activeWorkspace';
|
||||
import type { LayoutData } from './$types';
|
||||
import { PillNavigation } from '@manacore/shared-ui';
|
||||
import type { PillNavItem } from '@manacore/shared-ui';
|
||||
import { getPillAppItems } from '@manacore/shared-branding';
|
||||
|
||||
let { data, children }: { data: LayoutData; children: any } = $props();
|
||||
let sidebarCollapsed = $state(false);
|
||||
let mounted = $state(false);
|
||||
let mobileMenuOpen = $state(false);
|
||||
|
||||
// App switcher items
|
||||
const appItems = getPillAppItems('uload');
|
||||
|
||||
// User email for dropdown
|
||||
let userEmail = $derived(data.user?.email);
|
||||
|
||||
// Navigation items for uload
|
||||
const navItems: PillNavItem[] = [
|
||||
{ href: '/', label: 'Dashboard', icon: 'home' },
|
||||
{ href: '/links', label: 'Links', icon: 'link' },
|
||||
{ href: '/analytics', label: 'Analytics', icon: 'chart' },
|
||||
{ href: '/teams', label: 'Teams', icon: 'users' },
|
||||
{ href: '/settings', label: 'Settings', icon: 'settings' },
|
||||
];
|
||||
|
||||
let loading = $state(true);
|
||||
let isSidebarMode = $state(false);
|
||||
let isCollapsed = $state(false);
|
||||
let isDark = $state(false);
|
||||
|
||||
// Navigation shortcuts (Ctrl+1-5)
|
||||
const navRoutes = ['/', '/links', '/analytics', '/teams', '/settings'];
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
const target = event.target as HTMLElement;
|
||||
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((event.ctrlKey || event.metaKey) && !event.shiftKey && !event.altKey) {
|
||||
const num = parseInt(event.key);
|
||||
if (num >= 1 && num <= 5) {
|
||||
event.preventDefault();
|
||||
const route = navRoutes[num - 1];
|
||||
if (route) {
|
||||
goto(route);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleModeChange(isSidebar: boolean) {
|
||||
isSidebarMode = isSidebar;
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem('uload-nav-sidebar', String(isSidebar));
|
||||
}
|
||||
}
|
||||
|
||||
function handleCollapsedChange(collapsed: boolean) {
|
||||
isCollapsed = collapsed;
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem('uload-nav-collapsed', String(collapsed));
|
||||
}
|
||||
}
|
||||
|
||||
function handleToggleTheme() {
|
||||
isDark = !isDark;
|
||||
document.documentElement.classList.toggle('dark', isDark);
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem('uload-dark-mode', String(isDark));
|
||||
}
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
// Clear local storage and redirect
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.removeItem('auth_token');
|
||||
}
|
||||
goto('/login');
|
||||
}
|
||||
|
||||
// Watch for URL workspace parameter changes
|
||||
$effect(() => {
|
||||
const urlWorkspaceId = $page.url.searchParams.get('workspace');
|
||||
if (urlWorkspaceId) {
|
||||
// URL parameter takes precedence
|
||||
activeWorkspace.initFromUrl(urlWorkspaceId);
|
||||
}
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
mounted = true;
|
||||
|
||||
// Initialize both stores during migration
|
||||
if (data.user) {
|
||||
// Old accounts store for backwards compatibility
|
||||
accountsStore.init(data.user, data.sharedAccounts || [], data.viewingAs);
|
||||
// New workspaces store
|
||||
workspacesStore.init(
|
||||
data.user,
|
||||
data.personalWorkspace,
|
||||
|
|
@ -41,11 +102,9 @@
|
|||
data.currentWorkspaceId
|
||||
);
|
||||
|
||||
// Initialize active workspace from URL or localStorage
|
||||
const urlWorkspaceId = $page.url.searchParams.get('workspace');
|
||||
if (urlWorkspaceId) {
|
||||
activeWorkspace.initFromUrl(urlWorkspaceId);
|
||||
// Try to find workspace data
|
||||
const workspace =
|
||||
data.teamWorkspaces?.find((w) => w.id === urlWorkspaceId) ||
|
||||
(data.personalWorkspace?.id === urlWorkspaceId ? data.personalWorkspace : null);
|
||||
|
|
@ -55,93 +114,78 @@
|
|||
}
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
const stored = localStorage.getItem('sidebar-collapsed');
|
||||
if (stored !== null) {
|
||||
sidebarCollapsed = stored === 'true';
|
||||
// Restore nav mode from localStorage
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
const savedSidebar = localStorage.getItem('uload-nav-sidebar');
|
||||
if (savedSidebar === 'true') {
|
||||
isSidebarMode = true;
|
||||
}
|
||||
const savedCollapsed = localStorage.getItem('uload-nav-collapsed');
|
||||
if (savedCollapsed === 'true') {
|
||||
isCollapsed = true;
|
||||
}
|
||||
const savedDark = localStorage.getItem('uload-dark-mode');
|
||||
if (savedDark === 'true') {
|
||||
isDark = true;
|
||||
document.documentElement.classList.add('dark');
|
||||
}
|
||||
|
||||
// Listen for storage changes to sync sidebar state
|
||||
const handleStorageChange = () => {
|
||||
const stored = localStorage.getItem('sidebar-collapsed');
|
||||
if (stored !== null) {
|
||||
sidebarCollapsed = stored === 'true';
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('storage', handleStorageChange);
|
||||
return () => window.removeEventListener('storage', handleStorageChange);
|
||||
}
|
||||
|
||||
loading = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Full screen background container -->
|
||||
<div class="fixed inset-0 -z-10 bg-theme-background"></div>
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
<!-- Floating Sidebar for authenticated users on desktop -->
|
||||
<FloatingSidebar user={data.user} />
|
||||
|
||||
<!-- Mobile Sidebar (overlay) -->
|
||||
<MobileSidebar user={data.user} open={mobileMenuOpen} onClose={() => (mobileMenuOpen = false)} />
|
||||
|
||||
<!-- Top Navigation Bar with Menu Button for mobile/tablet -->
|
||||
{#if data.user}
|
||||
<nav
|
||||
class="bg-theme-surface/80 sticky top-0 z-30 border-b border-theme-border shadow-sm backdrop-blur-xl lg:hidden"
|
||||
>
|
||||
<div class="mx-auto max-w-7xl px-4 sm:px-6">
|
||||
<div class="flex h-16 items-center justify-between">
|
||||
<!-- Logo & Menu Button -->
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
onclick={() => (mobileMenuOpen = !mobileMenuOpen)}
|
||||
class="rounded-lg p-2 text-theme-text transition-colors hover:bg-theme-surface-hover"
|
||||
aria-label="Menu"
|
||||
>
|
||||
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<a href="/" class="flex items-center space-x-2 transition-opacity hover:opacity-80">
|
||||
<svg
|
||||
class="h-8 w-8 text-theme-primary"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"
|
||||
/>
|
||||
</svg>
|
||||
<span class="text-xl font-bold text-theme-text">uload</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Notifications & Workspace Switcher -->
|
||||
<div class="flex items-center gap-2">
|
||||
<NotificationBell />
|
||||
<WorkspaceSwitcher />
|
||||
</div>
|
||||
</div>
|
||||
{#if loading}
|
||||
<div class="flex min-h-screen items-center justify-center">
|
||||
<div class="text-center">
|
||||
<div
|
||||
class="mb-4 inline-block h-12 w-12 animate-spin rounded-full border-4 border-solid border-indigo-500 border-r-transparent"
|
||||
></div>
|
||||
<p class="text-gray-600 dark:text-gray-400">Laden...</p>
|
||||
</div>
|
||||
</nav>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex min-h-screen flex-col">
|
||||
<PillNavigation
|
||||
items={navItems}
|
||||
currentPath={$page.url.pathname}
|
||||
appName="uload"
|
||||
homeRoute="/"
|
||||
onLogout={handleLogout}
|
||||
onToggleTheme={handleToggleTheme}
|
||||
{isDark}
|
||||
{isSidebarMode}
|
||||
onModeChange={handleModeChange}
|
||||
{isCollapsed}
|
||||
onCollapsedChange={handleCollapsedChange}
|
||||
showThemeToggle={true}
|
||||
primaryColor="#6366f1"
|
||||
showAppSwitcher={true}
|
||||
{appItems}
|
||||
{userEmail}
|
||||
settingsHref="/settings"
|
||||
manaHref="/subscription"
|
||||
profileHref="/profile"
|
||||
allAppsHref="/apps"
|
||||
>
|
||||
{#snippet logo()}
|
||||
<span class="text-xl">🔗</span>
|
||||
<span class="pill-label font-bold">uload</span>
|
||||
{/snippet}
|
||||
</PillNavigation>
|
||||
|
||||
<!-- Main Content with responsive margin -->
|
||||
<main
|
||||
class="min-h-screen transition-all duration-300 {mounted && data.user
|
||||
? sidebarCollapsed
|
||||
? 'lg:pl-24'
|
||||
: 'lg:pl-72'
|
||||
: ''}"
|
||||
>
|
||||
{@render children?.()}
|
||||
</main>
|
||||
<main
|
||||
class="main-content flex-1 transition-all duration-300 {isCollapsed
|
||||
? ''
|
||||
: isSidebarMode
|
||||
? 'pl-[180px]'
|
||||
: 'pt-20'}"
|
||||
>
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
<script lang="ts">
|
||||
import { AppsPage } from '@manacore/shared-ui';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Alle Apps - uload</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="apps-page-wrapper">
|
||||
<AppsPage currentAppId="uload" locale="de" title="Alle Apps" />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.apps-page-wrapper {
|
||||
min-height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -5,10 +5,79 @@
|
|||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { initWebSocket, cleanup, isConnected } from '$lib/stores/jobs';
|
||||
import type { LayoutData } from './$types';
|
||||
import { PillNavigation } from '@manacore/shared-ui';
|
||||
import type { PillNavItem } from '@manacore/shared-ui';
|
||||
import { getPillAppItems } from '@manacore/shared-branding';
|
||||
|
||||
let { children, data }: { children: any; data: LayoutData } = $props();
|
||||
|
||||
// App switcher items
|
||||
const appItems = getPillAppItems('wisekeep');
|
||||
|
||||
// User email for dropdown
|
||||
let userEmail = $derived(authStore.user?.email);
|
||||
|
||||
// Navigation items for Wisekeep
|
||||
const navItems: PillNavItem[] = [
|
||||
{ href: '/dashboard', label: 'Dashboard', icon: 'home' },
|
||||
{ href: '/transcribe', label: 'Transcribe', icon: 'mic' },
|
||||
{ href: '/transcripts', label: 'Transcripts', icon: 'document' },
|
||||
{ href: '/playlists', label: 'Playlists', icon: 'list' },
|
||||
{ href: '/settings', label: 'Settings', icon: 'settings' },
|
||||
];
|
||||
|
||||
let isChecking = $state(true);
|
||||
let isSidebarMode = $state(false);
|
||||
let isCollapsed = $state(false);
|
||||
let isDark = $state(false);
|
||||
|
||||
// Navigation shortcuts (Ctrl+1-5)
|
||||
const navRoutes = ['/dashboard', '/transcribe', '/transcripts', '/playlists', '/settings'];
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
const target = event.target as HTMLElement;
|
||||
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((event.ctrlKey || event.metaKey) && !event.shiftKey && !event.altKey) {
|
||||
const num = parseInt(event.key);
|
||||
if (num >= 1 && num <= 5) {
|
||||
event.preventDefault();
|
||||
const route = navRoutes[num - 1];
|
||||
if (route) {
|
||||
goto(route);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleModeChange(isSidebar: boolean) {
|
||||
isSidebarMode = isSidebar;
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem('wisekeep-nav-sidebar', String(isSidebar));
|
||||
}
|
||||
}
|
||||
|
||||
function handleCollapsedChange(collapsed: boolean) {
|
||||
isCollapsed = collapsed;
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem('wisekeep-nav-collapsed', String(collapsed));
|
||||
}
|
||||
}
|
||||
|
||||
function handleToggleTheme() {
|
||||
isDark = !isDark;
|
||||
document.documentElement.classList.toggle('dark', isDark);
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem('wisekeep-dark-mode', String(isDark));
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSignOut() {
|
||||
await authStore.signOut();
|
||||
goto('/login');
|
||||
}
|
||||
|
||||
// Check auth on mount and redirect if not authenticated
|
||||
onMount(async () => {
|
||||
|
|
@ -27,6 +96,23 @@
|
|||
shouldRedirect = true;
|
||||
}
|
||||
|
||||
// Restore nav mode from localStorage
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
const savedSidebar = localStorage.getItem('wisekeep-nav-sidebar');
|
||||
if (savedSidebar === 'true') {
|
||||
isSidebarMode = true;
|
||||
}
|
||||
const savedCollapsed = localStorage.getItem('wisekeep-nav-collapsed');
|
||||
if (savedCollapsed === 'true') {
|
||||
isCollapsed = true;
|
||||
}
|
||||
const savedDark = localStorage.getItem('wisekeep-dark-mode');
|
||||
if (savedDark === 'true') {
|
||||
isDark = true;
|
||||
document.documentElement.classList.add('dark');
|
||||
}
|
||||
}
|
||||
|
||||
// Always set isChecking to false
|
||||
isChecking = false;
|
||||
|
||||
|
|
@ -38,87 +124,59 @@
|
|||
// Return cleanup function
|
||||
return () => cleanup();
|
||||
});
|
||||
|
||||
async function handleSignOut() {
|
||||
await authStore.signOut();
|
||||
goto('/login');
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
{#if isChecking}
|
||||
<!-- Loading state while checking auth -->
|
||||
<div class="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600"></div>
|
||||
<div class="flex min-h-screen items-center justify-center">
|
||||
<div class="text-center">
|
||||
<div
|
||||
class="mb-4 inline-block h-12 w-12 animate-spin rounded-full border-4 border-solid border-purple-600 border-r-transparent"
|
||||
></div>
|
||||
<p class="text-gray-600 dark:text-gray-400">Laden...</p>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="min-h-screen flex flex-col">
|
||||
<header class="bg-white shadow-sm border-b">
|
||||
<div class="max-w-7xl mx-auto px-4 py-4 flex items-center justify-between">
|
||||
<a href="/dashboard" class="text-xl font-bold text-purple-600">Wisekeep</a>
|
||||
<nav class="flex items-center gap-6">
|
||||
<a
|
||||
href="/dashboard"
|
||||
class="transition-colors {$page.url.pathname === '/dashboard'
|
||||
? 'text-purple-600 font-medium'
|
||||
: 'text-gray-600 hover:text-gray-900'}"
|
||||
>
|
||||
Dashboard
|
||||
</a>
|
||||
<a
|
||||
href="/transcribe"
|
||||
class="transition-colors {$page.url.pathname === '/transcribe'
|
||||
? 'text-purple-600 font-medium'
|
||||
: 'text-gray-600 hover:text-gray-900'}"
|
||||
>
|
||||
Transcribe
|
||||
</a>
|
||||
<a
|
||||
href="/transcripts"
|
||||
class="transition-colors {$page.url.pathname === '/transcripts'
|
||||
? 'text-purple-600 font-medium'
|
||||
: 'text-gray-600 hover:text-gray-900'}"
|
||||
>
|
||||
Transcripts
|
||||
</a>
|
||||
<a
|
||||
href="/playlists"
|
||||
class="transition-colors {$page.url.pathname === '/playlists'
|
||||
? 'text-purple-600 font-medium'
|
||||
: 'text-gray-600 hover:text-gray-900'}"
|
||||
>
|
||||
Playlists
|
||||
</a>
|
||||
</nav>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-2 h-2 rounded-full {$isConnected ? 'bg-green-500' : 'bg-red-500'}"></span>
|
||||
<span class="text-sm text-gray-500">
|
||||
{$isConnected ? 'Connected' : 'Disconnected'}
|
||||
</span>
|
||||
</div>
|
||||
{#if authStore.user}
|
||||
<span class="text-sm text-gray-600 hidden sm:block">
|
||||
{authStore.user.email}
|
||||
</span>
|
||||
{/if}
|
||||
<button
|
||||
onclick={handleSignOut}
|
||||
class="px-3 py-2 text-sm font-medium text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
|
||||
>
|
||||
Abmelden
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="flex min-h-screen flex-col">
|
||||
<PillNavigation
|
||||
items={navItems}
|
||||
currentPath={$page.url.pathname}
|
||||
appName="Wisekeep"
|
||||
homeRoute="/dashboard"
|
||||
onLogout={handleSignOut}
|
||||
onToggleTheme={handleToggleTheme}
|
||||
{isDark}
|
||||
{isSidebarMode}
|
||||
onModeChange={handleModeChange}
|
||||
{isCollapsed}
|
||||
onCollapsedChange={handleCollapsedChange}
|
||||
showThemeToggle={true}
|
||||
primaryColor="#9333ea"
|
||||
showAppSwitcher={true}
|
||||
{appItems}
|
||||
{userEmail}
|
||||
settingsHref="/settings"
|
||||
manaHref="/subscription"
|
||||
profileHref="/profile"
|
||||
allAppsHref="/apps"
|
||||
>
|
||||
{#snippet logo()}
|
||||
<span class="text-xl">🧠</span>
|
||||
<span class="pill-label font-bold">Wisekeep</span>
|
||||
{/snippet}
|
||||
</PillNavigation>
|
||||
|
||||
<main class="flex-1">
|
||||
{@render children()}
|
||||
<main
|
||||
class="main-content flex-1 transition-all duration-300 {isCollapsed
|
||||
? ''
|
||||
: isSidebarMode
|
||||
? 'pl-[180px]'
|
||||
: 'pt-20'}"
|
||||
>
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
{@render children()}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="bg-gray-100 border-t py-4">
|
||||
<div class="max-w-7xl mx-auto px-4 text-center text-sm text-gray-500">
|
||||
Wisekeep - AI-powered wisdom extraction from video
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
<script lang="ts">
|
||||
import { AppsPage } from '@manacore/shared-ui';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Alle Apps - Wisekeep</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="apps-page-wrapper">
|
||||
<AppsPage currentAppId="wisekeep" locale="de" title="Alle Apps" />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.apps-page-wrapper {
|
||||
min-height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -205,6 +205,7 @@
|
|||
settingsHref="/settings"
|
||||
manaHref="/mana"
|
||||
profileHref="/profile"
|
||||
allAppsHref="/apps"
|
||||
/>
|
||||
|
||||
<!-- Main Content with dynamic padding based on nav mode -->
|
||||
|
|
|
|||
14
apps/chat/apps/web/src/routes/(protected)/apps/+page.svelte
Normal file
14
apps/chat/apps/web/src/routes/(protected)/apps/+page.svelte
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<script lang="ts">
|
||||
import { AppsPage } from '@manacore/shared-ui';
|
||||
</script>
|
||||
|
||||
<div class="apps-page-wrapper">
|
||||
<AppsPage currentAppId="chat" locale="de" title="Alle Apps" />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.apps-page-wrapper {
|
||||
background-color: hsl(var(--background));
|
||||
min-height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -191,6 +191,7 @@
|
|||
settingsHref="/settings"
|
||||
manaHref="/mana"
|
||||
profileHref="/profile"
|
||||
allAppsHref="/apps"
|
||||
/>
|
||||
|
||||
<!-- Main content with dynamic padding -->
|
||||
|
|
|
|||
14
apps/manacore/apps/web/src/routes/(app)/apps/+page.svelte
Normal file
14
apps/manacore/apps/web/src/routes/(app)/apps/+page.svelte
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<script lang="ts">
|
||||
import { AppsPage } from '@manacore/shared-ui';
|
||||
</script>
|
||||
|
||||
<div class="apps-page-wrapper">
|
||||
<AppsPage currentAppId="manacore" locale="de" title="Alle Apps" />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.apps-page-wrapper {
|
||||
background-color: hsl(var(--background));
|
||||
min-height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -27,13 +27,11 @@
|
|||
// Get theme state
|
||||
let isDark = $derived(theme.isDark);
|
||||
|
||||
// Navigation items for ManaDeck
|
||||
// Navigation items for ManaDeck (Mana and Profile are in user dropdown)
|
||||
const navItems: PillNavItem[] = [
|
||||
{ href: '/decks', label: 'Decks', icon: 'archive' },
|
||||
{ href: '/explore', label: 'Explore', icon: 'search' },
|
||||
{ href: '/progress', label: 'Progress', icon: 'chart' },
|
||||
{ href: '/mana', label: 'Mana', icon: 'mana' },
|
||||
{ href: '/profile', label: 'Profile', icon: 'user' },
|
||||
];
|
||||
|
||||
// Theme variant dropdown items
|
||||
|
|
@ -196,6 +194,7 @@
|
|||
settingsHref="/settings"
|
||||
manaHref="/mana"
|
||||
profileHref="/profile"
|
||||
allAppsHref="/apps"
|
||||
/>
|
||||
|
||||
<!-- Main content with dynamic padding -->
|
||||
|
|
|
|||
14
apps/manadeck/apps/web/src/routes/(app)/apps/+page.svelte
Normal file
14
apps/manadeck/apps/web/src/routes/(app)/apps/+page.svelte
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<script lang="ts">
|
||||
import { AppsPage } from '@manacore/shared-ui';
|
||||
</script>
|
||||
|
||||
<div class="apps-page-wrapper">
|
||||
<AppsPage currentAppId="manadeck" locale="de" title="Alle Apps" />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.apps-page-wrapper {
|
||||
background-color: hsl(var(--background));
|
||||
min-height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -239,6 +239,7 @@
|
|||
settingsHref="/app/settings"
|
||||
manaHref="/app/mana"
|
||||
profileHref="/app/profile"
|
||||
allAppsHref="/app/apps"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
|
|
|
|||
14
apps/picture/apps/web/src/routes/app/apps/+page.svelte
Normal file
14
apps/picture/apps/web/src/routes/app/apps/+page.svelte
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<script lang="ts">
|
||||
import { AppsPage } from '@manacore/shared-ui';
|
||||
</script>
|
||||
|
||||
<div class="apps-page-wrapper">
|
||||
<AppsPage currentAppId="picture" locale="de" title="Alle Apps" />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.apps-page-wrapper {
|
||||
background-color: hsl(var(--color-background));
|
||||
min-height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -61,12 +61,8 @@
|
|||
let userEmail = $derived(auth.user?.email);
|
||||
|
||||
// Navigation items for Presi
|
||||
const navItems: PillNavItem[] = [
|
||||
{ href: '/', label: 'Decks', icon: 'document' },
|
||||
{ href: '/profile', label: 'Profil', icon: 'user' },
|
||||
{ href: '/mana', label: 'Mana', icon: 'sparkles' },
|
||||
{ href: '/settings', label: 'Einstellungen', icon: 'settings' },
|
||||
];
|
||||
// Profile, Mana, and Settings are in the user dropdown via profileHref, manaHref, settingsHref
|
||||
const navItems: PillNavItem[] = [{ href: '/', label: 'Decks', icon: 'document' }];
|
||||
|
||||
// Public routes that don't require auth
|
||||
const publicRoutes = ['/login', '/register', '/forgot-password'];
|
||||
|
|
@ -205,6 +201,7 @@
|
|||
settingsHref="/settings"
|
||||
manaHref="/mana"
|
||||
profileHref="/profile"
|
||||
allAppsHref="/apps"
|
||||
/>
|
||||
|
||||
<!-- Main Content with dynamic padding based on nav mode -->
|
||||
|
|
|
|||
14
apps/presi/apps/web/src/routes/apps/+page.svelte
Normal file
14
apps/presi/apps/web/src/routes/apps/+page.svelte
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<script lang="ts">
|
||||
import { AppsPage } from '@manacore/shared-ui';
|
||||
</script>
|
||||
|
||||
<div class="apps-page-wrapper">
|
||||
<AppsPage currentAppId="presi" locale="de" title="Alle Apps" />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.apps-page-wrapper {
|
||||
background-color: hsl(var(--background));
|
||||
min-height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -68,7 +68,7 @@
|
|||
|
||||
// Navigation items for Zitare
|
||||
const navItems: PillNavItem[] = [
|
||||
{ href: '/', label: 'Zitate', icon: 'chat' },
|
||||
{ href: '/', label: 'Zitate', icon: 'document' },
|
||||
{ href: '/search', label: 'Suche', icon: 'search' },
|
||||
{ href: '/authors', label: 'Autoren', icon: 'users' },
|
||||
{ href: '/favorites', label: 'Favoriten', icon: 'heart' },
|
||||
|
|
@ -214,10 +214,15 @@
|
|||
class="main-content bg-background"
|
||||
class:sidebar-mode={isSidebarMode && !isCollapsed}
|
||||
class:floating-mode={!isSidebarMode && !isCollapsed}
|
||||
class:full-height={$page.url.pathname === '/'}
|
||||
>
|
||||
<div class="content-wrapper">
|
||||
{#if $page.url.pathname === '/'}
|
||||
{@render children()}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="content-wrapper">
|
||||
{@render children()}
|
||||
</div>
|
||||
{/if}
|
||||
</main>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -244,6 +249,23 @@
|
|||
padding-left: 180px;
|
||||
}
|
||||
|
||||
/* Full height mode for scrollable pages like home */
|
||||
.main-content.full-height {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 100px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.main-content.full-height.floating-mode {
|
||||
height: 100vh;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.main-content.full-height.sidebar-mode {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
max-width: 80rem; /* max-w-7xl */
|
||||
margin-left: auto;
|
||||
|
|
|
|||
14
apps/zitare/apps/web/src/routes/apps/+page.svelte
Normal file
14
apps/zitare/apps/web/src/routes/apps/+page.svelte
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<script lang="ts">
|
||||
import { AppsPage } from '@manacore/shared-ui';
|
||||
</script>
|
||||
|
||||
<div class="apps-page-wrapper">
|
||||
<AppsPage currentAppId="zitare" locale="de" title="Alle Apps" />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.apps-page-wrapper {
|
||||
background-color: hsl(var(--background));
|
||||
min-height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -32,6 +32,7 @@
|
|||
"svelte": "^5.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@manacore/shared-branding": "workspace:*",
|
||||
"@manacore/shared-icons": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,3 +58,6 @@ export {
|
|||
SettingsDangerZone,
|
||||
SettingsDangerButton,
|
||||
} from './settings';
|
||||
|
||||
// Pages
|
||||
export { default as AppsPage } from './pages/AppsPage.svelte';
|
||||
|
|
|
|||
|
|
@ -4,9 +4,13 @@
|
|||
import PillDropdown from './PillDropdown.svelte';
|
||||
import PillTabGroup from './PillTabGroup.svelte';
|
||||
|
||||
// Convert app items to dropdown items
|
||||
function appItemsToDropdownItems(apps: PillAppItem[]): PillDropdownItem[] {
|
||||
return apps.map((app) => ({
|
||||
// Convert app items to dropdown items (will be computed as derived)
|
||||
function createAppDropdownItems(
|
||||
apps: PillAppItem[],
|
||||
allAppsUrl?: string,
|
||||
allAppsText?: string
|
||||
): PillDropdownItem[] {
|
||||
const items: PillDropdownItem[] = apps.map((app) => ({
|
||||
id: app.id,
|
||||
label: app.name,
|
||||
// Use image icon if available, otherwise use grid as fallback
|
||||
|
|
@ -23,6 +27,24 @@
|
|||
active: app.isCurrent,
|
||||
disabled: false,
|
||||
}));
|
||||
|
||||
// Add "All Apps" link at the end if href is provided
|
||||
if (allAppsUrl) {
|
||||
items.push(
|
||||
{ id: 'all-apps-divider', label: '', divider: true },
|
||||
{
|
||||
id: 'all-apps',
|
||||
label: allAppsText || 'Alle Apps',
|
||||
icon: 'grid',
|
||||
onClick: () => {
|
||||
window.location.href = allAppsUrl;
|
||||
},
|
||||
active: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
|
|
@ -86,6 +108,12 @@
|
|||
manaHref?: string;
|
||||
/** Profile page href */
|
||||
profileHref?: string;
|
||||
/** Login page href (shown when not logged in) */
|
||||
loginHref?: string;
|
||||
/** All Apps page href */
|
||||
allAppsHref?: string;
|
||||
/** All Apps label (default: "Alle Apps") */
|
||||
allAppsLabel?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
|
|
@ -119,6 +147,9 @@
|
|||
settingsHref = '/settings',
|
||||
manaHref,
|
||||
profileHref,
|
||||
loginHref,
|
||||
allAppsHref,
|
||||
allAppsLabel = 'Alle Apps',
|
||||
}: Props = $props();
|
||||
|
||||
// Type guards for elements
|
||||
|
|
@ -245,7 +276,7 @@
|
|||
<!-- Logo pill / App Switcher -->
|
||||
{#if showAppSwitcher && appItems.length > 0}
|
||||
<PillDropdown
|
||||
items={appItemsToDropdownItems(appItems)}
|
||||
items={createAppDropdownItems(appItems, allAppsHref, allAppsLabel)}
|
||||
direction="down"
|
||||
label={appName}
|
||||
icon="grid"
|
||||
|
|
@ -467,14 +498,29 @@
|
|||
},
|
||||
]
|
||||
: []),
|
||||
{ id: 'logout-divider', label: '', divider: true },
|
||||
{
|
||||
id: 'logout',
|
||||
label: 'Logout',
|
||||
icon: 'logout',
|
||||
onClick: () => onLogout?.(),
|
||||
danger: true,
|
||||
},
|
||||
{ id: 'auth-divider', label: '', divider: true },
|
||||
...(showLogout && onLogout
|
||||
? [
|
||||
{
|
||||
id: 'logout',
|
||||
label: 'Logout',
|
||||
icon: 'logout',
|
||||
onClick: () => onLogout?.(),
|
||||
danger: true,
|
||||
},
|
||||
]
|
||||
: loginHref
|
||||
? [
|
||||
{
|
||||
id: 'login',
|
||||
label: 'Login',
|
||||
icon: 'user',
|
||||
onClick: () => {
|
||||
window.location.href = loginHref;
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]}
|
||||
direction="down"
|
||||
label={truncateEmail(userEmail)}
|
||||
|
|
|
|||
616
packages/shared-ui/src/pages/AppsPage.svelte
Normal file
616
packages/shared-ui/src/pages/AppsPage.svelte
Normal file
|
|
@ -0,0 +1,616 @@
|
|||
<script lang="ts">
|
||||
import { MANA_APPS, APP_URLS, APP_STATUS_LABELS, type ManaApp, type AppIconId } from '@manacore/shared-branding';
|
||||
|
||||
interface Props {
|
||||
/** Current app ID to highlight */
|
||||
currentAppId?: AppIconId;
|
||||
/** Page title */
|
||||
title?: string;
|
||||
/** Locale for descriptions */
|
||||
locale?: 'de' | 'en';
|
||||
/** Custom app click handler */
|
||||
onAppClick?: (app: ManaApp) => void;
|
||||
}
|
||||
|
||||
let {
|
||||
currentAppId,
|
||||
title = 'Alle Apps',
|
||||
locale = 'de',
|
||||
onAppClick,
|
||||
}: Props = $props();
|
||||
|
||||
// Filter active apps (non-archived)
|
||||
const apps = $derived(MANA_APPS.filter((app) => !app.archived));
|
||||
|
||||
// Get status labels for current locale
|
||||
const statusLabels = $derived(APP_STATUS_LABELS[locale]);
|
||||
|
||||
// Modal state
|
||||
let selectedAppIndex = $state<number | null>(null);
|
||||
let hoveredAppIndex = $state<number | null>(null);
|
||||
let cardRotations = $state<{ [key: number]: { rotateX: number; rotateY: number } }>({});
|
||||
let modalScrollContainer = $state<HTMLDivElement | null>(null);
|
||||
|
||||
// Detect dev mode
|
||||
const isDev = $derived(
|
||||
typeof window !== 'undefined' &&
|
||||
(window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1')
|
||||
);
|
||||
|
||||
function getStatusColor(status: ManaApp['status']) {
|
||||
const colors = {
|
||||
published: '#4CAF50',
|
||||
beta: '#FFD700',
|
||||
development: '#FF9800',
|
||||
planning: '#F44336',
|
||||
};
|
||||
return colors[status];
|
||||
}
|
||||
|
||||
function getAppUrl(appId: AppIconId): string | undefined {
|
||||
const urls = APP_URLS[appId];
|
||||
if (!urls) return undefined;
|
||||
return isDev ? urls.dev : urls.prod;
|
||||
}
|
||||
|
||||
function openModal(index: number) {
|
||||
selectedAppIndex = index;
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
selectedAppIndex = null;
|
||||
}
|
||||
|
||||
function handleCardMouseMove(e: MouseEvent, index: number, cardElement: HTMLElement) {
|
||||
const rect = cardElement.getBoundingClientRect();
|
||||
const cardCenterX = rect.left + rect.width / 2;
|
||||
const cardCenterY = rect.top + rect.height / 2;
|
||||
|
||||
const mouseXRelative = e.clientX - cardCenterX;
|
||||
const mouseYRelative = e.clientY - cardCenterY;
|
||||
|
||||
const maxRotation = 3;
|
||||
const rotateY = (mouseXRelative / (rect.width / 2)) * maxRotation;
|
||||
const rotateX = -(mouseYRelative / (rect.height / 2)) * maxRotation;
|
||||
|
||||
cardRotations[index] = { rotateX, rotateY };
|
||||
}
|
||||
|
||||
function handleCardMouseLeave(index: number) {
|
||||
cardRotations[index] = { rotateX: 0, rotateY: 0 };
|
||||
}
|
||||
|
||||
function handleAppAction(app: ManaApp) {
|
||||
if (onAppClick) {
|
||||
onAppClick(app);
|
||||
} else {
|
||||
const url = getAppUrl(app.id);
|
||||
if (url) {
|
||||
if (app.id === currentAppId) {
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
window.open(url, '_blank', 'noopener,noreferrer');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape' && selectedAppIndex !== null) {
|
||||
closeModal();
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll to selected app in modal
|
||||
$effect(() => {
|
||||
if (selectedAppIndex !== null && modalScrollContainer) {
|
||||
const appIndex = selectedAppIndex;
|
||||
setTimeout(() => {
|
||||
const cardWidth = 360 + 24;
|
||||
const scrollPosition = appIndex * cardWidth;
|
||||
modalScrollContainer?.scrollTo({
|
||||
left: scrollPosition,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}, 50);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
<div class="apps-page">
|
||||
<h1 class="page-title">{title}</h1>
|
||||
|
||||
<div class="apps-grid">
|
||||
{#each apps as app, index}
|
||||
<button
|
||||
class="app-card"
|
||||
class:current={app.id === currentAppId}
|
||||
style="--app-color: {app.color};"
|
||||
onclick={() => openModal(index)}
|
||||
>
|
||||
<div
|
||||
class="status-indicator"
|
||||
style="background-color: {getStatusColor(app.status)};"
|
||||
title={statusLabels[app.status]}
|
||||
></div>
|
||||
|
||||
<div class="app-icon-wrapper">
|
||||
{#if app.icon}
|
||||
<img src={app.icon} alt={app.name} class="app-icon" />
|
||||
{:else}
|
||||
<div class="app-icon-fallback" style="color: {app.color};">
|
||||
{app.name.charAt(0)}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<h3 class="app-name">{app.name}</h3>
|
||||
<p class="app-description">{app.description[locale]}</p>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal -->
|
||||
{#if selectedAppIndex !== null}
|
||||
<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"
|
||||
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 class="modal-cards-wrapper">
|
||||
{#each apps as app, index}
|
||||
<div
|
||||
class="modal-card"
|
||||
class:active={selectedAppIndex === index}
|
||||
class:current={app.id === currentAppId}
|
||||
style="--app-color: {app.color}; transform: perspective(1000px) rotateX({cardRotations[
|
||||
index
|
||||
]?.rotateX || 0}deg) rotateY({cardRotations[index]?.rotateY || 0}deg);"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
selectedAppIndex = index;
|
||||
}}
|
||||
onmouseenter={() => (hoveredAppIndex = index)}
|
||||
onmousemove={(e) => handleCardMouseMove(e, index, e.currentTarget)}
|
||||
onmouseleave={() => {
|
||||
handleCardMouseLeave(index);
|
||||
hoveredAppIndex = null;
|
||||
}}
|
||||
onkeydown={() => {}}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div class="modal-card-status">
|
||||
<div class="modal-status-dot" style="background-color: {getStatusColor(app.status)};"></div>
|
||||
<span class="modal-status-label">{statusLabels[app.status]}</span>
|
||||
</div>
|
||||
|
||||
{#if app.icon}
|
||||
<img src={app.icon} alt={app.name} class="modal-app-icon" />
|
||||
{:else}
|
||||
<div class="modal-app-icon-fallback" style="color: {app.color};">
|
||||
{app.name.charAt(0)}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<h3 class="modal-app-name">{app.name}</h3>
|
||||
|
||||
<p class="modal-app-tagline" style="color: {app.color};">
|
||||
{app.description[locale]}
|
||||
</p>
|
||||
|
||||
<p class="modal-app-description">{app.longDescription[locale]}</p>
|
||||
|
||||
<div class="modal-app-action">
|
||||
{#if app.comingSoon}
|
||||
<span class="modal-coming-soon">{locale === 'de' ? 'Demnächst' : 'Coming Soon'}</span>
|
||||
{:else}
|
||||
<button
|
||||
class="modal-open-btn"
|
||||
style="background-color: {app.color}40; border-color: {app.color};"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleAppAction(app);
|
||||
}}
|
||||
>
|
||||
{app.id === currentAppId
|
||||
? locale === 'de'
|
||||
? 'Zur Startseite'
|
||||
: 'Go to Home'
|
||||
: locale === 'de'
|
||||
? 'App öffnen'
|
||||
: 'Open App'}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.apps-page {
|
||||
padding: 1rem;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 2rem;
|
||||
text-align: center;
|
||||
color: hsl(var(--color-foreground, 0 0% 0%));
|
||||
}
|
||||
|
||||
:global(.dark) .page-title {
|
||||
color: hsl(var(--color-foreground, 0 0% 100%));
|
||||
}
|
||||
|
||||
/* Grid Layout */
|
||||
.apps-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.apps-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.apps-grid {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* App Card (Grid) */
|
||||
.app-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 1.5rem 1rem;
|
||||
border-radius: 1rem;
|
||||
cursor: pointer;
|
||||
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;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
:global(.dark) .app-card {
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
background-color: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.app-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 12px 24px -8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
:global(.dark) .app-card:hover {
|
||||
box-shadow: 0 12px 24px -8px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.app-card.current {
|
||||
border-color: var(--app-color);
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--app-color) 20%, transparent);
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
position: absolute;
|
||||
top: 0.75rem;
|
||||
right: 0.75rem;
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
border-radius: 50%;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.app-icon-wrapper {
|
||||
width: 3.5rem;
|
||||
height: 3.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.app-icon {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.app-icon-fallback {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.app-name {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 0.375rem;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
:global(.dark) .app-name {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
.app-description {
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.4;
|
||||
color: #6b7280;
|
||||
margin: 0;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:global(.dark) .app-description {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
/* Modal Styles */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 50;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba(0, 0, 0, 0.9);
|
||||
backdrop-filter: blur(8px);
|
||||
animation: fadeIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
.modal-close-btn {
|
||||
position: fixed;
|
||||
top: 1.5rem;
|
||||
right: 1.5rem;
|
||||
z-index: 60;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s, transform 0.2s;
|
||||
}
|
||||
|
||||
.modal-close-btn:hover {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.modal-scroll-container {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow-x: auto;
|
||||
scroll-snap-type: x mandatory;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.scrollbar-hide {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.modal-cards-wrapper {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
padding: 2rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.modal-card {
|
||||
flex-shrink: 0;
|
||||
width: 340px;
|
||||
padding: 2rem;
|
||||
padding-top: 2.5rem;
|
||||
border-radius: 1.5rem;
|
||||
scroll-snap-align: center;
|
||||
position: relative;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
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;
|
||||
animation: modalCardIn 0.3s ease-out both;
|
||||
}
|
||||
|
||||
.modal-card:hover {
|
||||
background-color: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.modal-card.current {
|
||||
border-color: var(--app-color);
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--app-color) 30%, transparent);
|
||||
}
|
||||
|
||||
.modal-card-status {
|
||||
position: absolute;
|
||||
top: 0.875rem;
|
||||
right: 0.875rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.25rem 0.625rem 0.25rem 0.5rem;
|
||||
border-radius: 1rem;
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.modal-status-dot {
|
||||
width: 0.375rem;
|
||||
height: 0.375rem;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.modal-status-label {
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.modal-app-icon {
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
object-fit: contain;
|
||||
margin: 0 auto 1rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.modal-app-icon-fallback {
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
margin: 0 auto 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
.modal-app-name {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
margin: 0 0 0.5rem;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.modal-app-tagline {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
.modal-app-description {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.6;
|
||||
text-align: center;
|
||||
margin: 0 0 1.5rem;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.modal-app-action {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal-coming-soon {
|
||||
display: inline-block;
|
||||
padding: 0.625rem 1.25rem;
|
||||
border-radius: 2rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.modal-open-btn {
|
||||
padding: 0.75rem 2rem;
|
||||
border-radius: 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
border: 2px solid;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s, transform 0.2s;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.modal-open-btn:hover {
|
||||
opacity: 0.85;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes modalCardIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95) translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.modal-overlay,
|
||||
.modal-card {
|
||||
animation: none;
|
||||
}
|
||||
.status-indicator {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
|
|
@ -2882,6 +2882,9 @@ importers:
|
|||
|
||||
packages/shared-ui:
|
||||
dependencies:
|
||||
'@manacore/shared-branding':
|
||||
specifier: workspace:*
|
||||
version: link:../shared-branding
|
||||
'@manacore/shared-icons':
|
||||
specifier: workspace:*
|
||||
version: link:../shared-icons
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue