mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-20 16:29:24 +02:00
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:
parent
725db638ea
commit
ef70a1af0b
198 changed files with 11113 additions and 3656 deletions
42
packages/shared-ui/src/atoms/Badge.svelte
Normal file
42
packages/shared-ui/src/atoms/Badge.svelte
Normal 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>
|
||||
60
packages/shared-ui/src/atoms/Button.svelte
Normal file
60
packages/shared-ui/src/atoms/Button.svelte
Normal 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>
|
||||
53
packages/shared-ui/src/atoms/Text.svelte
Normal file
53
packages/shared-ui/src/atoms/Text.svelte
Normal 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>
|
||||
3
packages/shared-ui/src/atoms/index.ts
Normal file
3
packages/shared-ui/src/atoms/index.ts
Normal 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';
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
71
packages/shared-ui/src/molecules/Input.svelte
Normal file
71
packages/shared-ui/src/molecules/Input.svelte
Normal 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>
|
||||
37
packages/shared-ui/src/molecules/Toggle.svelte
Normal file
37
packages/shared-ui/src/molecules/Toggle.svelte
Normal 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>
|
||||
2
packages/shared-ui/src/molecules/index.ts
Normal file
2
packages/shared-ui/src/molecules/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { default as Toggle } from './Toggle.svelte';
|
||||
export { default as Input } from './Input.svelte';
|
||||
92
packages/shared-ui/src/organisms/Modal.svelte
Normal file
92
packages/shared-ui/src/organisms/Modal.svelte
Normal 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}
|
||||
1
packages/shared-ui/src/organisms/index.ts
Normal file
1
packages/shared-ui/src/organisms/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default as Modal } from './Modal.svelte';
|
||||
Loading…
Add table
Add a link
Reference in a new issue