diff --git a/apps/clock/apps/web/src/app.css b/apps/clock/apps/web/src/app.css new file mode 100644 index 000000000..c29749613 --- /dev/null +++ b/apps/clock/apps/web/src/app.css @@ -0,0 +1,10 @@ +@import "tailwindcss"; +@import "@manacore/shared-tailwind/themes.css"; + +/* Scan shared packages for Tailwind classes */ +@source "../../../../packages/shared-ui/src"; +@source "../../../../packages/shared-auth-ui/src"; +@source "../../../../packages/shared-branding/src"; +@source "../../../../packages/shared-theme-ui/src"; +@source "../../../../packages/shared-theme-ui/src/components"; +@source "../../../../packages/shared-theme-ui/src/pages"; diff --git a/apps/clock/apps/web/src/lib/components/ToastContainer.svelte b/apps/clock/apps/web/src/lib/components/ToastContainer.svelte new file mode 100644 index 000000000..28d60f24c --- /dev/null +++ b/apps/clock/apps/web/src/lib/components/ToastContainer.svelte @@ -0,0 +1,72 @@ + + +
+ {#each $toasts as toast (toast.id)} +
+ + {getIcon(toast.type)} + + {toast.message} + +
+ {/each} +
+ + diff --git a/apps/clock/apps/web/src/lib/components/skeletons/AppLoadingSkeleton.svelte b/apps/clock/apps/web/src/lib/components/skeletons/AppLoadingSkeleton.svelte new file mode 100644 index 000000000..21f6f848a --- /dev/null +++ b/apps/clock/apps/web/src/lib/components/skeletons/AppLoadingSkeleton.svelte @@ -0,0 +1,90 @@ + + +
+ +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ + +
+ +
+
+
+ + diff --git a/apps/clock/apps/web/src/lib/components/skeletons/index.ts b/apps/clock/apps/web/src/lib/components/skeletons/index.ts new file mode 100644 index 000000000..c6b8952ad --- /dev/null +++ b/apps/clock/apps/web/src/lib/components/skeletons/index.ts @@ -0,0 +1,8 @@ +/** + * Clock App Skeleton Components + * + * App-specific skeleton loaders for loading states. + */ + +// App Loading Skeleton +export { default as AppLoadingSkeleton } from './AppLoadingSkeleton.svelte'; diff --git a/apps/clock/apps/web/src/lib/i18n/index.ts b/apps/clock/apps/web/src/lib/i18n/index.ts new file mode 100644 index 000000000..e6c3a0023 --- /dev/null +++ b/apps/clock/apps/web/src/lib/i18n/index.ts @@ -0,0 +1,49 @@ +import { browser } from '$app/environment'; +import { init, register, locale, waitLocale } from 'svelte-i18n'; + +// List of supported locales +export const supportedLocales = ['de', 'en'] as const; +export type SupportedLocale = (typeof supportedLocales)[number]; + +// Default locale +const defaultLocale = 'de'; + +// Register all available locales +register('de', () => import('./locales/de.json')); +register('en', () => import('./locales/en.json')); + +// Get initial locale from browser or localStorage +function getInitialLocale(): SupportedLocale { + if (browser) { + // Check localStorage first + const stored = localStorage.getItem('clock_locale'); + if (stored && supportedLocales.includes(stored as SupportedLocale)) { + return stored as SupportedLocale; + } + + // Fall back to browser language + const browserLang = navigator.language.split('-')[0]; + if (supportedLocales.includes(browserLang as SupportedLocale)) { + return browserLang as SupportedLocale; + } + } + + return defaultLocale; +} + +// Initialize i18n at module scope (required for SSR) +init({ + fallbackLocale: defaultLocale, + initialLocale: getInitialLocale(), +}); + +// Set locale and persist to localStorage +export function setLocale(newLocale: SupportedLocale) { + locale.set(newLocale); + if (browser) { + localStorage.setItem('clock_locale', newLocale); + } +} + +// Wait for locale to be loaded (useful for SSR) +export { waitLocale }; diff --git a/apps/clock/apps/web/src/lib/i18n/locales/de.json b/apps/clock/apps/web/src/lib/i18n/locales/de.json new file mode 100644 index 000000000..fc35180f9 --- /dev/null +++ b/apps/clock/apps/web/src/lib/i18n/locales/de.json @@ -0,0 +1,23 @@ +{ + "app": { + "name": "Clock" + }, + "common": { + "back": "Zurück", + "cancel": "Abbrechen", + "loading": "Lade..." + }, + "nav": { + "home": "Startseite", + "settings": "Einstellungen" + }, + "clock": { + "title": "Life Clock", + "remaining": "Verbleibende Zeit", + "elapsed": "Vergangene Zeit" + }, + "messages": { + "saved": "Gespeichert", + "error": "Ein Fehler ist aufgetreten" + } +} diff --git a/apps/clock/apps/web/src/lib/i18n/locales/en.json b/apps/clock/apps/web/src/lib/i18n/locales/en.json new file mode 100644 index 000000000..f6f978137 --- /dev/null +++ b/apps/clock/apps/web/src/lib/i18n/locales/en.json @@ -0,0 +1,23 @@ +{ + "app": { + "name": "Clock" + }, + "common": { + "back": "Back", + "cancel": "Cancel", + "loading": "Loading..." + }, + "nav": { + "home": "Home", + "settings": "Settings" + }, + "clock": { + "title": "Life Clock", + "remaining": "Time remaining", + "elapsed": "Time elapsed" + }, + "messages": { + "saved": "Saved", + "error": "An error occurred" + } +} diff --git a/apps/clock/apps/web/src/lib/stores/theme.svelte.ts b/apps/clock/apps/web/src/lib/stores/theme.svelte.ts new file mode 100644 index 000000000..5784cfeec --- /dev/null +++ b/apps/clock/apps/web/src/lib/stores/theme.svelte.ts @@ -0,0 +1,7 @@ +import { createThemeStore } from '@manacore/shared-theme'; + +// Create theme store with Clock's styling +export const theme = createThemeStore({ + appId: 'clock', + defaultVariant: 'lume', +}); diff --git a/apps/clock/apps/web/src/lib/stores/toast.ts b/apps/clock/apps/web/src/lib/stores/toast.ts new file mode 100644 index 000000000..eb45c4b3c --- /dev/null +++ b/apps/clock/apps/web/src/lib/stores/toast.ts @@ -0,0 +1,44 @@ +import { writable } from 'svelte/store'; + +export interface Toast { + id: string; + type: 'success' | 'error' | 'info' | 'warning'; + message: string; + duration?: number; +} + +function createToastStore() { + const { subscribe, update } = writable([]); + + function addToast(toast: Omit) { + const id = crypto.randomUUID(); + const newToast = { ...toast, id }; + + update((toasts) => [...toasts, newToast]); + + // Auto-remove after duration + const duration = toast.duration || 5000; + setTimeout(() => { + removeToast(id); + }, duration); + + return id; + } + + function removeToast(id: string) { + update((toasts) => toasts.filter((t) => t.id !== id)); + } + + return { + subscribe, + success: (message: string, duration?: number) => + addToast({ type: 'success', message, duration }), + error: (message: string, duration?: number) => addToast({ type: 'error', message, duration }), + info: (message: string, duration?: number) => addToast({ type: 'info', message, duration }), + warning: (message: string, duration?: number) => + addToast({ type: 'warning', message, duration }), + remove: removeToast, + }; +} + +export const toasts = createToastStore();