142 lines
4.9 KiB
TypeScript
142 lines
4.9 KiB
TypeScript
'use client';
|
|
|
|
import { copy } from '@/shared/i18n';
|
|
import { cn } from '@/shared/lib/cn';
|
|
import {
|
|
RECOVERY_30S_MODE_LABEL,
|
|
Restart30sAction,
|
|
useRestart30s,
|
|
} from '@/features/restart-30s';
|
|
|
|
interface SpaceTimerHudWidgetProps {
|
|
timeDisplay?: string;
|
|
className?: string;
|
|
hasActiveSession?: boolean;
|
|
sessionPhase?: 'focus' | 'break' | null;
|
|
playbackState?: 'running' | 'paused' | null;
|
|
isControlsDisabled?: boolean;
|
|
isImmersionMode?: boolean;
|
|
canStart?: boolean;
|
|
canPause?: boolean;
|
|
canReset?: boolean;
|
|
onStartClick?: () => void;
|
|
onPauseClick?: () => void;
|
|
onResetClick?: () => void;
|
|
}
|
|
|
|
const HUD_ACTIONS = copy.space.timerHud.actions;
|
|
|
|
export const SpaceTimerHudWidget = ({
|
|
timeDisplay = '25:00',
|
|
className,
|
|
hasActiveSession = false,
|
|
sessionPhase = 'focus',
|
|
playbackState = 'paused',
|
|
isControlsDisabled = false,
|
|
isImmersionMode = false,
|
|
canStart = true,
|
|
canPause = false,
|
|
canReset = false,
|
|
onStartClick,
|
|
onPauseClick,
|
|
onResetClick,
|
|
}: SpaceTimerHudWidgetProps) => {
|
|
const { isBreatheMode, triggerRestart } = useRestart30s();
|
|
const modeLabel = isBreatheMode
|
|
? RECOVERY_30S_MODE_LABEL
|
|
: !hasActiveSession
|
|
? copy.space.timerHud.readyMode
|
|
: sessionPhase === 'break'
|
|
? copy.space.timerHud.breakMode
|
|
: copy.space.timerHud.focusMode;
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
'pointer-events-none fixed inset-x-0 z-20 flex justify-center px-4 sm:px-6',
|
|
className,
|
|
)}
|
|
style={{ bottom: 'calc(env(safe-area-inset-bottom, 0px) + 2rem)' }}
|
|
>
|
|
<div className="relative pointer-events-auto">
|
|
<section
|
|
className={cn(
|
|
'relative z-10 flex h-[3.5rem] items-center justify-between gap-6 overflow-hidden rounded-full px-5 transition-colors',
|
|
isImmersionMode
|
|
? 'border border-white/10 bg-black/20 backdrop-blur-2xl shadow-[0_8px_32px_rgba(0,0,0,0.12)]'
|
|
: 'border border-white/15 bg-black/30 backdrop-blur-2xl shadow-[0_8px_32px_rgba(0,0,0,0.16)]',
|
|
)}
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<span
|
|
className={cn(
|
|
'w-14 text-right text-[10px] font-bold uppercase tracking-[0.15em] opacity-80',
|
|
sessionPhase === 'break' ? 'text-emerald-400' : 'text-brand-primary'
|
|
)}
|
|
>
|
|
{modeLabel}
|
|
</span>
|
|
<span className="w-[1px] h-4 bg-white/10" />
|
|
<span
|
|
className={cn(
|
|
'w-20 text-[1.4rem] font-medium tracking-tight text-center',
|
|
isImmersionMode ? 'text-white/90' : 'text-white',
|
|
)}
|
|
>
|
|
{timeDisplay}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-1.5 pl-2 border-l border-white/10">
|
|
{HUD_ACTIONS.map((action) => {
|
|
const isStartAction = action.id === 'start';
|
|
const isPauseAction = action.id === 'pause';
|
|
const isResetAction = action.id === 'reset';
|
|
const isDisabled =
|
|
isControlsDisabled ||
|
|
(isStartAction ? !canStart : isPauseAction ? !canPause : !canReset);
|
|
const isHighlighted =
|
|
(isStartAction && playbackState !== 'running') ||
|
|
(isPauseAction && playbackState === 'running');
|
|
|
|
return (
|
|
<button
|
|
key={action.id}
|
|
type="button"
|
|
title={action.label}
|
|
aria-pressed={isHighlighted}
|
|
disabled={isDisabled}
|
|
onClick={() => {
|
|
if (isStartAction) onStartClick?.();
|
|
if (isPauseAction) onPauseClick?.();
|
|
if (isResetAction) onResetClick?.();
|
|
}}
|
|
className={cn(
|
|
'inline-flex h-8 w-8 items-center justify-center rounded-full text-sm transition-all duration-150 ease-out focus-visible:outline-none focus-visible:ring-2 active:scale-95 disabled:cursor-not-allowed disabled:opacity-30',
|
|
isImmersionMode
|
|
? 'text-white/70 hover:bg-white/10 hover:text-white'
|
|
: 'text-white/80 hover:bg-white/15 hover:text-white',
|
|
isStartAction && isHighlighted
|
|
? 'bg-white/10 text-white shadow-sm'
|
|
: '',
|
|
isPauseAction && isHighlighted
|
|
? 'bg-white/10 text-white shadow-sm'
|
|
: '',
|
|
)}
|
|
>
|
|
<span aria-hidden>{action.icon}</span>
|
|
<span className="sr-only">{action.label}</span>
|
|
</button>
|
|
);
|
|
})}
|
|
<Restart30sAction
|
|
onTrigger={triggerRestart}
|
|
className="h-8 w-8 ml-1"
|
|
/>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|