feat(space/hud): Exit 버튼 좌측 하단 Invisible Door UI로 재배치
맥락: - 기존의 우측 상단 Exit(나가기) 버튼이 너무 동떨어져 있었음. - 목표(Goal) 패널 하단에 Exit 버튼을 두려는 시도가 있었으나, '목표 유지'와 '목표 포기(Exit)'라는 상반된 의미가 한 공간에 묶여 인지적 충돌을 발생시킴. - 몰입을 방해하지 않는 투명함(Invisible UI)과 본능적인 이탈 경로가 필요함. 변경사항: - SpaceToolsDockWidget에 새로운 좌측 하단(Bottom-Left) 모서리 Exit 버튼 렌더링 영역 추가. - 평소에는 투명한 Escape(⎋) 아이콘만 노출하여 배경 공간의 방해 최소화. - 사용자가 마우스를 Hover할 때만 알약(Pill) 형태로 부드럽게 확장(Expansion)되며 ExitHoldButton(Bar)이 나타나는 고급 인터랙션 구현. - FloatingGoalWidget에 테스트로 추가했던 Exit 버튼 코드 원복(제거) 및 SpaceFocusHudWidget, SpaceWorkspaceWidget의 불필요한 prop 전달 정리. 검증: - npm run build 정상 통과. 세션-상태: 몰입 공간(/space)의 하이엔드 UI 레이아웃 재배치 및 디자인 고도화 완료. 세션-다음: 향후 필요 시 통계(Analytics) 또는 결제(Paywall) 세부 기능 구현. 세션-리스크: 없음.
This commit is contained in:
@@ -23,10 +23,10 @@ export const FloatingGoalWidget = ({
|
||||
const normalizedGoal = goal.trim().length > 0 ? goal.trim() : copy.space.timerHud.goalFallback;
|
||||
|
||||
return (
|
||||
<div className="pointer-events-none fixed left-0 top-0 z-20 w-full max-w-[800px] h-48 bg-[radial-gradient(ellipse_at_top_left,rgba(0,0,0,0.6)_0%,rgba(0,0,0,0)_60%)]">
|
||||
<div className="pointer-events-none fixed left-0 top-0 z-20 w-full max-w-[800px] h-48 bg-[radial-gradient(ellipse_at_top_left,rgba(0,0,0,0.6)_0%,rgba(0,0,0,0)_60%)] group">
|
||||
<div className="flex flex-col items-start gap-4 p-8 md:p-12">
|
||||
{/* Main Goal */}
|
||||
<div className="pointer-events-auto group relative flex items-center gap-4">
|
||||
<div className="pointer-events-auto flex items-center gap-4">
|
||||
<h2 className="text-2xl md:text-[1.75rem] font-medium tracking-tight text-white drop-shadow-[0_2px_4px_rgba(0,0,0,0.8)] [text-shadow:0_4px_24px_rgba(0,0,0,0.6)]">
|
||||
{normalizedGoal}
|
||||
</h2>
|
||||
@@ -34,7 +34,7 @@ export const FloatingGoalWidget = ({
|
||||
<button
|
||||
type="button"
|
||||
onClick={onGoalCompleteRequest}
|
||||
className="opacity-0 group-hover:opacity-100 shrink-0 rounded-full border border-white/20 bg-black/40 backdrop-blur-md px-3.5 py-1.5 text-[11px] font-medium text-white/90 shadow-lg transition-all hover:bg-black/60 hover:text-white"
|
||||
className="opacity-0 group-hover:opacity-100 shrink-0 rounded-full border border-white/20 bg-black/40 backdrop-blur-md px-3.5 py-1.5 text-[11px] font-medium text-white/90 shadow-lg transition-all hover:bg-black/60 hover:text-white focus-visible:opacity-100"
|
||||
>
|
||||
목표 달성
|
||||
</button>
|
||||
|
||||
@@ -21,6 +21,7 @@ interface SpaceFocusHudWidgetProps {
|
||||
onStartRequested?: () => void;
|
||||
onPauseRequested?: () => void;
|
||||
onRestartRequested?: () => void;
|
||||
onExitRequested?: () => void;
|
||||
onGoalUpdate: (nextGoal: string) => boolean | Promise<boolean>;
|
||||
onStatusMessage: (payload: HudStatusLinePayload) => void;
|
||||
}
|
||||
@@ -41,6 +42,7 @@ export const SpaceFocusHudWidget = ({
|
||||
onStartRequested,
|
||||
onPauseRequested,
|
||||
onRestartRequested,
|
||||
onExitRequested,
|
||||
onGoalUpdate,
|
||||
onStatusMessage,
|
||||
}: SpaceFocusHudWidgetProps) => {
|
||||
|
||||
@@ -155,14 +155,22 @@ export const SpaceToolsDockWidget = ({
|
||||
<>
|
||||
<div
|
||||
className={cn(
|
||||
'fixed z-30 transition-opacity right-[calc(env(safe-area-inset-right,0px)+0.75rem)] top-[calc(env(safe-area-inset-top,0px)+0.75rem)]',
|
||||
isFocusMode ? (isIdle ? 'opacity-40' : 'opacity-84') : 'opacity-92',
|
||||
'fixed z-30 transition-all duration-300 left-[calc(env(safe-area-inset-left,0px)+2rem)] bottom-[calc(env(safe-area-inset-bottom,0px)+2rem)] group flex items-center justify-start',
|
||||
isFocusMode ? (isIdle ? 'opacity-0 -translate-x-4 pointer-events-none' : 'opacity-80 hover:opacity-100') : 'opacity-92',
|
||||
)}
|
||||
>
|
||||
<ExitHoldButton
|
||||
variant={isFocusMode ? 'ring' : 'bar'}
|
||||
onConfirm={onExitRequested}
|
||||
/>
|
||||
<div className="relative flex items-center justify-start overflow-hidden rounded-full border border-white/10 bg-black/20 p-1.5 backdrop-blur-md transition-all duration-300 ease-out w-10 hover:w-[130px] hover:bg-black/40">
|
||||
<div className="flex shrink-0 h-7 w-7 items-center justify-center text-white/70">
|
||||
<span aria-hidden className="text-[14px]">⎋</span>
|
||||
</div>
|
||||
<div className="absolute left-10 whitespace-nowrap opacity-0 transition-opacity duration-300 group-hover:opacity-100">
|
||||
<ExitHoldButton
|
||||
variant="bar"
|
||||
onConfirm={onExitRequested}
|
||||
className="bg-transparent text-white/90 hover:bg-white/10 hover:text-white px-2 py-1 h-7"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FocusModeAnchors
|
||||
|
||||
@@ -291,7 +291,7 @@ export const SpaceWorkspaceWidget = () => {
|
||||
visible={isFocusMode}
|
||||
hasActiveSession={Boolean(currentSession)}
|
||||
playbackState={resolvedPlaybackState}
|
||||
sessionPhase={phase ?? "focus"}
|
||||
sessionPhase={phase ?? 'focus'}
|
||||
isSessionActionPending={isSessionMutating}
|
||||
canStartSession={controls.canStartSession}
|
||||
canPauseSession={controls.canPauseSession}
|
||||
@@ -305,6 +305,9 @@ export const SpaceWorkspaceWidget = () => {
|
||||
onRestartRequested={() => {
|
||||
void controls.handleRestartRequested();
|
||||
}}
|
||||
onExitRequested={() => {
|
||||
void controls.handleExitRequested();
|
||||
}}
|
||||
onStatusMessage={pushStatusLine}
|
||||
onGoalUpdate={controls.handleGoalAdvance}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user