mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-20 06:33:38 +02:00
feat: major update with network graphs, themes, todo extensions, and more
## New Features ### Network Graph Visualization (Contacts, Calendar, Todo) - D3.js force simulation for physics-based layout - Zoom & pan with mouse/touchpad - Keyboard shortcuts: +/- zoom, 0 reset, Esc deselect, / search, F focus - Filtering by tags, company/location/project, connection strength - Shared components in @manacore/shared-ui ### Central Tags API (mana-core-auth) - CRUD endpoints for tags - Schema: tags table with userId, name, color, app - Shared tag components in @manacore/shared-ui ### Custom Themes System - Theme editor with live preview and color picker - Community theme gallery - Theme sharing (public, unlisted, private) - Backend API in mana-core-auth ### Todo App Extensions - Glass-pill design for task input and items - Settings page with 20+ preferences - Task edit modal with inline editing - Statistics page with visualizations - PWA support with offline capabilities - Multiple kanban boards ### Contacts App Features - Duplicate detection - Photo upload - Batch operations - Enhanced favorites page with multiple view modes - Alphabet view improvements - Search modal ### Help System - @manacore/shared-help-content - @manacore/shared-help-ui - @manacore/shared-help-types ### Other Features - Themes page for all apps - Referral system frontend - CommandBar (global search) - Skeleton loaders - Settings page improvements ## Bug Fixes - Network graph simulation initialization - Database schema TEXT for user_id columns (Better Auth compatibility) - Various styling fixes ## Documentation - Daily report for 2025-12-10 - CI/CD deployment guide 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
e84371aa94
commit
ee42b6cc76
381 changed files with 39284 additions and 6275 deletions
|
|
@ -5,6 +5,8 @@
|
|||
@source "../../../packages/shared/src";
|
||||
@source "../../../../../packages/shared-ui/src";
|
||||
@source "../../../../../packages/shared-theme-ui/src";
|
||||
@source "../../../../../packages/shared-theme-ui/src/components";
|
||||
@source "../../../../../packages/shared-theme-ui/src/pages";
|
||||
|
||||
/* Clock-specific CSS Variables */
|
||||
@layer base {
|
||||
|
|
|
|||
|
|
@ -5,21 +5,21 @@
|
|||
*/
|
||||
|
||||
import type { Handle } from '@sveltejs/kit';
|
||||
import { env } from '$env/dynamic/private';
|
||||
|
||||
// Get client-side URLs from environment (Docker runtime)
|
||||
const PUBLIC_MANA_CORE_AUTH_URL_CLIENT =
|
||||
process.env.PUBLIC_MANA_CORE_AUTH_URL_CLIENT || process.env.PUBLIC_MANA_CORE_AUTH_URL || '';
|
||||
const PUBLIC_BACKEND_URL_CLIENT =
|
||||
process.env.PUBLIC_BACKEND_URL_CLIENT || process.env.PUBLIC_BACKEND_URL || '';
|
||||
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
// Get client-side URLs from environment at RUNTIME (not build time)
|
||||
// Use $env/dynamic/private to read actual runtime environment variables
|
||||
const authUrlClient = env.PUBLIC_MANA_CORE_AUTH_URL_CLIENT || env.PUBLIC_MANA_CORE_AUTH_URL || '';
|
||||
const backendUrlClient = env.PUBLIC_BACKEND_URL_CLIENT || env.PUBLIC_BACKEND_URL || '';
|
||||
|
||||
return resolve(event, {
|
||||
transformPageChunk: ({ html }) => {
|
||||
// Inject runtime environment variables into the HTML
|
||||
// These will be available on window.__PUBLIC_*__ for client-side code
|
||||
const envScript = `<script>
|
||||
window.__PUBLIC_MANA_CORE_AUTH_URL__ = "${authUrlClient}";
|
||||
window.__PUBLIC_BACKEND_URL__ = "${backendUrlClient}";
|
||||
window.__PUBLIC_MANA_CORE_AUTH_URL__ = "${PUBLIC_MANA_CORE_AUTH_URL_CLIENT}";
|
||||
window.__PUBLIC_BACKEND_URL__ = "${PUBLIC_BACKEND_URL_CLIENT}";
|
||||
</script>`;
|
||||
return html.replace('<head>', `<head>${envScript}`);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* AlarmsSkeleton - Skeleton for alarms grid
|
||||
* Shows placeholder tiles for alarm times
|
||||
*/
|
||||
|
||||
import { SkeletonBox } from '@manacore/shared-ui';
|
||||
</script>
|
||||
|
||||
<div role="status" aria-label="Wecker werden geladen...">
|
||||
<!-- Alarm Preset Grid (matches DEFAULT_ALARM_PRESETS layout) -->
|
||||
<div class="grid grid-cols-4 sm:grid-cols-6 gap-2">
|
||||
{#each Array(12) as _, i}
|
||||
<div
|
||||
class="bg-card rounded-xl p-3 border border-border flex flex-col items-center justify-center"
|
||||
style="opacity: {Math.max(0.4, 1 - i * 0.05)}"
|
||||
>
|
||||
<!-- Time -->
|
||||
<SkeletonBox width="48px" height="24px" borderRadius="4px" />
|
||||
<!-- Label -->
|
||||
<div class="mt-1">
|
||||
<SkeletonBox width="40px" height="10px" borderRadius="4px" />
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* AppLoadingSkeleton - Full-page loading skeleton for app initialization
|
||||
* Replaces spinner with clock-themed skeleton layout
|
||||
*/
|
||||
|
||||
import { SkeletonBox } from '@manacore/shared-ui';
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="flex min-h-screen items-center justify-center bg-background"
|
||||
role="status"
|
||||
aria-label="App wird geladen..."
|
||||
>
|
||||
<div class="w-full max-w-sm px-6 space-y-8">
|
||||
<!-- Clock Icon Area -->
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<SkeletonBox width="80px" height="80px" borderRadius="50%" />
|
||||
<SkeletonBox width="120px" height="24px" borderRadius="8px" />
|
||||
</div>
|
||||
|
||||
<!-- Time Display Skeleton -->
|
||||
<div class="bg-card rounded-2xl p-6 space-y-4">
|
||||
<!-- Large Time -->
|
||||
<div class="flex justify-center">
|
||||
<SkeletonBox width="200px" height="48px" borderRadius="8px" />
|
||||
</div>
|
||||
|
||||
<!-- Date -->
|
||||
<div class="flex justify-center">
|
||||
<SkeletonBox width="140px" height="16px" borderRadius="4px" />
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="flex justify-center gap-3 pt-2">
|
||||
{#each Array(4) as _, i}
|
||||
<SkeletonBox width="48px" height="48px" borderRadius="12px" />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading Text -->
|
||||
<div class="flex justify-center">
|
||||
<SkeletonBox width="100px" height="14px" borderRadius="4px" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* TimersSkeleton - Skeleton for active timers grid
|
||||
* Shows placeholder tiles for timer displays
|
||||
*/
|
||||
|
||||
import { SkeletonBox } from '@manacore/shared-ui';
|
||||
</script>
|
||||
|
||||
<div role="status" aria-label="Timer werden geladen...">
|
||||
<!-- Section Header -->
|
||||
<div class="mb-2">
|
||||
<SkeletonBox width="80px" height="12px" borderRadius="4px" />
|
||||
</div>
|
||||
|
||||
<!-- Timer Grid -->
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
||||
{#each Array(4) as _, i}
|
||||
<div
|
||||
class="bg-card rounded-xl p-3 border border-border"
|
||||
style="opacity: {Math.max(0.5, 1 - i * 0.12)}"
|
||||
>
|
||||
<!-- Time + Delete Button Row -->
|
||||
<div class="flex items-start justify-between mb-1">
|
||||
<SkeletonBox width="70px" height="24px" borderRadius="4px" />
|
||||
<SkeletonBox width="14px" height="14px" borderRadius="4px" />
|
||||
</div>
|
||||
|
||||
<!-- Label -->
|
||||
<div class="mb-2">
|
||||
<SkeletonBox width="50px" height="10px" borderRadius="4px" />
|
||||
</div>
|
||||
|
||||
<!-- Progress Bar -->
|
||||
<div class="mb-2">
|
||||
<SkeletonBox width="100%" height="4px" borderRadius="2px" />
|
||||
</div>
|
||||
|
||||
<!-- Control Buttons -->
|
||||
<div class="flex gap-1">
|
||||
<SkeletonBox width="100%" height="28px" borderRadius="6px" />
|
||||
<SkeletonBox width="48px" height="28px" borderRadius="6px" />
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* WorldClockSkeleton - Skeleton for world clock grid
|
||||
* Shows placeholder cards for city times
|
||||
*/
|
||||
|
||||
import { SkeletonBox } from '@manacore/shared-ui';
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3"
|
||||
role="status"
|
||||
aria-label="Weltuhren werden geladen..."
|
||||
>
|
||||
{#each Array(6) as _, i}
|
||||
<div
|
||||
class="bg-card rounded-xl p-4 border border-border"
|
||||
style="opacity: {Math.max(0.4, 1 - i * 0.1)}"
|
||||
>
|
||||
<!-- Day/Night + City Name -->
|
||||
<div class="mb-2 flex items-center gap-2">
|
||||
<SkeletonBox width="32px" height="12px" borderRadius="4px" />
|
||||
<SkeletonBox width="80px" height="14px" borderRadius="4px" />
|
||||
</div>
|
||||
|
||||
<!-- Large Time Display -->
|
||||
<SkeletonBox width="140px" height="40px" borderRadius="6px" />
|
||||
|
||||
<!-- Date and Offset -->
|
||||
<div class="mt-2 flex items-center justify-between">
|
||||
<SkeletonBox width="90px" height="13px" borderRadius="4px" />
|
||||
<SkeletonBox width="36px" height="14px" borderRadius="4px" />
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
18
apps/clock/apps/web/src/lib/components/skeletons/index.ts
Normal file
18
apps/clock/apps/web/src/lib/components/skeletons/index.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
/**
|
||||
* Clock App Skeleton Components
|
||||
*
|
||||
* App-specific skeleton loaders that match the exact layout of clock components.
|
||||
* Built on top of @manacore/shared-ui skeleton primitives.
|
||||
*/
|
||||
|
||||
// App Loading Skeleton
|
||||
export { default as AppLoadingSkeleton } from './AppLoadingSkeleton.svelte';
|
||||
|
||||
// World Clock Skeleton
|
||||
export { default as WorldClockSkeleton } from './WorldClockSkeleton.svelte';
|
||||
|
||||
// Alarms Skeleton
|
||||
export { default as AlarmsSkeleton } from './AlarmsSkeleton.svelte';
|
||||
|
||||
// Timers Skeleton
|
||||
export { default as TimersSkeleton } from './TimersSkeleton.svelte';
|
||||
|
|
@ -3,12 +3,22 @@
|
|||
import { page } from '$app/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import { locale } from 'svelte-i18n';
|
||||
import { PillNavigation } from '@manacore/shared-ui';
|
||||
import type { PillNavItem, PillDropdownItem } from '@manacore/shared-ui';
|
||||
import { PillNavigation, CommandBar } from '@manacore/shared-ui';
|
||||
import type {
|
||||
PillNavItem,
|
||||
PillDropdownItem,
|
||||
CommandBarItem,
|
||||
QuickAction,
|
||||
} from '@manacore/shared-ui';
|
||||
import { theme } from '$lib/stores/theme.svelte';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { userSettings } from '$lib/stores/user-settings.svelte';
|
||||
import { THEME_DEFINITIONS } from '@manacore/shared-theme';
|
||||
import {
|
||||
THEME_DEFINITIONS,
|
||||
DEFAULT_THEME_VARIANTS,
|
||||
EXTENDED_THEME_VARIANTS,
|
||||
} from '@manacore/shared-theme';
|
||||
import type { ThemeVariant } from '@manacore/shared-theme';
|
||||
import {
|
||||
isSidebarMode as sidebarModeStore,
|
||||
isNavCollapsed as collapsedStore,
|
||||
|
|
@ -16,21 +26,108 @@
|
|||
import { getLanguageDropdownItems, getCurrentLanguageLabel } from '@manacore/shared-i18n';
|
||||
import { getPillAppItems } from '@manacore/shared-branding';
|
||||
import { setLocale, supportedLocales } from '$lib/i18n';
|
||||
import { alarmsApi } from '$lib/api/alarms';
|
||||
import { timersApi } from '$lib/api/timers';
|
||||
|
||||
// App switcher items
|
||||
const appItems = getPillAppItems('clock');
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
// CommandBar state
|
||||
let commandBarOpen = $state(false);
|
||||
|
||||
// CommandBar quick actions
|
||||
const commandBarQuickActions: QuickAction[] = [
|
||||
{
|
||||
id: 'alarm',
|
||||
label: 'Neuen Wecker erstellen',
|
||||
icon: 'bell',
|
||||
href: '/alarms?new=true',
|
||||
shortcut: 'A',
|
||||
},
|
||||
{
|
||||
id: 'timer',
|
||||
label: 'Neuen Timer starten',
|
||||
icon: 'timer',
|
||||
href: '/timers?new=true',
|
||||
shortcut: 'T',
|
||||
},
|
||||
{ id: 'stopwatch', label: 'Stoppuhr', icon: 'stopwatch', href: '/stopwatch' },
|
||||
{ id: 'pomodoro', label: 'Pomodoro starten', icon: 'target', href: '/pomodoro' },
|
||||
{ id: 'worldclock', label: 'Weltzeituhr', icon: 'globe', href: '/world-clock' },
|
||||
{ id: 'settings', label: 'Einstellungen', icon: 'settings', href: '/settings' },
|
||||
];
|
||||
|
||||
// CommandBar search - search alarms and timers
|
||||
async function handleCommandBarSearch(query: string): Promise<CommandBarItem[]> {
|
||||
if (!query.trim()) return [];
|
||||
|
||||
const queryLower = query.toLowerCase();
|
||||
const results: CommandBarItem[] = [];
|
||||
|
||||
try {
|
||||
// Search alarms
|
||||
const alarms = await alarmsApi.getAll();
|
||||
const matchingAlarms = alarms
|
||||
.filter((alarm) => alarm.label?.toLowerCase().includes(queryLower))
|
||||
.slice(0, 5)
|
||||
.map((alarm) => ({
|
||||
id: `alarm-${alarm.id}`,
|
||||
title: alarm.label || 'Wecker',
|
||||
subtitle: `⏰ ${alarm.time} ${alarm.enabled ? '(aktiv)' : '(inaktiv)'}`,
|
||||
}));
|
||||
results.push(...matchingAlarms);
|
||||
|
||||
// Search timers
|
||||
const timers = await timersApi.getAll();
|
||||
const matchingTimers = timers
|
||||
.filter((timer) => timer.label?.toLowerCase().includes(queryLower))
|
||||
.slice(0, 5)
|
||||
.map((timer) => {
|
||||
const mins = Math.floor(timer.durationSeconds / 60);
|
||||
const secs = timer.durationSeconds % 60;
|
||||
return {
|
||||
id: `timer-${timer.id}`,
|
||||
title: timer.label || 'Timer',
|
||||
subtitle: `⏱️ ${mins}:${secs.toString().padStart(2, '0')} ${timer.status === 'running' ? '(läuft)' : ''}`,
|
||||
};
|
||||
});
|
||||
results.push(...matchingTimers);
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
|
||||
return results.slice(0, 10);
|
||||
}
|
||||
|
||||
function handleCommandBarSelect(item: CommandBarItem) {
|
||||
if (item.id.startsWith('alarm-')) {
|
||||
goto('/alarms');
|
||||
} else if (item.id.startsWith('timer-')) {
|
||||
goto('/timers');
|
||||
}
|
||||
}
|
||||
|
||||
let isSidebarMode = $state(false);
|
||||
let isCollapsed = $state(false);
|
||||
|
||||
// Use theme store's isDark directly
|
||||
let isDark = $derived(theme.isDark);
|
||||
|
||||
// Get pinned themes from user settings (extended themes only)
|
||||
let pinnedThemes = $derived<ThemeVariant[]>(
|
||||
(userSettings.theme?.pinnedThemes || []).filter((t): t is ThemeVariant =>
|
||||
EXTENDED_THEME_VARIANTS.includes(t as ThemeVariant)
|
||||
)
|
||||
);
|
||||
|
||||
// Visible themes in PillNav: default + pinned extended
|
||||
let visibleThemes = $derived<ThemeVariant[]>([...DEFAULT_THEME_VARIANTS, ...pinnedThemes]);
|
||||
|
||||
// Theme variant dropdown items (with SSR fallback)
|
||||
let themeVariantItems = $derived<PillDropdownItem[]>([
|
||||
...(theme.variants || []).map((variant) => ({
|
||||
...visibleThemes.map((variant) => ({
|
||||
id: variant,
|
||||
label: THEME_DEFINITIONS[variant]?.label || variant,
|
||||
icon: THEME_DEFINITIONS[variant]?.icon || '🎨',
|
||||
|
|
@ -83,6 +180,13 @@
|
|||
function handleKeydown(event: KeyboardEvent) {
|
||||
const target = event.target as HTMLElement;
|
||||
|
||||
// Cmd/Ctrl+K to open command bar (works even in inputs)
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === 'k') {
|
||||
event.preventDefault();
|
||||
commandBarOpen = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -206,6 +310,18 @@
|
|||
{@render children()}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Global Command Bar (Cmd/K) -->
|
||||
<CommandBar
|
||||
bind:open={commandBarOpen}
|
||||
onClose={() => (commandBarOpen = false)}
|
||||
onSearch={handleCommandBarSearch}
|
||||
onSelect={handleCommandBarSelect}
|
||||
quickActions={commandBarQuickActions}
|
||||
placeholder="Schnellzugriff..."
|
||||
emptyText="Keine Ergebnisse"
|
||||
searchingText="Suche..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
import { toast } from '$lib/stores/toast';
|
||||
import type { CreateAlarmInput, Alarm } from '@clock/shared';
|
||||
import { ALARM_SOUNDS, DEFAULT_ALARM_PRESETS } from '@clock/shared';
|
||||
import { AlarmsSkeleton } from '$lib/components/skeletons';
|
||||
|
||||
// Quick create form (inline)
|
||||
let newTime = $state('07:00');
|
||||
|
|
@ -194,11 +195,7 @@
|
|||
|
||||
<!-- Loading State -->
|
||||
{#if alarmsStore.loading}
|
||||
<div class="flex justify-center py-12">
|
||||
<div
|
||||
class="h-8 w-8 animate-spin rounded-full border-4 border-primary border-r-transparent"
|
||||
></div>
|
||||
</div>
|
||||
<AlarmsSkeleton />
|
||||
{:else}
|
||||
<!-- Default Alarm Presets (Grid) -->
|
||||
<div class="alarm-grid">
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { toast } from '$lib/stores/toast';
|
||||
import { QUICK_TIMER_PRESETS, formatDuration } from '@clock/shared';
|
||||
import { TimersSkeleton } from '$lib/components/skeletons';
|
||||
|
||||
// Form state (inline on page)
|
||||
let formMinutes = $state(5);
|
||||
|
|
@ -219,11 +220,7 @@
|
|||
|
||||
<!-- Loading State -->
|
||||
{#if timersStore.loading}
|
||||
<div class="flex justify-center py-8">
|
||||
<div
|
||||
class="h-6 w-6 animate-spin rounded-full border-2 border-primary border-r-transparent"
|
||||
></div>
|
||||
</div>
|
||||
<TimersSkeleton />
|
||||
{:else if allTimers.length > 0}
|
||||
<!-- Active Timers -->
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
import { toast } from '$lib/stores/toast';
|
||||
import { POPULAR_TIMEZONES } from '@clock/shared';
|
||||
import WorldMap from '$lib/components/WorldMap.svelte';
|
||||
import { WorldClockSkeleton } from '$lib/components/skeletons';
|
||||
|
||||
// State
|
||||
let showAddModal = $state(false);
|
||||
|
|
@ -205,11 +206,7 @@
|
|||
|
||||
<!-- World Clock List -->
|
||||
{#if worldClocksStore.loading}
|
||||
<div class="flex justify-center py-12">
|
||||
<div
|
||||
class="h-8 w-8 animate-spin rounded-full border-4 border-primary border-r-transparent"
|
||||
></div>
|
||||
</div>
|
||||
<WorldClockSkeleton />
|
||||
{:else if worldClocksStore.sortedWorldClocks.length === 0}
|
||||
<div class="card py-12 text-center">
|
||||
<p class="text-lg text-muted-foreground">{$_('worldClock.noClocks')}</p>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { waitLocale } from '$lib/i18n';
|
||||
import ToastContainer from '$lib/components/ToastContainer.svelte';
|
||||
import { AppLoadingSkeleton } from '$lib/components/skeletons';
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
|
|
@ -28,14 +29,7 @@
|
|||
<ToastContainer />
|
||||
|
||||
{#if $isLocaleLoading || loading}
|
||||
<div class="flex min-h-screen items-center justify-center bg-background">
|
||||
<div class="text-center">
|
||||
<div
|
||||
class="mb-4 inline-block h-12 w-12 animate-spin rounded-full border-4 border-solid border-primary border-r-transparent"
|
||||
></div>
|
||||
<p class="text-muted-foreground">Laden...</p>
|
||||
</div>
|
||||
</div>
|
||||
<AppLoadingSkeleton />
|
||||
{:else}
|
||||
<div class="min-h-screen bg-background text-foreground">
|
||||
{@render children()}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue