mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-22 02:26:41 +02:00
feat: unify UI components, AppSlider, and login screens across apps
SUMMARY:
Consolidate shared UI components into @manacore/shared-ui and add
AppSlider to all login screens for a consistent Mana ecosystem experience.
CHANGES:
1. UI Components Migration:
- Added Card.svelte to @manacore/shared-ui with variants (elevated, outlined, ghost)
- Migrated Manacore (7 files) and Manadeck (7 files) to use shared-ui
- Removed local ui/ directories from both apps (8 components total)
2. AppSlider Unification:
- Created shared AppSlider in @manacore/shared-ui with configurable props
- Props: apps[], title, isDark, statusLabels, comingSoonLabel, openAppLabel, onAppClick
- Supports both i18n and static text configurations
- Updated Memoro AppSlider to use shared component with svelte-i18n
- Updated Manacore AppSlider to use shared component
- Created new AppSlider for ManaDeck and Märchenzauber
3. Login Page Enhancements:
- Extended LoginPage in @manacore/shared-auth-ui with new snippets:
- appSlider: Renders AppSlider at bottom (initial mode only)
- headerControls: Renders controls (theme toggle, etc.) top-right
- Updated all app login pages to include AppSlider:
- ManaCore: indigo theme (#6366f1)
- ManaDeck: violet theme (#8b5cf6)
- Märchenzauber: pink theme (#FF6B9D)
4. Subscription Page Consolidation:
- Created SubscriptionPage component in @manacore/shared-subscription-ui
- Moved subscription data (plans, packages, costs) to shared package
- Reduced subscription page code from ~100 to ~18 lines per app
FILES CHANGED:
- packages/shared-ui: Added Card, AppSlider, updated exports
- packages/shared-auth-ui: Extended LoginPage with snippets
- packages/shared-subscription-ui: Added SubscriptionPage, data files
- manacore/web: Migrated 7 files to shared-ui, updated login
- manadeck/web: Migrated 7 files to shared-ui, added AppSlider, updated login
- maerchenzauber/web: Added AppSlider, updated login
- memoro/web: Updated AppSlider to use shared component
DELETED (moved to shared packages):
- manacore/web/src/lib/components/ui/* (3 files)
- manadeck/web/src/lib/components/ui/* (5 files)
- memoro/web/src/lib/data/*.json (3 files)
- Various pnpm-lock.yaml and pnpm-workspace.yaml files
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
96e0aceb93
commit
22cb7d2c5f
67 changed files with 894 additions and 22131 deletions
|
|
@ -1,55 +1,5 @@
|
|||
@import '@manacore/shared-tailwind/themes.css';
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
--surface: 0 0% 98%;
|
||||
--surface-elevated: 0 0% 100%;
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 222.2 84% 4.9%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
--surface: 217.2 32.6% 17.5%;
|
||||
--surface-elevated: 222.2 47.4% 11.2%;
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 212.7 26.8% 83.9%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
53
manadeck/apps/web/src/lib/components/AppSlider.svelte
Normal file
53
manadeck/apps/web/src/lib/components/AppSlider.svelte
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
<script lang="ts">
|
||||
import { AppSlider, type AppItem } from '@manacore/shared-ui';
|
||||
|
||||
const apps: AppItem[] = [
|
||||
{
|
||||
name: 'Memoro',
|
||||
description: 'AI Voice Memos',
|
||||
longDescription: 'Transform your voice recordings into organized, searchable notes with AI-powered transcription and insights.',
|
||||
icon: '/images/app-icons/memoro-logo-gradient.png',
|
||||
color: '#f8d62b',
|
||||
comingSoon: false,
|
||||
status: 'published'
|
||||
},
|
||||
{
|
||||
name: 'ManaDeck',
|
||||
description: 'AI Flashcards',
|
||||
longDescription: 'Create and study smart flashcards with AI-powered spaced repetition for efficient learning.',
|
||||
icon: '/images/app-icons/manadeck-logo-gradient.png',
|
||||
color: '#8b5cf6',
|
||||
comingSoon: false,
|
||||
status: 'development'
|
||||
},
|
||||
{
|
||||
name: 'Märchenzauber',
|
||||
description: 'AI Story Creator',
|
||||
longDescription: 'Create magical personalized stories for children with AI-generated illustrations and consistent characters.',
|
||||
icon: '/images/app-icons/maerchenzauber-logo-gradient.png',
|
||||
color: '#FF6B9D',
|
||||
comingSoon: true,
|
||||
status: 'beta'
|
||||
},
|
||||
{
|
||||
name: 'Manacore',
|
||||
description: 'Central Hub',
|
||||
longDescription: 'Your central hub for managing all Mana applications, subscriptions, and account settings.',
|
||||
icon: '/images/app-icons/manacore-logo-gradient.png',
|
||||
color: '#6366f1',
|
||||
comingSoon: true,
|
||||
status: 'development'
|
||||
}
|
||||
];
|
||||
|
||||
function handleAppClick(app: AppItem, index: number) {
|
||||
console.log('Opening app:', app.name);
|
||||
}
|
||||
</script>
|
||||
|
||||
<AppSlider
|
||||
{apps}
|
||||
title="Part of the Mana Ecosystem"
|
||||
isDark={false}
|
||||
onAppClick={handleAppClick}
|
||||
/>
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
<script lang="ts">
|
||||
import Modal from '$lib/components/ui/Modal.svelte';
|
||||
import Input from '$lib/components/ui/Input.svelte';
|
||||
import Button from '$lib/components/ui/Button.svelte';
|
||||
import { Modal, Input, Button } from '@manacore/shared-ui';
|
||||
import { deckStore } from '$lib/stores/deckStore.svelte';
|
||||
|
||||
interface Props {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
<script lang="ts">
|
||||
import type { Deck } from '$lib/types/deck';
|
||||
import Card from '$lib/components/ui/Card.svelte';
|
||||
import Badge from '$lib/components/ui/Badge.svelte';
|
||||
import { Card, Badge } from '@manacore/shared-ui';
|
||||
|
||||
interface Props {
|
||||
deck: Deck;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
{ label: 'Decks', href: '/decks', icon: '📚' },
|
||||
{ label: 'Explore', href: '/explore', icon: '🔍' },
|
||||
{ label: 'Progress', href: '/progress', icon: '📊' },
|
||||
{ label: 'Mana', href: '/subscription', icon: '⚡' },
|
||||
{ label: 'Profile', href: '/profile', icon: '👤' }
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
variant?: 'default' | 'secondary' | 'outline' | 'destructive';
|
||||
children: Snippet;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
variant = 'default',
|
||||
children,
|
||||
class: className = ''
|
||||
}: Props = $props();
|
||||
|
||||
const variantClasses = {
|
||||
default: 'bg-primary text-primary-foreground',
|
||||
secondary: 'bg-secondary text-secondary-foreground',
|
||||
outline: 'border border-border',
|
||||
destructive: 'bg-destructive text-destructive-foreground'
|
||||
};
|
||||
|
||||
const classes = `inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold transition-colors ${variantClasses[variant]} ${className}`;
|
||||
</script>
|
||||
|
||||
<span class={classes}>
|
||||
{@render children()}
|
||||
</span>
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'destructive';
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
type?: 'button' | 'submit' | 'reset';
|
||||
disabled?: boolean;
|
||||
loading?: boolean;
|
||||
fullWidth?: boolean;
|
||||
onclick?: (e: MouseEvent) => void;
|
||||
children: Snippet;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
variant = 'primary',
|
||||
size = 'md',
|
||||
type = 'button',
|
||||
disabled = false,
|
||||
loading = false,
|
||||
fullWidth = false,
|
||||
onclick,
|
||||
children,
|
||||
class: className = ''
|
||||
}: Props = $props();
|
||||
|
||||
const baseClasses = 'inline-flex items-center justify-center font-medium transition-colors rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none';
|
||||
|
||||
const variantClasses = {
|
||||
primary: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90'
|
||||
};
|
||||
|
||||
const sizeClasses = {
|
||||
sm: 'h-9 px-3 text-sm',
|
||||
md: 'h-10 px-4 py-2',
|
||||
lg: 'h-11 px-8 text-lg'
|
||||
};
|
||||
|
||||
const classes = `${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]} ${fullWidth ? 'w-full' : ''} ${className}`;
|
||||
</script>
|
||||
|
||||
<button
|
||||
{type}
|
||||
class={classes}
|
||||
disabled={disabled || loading}
|
||||
onclick={onclick}
|
||||
>
|
||||
{#if loading}
|
||||
<svg
|
||||
class="mr-2 h-4 w-4 animate-spin"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
class="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
></circle>
|
||||
<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"
|
||||
></path>
|
||||
</svg>
|
||||
{/if}
|
||||
{@render children()}
|
||||
</button>
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
variant?: 'elevated' | 'outlined' | 'ghost';
|
||||
padding?: 'none' | 'sm' | 'md' | 'lg';
|
||||
children: Snippet;
|
||||
class?: string;
|
||||
onclick?: (e: MouseEvent) => void;
|
||||
}
|
||||
|
||||
let {
|
||||
variant = 'elevated',
|
||||
padding = 'md',
|
||||
children,
|
||||
class: className = '',
|
||||
onclick
|
||||
}: Props = $props();
|
||||
|
||||
const variantClasses = {
|
||||
elevated: 'bg-surface-elevated shadow-md',
|
||||
outlined: 'bg-surface border border-border',
|
||||
ghost: 'bg-transparent'
|
||||
};
|
||||
|
||||
const paddingClasses = {
|
||||
none: '',
|
||||
sm: 'p-4',
|
||||
md: 'p-6',
|
||||
lg: 'p-8'
|
||||
};
|
||||
|
||||
const classes = `rounded-lg ${variantClasses[variant]} ${paddingClasses[padding]} ${className}`;
|
||||
</script>
|
||||
|
||||
<div class={classes} {onclick} role={onclick ? 'button' : undefined} tabindex={onclick ? 0 : undefined}>
|
||||
{@render children()}
|
||||
</div>
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { HTMLInputAttributes } from 'svelte/elements';
|
||||
|
||||
interface Props {
|
||||
type?: 'text' | 'email' | 'password' | 'number' | 'search';
|
||||
value?: string;
|
||||
placeholder?: string;
|
||||
label?: string;
|
||||
error?: string;
|
||||
disabled?: boolean;
|
||||
required?: boolean;
|
||||
autocomplete?: HTMLInputAttributes['autocomplete'];
|
||||
oninput?: (e: Event & { currentTarget: HTMLInputElement }) => void;
|
||||
class?: string;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
type = 'text',
|
||||
value = $bindable(''),
|
||||
placeholder = '',
|
||||
label,
|
||||
error,
|
||||
disabled = false,
|
||||
required = false,
|
||||
autocomplete,
|
||||
oninput,
|
||||
class: className = '',
|
||||
id
|
||||
}: Props = $props();
|
||||
|
||||
// Generate unique ID if not provided
|
||||
const inputId = id || `input-${Math.random().toString(36).substring(2, 9)}`;
|
||||
|
||||
const inputClasses = `
|
||||
flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm
|
||||
ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium
|
||||
placeholder:text-muted-foreground
|
||||
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2
|
||||
disabled:cursor-not-allowed disabled:opacity-50
|
||||
${error ? 'border-destructive' : ''}
|
||||
${className}
|
||||
`;
|
||||
</script>
|
||||
|
||||
<div class="space-y-2">
|
||||
{#if label}
|
||||
<label for={inputId} class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
||||
{label}
|
||||
{#if required}
|
||||
<span class="text-destructive">*</span>
|
||||
{/if}
|
||||
</label>
|
||||
{/if}
|
||||
|
||||
<input
|
||||
id={inputId}
|
||||
{type}
|
||||
bind:value
|
||||
{placeholder}
|
||||
{disabled}
|
||||
{required}
|
||||
autocomplete={autocomplete}
|
||||
{oninput}
|
||||
class={inputClasses}
|
||||
/>
|
||||
|
||||
{#if error}
|
||||
<p class="text-sm text-destructive">{error}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import Button from './Button.svelte';
|
||||
|
||||
interface Props {
|
||||
open?: boolean;
|
||||
title?: string;
|
||||
onClose?: () => void;
|
||||
children: Snippet;
|
||||
}
|
||||
|
||||
let { open = $bindable(false), title, onClose, children }: Props = $props();
|
||||
|
||||
function handleClose() {
|
||||
open = false;
|
||||
onClose?.();
|
||||
}
|
||||
|
||||
function handleBackdropClick(e: MouseEvent) {
|
||||
if (e.target === e.currentTarget) {
|
||||
handleClose();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if open}
|
||||
<div
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
|
||||
onclick={handleBackdropClick}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<div class="bg-surface-elevated rounded-lg shadow-xl max-w-lg w-full mx-4 max-h-[90vh] overflow-auto">
|
||||
<!-- Header -->
|
||||
{#if title}
|
||||
<div class="flex items-center justify-between p-6 border-b border-border">
|
||||
<h2 class="text-xl font-semibold">{title}</h2>
|
||||
<button
|
||||
onclick={handleClose}
|
||||
class="text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Content -->
|
||||
<div class="p-6">
|
||||
{@render children()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { deckStore } from '$lib/stores/deckStore.svelte';
|
||||
import Button from '$lib/components/ui/Button.svelte';
|
||||
import { Button } from '@manacore/shared-ui';
|
||||
import DeckCard from '$lib/components/deck/DeckCard.svelte';
|
||||
import CreateDeckModal from '$lib/components/deck/CreateDeckModal.svelte';
|
||||
|
||||
|
|
|
|||
|
|
@ -3,9 +3,7 @@
|
|||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { deckStore } from '$lib/stores/deckStore.svelte';
|
||||
import Button from '$lib/components/ui/Button.svelte';
|
||||
import Badge from '$lib/components/ui/Badge.svelte';
|
||||
import Card from '$lib/components/ui/Card.svelte';
|
||||
import { Button, Badge, Card } from '@manacore/shared-ui';
|
||||
|
||||
let deckId = $derived($page.params.id);
|
||||
let showDeleteConfirm = $state(false);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import Card from '$lib/components/ui/Card.svelte';
|
||||
import { Card } from '@manacore/shared-ui';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { authStore } from '$lib/stores/authStore.svelte';
|
||||
import Card from '$lib/components/ui/Card.svelte';
|
||||
import Button from '$lib/components/ui/Button.svelte';
|
||||
import { Card, Button } from '@manacore/shared-ui';
|
||||
|
||||
let credits = $state<number | null>(null);
|
||||
let loadingCredits = $state(false);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import Card from '$lib/components/ui/Card.svelte';
|
||||
import { Card } from '@manacore/shared-ui';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
|
|||
18
manadeck/apps/web/src/routes/(app)/subscription/+page.svelte
Normal file
18
manadeck/apps/web/src/routes/(app)/subscription/+page.svelte
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<script lang="ts">
|
||||
import { SubscriptionPage } from '@manacore/shared-subscription-ui';
|
||||
|
||||
function handleSubscribe(planId: string) {
|
||||
alert(`Subscribe to plan: ${planId}\n\nThis would trigger RevenueCat purchase flow.`);
|
||||
}
|
||||
|
||||
function handleBuyPackage(packageId: string) {
|
||||
alert(`Buy package: ${packageId}\n\nThis would trigger RevenueCat purchase flow.`);
|
||||
}
|
||||
</script>
|
||||
|
||||
<SubscriptionPage
|
||||
appName="ManaDeck"
|
||||
onSubscribe={handleSubscribe}
|
||||
onBuyPackage={handleBuyPackage}
|
||||
currentPlanId="free"
|
||||
/>
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
import { goto } from '$app/navigation';
|
||||
import { LoginPage } from '@manacore/shared-auth-ui';
|
||||
import ManaDeckLogo from '$lib/components/ManaDeckLogo.svelte';
|
||||
import AppSlider from '$lib/components/AppSlider.svelte';
|
||||
import { authStore } from '$lib/stores/authStore.svelte';
|
||||
|
||||
async function handleSignIn(email: string, password: string) {
|
||||
|
|
@ -26,4 +27,8 @@
|
|||
registerPath="/register"
|
||||
lightBackground="#faf5ff"
|
||||
darkBackground="#1a1625"
|
||||
/>
|
||||
>
|
||||
{#snippet appSlider()}
|
||||
<AppSlider />
|
||||
{/snippet}
|
||||
</LoginPage>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue