Timer implemented. No working tests.

This commit is contained in:
AG
2025-12-10 23:07:31 +02:00
parent 3df4abba47
commit b86664816d
24 changed files with 806 additions and 116 deletions

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from '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 { Plus, Trash2, PlayCircle, Dumbbell, Save, X, ChevronRight, List, ArrowUp, ArrowDown, Edit2, User, Flame, Timer as TimerIcon, Ruler, Footprints, Activity, Percent, CheckCircle, GripVertical, Scale, Search } from 'lucide-react';
import { WorkoutPlan, ExerciseDef, PlannedSet, Language, ExerciseType } from '../types';
import { getExercises, saveExercise } from '../services/storage';
import { t } from '../services/i18n';
@@ -12,6 +12,7 @@ import FilledInput from './FilledInput';
import { toTitleCase } from '../utils/text';
import { Button } from './ui/Button';
import { Card } from './ui/Card';
import { Modal } from './ui/Modal';
interface PlansProps {
lang: Language;
@@ -93,7 +94,10 @@ const Plans: React.FC<PlansProps> = ({ lang }) => {
exerciseId: ex.id,
exerciseName: ex.name,
exerciseType: ex.type,
isWeighted: false
isWeighted: false,
restTimeSeconds: 120 // Default new step to 120s? Or leave undefined to use profile default?
// Requirement: "fallback to user default". So maybe undefined/null is better for "inherit".
// But UI needs a value. Let's start with 120 or empty.
};
setSteps([...steps, newStep]);
setShowExerciseSelector(false);
@@ -134,6 +138,10 @@ const Plans: React.FC<PlansProps> = ({ lang }) => {
setSteps(steps.map(s => s.id === stepId ? { ...s, isWeighted: !s.isWeighted } : s));
};
const updateRestTime = (stepId: string, seconds: number | undefined) => {
setSteps(steps.map(s => s.id === stepId ? { ...s, restTimeSeconds: seconds } : s));
};
const removeStep = (stepId: string) => {
setSteps(steps.filter(s => s.id !== stepId));
};
@@ -222,18 +230,35 @@ const Plans: React.FC<PlansProps> = ({ lang }) => {
<div className="flex-1">
<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">
<div className={`w-4 h-4 border rounded flex items-center justify-center ${step.isWeighted ? 'bg-primary border-primary' : 'border-outline'}`}>
{step.isWeighted && <Dumbbell size={10} className="text-on-primary" />}
<div className="flex items-center gap-4 mt-1">
<label className="flex items-center gap-2 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'}`}>
{step.isWeighted && <Dumbbell size={10} className="text-on-primary" />}
</div>
<input
type="checkbox"
checked={step.isWeighted}
onChange={() => toggleWeighted(step.id)}
className="hidden"
/>
<span className="text-xs text-on-surface-variant">{t('weighted', lang)}</span>
</label>
<div className="flex items-center gap-2">
<TimerIcon size={14} className="text-on-surface-variant" />
<input
type="number"
placeholder="Rest (s)"
className="w-16 bg-transparent border-b border-outline-variant text-xs text-on-surface focus:border-primary focus:outline-none text-center"
value={step.restTimeSeconds || ''}
onChange={(e) => {
const val = parseInt(e.target.value);
updateRestTime(step.id, isNaN(val) ? undefined : val);
}}
/>
<span className="text-xs text-on-surface-variant">s</span>
</div>
<input
type="checkbox"
checked={step.isWeighted}
onChange={() => toggleWeighted(step.id)}
className="hidden"
/>
<span className="text-xs text-on-surface-variant">{t('weighted', lang)}</span>
</label>
</div>
</div>
<Button onClick={() => removeStep(step.id)} variant="ghost" size="icon" className="text-on-surface-variant hover:text-error hover:bg-error/10">
<X size={20} />
@@ -254,20 +279,19 @@ const Plans: React.FC<PlansProps> = ({ lang }) => {
</div>
</div>
{showExerciseSelector && (
<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 shrink-0">
<span className="font-medium text-on-surface">{t('select_exercise', lang)}</span>
<div className="flex gap-2">
<Button onClick={() => setIsCreatingExercise(true)} variant="ghost" size="icon" className="text-primary hover:bg-primary-container/20">
<Plus size={20} />
</Button>
<Button onClick={() => setShowExerciseSelector(false)} variant="ghost" size="icon">
<X size={20} />
</Button>
</div>
<Modal
isOpen={showExerciseSelector}
onClose={() => setShowExerciseSelector(false)}
title={t('select_exercise', lang)}
maxWidth="md"
>
<div className="flex flex-col h-[60vh]">
<div className="flex justify-end mb-2">
<Button onClick={() => setIsCreatingExercise(true)} variant="ghost" className="text-primary hover:bg-primary-container/20 flex gap-2">
<Plus size={18} /> {t('create_exercise', lang)}
</Button>
</div>
<div className="flex-1 overflow-y-auto p-2">
<div className="flex-1 overflow-y-auto -mx-6 px-6">
{availableExercises
.slice()
.sort((a, b) => a.name.localeCompare(b.name))
@@ -275,84 +299,80 @@ const Plans: React.FC<PlansProps> = ({ lang }) => {
<button
key={ex.id}
onClick={() => addStep(ex)}
className="w-full text-left p-4 border-b border-outline-variant hover:bg-surface-container-high text-on-surface flex justify-between"
className="w-full text-left p-4 border-b border-outline-variant hover:bg-surface-container-high text-on-surface flex justify-between group"
>
<span>{ex.name}</span>
<span className="group-hover:text-primary transition-colors">{ex.name}</span>
<span className="text-xs bg-secondary-container text-on-secondary-container px-2 py-1 rounded-full">{ex.type}</span>
</button>
))}
</div>
{isCreatingExercise && (
<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 shrink-0">
<h3 className="text-title-medium font-medium text-on-surface">{t('create_exercise', lang)}</h3>
<Button onClick={() => setIsCreatingExercise(false)} variant="ghost" size="icon" className="text-on-surface-variant hover:bg-white/5">
<X size={20} />
</Button>
</div>
<div className="p-4 space-y-6 overflow-y-auto flex-1">
<FilledInput
label={t('ex_name', lang)}
value={newExName}
onChange={(e: any) => setNewExName(e.target.value)}
type="text"
autoFocus
autocapitalize="words"
onBlur={() => setNewExName(toTitleCase(newExName))}
/>
<div>
<label className="block text-xs text-on-surface-variant font-medium mb-3">{t('ex_type', lang)}</label>
<div className="flex flex-wrap gap-2">
{[
{ id: ExerciseType.STRENGTH, label: exerciseTypeLabels[ExerciseType.STRENGTH], icon: Dumbbell },
{ id: ExerciseType.BODYWEIGHT, label: exerciseTypeLabels[ExerciseType.BODYWEIGHT], icon: User },
{ id: ExerciseType.CARDIO, label: exerciseTypeLabels[ExerciseType.CARDIO], icon: Flame },
{ id: ExerciseType.STATIC, label: exerciseTypeLabels[ExerciseType.STATIC], icon: TimerIcon },
{ id: ExerciseType.HIGH_JUMP, label: exerciseTypeLabels[ExerciseType.HIGH_JUMP], icon: ArrowUp },
{ id: ExerciseType.LONG_JUMP, label: exerciseTypeLabels[ExerciseType.LONG_JUMP], icon: Ruler },
{ id: ExerciseType.PLYOMETRIC, label: exerciseTypeLabels[ExerciseType.PLYOMETRIC], icon: Footprints },
].map((type) => (
<button
key={type.id}
onClick={() => setNewExType(type.id)}
className={`px-4 py-2 rounded-lg flex items-center gap-2 text-xs font-medium border transition-all ${newExType === type.id
? 'bg-secondary-container text-on-secondary-container border-transparent'
: 'bg-transparent text-on-surface-variant border-outline hover:border-on-surface-variant'
}`}
>
<type.icon size={14} /> {type.label}
</button>
))}
</div>
</div>
{newExType === ExerciseType.BODYWEIGHT && (
<FilledInput
label={t('body_weight_percent', lang)}
value={newExBwPercentage}
onChange={(e: any) => setNewExBwPercentage(e.target.value)}
icon={<Percent size={12} />}
/>
)}
<div className="flex justify-end mt-4">
<Button
onClick={handleCreateExercise}
fullWidth
size="lg"
>
<CheckCircle size={20} className="mr-2" />
{t('create_btn', lang)}
</Button>
</div>
</div>
</div>
)}
</div>
)}
</Modal>
<Modal
isOpen={isCreatingExercise}
onClose={() => setIsCreatingExercise(false)}
title={t('create_exercise', lang)}
maxWidth="md"
>
<div className="space-y-6 pt-2">
<FilledInput
label={t('ex_name', lang)}
value={newExName}
onChange={(e: any) => setNewExName(e.target.value)}
type="text"
autoFocus
autocapitalize="words"
onBlur={() => setNewExName(toTitleCase(newExName))}
/>
<div>
<label className="block text-xs text-on-surface-variant font-medium mb-3">{t('ex_type', lang)}</label>
<div className="flex flex-wrap gap-2">
{[
{ id: ExerciseType.STRENGTH, label: exerciseTypeLabels[ExerciseType.STRENGTH], icon: Dumbbell },
{ id: ExerciseType.BODYWEIGHT, label: exerciseTypeLabels[ExerciseType.BODYWEIGHT], icon: User },
{ id: ExerciseType.CARDIO, label: exerciseTypeLabels[ExerciseType.CARDIO], icon: Flame },
{ id: ExerciseType.STATIC, label: exerciseTypeLabels[ExerciseType.STATIC], icon: TimerIcon },
{ id: ExerciseType.HIGH_JUMP, label: exerciseTypeLabels[ExerciseType.HIGH_JUMP], icon: ArrowUp },
{ id: ExerciseType.LONG_JUMP, label: exerciseTypeLabels[ExerciseType.LONG_JUMP], icon: Ruler },
{ id: ExerciseType.PLYOMETRIC, label: exerciseTypeLabels[ExerciseType.PLYOMETRIC], icon: Footprints },
].map((type) => (
<button
key={type.id}
onClick={() => setNewExType(type.id)}
className={`px-4 py-2 rounded-lg flex items-center gap-2 text-xs font-medium border transition-all ${newExType === type.id
? 'bg-secondary-container text-on-secondary-container border-transparent'
: 'bg-transparent text-on-surface-variant border-outline hover:border-on-surface-variant'
}`}
>
<type.icon size={14} /> {type.label}
</button>
))}
</div>
</div>
{newExType === ExerciseType.BODYWEIGHT && (
<FilledInput
label={t('body_weight_percent', lang)}
value={newExBwPercentage}
onChange={(e: any) => setNewExBwPercentage(e.target.value)}
icon={<Percent size={12} />}
/>
)}
<div className="flex justify-end mt-6">
<Button
onClick={handleCreateExercise}
fullWidth
size="lg"
>
<CheckCircle size={20} className="mr-2" />
{t('create_btn', lang)}
</Button>
</div>
</div>
</Modal>
</div>
);
}

View File

@@ -7,6 +7,9 @@ import ExerciseModal from '../ExerciseModal';
import { useTracker } from './useTracker';
import SetLogger from './SetLogger';
import { formatSetMetrics } from '../../utils/setFormatting';
import { useAuth } from '../../context/AuthContext';
import { api } from '../../services/api';
import RestTimerFAB from '../ui/RestTimerFAB';
interface ActiveSessionViewProps {
tracker: ReturnType<typeof useTracker>;
@@ -71,6 +74,61 @@ const ActiveSessionView: React.FC<ActiveSessionViewProps> = ({ tracker, activeSe
exercises
} = tracker;
const { currentUser, updateUser } = useAuth();
// Timer Logic is now managed in useTracker to persist across re-renders/step changes
const { timer } = tracker;
const handleLogSet = async () => {
await handleAddSet();
// Determine next rest time
let nextTime = currentUser?.profile?.restTimerDefault || 120;
if (activePlan) {
// Logic: activePlan set just added. We are moving to next step?
// Tracker's handleAddSet calls addSet -> which calls ActiveWorkoutContext's addSet -> which increments currentStepIndex (logic inside context)
// But state update might be async or we might need to look at current index before update?
// Usually we want the rest time AFTER the set we just did.
// The user just configured the set for the *current* step index.
// So we look at activePlan.steps[currentStepIndex].restTime.
// BUT, if the user just finished step 0, and step 0 says "Rest 60s", then we rest 60s.
// If fallback, use default.
// Note: currentStepIndex might update immediately or after render.
// In a real app, we might get the next set's target time? No, rest is usually associated with the fatigue of the set just done.
// Requirement: "rest time after this set".
// So we use currentStepIndex (which likely points to the set we just logged, assuming UI hasn't advanced yet?
// Actually, handleAddSet likely appends set. Context might auto-advance.
// Let's assume we use the restTime of the step that matches the set just logged.
const currentStep = activePlan.steps[currentStepIndex];
if (currentStep && currentStep.restTimeSeconds) {
nextTime = currentStep.restTimeSeconds;
}
}
if (timer.status !== 'RUNNING') {
timer.reset(nextTime);
timer.start();
}
};
const handleDurationChange = async (newVal: number) => {
// Update user profile
try {
await api.patch('/auth/profile', { restTimerDefault: newVal });
if (currentUser) {
updateUser({
...currentUser,
profile: { ...currentUser.profile, restTimerDefault: newVal }
});
}
} catch (e) {
console.error("Failed to update default timer", e);
}
};
const isPlanFinished = activePlan && currentStepIndex >= activePlan.steps.length;
@@ -177,7 +235,7 @@ const ActiveSessionView: React.FC<ActiveSessionViewProps> = ({ tracker, activeSe
<SetLogger
tracker={tracker}
lang={lang}
onLogSet={handleAddSet}
onLogSet={handleLogSet}
/>
{activeSession.sets.length > 0 && (
@@ -397,6 +455,8 @@ const ActiveSessionView: React.FC<ActiveSessionViewProps> = ({ tracker, activeSe
</div>
</div>
)}
<RestTimerFAB timer={timer} onDurationChange={handleDurationChange} />
</div>
);
};

View File

@@ -6,6 +6,10 @@ import ExerciseModal from '../ExerciseModal';
import { useTracker } from './useTracker';
import SetLogger from './SetLogger';
import { formatSetMetrics } from '../../utils/setFormatting';
import { useAuth } from '../../context/AuthContext';
import { api } from '../../services/api';
// import { useRestTimer } from '../../hooks/useRestTimer'; // Not needed if using tracker.timer
import RestTimerFAB from '../ui/RestTimerFAB';
interface SporadicViewProps {
tracker: ReturnType<typeof useTracker>;
@@ -26,6 +30,31 @@ const SporadicView: React.FC<SporadicViewProps> = ({ tracker, lang }) => {
loadQuickLogSession
} = tracker;
const { currentUser, updateUser } = useAuth();
// Timer Logic is now managed in useTracker
const { timer } = tracker;
const handleLogSet = async () => {
await handleLogSporadicSet();
// Always usage default/current setting for sporadic
timer.start();
};
const handleDurationChange = async (newVal: number) => {
try {
await api.patch('/auth/profile', { restTimerDefault: newVal });
if (currentUser) {
updateUser({
...currentUser,
profile: { ...currentUser.profile, restTimerDefault: newVal }
});
}
} catch (e) {
console.error("Failed to update default timer", e);
}
};
const [todaysSets, setTodaysSets] = useState<WorkoutSet[]>([]);
const [editingSetId, setEditingSetId] = useState<string | null>(null);
const [editingSet, setEditingSet] = useState<WorkoutSet | null>(null);
@@ -72,7 +101,7 @@ const SporadicView: React.FC<SporadicViewProps> = ({ tracker, lang }) => {
<SetLogger
tracker={tracker}
lang={lang}
onLogSet={handleLogSporadicSet}
onLogSet={handleLogSet}
isSporadic={true}
/>
@@ -301,6 +330,8 @@ const SporadicView: React.FC<SporadicViewProps> = ({ tracker, lang }) => {
</div>
</div>
)}
<RestTimerFAB timer={tracker.timer} onDurationChange={handleDurationChange} />
</div>
);
};

View File

@@ -8,6 +8,7 @@ import { usePlanExecution } from '../../hooks/usePlanExecution';
import { useAuth } from '../../context/AuthContext';
import { useActiveWorkout } from '../../context/ActiveWorkoutContext';
import { useSession } from '../../context/SessionContext';
import { useRestTimer } from '../../hooks/useRestTimer';
export const useTracker = (props: any) => { // Props ignored/removed
const { currentUser } = useAuth();
@@ -61,6 +62,21 @@ export const useTracker = (props: any) => { // Props ignored/removed
const form = useWorkoutForm({ userId, onUpdateSet: handleUpdateSetWrapper });
const planExec = usePlanExecution({ activeSession, activePlan, exercises });
// Rest Timer Logic (Moved from ActiveSessionView to persist state)
const getTargetRestTime = () => {
if (activePlan) {
const currentStep = activePlan.steps[planExec.currentStepIndex];
if (currentStep && currentStep.restTimeSeconds) {
return currentStep.restTimeSeconds;
}
}
return currentUser?.profile?.restTimerDefault || 120;
};
const targetRestTime = getTargetRestTime();
const timer = useRestTimer({
defaultTime: targetRestTime
});
// Initial Data Load
useEffect(() => {
if (!userId) return;
@@ -247,7 +263,8 @@ export const useTracker = (props: any) => { // Props ignored/removed
onSessionEnd: endSession,
onSessionQuit: quitSession,
onRemoveSet: removeSet,
activeSession // Need this in view
activeSession, // Need this in view
timer // Expose timer to views
};
};

View File

@@ -0,0 +1,211 @@
import React, { useState, useEffect } from 'react';
import { Timer, Play, Pause, RotateCcw, Edit2, Plus, Minus, X, Check } from 'lucide-react';
import { useRestTimer } from '../../hooks/useRestTimer';
interface RestTimerFABProps {
timer: ReturnType<typeof useRestTimer>;
onDurationChange?: (newDuration: number) => void;
}
const RestTimerFAB: React.FC<RestTimerFABProps> = ({ timer, onDurationChange }) => {
const { timeLeft, status, start, pause, reset, setDuration } = timer;
// Render Helpers (moved up for initial state calculation)
const formatSeconds = (sec: number) => {
const m = Math.floor(sec / 60);
const s = sec % 60;
return `${m}:${s.toString().padStart(2, '0')}`;
};
const [isExpanded, setIsExpanded] = useState(false);
const [isEditing, setIsEditing] = useState(false);
const [editValue, setEditValue] = useState(120);
const [inputValue, setInputValue] = useState(formatSeconds(120));
// Auto-expand when running if not already expanded? No, requirement says "when time is running, show digits of the countdown on the enlarged timer FAB even if the menu is collapsed".
// So the FAB itself grows.
// Effects
useEffect(() => {
if (status === 'FINISHED') {
setIsExpanded(false);
setIsEditing(false);
}
}, [status]);
useEffect(() => {
// Sync input value when editValue changes (externally or via +/- buttons)
setInputValue(formatSeconds(editValue));
}, [editValue]);
const handleToggle = () => {
if (isEditing) return; // Don't toggle if editing
setIsExpanded(!isExpanded);
};
const handleStartPause = (e: React.MouseEvent) => {
e.stopPropagation();
if (status === 'RUNNING') pause();
else start();
};
const handleReset = (e: React.MouseEvent) => {
e.stopPropagation();
reset();
};
const handleEdit = (e: React.MouseEvent) => {
e.stopPropagation();
const initialVal = timeLeft > 0 ? timeLeft : 120;
setEditValue(initialVal);
setInputValue(formatSeconds(initialVal));
setIsEditing(true);
setIsExpanded(true); // Keep expanded
};
const saveEdit = (e: React.MouseEvent) => {
e.stopPropagation();
// Ensure we capture any pending input value on save
// (Though blur usually handles it, explicit save should too)
let finalVal = editValue;
// Try parsing current input just in case
if (inputValue.includes(':')) {
const [m, s] = inputValue.split(':').map(Number);
if (!isNaN(m) && !isNaN(s)) finalVal = m * 60 + s;
} else if (/^\d+$/.test(inputValue)) {
const v = parseInt(inputValue, 10);
if (!isNaN(v)) finalVal = v;
}
reset(finalVal);
if (onDurationChange) onDurationChange(finalVal);
setIsEditing(false);
setIsExpanded(true); // Keep expanded per user request
};
const adjustEdit = (curr: number, delta: number) => {
return Math.max(0, curr + delta);
};
const isRunningOrPaused = status === 'RUNNING' || status === 'PAUSED';
const isFinished = status === 'FINISHED';
// Base classes
const fabBaseClasses = `fixed bottom-24 right-6 shadow-elevation-3 transition-all duration-300 z-50 flex items-center justify-center font-medium`;
// Dimensions and Colors
let fabClasses = fabBaseClasses;
let content = null;
const handleInputBlur = () => {
let val = 0;
if (inputValue.includes(':')) {
const [m, s] = inputValue.split(':').map(Number);
if (!isNaN(m) && !isNaN(s)) val = m * 60 + s;
} else {
val = parseInt(inputValue, 10);
}
if (!isNaN(val)) {
setEditValue(val);
setInputValue(formatSeconds(val)); // Re-format to standardized look
} else {
setInputValue(formatSeconds(editValue)); // Revert if invalid
}
};
if (isFinished) {
fabClasses += ` w-14 h-14 rounded-[16px] bg-error text-on-error animate-pulse`;
content = <Timer size={24} />;
} else if (isRunningOrPaused && !isExpanded) {
// Requirements: "when time is running, show digits of the countdown on the enlarged timer FAB even if the menu is collapsed"
// So it should be wide enough to show digits.
fabClasses += ` h-14 rounded-[16px] bg-primary-container text-on-primary-container px-4 min-w-[56px] cursor-pointer hover:brightness-95`;
content = (
<div className="flex items-center gap-2 pointer-events-none">
<span className="font-mono text-lg font-bold">{formatSeconds(timeLeft)}</span>
</div>
);
} else if (isExpanded) {
// Expanded: No common background, just a column of buttons right-aligned (Speed Dial)
fabClasses += ` w-auto bg-transparent flex-col-reverse items-end justify-end pb-0 overflow-visible`;
content = (
<div className="flex flex-col items-end gap-3 pointer-events-auto">
{/* Options List (Bottom to Top) */}
{isEditing ? (
<div className="flex flex-col items-end gap-2 animate-in slide-in-from-bottom-4 fade-in duration-200 mb-2 mr-1">
<button onClick={(e) => setEditValue(v => adjustEdit(v, 5))} className="w-10 h-10 flex items-center justify-center bg-surface-container-high text-on-surface rounded-full shadow-elevation-2 hover:brightness-110"><Plus size={20} /></button>
{/* Manual Input Field */}
<div className="bg-surface-container shadow-sm rounded px-2 py-1 my-1 flex items-center justify-center min-w-[60px]">
<input
type="text"
className="bg-transparent text-on-surface font-mono font-bold text-lg text-center w-full focus:outline-none"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onFocus={(e) => e.target.select()}
onBlur={handleInputBlur}
onKeyDown={(e) => {
if (e.key === 'Enter') e.currentTarget.blur();
}}
/>
</div>
<button onClick={(e) => setEditValue(v => adjustEdit(v, -5))} className="w-10 h-10 flex items-center justify-center bg-surface-container-high text-on-surface rounded-full shadow-elevation-2 hover:brightness-110"><Minus size={20} /></button>
<button onClick={saveEdit} className="w-10 h-10 flex items-center justify-center bg-primary text-on-primary rounded-full shadow-elevation-2 mt-1 hover:brightness-110"><Check size={20} /></button>
</div>
) : (
<div className="flex flex-col items-end gap-3 animate-in slide-in-from-bottom-4 fade-in duration-200 mb-4 mr-1">
{/* Mini FABs */}
<button onClick={handleEdit} className="w-10 h-10 flex items-center justify-center bg-surface-container-high text-on-surface hover:text-primary rounded-full shadow-elevation-2 hover:scale-110 transition-all" aria-label="Edit">
<Edit2 size={18} />
</button>
<button onClick={handleReset} className="w-10 h-10 flex items-center justify-center bg-surface-container-high text-on-surface hover:text-primary rounded-full shadow-elevation-2 hover:scale-110 transition-all" aria-label="Reset">
<RotateCcw size={18} />
</button>
<button onClick={handleStartPause} className="w-12 h-12 flex items-center justify-center bg-primary-container text-on-primary-container rounded-full shadow-elevation-2 hover:scale-110 transition-all" aria-label={status === 'RUNNING' ? 'Pause' : 'Start'}>
{status === 'RUNNING' ? <Pause size={24} /> : <Play size={24} className="ml-1" />}
</button>
</div>
)}
{/* Main Toggle Button (Bottom) */}
<button
onClick={handleToggle}
className={`w-16 h-16 rounded-full flex items-center justify-center shadow-elevation-3 hover:scale-105 transition-all active:scale-95 ${isEditing
? 'bg-error-container text-on-error-container hover:bg-error-container/80' // Light Red for cancel/close
: 'bg-primary text-on-primary'
}`}
>
{isEditing ? <X size={28} /> : <span className="font-mono text-sm font-bold">{formatSeconds(timeLeft)}</span>}
</button>
</div>
);
} else {
// Idle state
fabClasses += ` w-14 h-14 rounded-[16px] bg-secondary-container text-on-secondary-container hover:brightness-95 cursor-pointer`;
content = (
<div className="w-full h-full flex items-center justify-center pointer-events-none">
<Timer size={24} />
</div>
);
}
if (isExpanded) {
// Override base classes for expanded state - remove shadow/bg from container
fabClasses = `fixed bottom-24 right-6 z-50 flex flex-col items-end pointer-events-none`; // pointer-events-none prevents container from blocking clicks, children have pointer-events-auto
}
return (
<div className={fabClasses} onClick={!isExpanded ? handleToggle : undefined}>
{content}
</div>
);
};
export default RestTimerFAB;