mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 22:01:09 +02:00
♻️ refactor: unify web app patterns across monorepo
- Rename authStore.svelte.ts to auth.svelte.ts (manacore, manadeck, moodlit) - Add i18n setup to Finance and Mail apps (DE, EN, FR, ES, IT) - Add feedback pages using shared component to Finance and Mail - Create @manacore/shared-api-client package with API client factory - Create @manacore/shared-vite-config package with SSR config helpers - Create @manacore/shared-stores package with toast, navigation, theme factories 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
c93aca0cce
commit
cfbc8a2c15
60 changed files with 2213 additions and 173 deletions
21
packages/shared-stores/package.json
Normal file
21
packages/shared-stores/package.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"name": "@manacore/shared-stores",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"type-check": "echo 'Skipping: shared-stores uses Svelte 5 runes, type-checked at build time'"
|
||||
},
|
||||
"devDependencies": {
|
||||
"svelte": "^5.0.0",
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@manacore/shared-auth": "workspace:*"
|
||||
}
|
||||
}
|
||||
12
packages/shared-stores/src/index.ts
Normal file
12
packages/shared-stores/src/index.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
/**
|
||||
* Shared Store Factories for ManaCore Apps
|
||||
* Provides reusable Svelte 5 runes-based stores.
|
||||
*/
|
||||
|
||||
export { createToastStore, type Toast, type ToastStore, type ToastType } from './toast.svelte';
|
||||
export {
|
||||
createNavigationStore,
|
||||
type NavigationItem,
|
||||
type NavigationStore,
|
||||
} from './navigation.svelte';
|
||||
export { createThemeStore, type ThemeStore, type ThemeMode } from './theme.svelte';
|
||||
117
packages/shared-stores/src/navigation.svelte.ts
Normal file
117
packages/shared-stores/src/navigation.svelte.ts
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
/**
|
||||
* Navigation Store Factory
|
||||
* Creates a navigation state store with Svelte 5 runes.
|
||||
*/
|
||||
|
||||
export interface NavigationItem {
|
||||
href: string;
|
||||
label: string;
|
||||
icon?: string;
|
||||
badge?: string | number;
|
||||
children?: NavigationItem[];
|
||||
}
|
||||
|
||||
export interface NavigationStore {
|
||||
readonly items: NavigationItem[];
|
||||
readonly isOpen: boolean;
|
||||
readonly isSidebarMode: boolean;
|
||||
readonly isCollapsed: boolean;
|
||||
setItems: (items: NavigationItem[]) => void;
|
||||
toggle: () => void;
|
||||
open: () => void;
|
||||
close: () => void;
|
||||
setSidebarMode: (isSidebar: boolean) => void;
|
||||
setCollapsed: (collapsed: boolean) => void;
|
||||
}
|
||||
|
||||
export interface NavigationStoreConfig {
|
||||
/** Initial navigation items */
|
||||
initialItems?: NavigationItem[];
|
||||
/** Storage key for persisting sidebar mode */
|
||||
storageKey?: string;
|
||||
/** Whether to start in sidebar mode */
|
||||
defaultSidebarMode?: boolean;
|
||||
/** Whether to start collapsed */
|
||||
defaultCollapsed?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a navigation store with Svelte 5 runes.
|
||||
*/
|
||||
export function createNavigationStore(config: NavigationStoreConfig = {}): NavigationStore {
|
||||
const {
|
||||
initialItems = [],
|
||||
storageKey,
|
||||
defaultSidebarMode = false,
|
||||
defaultCollapsed = false,
|
||||
} = config;
|
||||
|
||||
let items = $state<NavigationItem[]>(initialItems);
|
||||
let isOpen = $state(false);
|
||||
let isSidebarMode = $state(defaultSidebarMode);
|
||||
let isCollapsed = $state(defaultCollapsed);
|
||||
|
||||
// Load from localStorage if available
|
||||
if (storageKey && typeof localStorage !== 'undefined') {
|
||||
const savedSidebar = localStorage.getItem(`${storageKey}-sidebar`);
|
||||
const savedCollapsed = localStorage.getItem(`${storageKey}-collapsed`);
|
||||
|
||||
if (savedSidebar !== null) {
|
||||
isSidebarMode = savedSidebar === 'true';
|
||||
}
|
||||
if (savedCollapsed !== null) {
|
||||
isCollapsed = savedCollapsed === 'true';
|
||||
}
|
||||
}
|
||||
|
||||
function setItems(newItems: NavigationItem[]) {
|
||||
items = newItems;
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
isOpen = !isOpen;
|
||||
}
|
||||
|
||||
function open() {
|
||||
isOpen = true;
|
||||
}
|
||||
|
||||
function close() {
|
||||
isOpen = false;
|
||||
}
|
||||
|
||||
function setSidebarMode(sidebar: boolean) {
|
||||
isSidebarMode = sidebar;
|
||||
if (storageKey && typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem(`${storageKey}-sidebar`, String(sidebar));
|
||||
}
|
||||
}
|
||||
|
||||
function setCollapsed(collapsed: boolean) {
|
||||
isCollapsed = collapsed;
|
||||
if (storageKey && typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem(`${storageKey}-collapsed`, String(collapsed));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
get items() {
|
||||
return items;
|
||||
},
|
||||
get isOpen() {
|
||||
return isOpen;
|
||||
},
|
||||
get isSidebarMode() {
|
||||
return isSidebarMode;
|
||||
},
|
||||
get isCollapsed() {
|
||||
return isCollapsed;
|
||||
},
|
||||
setItems,
|
||||
toggle,
|
||||
open,
|
||||
close,
|
||||
setSidebarMode,
|
||||
setCollapsed,
|
||||
};
|
||||
}
|
||||
125
packages/shared-stores/src/theme.svelte.ts
Normal file
125
packages/shared-stores/src/theme.svelte.ts
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
/**
|
||||
* Theme Store Factory
|
||||
* Creates a theme state store with Svelte 5 runes.
|
||||
*/
|
||||
|
||||
export type ThemeMode = 'light' | 'dark' | 'system';
|
||||
|
||||
export interface ThemeStore {
|
||||
readonly isDark: boolean;
|
||||
readonly mode: ThemeMode;
|
||||
readonly variant: string;
|
||||
initialize: () => () => void;
|
||||
setMode: (mode: ThemeMode) => void;
|
||||
setVariant: (variant: string) => void;
|
||||
toggle: () => void;
|
||||
}
|
||||
|
||||
export interface ThemeStoreConfig {
|
||||
/** Storage key prefix (default: 'theme') */
|
||||
storagePrefix?: string;
|
||||
/** Default theme mode */
|
||||
defaultMode?: ThemeMode;
|
||||
/** Default theme variant */
|
||||
defaultVariant?: string;
|
||||
/** CSS class to add/remove for dark mode */
|
||||
darkClass?: string;
|
||||
/** Data attribute for variant */
|
||||
variantAttribute?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a theme store with Svelte 5 runes.
|
||||
*/
|
||||
export function createThemeStore(config: ThemeStoreConfig = {}): ThemeStore {
|
||||
const {
|
||||
storagePrefix = 'theme',
|
||||
defaultMode = 'system',
|
||||
defaultVariant = 'default',
|
||||
darkClass = 'dark',
|
||||
variantAttribute = 'data-theme',
|
||||
} = config;
|
||||
|
||||
let isDark = $state(false);
|
||||
let mode = $state<ThemeMode>(defaultMode);
|
||||
let variant = $state(defaultVariant);
|
||||
|
||||
function updateTheme() {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
let shouldBeDark = false;
|
||||
if (mode === 'dark') {
|
||||
shouldBeDark = true;
|
||||
} else if (mode === 'system') {
|
||||
shouldBeDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
}
|
||||
|
||||
isDark = shouldBeDark;
|
||||
document.documentElement.classList.toggle(darkClass, isDark);
|
||||
}
|
||||
|
||||
function initialize(): () => void {
|
||||
if (typeof window === 'undefined') return () => {};
|
||||
|
||||
// Load from localStorage
|
||||
const savedMode = localStorage.getItem(`${storagePrefix}-mode`) as ThemeMode | null;
|
||||
const savedVariant = localStorage.getItem(`${storagePrefix}-variant`);
|
||||
|
||||
if (savedMode) mode = savedMode;
|
||||
if (savedVariant) {
|
||||
variant = savedVariant;
|
||||
document.documentElement.setAttribute(variantAttribute, variant);
|
||||
}
|
||||
|
||||
updateTheme();
|
||||
|
||||
// Listen for system theme changes
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const handleChange = () => {
|
||||
if (mode === 'system') {
|
||||
updateTheme();
|
||||
}
|
||||
};
|
||||
|
||||
mediaQuery.addEventListener('change', handleChange);
|
||||
return () => mediaQuery.removeEventListener('change', handleChange);
|
||||
}
|
||||
|
||||
function setMode(newMode: ThemeMode) {
|
||||
mode = newMode;
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem(`${storagePrefix}-mode`, newMode);
|
||||
}
|
||||
updateTheme();
|
||||
}
|
||||
|
||||
function setVariant(newVariant: string) {
|
||||
variant = newVariant;
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem(`${storagePrefix}-variant`, newVariant);
|
||||
}
|
||||
if (typeof document !== 'undefined') {
|
||||
document.documentElement.setAttribute(variantAttribute, newVariant);
|
||||
}
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
setMode(isDark ? 'light' : 'dark');
|
||||
}
|
||||
|
||||
return {
|
||||
get isDark() {
|
||||
return isDark;
|
||||
},
|
||||
get mode() {
|
||||
return mode;
|
||||
},
|
||||
get variant() {
|
||||
return variant;
|
||||
},
|
||||
initialize,
|
||||
setMode,
|
||||
setVariant,
|
||||
toggle,
|
||||
};
|
||||
}
|
||||
76
packages/shared-stores/src/toast.svelte.ts
Normal file
76
packages/shared-stores/src/toast.svelte.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
/**
|
||||
* Toast Store Factory
|
||||
* Creates a toast notification store with Svelte 5 runes.
|
||||
*/
|
||||
|
||||
export type ToastType = 'success' | 'error' | 'info' | 'warning';
|
||||
|
||||
export interface Toast {
|
||||
id: string;
|
||||
type: ToastType;
|
||||
message: string;
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
export interface ToastStore {
|
||||
readonly toasts: Toast[];
|
||||
show: (message: string, type?: ToastType, duration?: number) => void;
|
||||
success: (message: string, duration?: number) => void;
|
||||
error: (message: string, duration?: number) => void;
|
||||
info: (message: string, duration?: number) => void;
|
||||
warning: (message: string, duration?: number) => void;
|
||||
dismiss: (id: string) => void;
|
||||
clear: () => void;
|
||||
}
|
||||
|
||||
export interface ToastStoreConfig {
|
||||
/** Default duration in milliseconds (default: 5000) */
|
||||
defaultDuration?: number;
|
||||
/** Maximum number of toasts visible at once */
|
||||
maxToasts?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a toast store with Svelte 5 runes.
|
||||
*/
|
||||
export function createToastStore(config: ToastStoreConfig = {}): ToastStore {
|
||||
const { defaultDuration = 5000, maxToasts = 5 } = config;
|
||||
|
||||
let toasts = $state<Toast[]>([]);
|
||||
|
||||
function generateId(): string {
|
||||
return Math.random().toString(36).substring(2, 9);
|
||||
}
|
||||
|
||||
function show(message: string, type: ToastType = 'info', duration: number = defaultDuration) {
|
||||
const id = generateId();
|
||||
const toast: Toast = { id, type, message, duration };
|
||||
|
||||
toasts = [...toasts.slice(-(maxToasts - 1)), toast];
|
||||
|
||||
if (duration > 0) {
|
||||
setTimeout(() => dismiss(id), duration);
|
||||
}
|
||||
}
|
||||
|
||||
function dismiss(id: string) {
|
||||
toasts = toasts.filter((t) => t.id !== id);
|
||||
}
|
||||
|
||||
function clear() {
|
||||
toasts = [];
|
||||
}
|
||||
|
||||
return {
|
||||
get toasts() {
|
||||
return toasts;
|
||||
},
|
||||
show,
|
||||
success: (message: string, duration?: number) => show(message, 'success', duration),
|
||||
error: (message: string, duration?: number) => show(message, 'error', duration),
|
||||
info: (message: string, duration?: number) => show(message, 'info', duration),
|
||||
warning: (message: string, duration?: number) => show(message, 'warning', duration),
|
||||
dismiss,
|
||||
clear,
|
||||
};
|
||||
}
|
||||
16
packages/shared-stores/tsconfig.json
Normal file
16
packages/shared-stores/tsconfig.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue