Commit Message feat: implement comprehensive shared packages architecture for monorepo SUMMARY: Introduce 10 shared packages to unify common code across all 4 web apps, reducing ~3,000 lines of duplicated code and establishing consistent patterns for authentication, UI components, theming, and utilities. NEW SHARED PACKAGES: - @manacore/shared-auth: Unified auth logic (token management, JWT utils, fetch interceptor, storage/device/network adapters) - @manacore/shared-auth-ui: Reusable auth UI (LoginPage, RegisterPage, OAuth buttons for Google/Apple) - @manacore/shared-tailwind: Unified Tailwind config with 4 themes (lume, nature, stone, ocean) and light/dark mode support - @manacore/shared-icons: Phosphor-based icon library (40+ icons) - @manacore/shared-ui: Atomic design system (Text, Button, Badge, Toggle, Input, Modal) - @manacore/shared-i18n: Unified i18n setup with locale detection - @manacore/shared-config: Environment validation with Zod - @manacore/shared-subscriptio n-types: Subscription type definitions - @manacore/shared-subscriptio n-ui: Subscription UI components (planned) EXTENDED PACKAGES: - @manacore/shared-types: Added auth.ts, theme.ts, ui.ts, common.ts - @manacore/shared-utils: Added format.ts, validation.ts APP MIGRATIONS: - memoro/web: Migrated login (549→46 LOC), tailwind (165→12 LOC), removed 15+ duplicate components - manacore/web: Migrated to client-side auth with shared-auth, added new components (Icon, ThemeToggle, Logo) - manadeck/web: Replaced local authService/tokenManager with shared-auth, migrated auth pages - maerchenzauber/web: Added auth setup, stores, components, routes DELETED FILES (migrated to shared packages): - OAuth buttons (Google/Apple) from memoro, manacore, manadeck - Local authService, tokenManager, deviceManager, jwt utils - Duplicate Modal, Toggle, Text components - iconPaths and ManaIcon components - Subscription-related components (CostCard, PackageCard, etc.) BENEFITS: - 92% reduction in login page code - 93% reduction in tailwind config code - Consistent theming across all apps - Single source of truth for auth logic - Easier maintenance and updates BREAKING CHANGES: - Icon imports now from @manacore/shared-icons - Modal imports from @manacore/shared-ui - OAuth config via setGoogleCl ientId()/setAppleConfig()

This commit is contained in:
Till-JS 2025-11-24 21:09:20 +01:00
parent 725db638ea
commit ef70a1af0b
198 changed files with 11113 additions and 3656 deletions

View file

@ -0,0 +1,42 @@
<script lang="ts">
import type { Snippet } from 'svelte';
type BadgeVariant = 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'info';
type BadgeSize = 'sm' | 'md';
interface Props {
variant?: BadgeVariant;
size?: BadgeSize;
class?: string;
children?: Snippet;
}
let {
variant = 'default',
size = 'md',
class: className = '',
children
}: Props = $props();
const variantClasses: Record<BadgeVariant, string> = {
default: 'bg-menu text-theme border-theme',
primary: 'bg-primary/20 text-primary border-primary/30',
success: 'bg-green-500/20 text-green-600 dark:text-green-400 border-green-500/30',
warning: 'bg-yellow-500/20 text-yellow-600 dark:text-yellow-400 border-yellow-500/30',
danger: 'bg-red-500/20 text-red-600 dark:text-red-400 border-red-500/30',
info: 'bg-blue-500/20 text-blue-600 dark:text-blue-400 border-blue-500/30'
};
const sizeClasses: Record<BadgeSize, string> = {
sm: 'px-1.5 py-0.5 text-xs',
md: 'px-2 py-1 text-sm'
};
const classes = $derived(
`inline-flex items-center rounded-full border font-medium ${variantClasses[variant]} ${sizeClasses[size]} ${className}`
);
</script>
<span class={classes}>
{@render children?.()}
</span>

View file

@ -0,0 +1,60 @@
<script lang="ts">
import type { Snippet } from 'svelte';
type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger';
type ButtonSize = 'sm' | 'md' | 'lg';
interface Props {
variant?: ButtonVariant;
size?: ButtonSize;
disabled?: boolean;
loading?: boolean;
class?: string;
onclick?: (e: MouseEvent) => void;
type?: 'button' | 'submit' | 'reset';
children?: Snippet;
}
let {
variant = 'primary',
size = 'md',
disabled = false,
loading = false,
class: className = '',
onclick,
type = 'button',
children
}: Props = $props();
const variantClasses: Record<ButtonVariant, string> = {
primary: 'bg-primary text-white hover:bg-primary/90 border-transparent',
secondary: 'bg-menu text-theme hover:bg-menu-hover border-theme',
ghost: 'bg-transparent text-theme hover:bg-menu-hover border-transparent',
danger: 'bg-red-600 text-white hover:bg-red-700 border-transparent'
};
const sizeClasses: Record<ButtonSize, string> = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg'
};
const classes = $derived(
`inline-flex items-center justify-center gap-2 rounded-lg border font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-primary/50 disabled:opacity-50 disabled:cursor-not-allowed ${variantClasses[variant]} ${sizeClasses[size]} ${className}`
);
</script>
<button
{type}
class={classes}
disabled={disabled || loading}
{onclick}
>
{#if loading}
<svg class="h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
{/if}
{@render children?.()}
</button>

View file

@ -0,0 +1,53 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
type TextVariant = 'body' | 'body-secondary' | 'small' | 'large' | 'muted';
type TextAlign = 'left' | 'center' | 'right';
type TextWeight = 'normal' | 'medium' | 'semibold' | 'bold';
interface Props extends HTMLAttributes<HTMLParagraphElement> {
variant?: TextVariant;
align?: TextAlign;
weight?: TextWeight;
class?: string;
children?: any;
}
let {
variant = 'body',
align = 'left',
weight = 'normal',
class: className = '',
children,
...restProps
}: Props = $props();
const variantClasses: Record<TextVariant, string> = {
body: 'text-base text-theme leading-relaxed',
'body-secondary': 'text-base text-theme-secondary leading-relaxed',
small: 'text-sm text-theme',
large: 'text-lg text-theme leading-relaxed',
muted: 'text-sm text-theme-muted'
};
const alignClasses: Record<TextAlign, string> = {
left: 'text-left',
center: 'text-center',
right: 'text-right'
};
const weightClasses: Record<TextWeight, string> = {
normal: 'font-normal',
medium: 'font-medium',
semibold: 'font-semibold',
bold: 'font-bold'
};
const classes = $derived(
`${variantClasses[variant]} ${alignClasses[align]} ${weightClasses[weight]} ${className}`
);
</script>
<p class={classes} {...restProps}>
{@render children?.()}
</p>

View file

@ -0,0 +1,3 @@
export { default as Text } from './Text.svelte';
export { default as Button } from './Button.svelte';
export { default as Badge } from './Badge.svelte';

View file

@ -1,24 +1,8 @@
/**
* Shared React Native UI components for Manacore monorepo
*
* This package will contain common UI components used across all mobile apps.
*
* Planned components:
* - Button
* - Text
* - Input
* - Card
* - Modal
* - Loading indicators
* - Icons
*/
// Atoms
export { Text, Button, Badge } from './atoms';
// Placeholder export until components are migrated
export const SHARED_UI_VERSION = '0.1.0';
// Molecules
export { Toggle, Input } from './molecules';
// Future exports will include:
// export { Button } from './components/Button';
// export { Text } from './components/Text';
// export { Input } from './components/Input';
// export { Card } from './components/Card';
// export { Modal } from './components/Modal';
// Organisms
export { Modal } from './organisms';

View file

@ -0,0 +1,71 @@
<script lang="ts">
import type { HTMLInputAttributes } from 'svelte/elements';
interface Props {
value: string;
oninput?: (value: string) => void;
onchange?: (value: string) => void;
label?: string;
placeholder?: string;
type?: 'text' | 'email' | 'password' | 'number' | 'tel' | 'url';
error?: string;
disabled?: boolean;
required?: boolean;
autocomplete?: HTMLInputAttributes['autocomplete'];
class?: string;
}
let {
value = $bindable(),
oninput,
onchange,
label,
placeholder,
type = 'text',
error,
disabled = false,
required = false,
autocomplete,
class: className = ''
}: Props = $props();
function handleInput(e: Event) {
const target = e.target as HTMLInputElement;
value = target.value;
oninput?.(target.value);
}
function handleChange(e: Event) {
const target = e.target as HTMLInputElement;
onchange?.(target.value);
}
</script>
<div class="flex flex-col gap-1.5 {className}">
{#if label}
<label class="text-sm font-medium text-theme">
{label}
{#if required}
<span class="text-red-500">*</span>
{/if}
</label>
{/if}
<input
{type}
{value}
{placeholder}
{disabled}
{required}
autocomplete={autocomplete as HTMLInputAttributes['autocomplete']}
oninput={handleInput}
onchange={handleChange}
class="w-full rounded-lg border px-4 py-2.5 text-theme bg-content transition-colors focus:outline-none focus:ring-2 focus:ring-primary/50 disabled:opacity-50 disabled:cursor-not-allowed {error
? 'border-red-500 focus:ring-red-500/50'
: 'border-theme'}"
/>
{#if error}
<p class="text-sm text-red-500">{error}</p>
{/if}
</div>

View file

@ -0,0 +1,37 @@
<script lang="ts">
interface Props {
isOn: boolean;
onToggle: (value: boolean) => void;
disabled?: boolean;
size?: 'sm' | 'md';
}
let { isOn = false, onToggle, disabled = false, size = 'md' }: Props = $props();
function handleToggle() {
if (!disabled) {
onToggle(!isOn);
}
}
const sizeClasses = {
sm: { track: 'h-6 w-10', thumb: 'h-4 w-4 top-1 left-1', translate: 'translate-x-4' },
md: { track: 'h-8 w-14', thumb: 'h-6 w-6 top-1 left-1', translate: 'translate-x-6' }
};
</script>
<button
onclick={handleToggle}
class="relative {sizeClasses[size].track} flex-shrink-0 rounded-full transition-colors {isOn
? 'bg-primary'
: 'bg-menu'} {disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'}"
role="switch"
aria-checked={isOn}
{disabled}
>
<span
class="absolute {sizeClasses[size].thumb} rounded-full bg-white shadow-md transition-transform {isOn
? sizeClasses[size].translate
: 'translate-x-0'}"
></span>
</button>

View file

@ -0,0 +1,2 @@
export { default as Toggle } from './Toggle.svelte';
export { default as Input } from './Input.svelte';

View file

@ -0,0 +1,92 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import { Icon } from '@manacore/shared-icons';
import Text from '../atoms/Text.svelte';
interface Props {
visible: boolean;
onClose: () => void;
title?: string;
icon?: Snippet;
children: Snippet;
footer?: Snippet;
maxWidth?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl';
showHeader?: boolean;
}
let { visible, onClose, title, icon, children, footer, maxWidth = 'lg', showHeader = true }: Props = $props();
const maxWidthClasses = {
sm: 'max-w-sm',
md: 'max-w-md',
lg: 'max-w-lg',
xl: 'max-w-xl',
'2xl': 'max-w-2xl',
'3xl': 'max-w-3xl'
};
function handleBackdropClick(e: MouseEvent) {
if (e.target === e.currentTarget) {
onClose();
}
}
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Escape' && visible) {
onClose();
}
}
</script>
<svelte:window onkeydown={handleKeydown} />
{#if visible}
<!-- Modal Backdrop -->
<div
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4"
onclick={handleBackdropClick}
role="dialog"
aria-modal="true"
>
<!-- Modal Content -->
<div
class="relative flex max-h-[90vh] w-full {maxWidthClasses[maxWidth]} flex-col rounded-xl border border-theme bg-menu shadow-xl"
onclick={(e) => e.stopPropagation()}
>
{#if showHeader}
<!-- Header -->
<div class="flex items-center justify-between p-6 border-b border-theme">
<div class="flex items-center gap-2 flex-1">
{#if icon}
{@render icon()}
{/if}
{#if title}
<Text variant="large" weight="semibold">
{title}
</Text>
{/if}
</div>
<button
onclick={onClose}
class="p-2 rounded-full hover:bg-menu-hover transition-colors"
aria-label="Close"
>
<Icon name="x" size={20} class="text-theme-muted" />
</button>
</div>
{/if}
<!-- Body (scrollable) -->
<div class="flex-1 overflow-y-auto p-6">
{@render children()}
</div>
<!-- Footer (optional) -->
{#if footer}
<div class="border-t border-theme p-6">
{@render footer()}
</div>
{/if}
</div>
</div>
{/if}

View file

@ -0,0 +1 @@
export { default as Modal } from './Modal.svelte';