mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-21 09:16:41 +02:00
feat(shared-ui, manacore/web): cross-app navigation enhancement (3 phases)
Phase 1: Enhanced App Drawer in PillNavigation - appNavigationStore: localStorage-backed favorites, recents, usage counts - AppDrawer: replaces PillDropdown for apps with search, favorites, recents, grid - All apps using PillNavigation get this automatically Phase 2: Cmd+K Command Palette (GlobalSpotlight) - GlobalSpotlight: modal with app search, quick actions, keyboard navigation - useGlobalSpotlight: Cmd+K / Ctrl+K keyboard listener - Integrated into PillNavigation via optional spotlightActions prop Phase 3: Improved /home page - AppRow: horizontal scrollable app row for favorites/recents with pin toggle - ActivityFeed: cross-app timeline (completed tasks, upcoming events, contacts) - Replaced hardcoded 3-category layout with dynamic favorites, recents, activity feed, and usage-frequency sorted app grid Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4dfa2cc899
commit
ffd608cbb9
10 changed files with 1985 additions and 133 deletions
225
apps/manacore/apps/web/src/lib/components/ActivityFeed.svelte
Normal file
225
apps/manacore/apps/web/src/lib/components/ActivityFeed.svelte
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
<script lang="ts">
|
||||
import {
|
||||
crossTaskCollection,
|
||||
crossEventCollection,
|
||||
crossContactCollection,
|
||||
} from '$lib/data/cross-app-stores';
|
||||
|
||||
interface Props {
|
||||
maxItems?: number;
|
||||
locale?: 'de' | 'en';
|
||||
}
|
||||
|
||||
let { maxItems = 8, locale = 'de' }: Props = $props();
|
||||
|
||||
interface FeedItem {
|
||||
id: string;
|
||||
type: 'task' | 'event' | 'contact';
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
timestamp: string;
|
||||
color: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
// Query recent completed tasks
|
||||
let recentTasks = $state<FeedItem[]>([]);
|
||||
let upcomingEvents = $state<FeedItem[]>([]);
|
||||
let recentContacts = $state<FeedItem[]>([]);
|
||||
|
||||
$effect(() => {
|
||||
loadFeed();
|
||||
});
|
||||
|
||||
async function loadFeed() {
|
||||
try {
|
||||
// Completed tasks from today
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const todayStr = today.toISOString();
|
||||
|
||||
const tasks = await crossTaskCollection.getAll();
|
||||
recentTasks = tasks
|
||||
.filter((t) => t.isCompleted && t.completedAt && t.completedAt >= todayStr)
|
||||
.sort((a, b) => (b.completedAt || '').localeCompare(a.completedAt || ''))
|
||||
.slice(0, 5)
|
||||
.map((t) => ({
|
||||
id: `task-${t.id}`,
|
||||
type: 'task' as const,
|
||||
title: t.title,
|
||||
subtitle: locale === 'de' ? 'Erledigt' : 'Completed',
|
||||
timestamp: t.completedAt || t.updatedAt,
|
||||
color: '#22c55e',
|
||||
icon: '\u2705',
|
||||
}));
|
||||
} catch {
|
||||
recentTasks = [];
|
||||
}
|
||||
|
||||
try {
|
||||
// Upcoming events (next 24h)
|
||||
const now = new Date().toISOString();
|
||||
const tomorrow = new Date(Date.now() + 86400000).toISOString();
|
||||
|
||||
const events = await crossEventCollection.getAll();
|
||||
upcomingEvents = events
|
||||
.filter((e) => e.startDate >= now && e.startDate <= tomorrow)
|
||||
.sort((a, b) => a.startDate.localeCompare(b.startDate))
|
||||
.slice(0, 5)
|
||||
.map((e) => ({
|
||||
id: `event-${e.id}`,
|
||||
type: 'event' as const,
|
||||
title: e.title,
|
||||
subtitle: formatTime(e.startDate),
|
||||
timestamp: e.startDate,
|
||||
color: e.color || '#6366f1',
|
||||
icon: '\uD83D\uDCC5',
|
||||
}));
|
||||
} catch {
|
||||
upcomingEvents = [];
|
||||
}
|
||||
|
||||
try {
|
||||
// Recently added contacts
|
||||
const contacts = await crossContactCollection.getAll();
|
||||
recentContacts = contacts
|
||||
.filter((c) => !c.isArchived)
|
||||
.sort((a, b) => (b.createdAt || '').localeCompare(a.createdAt || ''))
|
||||
.slice(0, 3)
|
||||
.map((c) => {
|
||||
const name = [c.firstName, c.lastName].filter(Boolean).join(' ') || c.email || '?';
|
||||
return {
|
||||
id: `contact-${c.id}`,
|
||||
type: 'contact' as const,
|
||||
title: name,
|
||||
subtitle: c.company || undefined,
|
||||
timestamp: c.createdAt,
|
||||
color: '#8b5cf6',
|
||||
icon: '\uD83D\uDC64',
|
||||
};
|
||||
});
|
||||
} catch {
|
||||
recentContacts = [];
|
||||
}
|
||||
}
|
||||
|
||||
let feedItems = $derived(
|
||||
[...recentTasks, ...upcomingEvents, ...recentContacts]
|
||||
.sort((a, b) => b.timestamp.localeCompare(a.timestamp))
|
||||
.slice(0, maxItems)
|
||||
);
|
||||
|
||||
function formatTime(isoString: string): string {
|
||||
try {
|
||||
const date = new Date(isoString);
|
||||
return date.toLocaleTimeString(locale === 'de' ? 'de-DE' : 'en-US', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function formatRelative(isoString: string): string {
|
||||
try {
|
||||
const diff = Date.now() - new Date(isoString).getTime();
|
||||
const mins = Math.floor(diff / 60000);
|
||||
if (mins < 1) return locale === 'de' ? 'gerade eben' : 'just now';
|
||||
if (mins < 60) return `${mins}m`;
|
||||
const hrs = Math.floor(mins / 60);
|
||||
if (hrs < 24) return `${hrs}h`;
|
||||
return `${Math.floor(hrs / 24)}d`;
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if feedItems.length > 0}
|
||||
<section class="feed-section">
|
||||
<h2 class="feed-title">{locale === 'de' ? 'Aktivität' : 'Activity'}</h2>
|
||||
<div class="feed-list">
|
||||
{#each feedItems as item (item.id)}
|
||||
<div class="feed-item">
|
||||
<span class="feed-icon">{item.icon}</span>
|
||||
<div class="feed-content">
|
||||
<span class="feed-item-title">{item.title}</span>
|
||||
{#if item.subtitle}
|
||||
<span class="feed-item-sub">{item.subtitle}</span>
|
||||
{/if}
|
||||
</div>
|
||||
<span class="feed-time">{formatRelative(item.timestamp)}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.feed-section {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.feed-title {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: hsl(var(--muted-foreground, 0 0% 45%));
|
||||
margin: 0 0 0.625rem;
|
||||
}
|
||||
|
||||
.feed-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.125rem;
|
||||
}
|
||||
|
||||
.feed-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.625rem;
|
||||
padding: 0.5rem 0.625rem;
|
||||
border-radius: 0.5rem;
|
||||
transition: background 0.1s;
|
||||
}
|
||||
|
||||
.feed-item:hover {
|
||||
background: hsl(var(--muted, 0 0% 96%) / 0.5);
|
||||
}
|
||||
|
||||
.feed-icon {
|
||||
font-size: 0.875rem;
|
||||
flex-shrink: 0;
|
||||
width: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.feed-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.feed-item-title {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--foreground, 0 0% 9%));
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.feed-item-sub {
|
||||
font-size: 0.6875rem;
|
||||
color: hsl(var(--muted-foreground, 0 0% 45%));
|
||||
}
|
||||
|
||||
.feed-time {
|
||||
font-size: 0.6875rem;
|
||||
color: hsl(var(--muted-foreground, 0 0% 45%));
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
167
apps/manacore/apps/web/src/lib/components/AppRow.svelte
Normal file
167
apps/manacore/apps/web/src/lib/components/AppRow.svelte
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
<script lang="ts">
|
||||
import type { ManaApp, AppIconId } from '@manacore/shared-branding';
|
||||
import { APP_URLS } from '@manacore/shared-branding';
|
||||
|
||||
interface Props {
|
||||
apps: ManaApp[];
|
||||
title: string;
|
||||
emptyText?: string;
|
||||
showPin?: boolean;
|
||||
onAppClick: (app: ManaApp) => void;
|
||||
onTogglePin?: (appId: string) => void;
|
||||
pinnedIds?: string[];
|
||||
}
|
||||
|
||||
let {
|
||||
apps,
|
||||
title,
|
||||
emptyText,
|
||||
showPin = false,
|
||||
onAppClick,
|
||||
onTogglePin,
|
||||
pinnedIds = [],
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if apps.length > 0}
|
||||
<section class="app-row-section">
|
||||
<h2 class="row-title">{title}</h2>
|
||||
<div class="row-scroll">
|
||||
{#each apps as app (app.id)}
|
||||
<button class="row-card" style="--app-color: {app.color};" onclick={() => onAppClick(app)}>
|
||||
<div class="row-card-icon">
|
||||
{#if app.icon}
|
||||
<img src={app.icon} alt={app.name} class="row-icon" />
|
||||
{:else}
|
||||
<span class="row-icon-letter" style="color: {app.color};">{app.name.charAt(0)}</span>
|
||||
{/if}
|
||||
</div>
|
||||
<span class="row-card-name">{app.name}</span>
|
||||
{#if showPin && onTogglePin}
|
||||
<button
|
||||
class="pin-btn"
|
||||
class:pinned={pinnedIds.includes(app.id)}
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
onTogglePin(app.id);
|
||||
}}
|
||||
title={pinnedIds.includes(app.id)
|
||||
? 'Aus Favoriten entfernen'
|
||||
: 'Zu Favoriten hinzufügen'}
|
||||
>
|
||||
{pinnedIds.includes(app.id) ? '\u2605' : '\u2606'}
|
||||
</button>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
{:else if emptyText}
|
||||
<section class="app-row-section">
|
||||
<h2 class="row-title">{title}</h2>
|
||||
<p class="row-empty">{emptyText}</p>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.app-row-section {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.row-title {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: hsl(var(--muted-foreground, 0 0% 45%));
|
||||
margin: 0 0 0.625rem;
|
||||
}
|
||||
|
||||
.row-scroll {
|
||||
display: flex;
|
||||
gap: 0.625rem;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 0.25rem;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.row-scroll::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.row-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid hsl(var(--border, 0 0% 90%));
|
||||
background: hsl(var(--card, 0 0% 100%));
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
min-width: 5.5rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.row-card:hover {
|
||||
border-color: color-mix(in srgb, var(--app-color) 40%, hsl(var(--border, 0 0% 90%)));
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.row-card-icon {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.row-icon {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.row-icon-letter {
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.row-card-name {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--foreground, 0 0% 9%));
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pin-btn {
|
||||
position: absolute;
|
||||
top: 0.25rem;
|
||||
right: 0.25rem;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.3;
|
||||
transition: opacity 0.15s;
|
||||
padding: 0.125rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.pin-btn:hover,
|
||||
.pin-btn.pinned {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.pin-btn.pinned {
|
||||
color: #eab308;
|
||||
}
|
||||
|
||||
.row-empty {
|
||||
font-size: 0.8125rem;
|
||||
color: hsl(var(--muted-foreground, 0 0% 45%));
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,16 +1,19 @@
|
|||
<script lang="ts">
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { locale } from 'svelte-i18n';
|
||||
import {
|
||||
MANA_APPS,
|
||||
APP_URLS,
|
||||
APP_STATUS_LABELS,
|
||||
getAccessibleManaApps,
|
||||
type ManaApp,
|
||||
type AppIconId,
|
||||
} from '@manacore/shared-branding';
|
||||
import { createAppNavigationStore } from '@manacore/shared-ui';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { ManaCoreEvents } from '@manacore/shared-utils/analytics';
|
||||
import AppRow from '$lib/components/AppRow.svelte';
|
||||
import ActivityFeed from '$lib/components/ActivityFeed.svelte';
|
||||
|
||||
const store = createAppNavigationStore();
|
||||
|
||||
// Detect dev mode
|
||||
const isDev =
|
||||
|
|
@ -25,56 +28,29 @@
|
|||
let userTier = $derived(authStore.user?.tier || 'public');
|
||||
let activeApps = $derived(getAccessibleManaApps(userTier));
|
||||
|
||||
// Group apps by category
|
||||
interface AppCategory {
|
||||
id: string;
|
||||
titleDe: string;
|
||||
titleEn: string;
|
||||
descDe: string;
|
||||
descEn: string;
|
||||
icon: string;
|
||||
apps: ManaApp[];
|
||||
}
|
||||
// Favorites resolved to ManaApp objects
|
||||
let favoriteApps = $derived(
|
||||
store.favorites
|
||||
.map((id) => activeApps.find((a) => a.id === id))
|
||||
.filter((a): a is ManaApp => !!a)
|
||||
);
|
||||
|
||||
const aiAppIds: AppIconId[] = ['chat', 'picture', 'questions', 'context', 'presi', 'mail'];
|
||||
const productivityIds: AppIconId[] = ['todo', 'calendar', 'contacts', 'cards', 'inventory'];
|
||||
const utilityIds: AppIconId[] = ['clock', 'zitare', 'storage', 'moodlit', 'matrix'];
|
||||
// Recently used resolved to ManaApp objects
|
||||
let recentApps = $derived(
|
||||
store.recentApps
|
||||
.map((r) => activeApps.find((a) => a.id === r.id))
|
||||
.filter((a): a is ManaApp => !!a)
|
||||
.slice(0, 5)
|
||||
);
|
||||
|
||||
function getAppsForCategory(ids: AppIconId[], apps: ManaApp[]): ManaApp[] {
|
||||
return ids
|
||||
.map((id) => apps.find((app) => app.id === id))
|
||||
.filter((app): app is ManaApp => !!app);
|
||||
}
|
||||
|
||||
let categories = $derived([
|
||||
{
|
||||
id: 'ai',
|
||||
titleDe: 'KI & Kreativ',
|
||||
titleEn: 'AI & Creative',
|
||||
descDe: 'Intelligente Assistenten und kreative Werkzeuge',
|
||||
descEn: 'Intelligent assistants and creative tools',
|
||||
icon: '🤖',
|
||||
apps: getAppsForCategory(aiAppIds, activeApps),
|
||||
},
|
||||
{
|
||||
id: 'productivity',
|
||||
titleDe: 'Produktivität',
|
||||
titleEn: 'Productivity',
|
||||
descDe: 'Organisiere deinen Alltag',
|
||||
descEn: 'Organize your daily life',
|
||||
icon: '📋',
|
||||
apps: getAppsForCategory(productivityIds, activeApps),
|
||||
},
|
||||
{
|
||||
id: 'utility',
|
||||
titleDe: 'Tools & Utilities',
|
||||
titleEn: 'Tools & Utilities',
|
||||
descDe: 'Praktische Helferlein',
|
||||
descEn: 'Handy helpers',
|
||||
icon: '🔧',
|
||||
apps: getAppsForCategory(utilityIds, activeApps),
|
||||
},
|
||||
] satisfies AppCategory[]);
|
||||
// All apps sorted by usage frequency
|
||||
let sortedApps = $derived(
|
||||
[...activeApps].sort((a, b) => {
|
||||
const countA = store.usageCounts[a.id] || 0;
|
||||
const countB = store.usageCounts[b.id] || 0;
|
||||
return countB - countA;
|
||||
})
|
||||
);
|
||||
|
||||
function getStatusColor(status: ManaApp['status']): string {
|
||||
const colors = {
|
||||
|
|
@ -103,6 +79,7 @@
|
|||
}
|
||||
|
||||
function handleAppClick(app: ManaApp) {
|
||||
store.recordAppVisit(app.id);
|
||||
ManaCoreEvents.appOpened(app.id);
|
||||
const url = getAppUrl(app.id);
|
||||
if (url) {
|
||||
|
|
@ -164,68 +141,78 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- App Categories -->
|
||||
{#each categories as category}
|
||||
<section class="category">
|
||||
<div class="category-header">
|
||||
<span class="category-icon">{category.icon}</span>
|
||||
<div>
|
||||
<h2 class="category-title">
|
||||
{currentLocale === 'en' ? category.titleEn : category.titleDe}
|
||||
</h2>
|
||||
<p class="category-desc">
|
||||
{currentLocale === 'en' ? category.descEn : category.descDe}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Favorites -->
|
||||
<AppRow
|
||||
apps={favoriteApps}
|
||||
title={currentLocale === 'en' ? 'Favorites' : 'Favoriten'}
|
||||
showPin={true}
|
||||
onAppClick={handleAppClick}
|
||||
onTogglePin={(id) => store.toggleFavorite(id)}
|
||||
pinnedIds={store.favorites}
|
||||
/>
|
||||
|
||||
<div class="app-grid">
|
||||
{#each category.apps as app}
|
||||
<button
|
||||
class="app-card"
|
||||
style="--app-color: {app.color};"
|
||||
onclick={() => handleAppClick(app)}
|
||||
>
|
||||
<div class="app-card-top">
|
||||
<div class="app-icon-wrap">
|
||||
{#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>
|
||||
<div
|
||||
class="status-badge"
|
||||
style="color: {getStatusColor(app.status)}; background: {getStatusBgColor(
|
||||
app.status
|
||||
)};"
|
||||
>
|
||||
<span class="status-dot" style="background: {getStatusColor(app.status)};"></span>
|
||||
{statusLabels[app.status]}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Recently Used -->
|
||||
<AppRow
|
||||
apps={recentApps}
|
||||
title={currentLocale === 'en' ? 'Recently Used' : 'Zuletzt verwendet'}
|
||||
onAppClick={handleAppClick}
|
||||
/>
|
||||
|
||||
<h3 class="app-name">{app.name}</h3>
|
||||
<p class="app-tagline">{app.description[currentLocale] || app.description.de}</p>
|
||||
<!-- Activity Feed -->
|
||||
<ActivityFeed locale={currentLocale} />
|
||||
|
||||
<div class="app-card-footer">
|
||||
{#if app.comingSoon}
|
||||
<span class="coming-soon-label">
|
||||
{currentLocale === 'en' ? 'Coming Soon' : 'Demnächst'}
|
||||
</span>
|
||||
<!-- All Apps (sorted by usage) -->
|
||||
<section class="category">
|
||||
<h2 class="section-title">
|
||||
{currentLocale === 'en' ? 'All Apps' : 'Alle Apps'}
|
||||
</h2>
|
||||
|
||||
<div class="app-grid">
|
||||
{#each sortedApps as app (app.id)}
|
||||
<button
|
||||
class="app-card"
|
||||
style="--app-color: {app.color};"
|
||||
onclick={() => handleAppClick(app)}
|
||||
>
|
||||
<div class="app-card-top">
|
||||
<div class="app-icon-wrap">
|
||||
{#if app.icon}
|
||||
<img src={app.icon} alt={app.name} class="app-icon" />
|
||||
{:else}
|
||||
<span class="open-label" style="color: {app.color};">
|
||||
{currentLocale === 'en' ? 'Open' : 'Öffnen'} →
|
||||
</span>
|
||||
<div class="app-icon-fallback" style="color: {app.color};">
|
||||
{app.name.charAt(0)}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
{/each}
|
||||
<div
|
||||
class="status-badge"
|
||||
style="color: {getStatusColor(app.status)}; background: {getStatusBgColor(
|
||||
app.status
|
||||
)};"
|
||||
>
|
||||
<span class="status-dot" style="background: {getStatusColor(app.status)};"></span>
|
||||
{statusLabels[app.status]}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="app-name">{app.name}</h3>
|
||||
<p class="app-tagline">{app.description[currentLocale] || app.description.de}</p>
|
||||
|
||||
<div class="app-card-footer">
|
||||
{#if app.comingSoon}
|
||||
<span class="coming-soon-label">
|
||||
{currentLocale === 'en' ? 'Coming Soon' : 'Demnächst'}
|
||||
</span>
|
||||
{:else}
|
||||
<span class="open-label" style="color: {app.color};">
|
||||
{currentLocale === 'en' ? 'Open' : 'Öffnen'} →
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Legend -->
|
||||
<div class="legend">
|
||||
|
|
@ -318,33 +305,18 @@
|
|||
color: hsl(var(--muted-foreground, 0 0% 45%));
|
||||
}
|
||||
|
||||
/* Categories */
|
||||
/* Sections */
|
||||
.category {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.category-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.category-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.category-title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--foreground, 0 0% 9%));
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.category-desc {
|
||||
.section-title {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: hsl(var(--muted-foreground, 0 0% 45%));
|
||||
margin: 0;
|
||||
margin: 0 0 0.625rem;
|
||||
}
|
||||
|
||||
/* App Grid */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue