Side Sheets instead of Modals

This commit is contained in:
AG
2025-12-12 00:25:15 +02:00
parent 87f639e320
commit e1e956d6b2
7 changed files with 145 additions and 29 deletions

View File

@@ -3,7 +3,7 @@ import { Dumbbell, Activity, Percent, Timer, ArrowRight, ArrowUp, Save, Calendar
import { WorkoutSet, ExerciseType, ExerciseDef, Language } from '../types'; import { WorkoutSet, ExerciseType, ExerciseDef, Language } from '../types';
import { t } from '../services/i18n'; import { t } from '../services/i18n';
import { formatSetMetrics } from '../utils/setFormatting'; import { formatSetMetrics } from '../utils/setFormatting';
import { Modal } from './ui/Modal'; import { SideSheet } from './ui/SideSheet';
import { Button } from './ui/Button'; import { Button } from './ui/Button';
interface EditSetModalProps { interface EditSetModalProps {
@@ -84,11 +84,11 @@ const EditSetModal: React.FC<EditSetModalProps> = ({
const hasHeight = ['HIGH_JUMP', 'High Jump'].includes(type) || set.height !== null; const hasHeight = ['HIGH_JUMP', 'High Jump'].includes(type) || set.height !== null;
return ( return (
<Modal <SideSheet
isOpen={isOpen} isOpen={isOpen}
onClose={onClose} onClose={onClose}
title={t('edit_set', lang) || 'Edit Set'} title={t('edit_set', lang) || 'Edit Set'}
maxWidth="sm" width="md"
> >
<div className="space-y-6"> <div className="space-y-6">
<div> <div>
@@ -210,7 +210,7 @@ const EditSetModal: React.FC<EditSetModalProps> = ({
</Button> </Button>
</div> </div>
</div> </div>
</Modal> </SideSheet>
); );
}; };

View File

@@ -5,7 +5,7 @@ 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 { SideSheet } from './ui/SideSheet';
import { Button } from './ui/Button'; import { Button } from './ui/Button';
interface ExerciseModalProps { interface ExerciseModalProps {
@@ -64,19 +64,19 @@ const ExerciseModal: React.FC<ExerciseModalProps> = ({ isOpen, onClose, onSave,
}; };
return ( return (
<Modal <SideSheet
isOpen={isOpen} isOpen={isOpen}
onClose={() => { onClose={() => {
console.log('ExerciseModal onClose'); console.log('ExerciseModal onClose');
onClose(); onClose();
}} }}
title={t('create_exercise', lang)} title={t('create_exercise', lang)}
maxWidth="sm" width="md"
> >
{console.log('ExerciseModal Rendering. isOpen:', isOpen)} {console.log('ExerciseModal Rendering. isOpen:', isOpen)}
<div className="space-y-6"> <div className="space-y-6">
<div> <div>
<FilledInput <GymFilledInput
label={t('ex_name', lang)} label={t('ex_name', lang)}
value={newName} value={newName}
onChange={(e: any) => { onChange={(e: any) => {
@@ -120,7 +120,7 @@ const ExerciseModal: React.FC<ExerciseModalProps> = ({ isOpen, onClose, onSave,
</div> </div>
{newType === ExerciseType.BODYWEIGHT && ( {newType === ExerciseType.BODYWEIGHT && (
<FilledInput <GymFilledInput
label={t('body_weight_percent', lang)} label={t('body_weight_percent', lang)}
value={newBwPercentage} value={newBwPercentage}
onChange={(e: any) => setNewBwPercentage(e.target.value)} onChange={(e: any) => setNewBwPercentage(e.target.value)}
@@ -150,7 +150,7 @@ const ExerciseModal: React.FC<ExerciseModalProps> = ({ isOpen, onClose, onSave,
</Button> </Button>
</div> </div>
</div> </div>
</Modal> </SideSheet>
); );
}; };

View File

@@ -9,6 +9,7 @@ import { getExercises } from '../services/storage';
import { Button } from './ui/Button'; import { Button } from './ui/Button';
import { Card } from './ui/Card'; import { Card } from './ui/Card';
import { Modal } from './ui/Modal'; import { Modal } from './ui/Modal';
import { SideSheet } from './ui/SideSheet';
import EditSetModal from './EditSetModal'; import EditSetModal from './EditSetModal';
import FilledInput from './FilledInput'; import FilledInput from './FilledInput';
@@ -366,11 +367,11 @@ const History: React.FC<HistoryProps> = ({ lang }) => {
{/* EDIT SESSION MODAL */} {/* EDIT SESSION MODAL */}
{editingSession && ( {editingSession && (
<Modal <SideSheet
isOpen={!!editingSession} isOpen={!!editingSession}
onClose={() => setEditingSession(null)} onClose={() => setEditingSession(null)}
title={t('edit', lang)} title={t('edit', lang)}
maxWidth="lg" width="lg"
> >
<div className="space-y-6"> <div className="space-y-6">
{/* Meta Info */} {/* Meta Info */}
@@ -449,7 +450,7 @@ const History: React.FC<HistoryProps> = ({ lang }) => {
</Button> </Button>
</div> </div>
</div> </div>
</Modal> </SideSheet>
) )
} }
{editingSetInfo && ( {editingSetInfo && (

View File

@@ -13,6 +13,7 @@ import { toTitleCase } from '../utils/text';
import { Button } from './ui/Button'; import { Button } from './ui/Button';
import { Card } from './ui/Card'; import { Card } from './ui/Card';
import { Modal } from './ui/Modal'; import { Modal } from './ui/Modal';
import { SideSheet } from './ui/SideSheet';
interface PlansProps { interface PlansProps {
lang: Language; lang: Language;
@@ -297,11 +298,11 @@ const Plans: React.FC<PlansProps> = ({ lang }) => {
</div> </div>
</div> </div>
<Modal <SideSheet
isOpen={showExerciseSelector} isOpen={showExerciseSelector}
onClose={() => setShowExerciseSelector(false)} onClose={() => setShowExerciseSelector(false)}
title={t('select_exercise', lang)} title={t('select_exercise', lang)}
maxWidth="md" width="lg"
> >
<div className="flex flex-col h-[60vh]"> <div className="flex flex-col h-[60vh]">
<div className="flex justify-end mb-2"> <div className="flex justify-end mb-2">
@@ -325,13 +326,13 @@ const Plans: React.FC<PlansProps> = ({ lang }) => {
))} ))}
</div> </div>
</div> </div>
</Modal> </SideSheet>
<Modal <SideSheet
isOpen={isCreatingExercise} isOpen={isCreatingExercise}
onClose={() => setIsCreatingExercise(false)} onClose={() => setIsCreatingExercise(false)}
title={t('create_exercise', lang)} title={t('create_exercise', lang)}
maxWidth="md" width="md"
> >
<div className="space-y-6 pt-2"> <div className="space-y-6 pt-2">
<FilledInput <FilledInput
@@ -390,7 +391,7 @@ const Plans: React.FC<PlansProps> = ({ lang }) => {
</Button> </Button>
</div> </div>
</div> </div>
</Modal> </SideSheet>
</div> </div>
); );
} }
@@ -468,19 +469,23 @@ const Plans: React.FC<PlansProps> = ({ lang }) => {
{/* Preparation Modal */} {/* Preparation Modal */}
{showPlanPrep && ( {showPlanPrep && (
<div className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-6 backdrop-blur-sm"> <SideSheet
<div className="w-full max-w-sm bg-surface-container rounded-[28px] p-6 shadow-elevation-3"> isOpen={!!showPlanPrep}
<h3 className="text-2xl font-normal text-on-surface mb-4">{showPlanPrep.name}</h3> onClose={() => setShowPlanPrep(null)}
<div className="bg-surface-container-high p-4 rounded-xl text-on-surface-variant text-sm mb-8"> title={showPlanPrep.name}
width="md"
>
<div className="space-y-8">
<div className="bg-surface-container-high p-4 rounded-xl text-on-surface-variant text-sm">
<div className="text-xs font-bold text-primary mb-2">{t('prep_title', lang)}</div> <div className="text-xs font-bold text-primary mb-2">{t('prep_title', lang)}</div>
{showPlanPrep.description || t('prep_no_instructions', lang)} {showPlanPrep.description || t('prep_no_instructions', lang)}
</div> </div>
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
<button onClick={() => setShowPlanPrep(null)} className="px-6 py-2.5 rounded-full text-primary font-medium hover:bg-white/5">{t('cancel', lang)}</button> <Button onClick={() => setShowPlanPrep(null)} variant="ghost">{t('cancel', lang)}</Button>
<button onClick={confirmPlanStart} className="px-6 py-2.5 rounded-full bg-primary text-on-primary font-medium">{t('start', lang)}</button> <Button onClick={confirmPlanStart}>{t('start', lang)}</Button>
</div> </div>
</div> </div>
</div> </SideSheet>
)} )}
</div> </div>
); );

View File

@@ -12,6 +12,7 @@ import Snackbar from './Snackbar';
import { Button } from './ui/Button'; import { Button } from './ui/Button';
import { Card } from './ui/Card'; import { Card } from './ui/Card';
import { Modal } from './ui/Modal'; import { Modal } from './ui/Modal';
import { SideSheet } from './ui/SideSheet';
interface ProfileProps { interface ProfileProps {
user: User; user: User;
@@ -601,11 +602,11 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
{/* Edit Exercise Modal */} {/* Edit Exercise Modal */}
{editingExercise && ( {editingExercise && (
<Modal <SideSheet
isOpen={!!editingExercise} isOpen={!!editingExercise}
onClose={() => setEditingExercise(null)} onClose={() => setEditingExercise(null)}
title={t('edit', lang)} title={t('edit', lang)}
maxWidth="sm" width="md"
> >
<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">
@@ -632,7 +633,7 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
<Button onClick={handleSaveExerciseEdit}>{t('save', lang)}</Button> <Button onClick={handleSaveExerciseEdit}>{t('save', lang)}</Button>
</div> </div>
</div> </div>
</Modal> </SideSheet>
)} )}
{/* Create Exercise Modal */} {/* Create Exercise Modal */}

View File

@@ -0,0 +1,107 @@
import React, { useEffect, useState } from 'react';
import { X } from 'lucide-react';
import { createPortal } from 'react-dom';
interface SideSheetProps {
isOpen: boolean;
onClose: () => void;
title?: string;
children: React.ReactNode;
width?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
}
export const SideSheet: React.FC<SideSheetProps> = ({ isOpen, onClose, title, children, width = 'md' }) => {
const [isMounted, setIsMounted] = useState(false);
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
setIsMounted(true);
}, []);
useEffect(() => {
if (isOpen) {
setIsVisible(true);
document.body.style.overflow = 'hidden';
} else {
const timer = setTimeout(() => setIsVisible(false), 300); // Allow exit animation
document.body.style.overflow = 'unset';
return () => clearTimeout(timer);
}
return () => {
document.body.style.overflow = 'unset';
};
}, [isOpen]);
if (!isMounted || (!isOpen && !isVisible)) return null;
// Width classes for Desktop side sheet
const widthClasses = {
sm: 'sm:max-w-sm',
md: 'sm:max-w-md',
lg: 'sm:max-w-lg',
xl: 'sm:max-w-xl',
full: 'sm:max-w-full'
};
return createPortal(
<div
className={`fixed inset-0 z-50 flex flex-col sm:flex-row justify-end items-end sm:items-stretch transition-opacity duration-300 ${isOpen ? 'opacity-100' : 'opacity-0 pointer-events-none'}`}
role="dialog"
aria-modal="true"
>
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/40 backdrop-blur-sm"
onClick={onClose}
/>
{/* Sheet */}
<div
className={`
relative
bg-surface-container
w-full
${widthClasses[width]}
/* Mobile Styles (Bottom Sheet) */
rounded-t-[28px] sm:rounded-none sm:rounded-l-[28px]
max-h-[85vh] sm:max-h-full h-full sm:h-full
flex flex-col
shadow-elevation-3
/* Animations */
transition-transform duration-300 ease-out
${isOpen
? 'translate-y-0 sm:translate-y-0 sm:translate-x-0'
: 'translate-y-full sm:translate-y-0 sm:translate-x-full'
}
`}
onClick={(e) => e.stopPropagation()}
>
{/* Mobile Drag Handle */}
<div className="sm:hidden flex justify-center pt-4 pb-2 shrink-0">
<div className="w-8 h-1 bg-outline-variant rounded-full opacity-40" />
</div>
{/* Header */}
<div className="flex items-center justify-between px-6 pb-6 pt-2 sm:pt-6 shrink-0 border-b border-transparent">
<h3 className="text-[22px] leading-[28px] 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"
aria-label="Close"
>
<X size={24} />
</button>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto px-6 pb-6 pt-0">
{children}
</div>
</div>
</div>,
document.body
);
};

View File

@@ -97,6 +97,7 @@ const translations = {
delete_set_confirm: 'Are you sure you want to delete this set?', delete_set_confirm: 'Are you sure you want to delete this set?',
delete: 'Delete', delete: 'Delete',
edit: 'Edit', edit: 'Edit',
edit_set: 'Edit Set',
save: 'Save', save: 'Save',
start_time: 'Start', start_time: 'Start',
end_time: 'End', end_time: 'End',
@@ -266,6 +267,7 @@ const translations = {
delete_set_confirm: 'Вы уверены, что хотите удалить этот подход?', delete_set_confirm: 'Вы уверены, что хотите удалить этот подход?',
delete: 'Удалить', delete: 'Удалить',
edit: 'Редактировать', edit: 'Редактировать',
edit_set: 'Редактировать подход',
save: 'Сохранить', save: 'Сохранить',
start_time: 'Начало', start_time: 'Начало',
end_time: 'Конец', end_time: 'Конец',