UI refactoring: Profile, History, and Plans Components

This commit is contained in:
AG
2025-12-07 23:59:33 +02:00
parent 57f7ad077e
commit a3a9aa7194
8 changed files with 829 additions and 667 deletions

Binary file not shown.

View File

@@ -1,10 +1,12 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { toTitleCase } from '../utils/text'; import { toTitleCase } from '../utils/text';
import { X, Dumbbell, User, Flame, Timer as TimerIcon, ArrowUp, ArrowRight, Footprints, Ruler, Percent } from 'lucide-react'; import { Dumbbell, User, Flame, Timer as TimerIcon, ArrowUp, ArrowRight, Footprints, Ruler, Percent } from 'lucide-react';
import { ExerciseDef, ExerciseType, Language } from '../types'; import { ExerciseDef, ExerciseType, Language } from '../types';
import { t } from '../services/i18n'; import { t } from '../services/i18n';
import { generateId } from '../utils/uuid'; import { generateId } from '../utils/uuid';
import FilledInput from './FilledInput'; import FilledInput from './FilledInput';
import { Modal } from './ui/Modal';
import { Button } from './ui/Button';
interface ExerciseModalProps { interface ExerciseModalProps {
isOpen: boolean; isOpen: boolean;
@@ -58,19 +60,16 @@ const ExerciseModal: React.FC<ExerciseModalProps> = ({ isOpen, onClose, onSave,
setNewBwPercentage('100'); setNewBwPercentage('100');
setIsUnilateral(false); setIsUnilateral(false);
setError(''); setError('');
onClose(); onClose(); // Modal controls its own open state usually, but here checking prop
}; };
if (!isOpen) return null;
return ( return (
<div className="fixed inset-0 bg-black/60 z-[60] flex items-end sm:items-center justify-center p-4 backdrop-blur-sm"> <Modal
<div className="bg-surface-container w-full max-w-sm rounded-[28px] p-6 shadow-elevation-3 animate-in slide-in-from-bottom-10 duration-200"> isOpen={isOpen}
<div className="flex justify-between items-center mb-6"> onClose={onClose}
<h3 className="text-2xl font-normal text-on-surface">{t('create_exercise', lang)}</h3> title={t('create_exercise', lang)}
<button onClick={onClose} className="p-2 bg-surface-container-high rounded-full hover:bg-outline-variant/20"><X size={20} /></button> maxWidth="sm"
</div> >
<div className="space-y-6"> <div className="space-y-6">
<div> <div>
<FilledInput <FilledInput
@@ -138,17 +137,16 @@ const ExerciseModal: React.FC<ExerciseModalProps> = ({ isOpen, onClose, onSave,
</label> </label>
</div> </div>
<div className="flex justify-end mt-4"> <div className="flex justify-end mt-4 pt-4">
<button <Button
onClick={handleCreateExercise} onClick={handleCreateExercise}
className="px-8 py-3 bg-primary text-on-primary rounded-full font-medium shadow-elevation-1" fullWidth
> >
{t('create_btn', lang)} {t('create_btn', lang)}
</button> </Button>
</div>
</div>
</div> </div>
</div> </div>
</Modal>
); );
}; };

View File

@@ -1,9 +1,12 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Calendar, Clock, TrendingUp, Gauge, Pencil, Trash2, X, Save, ArrowRight, ArrowUp, Timer, Activity, Dumbbell, Percent } from 'lucide-react'; import { Calendar, Clock, TrendingUp, Gauge, Pencil, Trash2, X, Save, ArrowRight, ArrowUp, Timer, Activity, Dumbbell, Percent } from 'lucide-react';
import { WorkoutSession, ExerciseType, WorkoutSet, Language } from '../types'; import { WorkoutSession, ExerciseType, WorkoutSet, Language } from '../types';
import { t } from '../services/i18n'; import { t } from '../services/i18n';
import { useSession } from '../context/SessionContext'; import { useSession } from '../context/SessionContext';
import { Button } from './ui/Button';
import { Card } from './ui/Card';
import { Modal } from './ui/Modal';
import FilledInput from './FilledInput';
interface HistoryProps { interface HistoryProps {
lang: Language; lang: Language;
@@ -116,8 +119,6 @@ const History: React.FC<HistoryProps> = ({ lang }) => {
} }
if (sessions.length === 0) { if (sessions.length === 0) {
return ( return (
<div className="flex flex-col items-center justify-center h-full text-on-surface-variant p-8 text-center"> <div className="flex flex-col items-center justify-center h-full text-on-surface-variant p-8 text-center">
@@ -129,19 +130,20 @@ const History: React.FC<HistoryProps> = ({ lang }) => {
return ( return (
<div className="h-full flex flex-col bg-surface"> <div className="h-full flex flex-col bg-surface">
<div className="p-4 bg-surface-container shadow-elevation-1 z-10"> <div className="p-4 bg-surface-container shadow-elevation-1 z-10 shrink-0">
<h2 className="text-2xl font-normal text-on-surface">{t('tab_history', lang)}</h2> <h2 className="text-2xl font-normal text-on-surface">{t('tab_history', lang)}</h2>
</div> </div>
<div className="flex-1 overflow-y-auto p-4 space-y-4 pb-20"> <div className="flex-1 overflow-y-auto p-4 pb-20">
<div className="max-w-2xl mx-auto space-y-4">
{/* Regular Workout Sessions */} {/* Regular Workout Sessions */}
{sessions.filter(s => s.type === 'STANDARD').map((session) => { {sessions.filter(s => s.type === 'STANDARD').map((session) => {
const totalWork = calculateSessionWork(session); const totalWork = calculateSessionWork(session);
return ( return (
<div <Card
key={session.id} key={session.id}
className="bg-surface-container rounded-xl p-5 shadow-elevation-1 border border-outline-variant/20 cursor-pointer hover:bg-surface-container-high transition-colors" className="cursor-pointer hover:bg-surface-container-high transition-colors"
onClick={() => setEditingSession(JSON.parse(JSON.stringify(session)))} onClick={() => setEditingSession(JSON.parse(JSON.stringify(session)))}
> >
<div className="flex justify-between items-start"> <div className="flex justify-between items-start">
@@ -181,27 +183,31 @@ const History: React.FC<HistoryProps> = ({ lang }) => {
</div> </div>
<div className="flex gap-1"> <div className="flex gap-1">
<button <Button
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
setEditingSession(JSON.parse(JSON.stringify(session))); setEditingSession(JSON.parse(JSON.stringify(session)));
}} }}
className="p-2 text-on-surface-variant hover:text-primary hover:bg-surface-container-high rounded-full transition-colors" variant="ghost"
size="icon"
className="text-on-surface-variant hover:text-primary"
> >
<Pencil size={20} /> <Pencil size={20} />
</button> </Button>
<button <Button
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
setDeletingId(session.id); setDeletingId(session.id);
}} }}
className="p-2 text-on-surface-variant hover:text-error hover:bg-surface-container-high rounded-full transition-colors" variant="ghost"
size="icon"
className="text-on-surface-variant hover:text-error"
> >
<Trash2 size={20} /> <Trash2 size={20} />
</button> </Button>
</div>
</div> </div>
</div> </div>
</Card>
) )
})} })}
@@ -209,25 +215,27 @@ const History: React.FC<HistoryProps> = ({ lang }) => {
{sessions.filter(s => s.type === 'QUICK_LOG').length > 0 && ( {sessions.filter(s => s.type === 'QUICK_LOG').length > 0 && (
<div className="mt-8"> <div className="mt-8">
<h3 className="text-xl font-medium text-on-surface mb-4 px-2">{t('quick_log', lang)}</h3> <h3 className="text-xl font-medium text-on-surface mb-4 px-2">{t('quick_log', lang)}</h3>
{Object.entries( {(Object.entries(
sessions sessions
.filter(s => s.type === 'QUICK_LOG') .filter(s => s.type === 'QUICK_LOG')
.reduce((groups: Record<string, WorkoutSession[]>, session) => { .reduce<Record<string, WorkoutSession[]>>((groups, session) => {
const date = new Date(session.startTime).toISOString().split('T')[0]; const date = new Date(session.startTime).toISOString().split('T')[0];
if (!groups[date]) groups[date] = []; if (!groups[date]) groups[date] = [];
groups[date].push(session); groups[date].push(session);
return groups; return groups;
}, {}) }, {})
) ) as [string, WorkoutSession[]][])
.sort(([a], [b]) => b.localeCompare(a)) .sort(([a], [b]) => b.localeCompare(a))
.map(([date, daySessions]) => ( .map(([date, daySessions]) => (
<div key={date} className="mb-4"> <div key={date} className="mb-4">
<div className="text-sm text-on-surface-variant px-2 mb-2 font-medium">{date}</div> <div className="text-sm text-on-surface-variant px-2 mb-2 font-medium">{date}</div>
<div className="space-y-2"> <div className="space-y-2">
{daySessions.flatMap(session => session.sets).map((set, idx) => ( {daySessions
<div .reduce<WorkoutSet[]>((acc, session) => acc.concat(session.sets), [])
.map((set, idx) => (
<Card
key={set.id} key={set.id}
className="bg-surface-container-low rounded-xl p-4 border border-outline-variant/10 flex justify-between items-center" className="bg-surface-container-low flex justify-between items-center"
> >
<div className="flex-1"> <div className="flex-1">
<div className="font-medium text-on-surface"> <div className="font-medium text-on-surface">
@@ -248,7 +256,7 @@ const History: React.FC<HistoryProps> = ({ lang }) => {
</div> </div>
</div> </div>
<div className="flex gap-1"> <div className="flex gap-1">
<button <Button
onClick={() => { onClick={() => {
// Find the session this set belongs to and open edit mode // Find the session this set belongs to and open edit mode
const parentSession = daySessions.find(s => s.sets.some(st => st.id === set.id)); const parentSession = daySessions.find(s => s.sets.some(st => st.id === set.id));
@@ -256,11 +264,13 @@ const History: React.FC<HistoryProps> = ({ lang }) => {
setEditingSession(JSON.parse(JSON.stringify(parentSession))); setEditingSession(JSON.parse(JSON.stringify(parentSession)));
} }
}} }}
className="p-2 text-on-surface-variant hover:text-primary hover:bg-surface-container-high rounded-full transition-colors" variant="ghost"
size="icon"
className="h-8 w-8"
> >
<Pencil size={18} /> <Pencil size={16} />
</button> </Button>
<button <Button
onClick={() => { onClick={() => {
// Find the session and set up for deletion // Find the session and set up for deletion
const parentSession = daySessions.find(s => s.sets.some(st => st.id === set.id)); const parentSession = daySessions.find(s => s.sets.some(st => st.id === set.id));
@@ -268,69 +278,70 @@ const History: React.FC<HistoryProps> = ({ lang }) => {
setDeletingSetInfo({ sessionId: parentSession.id, setId: set.id }); setDeletingSetInfo({ sessionId: parentSession.id, setId: set.id });
} }
}} }}
className="p-2 text-on-surface-variant hover:text-error hover:bg-surface-container-high rounded-full transition-colors" variant="ghost"
size="icon"
className="h-8 w-8 text-error hover:text-error"
> >
<Trash2 size={18} /> <Trash2 size={16} />
</button> </Button>
</div>
</div> </div>
</Card>
))} ))}
</div> </div>
</div> </div>
))} ))}
</div> </div>
)} )}
</div>
</div> </div>
{/* DELETE CONFIRMATION DIALOG (MD3) */} {/* DELETE CONFIRMATION MODAL */}
{(deletingId || deletingSetInfo) && ( <Modal
<div className="fixed inset-0 z-[60] flex items-center justify-center bg-black/60 backdrop-blur-sm p-4"> isOpen={!!(deletingId || deletingSetInfo)}
<div className="bg-surface-container w-full max-w-xs rounded-[28px] p-6 shadow-elevation-3"> onClose={() => {
<h3 className="text-xl font-normal text-on-surface mb-2"> setDeletingId(null);
{deletingId ? t('delete_workout', lang) : t('delete_set', lang) || 'Delete Set'} setDeletingSetInfo(null);
</h3> }}
<p className="text-sm text-on-surface-variant mb-8"> title={deletingId ? t('delete_workout', lang) : t('delete_set', lang) || 'Delete Set'}
maxWidth="sm"
>
<div className="space-y-6">
<p className="text-sm text-on-surface-variant">
{deletingId ? t('delete_confirm', lang) : t('delete_set_confirm', lang) || 'Are you sure you want to delete this set?'} {deletingId ? t('delete_confirm', lang) : t('delete_set_confirm', lang) || 'Are you sure you want to delete this set?'}
</p> </p>
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
<button <Button
onClick={() => { onClick={() => {
setDeletingId(null); setDeletingId(null);
setDeletingSetInfo(null); setDeletingSetInfo(null);
}} }}
className="px-4 py-2 rounded-full text-primary font-medium hover:bg-white/5" variant="ghost"
size="sm"
> >
{t('cancel', lang)} {t('cancel', lang)}
</button> </Button>
<button <Button
onClick={handleConfirmDelete} onClick={handleConfirmDelete}
className="px-4 py-2 rounded-full bg-error-container text-on-error-container font-medium" variant="destructive"
size="sm"
> >
{t('delete', lang)} {t('delete', lang)}
</button> </Button>
</div> </div>
</div> </div>
</div> </Modal>
)}
{/* EDIT SESSION FULLSCREEN DIALOG */} {/* EDIT SESSION MODAL */}
{editingSession && ( {editingSession && (
<div className="fixed inset-0 z-[60] bg-surface flex flex-col animate-in slide-in-from-bottom-10 duration-200"> <Modal
<div className="px-4 py-3 border-b border-outline-variant flex justify-between items-center bg-surface-container shadow-elevation-1"> isOpen={!!editingSession}
<button onClick={() => setEditingSession(null)} className="text-on-surface-variant hover:text-on-surface"> onClose={() => setEditingSession(null)}
<X /> title={t('edit', lang)}
</button> maxWidth="lg"
<h2 className="text-lg font-medium text-on-surface">{t('edit', lang)}</h2> >
<button onClick={handleSaveEdit} className="text-primary font-medium flex items-center gap-2"> <div className="space-y-6">
{t('save', lang)}
</button>
</div>
<div className="flex-1 overflow-y-auto p-4 space-y-6">
{/* Meta Info */} {/* Meta Info */}
<div className="bg-surface-container p-4 rounded-xl border border-outline-variant/20 space-y-4"> <div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-2 gap-3">
<div className="bg-surface-container-high rounded-t-lg px-3 py-2 border-b border-outline-variant"> <div className="bg-surface-container-high rounded-t-lg px-3 py-2 border-b border-outline-variant">
<label className="text-[10px] text-on-surface-variant font-bold block">{t('start_time', lang)}</label> <label className="text-[10px] text-on-surface-variant font-bold block">{t('start_time', lang)}</label>
<input <input
@@ -359,27 +370,28 @@ const History: React.FC<HistoryProps> = ({ lang }) => {
className="w-full bg-transparent text-on-surface focus:outline-none text-lg mt-1" className="w-full bg-transparent text-on-surface focus:outline-none text-lg mt-1"
/> />
</div> </div>
</div>
<div className="space-y-3"> <div className="space-y-3">
<h3 className="text-sm font-medium text-primary ml-1">{t('sets_count', lang)} ({editingSession.sets.length})</h3> <h3 className="text-sm font-medium text-primary ml-1">{t('sets_count', lang)} ({editingSession.sets.length})</h3>
{editingSession.sets.map((set, idx) => ( {editingSession.sets.map((set, idx) => (
<div key={set.id} className="bg-surface-container p-3 rounded-xl border border-outline-variant/20 flex flex-col gap-3 shadow-sm"> <div key={set.id} className="bg-surface-container-low p-3 rounded-xl border border-outline-variant/20 flex flex-col gap-3">
<div className="flex justify-between items-center border-b border-outline-variant pb-2"> <div className="flex justify-between items-center border-b border-outline-variant/50 pb-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="w-6 h-6 rounded-full bg-secondary-container text-on-secondary-container text-xs font-bold flex items-center justify-center">{idx + 1}</span> <span className="w-6 h-6 rounded-full bg-secondary-container text-on-secondary-container text-xs font-bold flex items-center justify-center">{idx + 1}</span>
<span className="font-medium text-on-surface text-sm">{set.exerciseName}{set.side && <span className="ml-2 text-xs font-medium text-on-surface-variant">{t(set.side.toLowerCase(), lang)}</span>}</span> <span className="font-medium text-on-surface text-sm">{set.exerciseName}{set.side && <span className="ml-2 text-xs font-medium text-on-surface-variant">{t(set.side.toLowerCase() as any, lang)}</span>}</span>
</div> </div>
<button <Button
onClick={() => handleDeleteSet(set.id)} onClick={() => handleDeleteSet(set.id)}
className="text-on-surface-variant hover:text-error p-1 rounded hover:bg-error-container/10 transition-colors" variant="ghost"
size="icon"
className="h-8 w-8 text-on-surface-variant hover:text-error hover:bg-error-container/10"
title={t('delete', lang)} title={t('delete', lang)}
> >
<Trash2 size={18} /> <Trash2 size={16} />
</button> </Button>
</div> </div>
<div className="grid grid-cols-3 gap-2"> <div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
{(set.type === ExerciseType.STRENGTH || set.type === ExerciseType.BODYWEIGHT || set.type === ExerciseType.STATIC) && ( {(set.type === ExerciseType.STRENGTH || set.type === ExerciseType.BODYWEIGHT || set.type === ExerciseType.STATIC) && (
<div className="bg-surface-container-high rounded px-2 py-1"> <div className="bg-surface-container-high rounded px-2 py-1">
<label className="text-[10px] text-on-surface-variant flex gap-1 items-center"><Dumbbell size={10} /> {t('weight_kg', lang)}</label> <label className="text-[10px] text-on-surface-variant flex gap-1 items-center"><Dumbbell size={10} /> {t('weight_kg', lang)}</label>
@@ -450,11 +462,16 @@ const History: React.FC<HistoryProps> = ({ lang }) => {
</div> </div>
))} ))}
</div> </div>
<div className="flex justify-end pt-4 border-t border-outline-variant">
<Button onClick={handleSaveEdit}>
<Save size={16} className="mr-2" />
{t('save', lang)}
</Button>
</div> </div>
</div> </div>
</Modal>
)} )}
</div> </div>
); );
}; };

View File

@@ -1,6 +1,5 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Plus, Trash2, PlayCircle, Dumbbell, Save, X, ChevronRight, List, ArrowUp, ArrowDown, Scale, Edit2, User, Flame, Timer as TimerIcon, Ruler, Footprints, Activity, Percent, CheckCircle, GripVertical } from 'lucide-react'; import { Plus, Trash2, PlayCircle, Dumbbell, Save, X, ChevronRight, List, ArrowUp, ArrowDown, Edit2, User, Flame, Timer as TimerIcon, Ruler, Footprints, Activity, Percent, CheckCircle, GripVertical, Scale } from 'lucide-react';
import { WorkoutPlan, ExerciseDef, PlannedSet, Language, ExerciseType } from '../types'; import { WorkoutPlan, ExerciseDef, PlannedSet, Language, ExerciseType } from '../types';
import { getExercises, saveExercise } from '../services/storage'; import { getExercises, saveExercise } from '../services/storage';
import { t } from '../services/i18n'; import { t } from '../services/i18n';
@@ -11,6 +10,8 @@ import { useActiveWorkout } from '../context/ActiveWorkoutContext';
import FilledInput from './FilledInput'; import FilledInput from './FilledInput';
import { toTitleCase } from '../utils/text'; import { toTitleCase } from '../utils/text';
import { Button } from './ui/Button';
import { Card } from './ui/Card';
interface PlansProps { interface PlansProps {
lang: Language; lang: Language;
@@ -162,26 +163,25 @@ const Plans: React.FC<PlansProps> = ({ lang }) => {
if (isEditing) { if (isEditing) {
return ( return (
<div className="h-full flex flex-col bg-surface"> <div className="h-full flex flex-col bg-surface">
<div className="px-4 py-3 bg-surface-container border-b border-outline-variant flex justify-between items-center"> <div className="px-4 py-3 bg-surface-container border-b border-outline-variant flex justify-between items-center shrink-0">
<button onClick={() => setIsEditing(false)} className="p-2 text-on-surface-variant hover:bg-white/5 rounded-full"><X /></button> <Button onClick={() => setIsEditing(false)} variant="ghost" size="icon">
<X size={20} />
</Button>
<h2 className="text-title-medium font-medium text-on-surface">{t('plan_editor', lang)}</h2> <h2 className="text-title-medium font-medium text-on-surface">{t('plan_editor', lang)}</h2>
<button onClick={handleSave} className="p-2 text-primary font-medium"> <Button onClick={handleSave} variant="ghost" className="text-primary font-medium hover:bg-primary-container/10">
{t('save', lang)} {t('save', lang)}
</button> </Button>
</div> </div>
<div className="flex-1 overflow-y-auto p-4 space-y-6"> <div className="flex-1 overflow-y-auto p-4 space-y-6">
<div className="bg-surface-container-high rounded-t-lg border-b border-outline-variant px-4 py-2"> <FilledInput
<label className="text-[10px] text-on-surface-variant font-medium">{t('ex_name', lang)}</label> label={t('ex_name', lang)}
<input
className="w-full bg-transparent text-xl text-on-surface focus:outline-none pt-1 pb-2"
placeholder={t('plan_name_ph', lang)}
value={name} value={name}
onChange={(e) => setName(e.target.value)} onChange={(e: any) => setName(e.target.value)}
autoCapitalize="words" type="text"
autocapitalize="words"
onBlur={() => setName(toTitleCase(name))} onBlur={() => setName(toTitleCase(name))}
/> />
</div>
<div className="bg-surface-container-high rounded-t-lg border-b border-outline-variant px-4 py-2"> <div className="bg-surface-container-high rounded-t-lg border-b border-outline-variant px-4 py-2">
<label className="text-[10px] text-on-surface-variant font-medium">{t('prep_title', lang)}</label> <label className="text-[10px] text-on-surface-variant font-medium">{t('prep_title', lang)}</label>
@@ -200,9 +200,9 @@ const Plans: React.FC<PlansProps> = ({ lang }) => {
<div className="space-y-2"> <div className="space-y-2">
{steps.map((step, idx) => ( {steps.map((step, idx) => (
<div <Card
key={step.id} key={step.id}
className={`bg-surface-container rounded-xl p-3 flex items-center gap-3 shadow-elevation-1 cursor-move transition-all hover:bg-surface-container-high ${draggingIndex === idx ? 'opacity-50 ring-2 ring-primary bg-surface-container-high' : ''}`} className={`flex items-center gap-3 transition-all hover:bg-surface-container-high ${draggingIndex === idx ? 'opacity-50 ring-2 ring-primary bg-surface-container-high' : ''}`}
draggable draggable
onDragStart={() => onDragStart(idx)} onDragStart={() => onDragStart(idx)}
onDragEnter={() => onDragEnter(idx)} onDragEnter={() => onDragEnter(idx)}
@@ -221,7 +221,7 @@ const Plans: React.FC<PlansProps> = ({ lang }) => {
<div className="text-base font-medium text-on-surface">{step.exerciseName}</div> <div className="text-base font-medium text-on-surface">{step.exerciseName}</div>
<label className="flex items-center gap-2 mt-1 cursor-pointer w-fit"> <label className="flex items-center gap-2 mt-1 cursor-pointer w-fit">
<div className={`w-4 h-4 border rounded flex items-center justify-center ${step.isWeighted ? 'bg-primary border-primary' : 'border-outline'}`}> <div className={`w-4 h-4 border rounded flex items-center justify-center ${step.isWeighted ? 'bg-primary border-primary' : 'border-outline'}`}>
{step.isWeighted && <Scale size={10} className="text-on-primary" />} {step.isWeighted && <Dumbbell size={10} className="text-on-primary" />}
</div> </div>
<input <input
type="checkbox" type="checkbox"
@@ -232,32 +232,36 @@ const Plans: React.FC<PlansProps> = ({ lang }) => {
<span className="text-xs text-on-surface-variant">{t('weighted', lang)}</span> <span className="text-xs text-on-surface-variant">{t('weighted', lang)}</span>
</label> </label>
</div> </div>
<button onClick={() => removeStep(step.id)} className="text-on-surface-variant hover:text-error p-2"> <Button onClick={() => removeStep(step.id)} variant="ghost" size="icon" className="text-on-surface-variant hover:text-error hover:bg-error/10">
<X size={20} /> <X size={20} />
</button> </Button>
</div> </Card>
))} ))}
</div> </div>
<button <Button
onClick={() => setShowExerciseSelector(true)} onClick={() => setShowExerciseSelector(true)}
className="w-full py-4 rounded-full border border-outline text-primary font-medium flex items-center justify-center gap-2 hover:bg-primary-container/10 transition-all" variant="outline"
fullWidth
className="py-6 rounded-full border border-outline text-primary font-medium flex items-center justify-center gap-2 hover:bg-primary-container/10 transition-all h-auto"
> >
<Plus size={20} /> <Plus size={20} />
{t('add_exercise', lang)} {t('add_exercise', lang)}
</button> </Button>
</div> </div>
</div> </div>
{showExerciseSelector && ( {showExerciseSelector && (
<div className="fixed inset-0 bg-surface z-50 flex flex-col animate-in slide-in-from-bottom-full duration-200"> <div className="fixed inset-0 bg-surface z-50 flex flex-col animate-in slide-in-from-bottom-full duration-200">
<div className="px-4 py-3 border-b border-outline-variant flex justify-between items-center bg-surface-container"> <div className="px-4 py-3 border-b border-outline-variant flex justify-between items-center bg-surface-container shrink-0">
<span className="font-medium text-on-surface">{t('select_exercise', lang)}</span> <span className="font-medium text-on-surface">{t('select_exercise', lang)}</span>
<div className="flex gap-2"> <div className="flex gap-2">
<button onClick={() => setIsCreatingExercise(true)} className="p-2 text-primary hover:bg-primary-container/20 rounded-full"> <Button onClick={() => setIsCreatingExercise(true)} variant="ghost" size="icon" className="text-primary hover:bg-primary-container/20">
<Plus size={20} /> <Plus size={20} />
</button> </Button>
<button onClick={() => setShowExerciseSelector(false)}><X /></button> <Button onClick={() => setShowExerciseSelector(false)} variant="ghost" size="icon">
<X size={20} />
</Button>
</div> </div>
</div> </div>
<div className="flex-1 overflow-y-auto p-2"> <div className="flex-1 overflow-y-auto p-2">
@@ -278,9 +282,11 @@ const Plans: React.FC<PlansProps> = ({ lang }) => {
{isCreatingExercise && ( {isCreatingExercise && (
<div className="fixed inset-0 bg-surface z-[60] flex flex-col animate-in slide-in-from-bottom-full duration-200"> <div className="fixed inset-0 bg-surface z-[60] flex flex-col animate-in slide-in-from-bottom-full duration-200">
<div className="px-4 py-3 border-b border-outline-variant flex justify-between items-center bg-surface-container"> <div className="px-4 py-3 border-b border-outline-variant flex justify-between items-center bg-surface-container shrink-0">
<h3 className="text-title-medium font-medium text-on-surface">{t('create_exercise', lang)}</h3> <h3 className="text-title-medium font-medium text-on-surface">{t('create_exercise', lang)}</h3>
<button onClick={() => setIsCreatingExercise(false)} className="p-2 text-on-surface-variant hover:bg-white/5 rounded-full"><X /></button> <Button onClick={() => setIsCreatingExercise(false)} variant="ghost" size="icon" className="text-on-surface-variant hover:bg-white/5">
<X size={20} />
</Button>
</div> </div>
<div className="p-4 space-y-6 overflow-y-auto flex-1"> <div className="p-4 space-y-6 overflow-y-auto flex-1">
@@ -330,13 +336,14 @@ const Plans: React.FC<PlansProps> = ({ lang }) => {
)} )}
<div className="flex justify-end mt-4"> <div className="flex justify-end mt-4">
<button <Button
onClick={handleCreateExercise} onClick={handleCreateExercise}
className="w-full h-14 bg-primary text-on-primary rounded-full font-medium shadow-elevation-1 flex items-center justify-center gap-2" fullWidth
size="lg"
> >
<CheckCircle size={20} /> <CheckCircle size={20} className="mr-2" />
{t('create_btn', lang)} {t('create_btn', lang)}
</button> </Button>
</div> </div>
</div> </div>
</div> </div>
@@ -349,7 +356,7 @@ const Plans: React.FC<PlansProps> = ({ lang }) => {
return ( return (
<div className="h-full flex flex-col bg-surface relative"> <div className="h-full flex flex-col bg-surface relative">
<div className="p-4 bg-surface-container shadow-elevation-1 z-10"> <div className="p-4 bg-surface-container shadow-elevation-1 z-10 shrink-0">
<h2 className="text-2xl font-normal text-on-surface">{t('my_plans', lang)}</h2> <h2 className="text-2xl font-normal text-on-surface">{t('my_plans', lang)}</h2>
</div> </div>
@@ -363,23 +370,27 @@ const Plans: React.FC<PlansProps> = ({ lang }) => {
</div> </div>
) : ( ) : (
plans.map(plan => ( plans.map(plan => (
<div key={plan.id} className="bg-surface-container rounded-xl p-4 shadow-elevation-1 border border-outline-variant/20 relative overflow-hidden"> <Card key={plan.id} className="relative overflow-hidden">
<div className="flex justify-between items-start mb-2"> <div className="flex justify-between items-start mb-2">
<h3 className="text-xl font-normal text-on-surface">{plan.name}</h3> <h3 className="text-xl font-normal text-on-surface">{plan.name}</h3>
<button <Button
onClick={(e) => handleDelete(plan.id, e)} onClick={(e) => handleDelete(plan.id, e)}
className="text-on-surface-variant hover:text-error p-2 rounded-full hover:bg-white/5" variant="ghost"
size="icon"
className="text-on-surface-variant hover:text-error hover:bg-white/5"
> >
<Trash2 size={20} /> <Trash2 size={20} />
</button> </Button>
</div> </div>
<div className="absolute top-4 right-14"> <div className="absolute top-4 right-14">
<button <Button
onClick={(e) => { e.stopPropagation(); handleEdit(plan); }} onClick={(e) => { e.stopPropagation(); handleEdit(plan); }}
className="text-on-surface-variant hover:text-primary p-2 rounded-full hover:bg-white/5" variant="ghost"
size="icon"
className="text-on-surface-variant hover:text-primary hover:bg-white/5"
> >
<Edit2 size={20} /> <Edit2 size={20} />
</button> </Button>
</div> </div>
<p className="text-on-surface-variant text-sm line-clamp-2 mb-4 min-h-[1.25rem]"> <p className="text-on-surface-variant text-sm line-clamp-2 mb-4 min-h-[1.25rem]">
{plan.description || t('prep_no_instructions', lang)} {plan.description || t('prep_no_instructions', lang)}
@@ -388,15 +399,15 @@ const Plans: React.FC<PlansProps> = ({ lang }) => {
<div className="text-xs font-medium text-primary bg-primary-container/20 px-3 py-1 rounded-full"> <div className="text-xs font-medium text-primary bg-primary-container/20 px-3 py-1 rounded-full">
{plan.steps.length} {t('exercises_count', lang)} {plan.steps.length} {t('exercises_count', lang)}
</div> </div>
<button <Button
onClick={() => startSession(plan)} onClick={() => startSession(plan)}
className="flex items-center gap-2 bg-primary text-on-primary px-5 py-2 rounded-full text-sm font-medium hover:shadow-elevation-2 transition-all" className="flex items-center gap-2"
> >
<PlayCircle size={18} /> <PlayCircle size={18} />
{t('start', lang)} {t('start', lang)}
</button> </Button>
</div>
</div> </div>
</Card>
)) ))
)} )}
</div> </div>

View File

@@ -9,6 +9,9 @@ import ExerciseModal from './ExerciseModal';
import FilledInput from './FilledInput'; import FilledInput from './FilledInput';
import { t } from '../services/i18n'; import { t } from '../services/i18n';
import Snackbar from './Snackbar'; import Snackbar from './Snackbar';
import { Button } from './ui/Button';
import { Card } from './ui/Card';
import { Modal } from './ui/Modal';
interface ProfileProps { interface ProfileProps {
user: User; user: User;
@@ -238,20 +241,21 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
return ( return (
<div className="h-full flex flex-col bg-surface"> <div className="h-full flex flex-col bg-surface">
<div className="p-4 bg-surface-container shadow-elevation-1 flex items-center justify-between z-10"> <div className="p-4 bg-surface-container shadow-elevation-1 flex items-center justify-between z-10 shrink-0">
<h2 className="text-xl font-normal text-on-surface flex items-center gap-2"> <h2 className="text-xl font-normal text-on-surface flex items-center gap-2">
<UserIcon size={20} /> <UserIcon size={20} />
{t('profile_title', lang)} {t('profile_title', lang)}
</h2> </h2>
<button onClick={onLogout} className="text-error flex items-center gap-1 text-sm font-medium hover:bg-error-container/10 px-3 py-1 rounded-full"> <Button onClick={onLogout} variant="ghost" size="sm" className="text-error hover:bg-error-container/10">
<LogOut size={16} /> {t('logout', lang)} <LogOut size={16} className="mr-1" /> {t('logout', lang)}
</button> </Button>
</div> </div>
<div className="flex-1 overflow-y-auto p-4 space-y-6 pb-24"> <div className="flex-1 overflow-y-auto p-4 space-y-6 pb-24">
<div className="max-w-2xl mx-auto space-y-6">
{/* User Info Card */} {/* User Info Card */}
<div className="bg-surface-container rounded-xl p-4 border border-outline-variant/20"> <Card>
<div className="flex items-center gap-4 mb-6"> <div className="flex items-center gap-4 mb-6">
<div className="w-14 h-14 rounded-full bg-primary-container text-on-primary-container flex items-center justify-center text-xl font-bold"> <div className="w-14 h-14 rounded-full bg-primary-container text-on-primary-container flex items-center justify-center text-xl font-bold">
{user.email[0].toUpperCase()} {user.email[0].toUpperCase()}
@@ -303,13 +307,13 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
</select> </select>
</div> </div>
<button onClick={handleSaveProfile} className="w-full py-2 rounded-full border border-outline text-primary text-sm font-medium hover:bg-primary-container/10 flex justify-center gap-2 items-center"> <Button onClick={handleSaveProfile} variant="outline" fullWidth>
<Save size={16} /> {t('save_profile', lang)} <Save size={16} className="mr-2" /> {t('save_profile', lang)}
</button> </Button>
</div> </Card>
{/* WEIGHT TRACKER */} {/* WEIGHT TRACKER */}
<div className="bg-surface-container rounded-xl p-4 border border-outline-variant/20"> <Card>
<button <button
onClick={() => setShowWeightTracker(!showWeightTracker)} onClick={() => setShowWeightTracker(!showWeightTracker)}
className="w-full flex justify-between items-center text-sm font-bold text-primary" className="w-full flex justify-between items-center text-sm font-bold text-primary"
@@ -332,12 +336,12 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
placeholder="Enter weight..." placeholder="Enter weight..."
/> />
</div> </div>
<button <Button
onClick={handleLogWeight} onClick={handleLogWeight}
className="bg-primary text-on-primary px-4 py-3 rounded-lg font-medium text-sm mb-[1px]" className="mb-[1px]"
> >
Log Log
</button> </Button>
</div> </div>
<div className="space-y-2 max-h-60 overflow-y-auto"> <div className="space-y-2 max-h-60 overflow-y-auto">
@@ -355,10 +359,10 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
</div> </div>
</div> </div>
)} )}
</div> </Card>
{/* EXERCISE MANAGER */} {/* EXERCISE MANAGER */}
<div className="bg-surface-container rounded-xl p-4 border border-outline-variant/20"> <Card>
<button <button
onClick={() => setShowExercises(!showExercises)} onClick={() => setShowExercises(!showExercises)}
className="w-full flex justify-between items-center text-sm font-bold text-primary" className="w-full flex justify-between items-center text-sm font-bold text-primary"
@@ -380,7 +384,7 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
label={t('filter_by_name', lang) || 'Filter by name'} label={t('filter_by_name', lang) || 'Filter by name'}
value={exerciseNameFilter} value={exerciseNameFilter}
onChange={(e: any) => setExerciseNameFilter(e.target.value)} onChange={(e: any) => setExerciseNameFilter(e.target.value)}
icon={<i className="hidden" />} // No icon needed or maybe use Search icon? Profile doesn't import Search. I'll omit icon if optional. icon={<i className="hidden" />}
type="text" type="text"
autoFocus={false} autoFocus={false}
/> />
@@ -426,10 +430,10 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
</div> </div>
</div> </div>
)} )}
</div> </Card>
{/* Change Password */} {/* Change Password */}
<div className="bg-surface-container rounded-xl p-4 border border-outline-variant/20"> <Card>
<h3 className="text-sm font-bold text-primary mb-4 flex items-center gap-2"><Lock size={14} /> {t('change_pass_btn', lang)}</h3> <h3 className="text-sm font-bold text-primary mb-4 flex items-center gap-2"><Lock size={14} /> {t('change_pass_btn', lang)}</h3>
<div className="flex gap-2"> <div className="flex gap-2">
<input <input
@@ -439,14 +443,14 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
onChange={(e) => setNewPassword(e.target.value)} onChange={(e) => setNewPassword(e.target.value)}
className="flex-1 bg-surface-container-high border-b border-outline-variant px-3 py-2 text-on-surface focus:outline-none rounded-t-lg" className="flex-1 bg-surface-container-high border-b border-outline-variant px-3 py-2 text-on-surface focus:outline-none rounded-t-lg"
/> />
<button onClick={handleChangePassword} className="bg-secondary-container text-on-secondary-container px-4 rounded-lg font-medium text-sm">OK</button> <Button onClick={handleChangePassword} size="sm" variant="secondary">OK</Button>
</div> </div>
{passMsg && <p className="text-xs text-primary mt-2">{passMsg}</p>} {passMsg && <p className="text-xs text-primary mt-2">{passMsg}</p>}
</div> </Card>
{/* User Self Deletion (Not for Admin) */} {/* User Self Deletion (Not for Admin) */}
{user.role !== 'ADMIN' && ( {user.role !== 'ADMIN' && (
<div className="bg-surface-container rounded-xl p-4 border border-error/30"> <Card className="border-error/30">
<h3 className="text-sm font-bold text-error mb-2 flex items-center gap-2"><Trash2 size={14} /> {t('delete_account', lang)}</h3> <h3 className="text-sm font-bold text-error mb-2 flex items-center gap-2"><Trash2 size={14} /> {t('delete_account', lang)}</h3>
{!showDeleteConfirm ? ( {!showDeleteConfirm ? (
<button onClick={() => setShowDeleteConfirm(true)} className="text-error text-sm hover:underline"> <button onClick={() => setShowDeleteConfirm(true)} className="text-error text-sm hover:underline">
@@ -456,17 +460,17 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
<div className="space-y-2"> <div className="space-y-2">
<p className="text-xs text-error">{t('delete_account_confirm', lang)}</p> <p className="text-xs text-error">{t('delete_account_confirm', lang)}</p>
<div className="flex gap-2"> <div className="flex gap-2">
<button onClick={() => setShowDeleteConfirm(false)} className="text-xs px-3 py-1 bg-surface-container-high rounded-full">{t('cancel', lang)}</button> <Button onClick={() => setShowDeleteConfirm(false)} size="sm" variant="ghost">{t('cancel', lang)}</Button>
<button onClick={handleDeleteMyAccount} className="text-xs px-3 py-1 bg-error text-on-error rounded-full">{t('delete', lang)}</button> <Button onClick={handleDeleteMyAccount} size="sm" variant="destructive">{t('delete', lang)}</Button>
</div> </div>
</div> </div>
)} )}
</div> </Card>
)} )}
{/* ADMIN AREA */} {/* ADMIN AREA */}
{user.role === 'ADMIN' && ( {user.role === 'ADMIN' && (
<div className="bg-surface-container rounded-xl p-4 border border-primary/30 relative overflow-hidden"> <Card className="border-primary/30 relative overflow-hidden">
<div className="absolute top-0 right-0 p-2 bg-primary/10 rounded-bl-xl"> <div className="absolute top-0 right-0 p-2 bg-primary/10 rounded-bl-xl">
<Shield size={16} className="text-primary" /> <Shield size={16} className="text-primary" />
</div> </div>
@@ -487,9 +491,9 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
onChange={(e) => setNewUserPass(e.target.value)} onChange={(e) => setNewUserPass(e.target.value)}
type="text" type="text"
/> />
<button onClick={handleCreateUser} className="w-full py-2 bg-primary text-on-primary rounded-full text-sm font-medium"> <Button onClick={handleCreateUser} fullWidth>
{t('create_btn', lang)} {t('create_btn', lang)}
</button> </Button>
{createMsg && <p className="text-xs text-error text-center font-medium">{createMsg}</p>} {createMsg && <p className="text-xs text-error text-center font-medium">{createMsg}</p>}
</div> </div>
@@ -549,12 +553,13 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
onChange={(e) => setAdminPassResetInput({ ...adminPassResetInput, [u.id]: e.target.value })} onChange={(e) => setAdminPassResetInput({ ...adminPassResetInput, [u.id]: e.target.value })}
/> />
</div> </div>
<button <Button
onClick={() => handleAdminResetPass(u.id)} onClick={() => handleAdminResetPass(u.id)}
className="text-xs bg-secondary-container text-on-secondary-container px-3 py-2 rounded font-medium" size="sm"
variant="secondary"
> >
{t('reset_pass', lang)} {t('reset_pass', lang)}
</button> </Button>
</div> </div>
)} )}
</div> </div>
@@ -562,14 +567,17 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
</div> </div>
)} )}
</div> </div>
</div> </Card>
)} )}
{/* Edit Exercise Modal */} {/* Edit Exercise Modal */}
{editingExercise && ( {editingExercise && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4"> <Modal
<div className="bg-surface-container w-full max-w-sm rounded-[28px] p-6 shadow-elevation-3"> isOpen={!!editingExercise}
<h3 className="text-xl font-normal text-on-surface mb-4">{t('edit', lang)}</h3> onClose={() => setEditingExercise(null)}
title={t('edit', lang)}
maxWidth="sm"
>
<div className="space-y-4"> <div className="space-y-4">
<div className="bg-surface-container-high rounded-t-lg border-b border-outline-variant px-3 py-2"> <div className="bg-surface-container-high rounded-t-lg border-b border-outline-variant px-3 py-2">
<label className="text-[10px] text-on-surface-variant font-medium">{t('ex_name', lang)}</label> <label className="text-[10px] text-on-surface-variant font-medium">{t('ex_name', lang)}</label>
@@ -591,12 +599,11 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
</div> </div>
)} )}
<div className="flex justify-end gap-2 pt-2"> <div className="flex justify-end gap-2 pt-2">
<button onClick={() => setEditingExercise(null)} className="px-4 py-2 rounded-full text-primary font-medium hover:bg-white/5">{t('cancel', lang)}</button> <Button onClick={() => setEditingExercise(null)} variant="ghost">{t('cancel', lang)}</Button>
<button onClick={handleSaveExerciseEdit} className="px-4 py-2 rounded-full bg-primary text-on-primary font-medium">{t('save', lang)}</button> <Button onClick={handleSaveExerciseEdit}>{t('save', lang)}</Button>
</div>
</div>
</div> </div>
</div> </div>
</Modal>
)} )}
{/* Create Exercise Modal */} {/* Create Exercise Modal */}
@@ -618,6 +625,7 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
onClose={() => setSnackbar(prev => ({ ...prev, isOpen: false }))} onClose={() => setSnackbar(prev => ({ ...prev, isOpen: false }))}
/> />
</div> </div>
</div>
); );
}; };

View File

@@ -0,0 +1,49 @@
import React from 'react';
import { Loader2 } from 'lucide-react';
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'destructive';
size?: 'sm' | 'md' | 'lg' | 'icon';
fullWidth?: boolean;
loading?: boolean;
children: React.ReactNode;
}
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className = '', variant = 'primary', size = 'md', fullWidth = false, loading = false, children, disabled, ...props }, ref) => {
const baseStyles = "inline-flex items-center justify-center rounded-full font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-50";
const variants = {
primary: "bg-primary text-on-primary hover:bg-primary/90",
secondary: "bg-secondary-container text-on-secondary-container hover:bg-secondary-container/80",
outline: "border border-outline text-primary hover:bg-primary-container/10",
ghost: "text-on-surface hover:bg-surface-container-high",
destructive: "bg-error text-on-error hover:bg-error/90",
};
const sizes = {
sm: "h-8 px-3 text-xs",
md: "h-10 px-4 text-sm",
lg: "h-12 px-8 text-base",
icon: "h-10 w-10",
};
const width = fullWidth ? "w-full" : "";
return (
<button
ref={ref}
className={`${baseStyles} ${variants[variant]} ${sizes[size]} ${width} ${className}`}
disabled={disabled || loading}
{...props}
>
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{children}
</button>
);
}
);
Button.displayName = "Button";

View File

@@ -0,0 +1,18 @@
import React from 'react';
interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
className?: string;
noPadding?: boolean;
}
export const Card: React.FC<CardProps> = ({ className = '', children, noPadding = false, ...props }) => {
return (
<div
className={`bg-surface-container rounded-xl border border-outline-variant/20 shadow-elevation-1 overflow-hidden ${!noPadding ? 'p-4' : ''} ${className}`}
{...props}
>
{children}
</div>
);
};

View File

@@ -0,0 +1,61 @@
import React, { useEffect, useState } from 'react';
import { X } from 'lucide-react';
import { createPortal } from 'react-dom';
interface ModalProps {
isOpen: boolean;
onClose: () => void;
title?: string;
children: React.ReactNode;
maxWidth?: 'sm' | 'md' | 'lg' | 'xl';
}
export const Modal: React.FC<ModalProps> = ({ isOpen, onClose, title, children, maxWidth = 'sm' }) => {
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
setIsMounted(true);
if (isOpen) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = 'unset';
}
return () => {
document.body.style.overflow = 'unset';
};
}, [isOpen]);
if (!isMounted || !isOpen) return null;
const maxWidthClasses = {
sm: 'max-w-sm',
md: 'max-w-md',
lg: 'max-w-lg',
xl: 'max-w-xl',
};
return createPortal(
<div className="fixed inset-0 z-50 flex items-end sm:items-center justify-center p-4 sm:p-6">
<div
className="absolute inset-0 bg-black/60 backdrop-blur-sm transition-opacity"
onClick={onClose}
/>
<div className={`relative bg-surface-container w-full ${maxWidthClasses[maxWidth]} rounded-[28px] shadow-elevation-3 animate-in slide-in-from-bottom-10 zoom-in-95 duration-200 flex flex-col max-h-[90vh]`}>
<div className="flex items-center justify-between p-6 pb-2 shrink-0">
<h3 className="text-xl font-normal text-on-surface">{title}</h3>
<button
onClick={onClose}
className="p-2 -mr-2 text-on-surface-variant hover:text-on-surface hover:bg-surface-container-high rounded-full transition-colors"
>
<X size={20} />
</button>
</div>
<div className="p-6 pt-2 overflow-y-auto">
{children}
</div>
</div>
</div>,
document.body
);
};