refactor(space): 하단 다이내믹 독 형태의 잡념 수집기 UIUX 재설계 및 오류 수정

- 중앙 모놀리스 영역에서 벗어나 화면 최하단의 플로팅 바 형식으로 UI 전면 개편

- 마우스 호버 시 알약(Pill) 형태로 변하고, 활성화 시 거대한 입력창으로 몰핑되는 부드러운 트랜지션 적용

- backdrop-filter 팝인 버그 수정을 위한 transition 개별 요소 적용

- Cmd+K (Mac) 및 Ctrl+K (Windows) 단축키 인식 오류 수정 및 전역 이벤트 리스너 리팩토링

- 마우스 호버 시 뷰가 부르르 떨리는 현상(Jittering)을 가상 요소(before) 히트박스로 원천 차단
This commit is contained in:
2026-03-18 14:38:21 +09:00
parent 14d7165ffe
commit 9b013f1843
4 changed files with 84 additions and 91 deletions

View File

@@ -101,9 +101,14 @@ export const EndSessionConfirmModal = ({
> >
{/* Abyssal Backdrop: Direct filter animation to prevent WebKit blur pop-in */} {/* Abyssal Backdrop: Direct filter animation to prevent WebKit blur pop-in */}
<div <div
onClick={() => {
if (activeStage === 'decision' || activeStage === 'unfinished-confirm') {
onClose();
}
}}
className={cn( className={cn(
'absolute inset-0 transition-all duration-1000 ease-[cubic-bezier(0.16,1,0.3,1)] transform-gpu', 'absolute inset-0 transition-all duration-1000 ease-[cubic-bezier(0.16,1,0.3,1)] transform-gpu',
open ? 'bg-black/80 backdrop-blur-[40px]' : 'bg-transparent backdrop-blur-none', open ? 'bg-black/80 backdrop-blur-[40px] pointer-events-auto' : 'bg-transparent backdrop-blur-none pointer-events-none',
)} )}
> >
<div className={cn("absolute inset-0 bg-[url('/noise.png')] mix-blend-overlay pointer-events-none transition-opacity duration-1000", open ? "opacity-20" : "opacity-0")} /> <div className={cn("absolute inset-0 bg-[url('/noise.png')] mix-blend-overlay pointer-events-none transition-opacity duration-1000", open ? "opacity-20" : "opacity-0")} />

View File

@@ -7,9 +7,10 @@ interface InlineMicrostepProps {
microStep: string | null; microStep: string | null;
isBusy: boolean; isBusy: boolean;
onUpdate: (nextStep: string | null) => Promise<boolean>; onUpdate: (nextStep: string | null) => Promise<boolean>;
isHidden?: boolean;
} }
export const InlineMicrostep = ({ microStep, isBusy, onUpdate }: InlineMicrostepProps) => { export const InlineMicrostep = ({ microStep, isBusy, onUpdate, isHidden = false }: InlineMicrostepProps) => {
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const [draft, setDraft] = useState(''); const [draft, setDraft] = useState('');
const [isCompleting, setIsCompleting] = useState(false); const [isCompleting, setIsCompleting] = useState(false);
@@ -77,9 +78,12 @@ export const InlineMicrostep = ({ microStep, isBusy, onUpdate }: InlineMicrostep
<div className="flex w-full justify-center perspective-[1000px]"> <div className="flex w-full justify-center perspective-[1000px]">
<div <div
className={cn( className={cn(
"relative overflow-hidden transition-all duration-500 ease-[cubic-bezier(0.2,0.8,0.2,1)]", "relative overflow-hidden ease-[cubic-bezier(0.2,0.8,0.2,1)]",
// Base container styling: Volumetric Glass // Base container styling: Volumetric Glass
"rounded-full border backdrop-blur-2xl flex items-center", "rounded-full border flex items-center",
// Hidden Override (For Monolith transition sync)
isHidden ? "opacity-0 scale-95 pointer-events-none transition-all duration-700 backdrop-blur-none" : "transition-all duration-500 backdrop-blur-2xl opacity-100 scale-100",
// State-based Morphing Choreography // State-based Morphing Choreography
state === 'idle' && "w-[240px] h-[42px] bg-black/20 border-white/5 shadow-[0_8px_32px_rgba(0,0,0,0.12)] cursor-pointer hover:bg-black/30 hover:border-white/10 hover:w-[250px] active:scale-[0.98]", state === 'idle' && "w-[240px] h-[42px] bg-black/20 border-white/5 shadow-[0_8px_32px_rgba(0,0,0,0.12)] cursor-pointer hover:bg-black/30 hover:border-white/10 hover:w-[250px] active:scale-[0.98]",

View File

@@ -142,14 +142,15 @@ export const SpaceFocusHudWidget = ({
{/* The Monolith (Central Hub) */} {/* The Monolith (Central Hub) */}
<div <div
className={cn( className={cn(
"pointer-events-auto relative flex flex-col items-center text-center max-w-4xl px-6 transition-all duration-700 ease-[cubic-bezier(0.16,1,0.3,1)]", "pointer-events-auto relative flex flex-col items-center text-center max-w-4xl px-6",
isOverlayOpen isOverlayOpen ? "pointer-events-none" : ""
? "opacity-0 scale-95 blur-md"
: "opacity-100 scale-100 blur-0",
)} )}
> >
{/* Massive Unstoppable Timer */} {/* Massive Unstoppable Timer */}
<div className="relative group cursor-default"> <div className={cn(
"relative group cursor-default transition-all duration-700 ease-[cubic-bezier(0.16,1,0.3,1)]",
isOverlayOpen ? "opacity-0 scale-95" : "opacity-100 scale-100"
)}>
<p <p
className={cn( className={cn(
"text-[8rem] md:text-[14rem] font-light tracking-tighter leading-none transition-colors duration-500", "text-[8rem] md:text-[14rem] font-light tracking-tighter leading-none transition-colors duration-500",
@@ -165,7 +166,10 @@ export const SpaceFocusHudWidget = ({
{/* Core Intent */} {/* Core Intent */}
<div className="mt-8 flex flex-col items-center group w-full"> <div className="mt-8 flex flex-col items-center group w-full">
{/* Immutable Goal */} {/* Immutable Goal */}
<h2 className="text-2xl md:text-4xl font-light tracking-tight text-white drop-shadow-[0_1px_4px_rgba(0,0,0,0.6)] drop-shadow-[0_8px_24px_rgba(0,0,0,0.3)]"> <h2 className={cn(
"text-2xl md:text-4xl font-light tracking-tight text-white drop-shadow-[0_1px_4px_rgba(0,0,0,0.6)] drop-shadow-[0_8px_24px_rgba(0,0,0,0.3)] transition-all duration-700 ease-[cubic-bezier(0.16,1,0.3,1)]",
isOverlayOpen ? "opacity-0 scale-95" : "opacity-100 scale-100"
)}>
{normalizedGoal} {normalizedGoal}
</h2> </h2>
@@ -175,6 +179,7 @@ export const SpaceFocusHudWidget = ({
microStep={microStep ?? null} microStep={microStep ?? null}
isBusy={isSavingIntent} isBusy={isSavingIntent}
onUpdate={handleInlineMicrostepUpdate} onUpdate={handleInlineMicrostepUpdate}
isHidden={isOverlayOpen}
/> />
</div> </div>
@@ -184,7 +189,10 @@ export const SpaceFocusHudWidget = ({
<button <button
type="button" type="button"
onClick={() => setOverlay("end-session")} onClick={() => setOverlay("end-session")}
className="rounded-full border border-white/10 bg-black/20 px-5 py-2 text-[11px] font-bold uppercase tracking-[0.25em] text-white/70 backdrop-blur-md shadow-md transition-all hover:border-white/20 hover:bg-black/40 hover:text-white active:scale-95" className={cn(
"rounded-full border border-white/10 bg-black/20 px-5 py-2 text-[11px] font-bold uppercase tracking-[0.25em] text-white/70 backdrop-blur-md shadow-md hover:border-white/20 hover:bg-black/40 hover:text-white active:scale-95",
isOverlayOpen ? "opacity-0 scale-95 pointer-events-none transition-all duration-700 ease-[cubic-bezier(0.16,1,0.3,1)] backdrop-blur-none" : "opacity-100 scale-100 transition-all duration-700 ease-[cubic-bezier(0.16,1,0.3,1)]"
)}
> >
<span className="drop-shadow-md">{copy.space.endSession.trigger}</span> <span className="drop-shadow-md">{copy.space.endSession.trigger}</span>
</button> </button>

View File

@@ -22,27 +22,32 @@ export const ThoughtOrb = ({ isFocusMode, onCaptureThought }: ThoughtOrbProps) =
}, [isOpen]); }, [isOpen]);
useEffect(() => { useEffect(() => {
if (!isOpen) return; const handleKeyDownGlobal = (e: KeyboardEvent) => {
// Escape to close
const handleEscape = (e: KeyboardEvent) => { if (e.key === 'Escape' && isOpen) {
if (e.key === 'Escape') {
setIsOpen(false); setIsOpen(false);
// Slightly delay clearing text to allow fade out animation // Slightly delay clearing text to allow fade out animation
setTimeout(() => setDraft(''), 300); setTimeout(() => setDraft(''), 300);
} }
// Cmd+K (Mac) or Ctrl+K (Windows/Linux) or / to open globally
if (!isOpen && (( (e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') || e.key === '/')) {
// Prevent default browser shortcuts (Find, Search)
e.preventDefault();
setIsOpen(true);
}
}; };
const handleOutsideClick = (e: MouseEvent) => { const handleOutsideClick = (e: MouseEvent) => {
if (containerRef.current && !containerRef.current.contains(e.target as Node)) { if (isOpen && containerRef.current && !containerRef.current.contains(e.target as Node)) {
setIsOpen(false); setIsOpen(false);
setTimeout(() => setDraft(''), 300); setTimeout(() => setDraft(''), 300);
} }
}; };
window.addEventListener('keydown', handleEscape); window.addEventListener('keydown', handleKeyDownGlobal);
document.addEventListener('mousedown', handleOutsideClick); document.addEventListener('mousedown', handleOutsideClick);
return () => { return () => {
window.removeEventListener('keydown', handleEscape); window.removeEventListener('keydown', handleKeyDownGlobal);
document.removeEventListener('mousedown', handleOutsideClick); document.removeEventListener('mousedown', handleOutsideClick);
}; };
}, [isOpen]); }, [isOpen]);
@@ -69,83 +74,54 @@ export const ThoughtOrb = ({ isFocusMode, onCaptureThought }: ThoughtOrbProps) =
}; };
return ( return (
<div ref={containerRef} className="fixed top-8 right-8 z-40 flex flex-col items-end gap-3 pointer-events-none"> <div ref={containerRef} className="fixed bottom-10 left-1/2 -translate-x-1/2 z-40 flex flex-col items-center pointer-events-none perspective-[1000px]">
<div className="pointer-events-auto relative group"> <form
{/* The Magnetic Orb */} onSubmit={handleSubmit}
<button
type="button"
onClick={() => setIsOpen((prev) => !prev)}
className={cn( className={cn(
"relative inline-flex h-12 w-12 items-center justify-center rounded-full transition-all duration-[800ms] ease-[cubic-bezier(0.16,1,0.3,1)]", "pointer-events-auto relative flex items-center justify-center transition-all duration-[800ms] ease-[cubic-bezier(0.16,1,0.3,1)] transform-gpu outline-none group",
isOpen || isAbsorbing // The Hitbox Extender: prevents jitter when animating upwards by ensuring the mouse never leaves the element
? "scale-110" "before:absolute before:-inset-y-10 before:-inset-x-20 before:z-[-1]",
: "hover:scale-110" // Morphing Dimensions Geometry (The Dynamic Line -> The Pill -> The Input)
isOpen
? "w-[30rem] h-14 rounded-full translate-y-0"
: "w-[12rem] h-[4px] rounded-full hover:w-[16rem] hover:h-10 hover:-translate-y-2 opacity-60 hover:opacity-100",
// Container Material Optics
isOpen
? "bg-white/[0.04] backdrop-blur-[40px] border border-white/10 shadow-[inset_0_0_30px_rgba(255,255,255,0.03),0_20px_40px_rgba(0,0,0,0.5)]"
: "bg-white/20 backdrop-blur-sm border border-transparent shadow-[0_2px_10px_rgba(0,0,0,0.2)] hover:bg-white/[0.06] hover:backdrop-blur-xl hover:border-white/10 hover:shadow-[0_10px_30px_rgba(0,0,0,0.4)]",
// Sucking Animation (Feedback)
isAbsorbing && "scale-50 opacity-0 blur-xl translate-y-10 border-transparent bg-white shadow-[0_0_60px_rgba(255,255,255,1)]"
)} )}
onClick={() => {
if (!isOpen && !isAbsorbing) setIsOpen(true);
}}
> >
{/* Glowing Aura (Only visible on hover/active) */} {/* Idle Hint Text (Only visible on hover when closed) */}
<div className={cn( <div className={cn(
"absolute inset-0 rounded-full bg-white/20 blur-xl transition-all duration-[800ms] ease-[cubic-bezier(0.16,1,0.3,1)]", "absolute inset-0 flex items-center justify-center pointer-events-none transition-all duration-500",
(isOpen || isAbsorbing) ? "opacity-100 scale-125 bg-white/30" : "opacity-0 group-hover:opacity-100 group-hover:scale-150" (!isOpen && !isAbsorbing) ? "opacity-0 scale-95 group-hover:opacity-100 group-hover:scale-100" : "opacity-0 scale-105 hidden"
)} />
{/* Solid Core Ring */}
<div className={cn(
"absolute inset-0 rounded-full border transition-all duration-[800ms] ease-[cubic-bezier(0.16,1,0.3,1)]",
isOpen || isAbsorbing ? "bg-white/[0.08] border-white/20 backdrop-blur-md" : "bg-black/10 border-white/[0.04] backdrop-blur-sm group-hover:bg-black/30 group-hover:border-white/10"
)} />
{/* Inner Light Dot (The Silent Anchor) */}
<div className={cn(
"relative z-10 rounded-full transition-all duration-[600ms] ease-[cubic-bezier(0.16,1,0.3,1)]",
isOpen || isAbsorbing
? "h-5 w-5 bg-white shadow-[0_0_20px_rgba(255,255,255,0.9)]"
: "h-1.5 w-1.5 bg-white/20 group-hover:bg-white group-hover:h-2.5 group-hover:w-2.5 group-hover:shadow-[0_0_12px_rgba(255,255,255,0.8)]"
)} />
</button>
{/* Tooltip */}
<div className={cn(
"absolute right-full top-1/2 -translate-y-1/2 mr-6 pointer-events-none transition-all duration-500 ease-out delay-75",
(!isOpen && !isAbsorbing) ? "opacity-0 translate-x-2 group-hover:opacity-100 group-hover:translate-x-0" : "opacity-0"
)}> )}>
<span className="bg-black/60 backdrop-blur-2xl border border-white/10 text-white/90 text-[10px] font-bold uppercase tracking-widest px-4 py-2 rounded-full whitespace-nowrap shadow-lg"> <p className="text-[12px] font-medium tracking-wide text-white/50 flex items-center gap-2">
Brain Dump <kbd className="font-sans px-2 py-0.5 rounded-md bg-white/10 text-white/70 text-[10px]">Ctrl / </kbd>
</span> <kbd className="font-sans px-1.5 py-0.5 rounded-md bg-white/10 text-white/70 text-[10px]">K</kbd>
</div> <span className="ml-1">Brain Dump</span>
</p>
</div> </div>
{/* Input Field */} {/* Morphing Input Field */}
<div
className={cn(
"pointer-events-auto origin-top-right transition-all duration-700 ease-[cubic-bezier(0.16,1,0.3,1)] transform-gpu",
isOpen
? "scale-100 translate-y-0"
: "scale-75 -translate-y-6 pointer-events-none",
isAbsorbing && "scale-50 translate-x-12 -translate-y-12 blur-xl" // Extreme ethereal sucking animation
)}
>
<form onSubmit={handleSubmit} className="relative w-[24rem]">
{/* Ethereal Volumetric Glass Container */}
<div
className={cn(
"absolute inset-0 rounded-full transition-all duration-700 ease-[cubic-bezier(0.16,1,0.3,1)] transform-gpu",
isOpen
? "bg-black/60 backdrop-blur-[40px] shadow-[inset_0_0_20px_rgba(255,255,255,0.05),0_20px_40px_rgba(0,0,0,0.3)] border border-white/[0.08]"
: "bg-transparent backdrop-blur-none border-transparent shadow-none"
)}
/>
<input <input
ref={inputRef} ref={inputRef}
value={draft} value={draft}
onChange={(e) => setDraft(e.target.value)} onChange={(e) => setDraft(e.target.value)}
disabled={isAbsorbing} disabled={isAbsorbing || !isOpen}
placeholder="What's distracting you?" placeholder="Empty your mind..."
className={cn( className={cn(
"relative z-10 w-full bg-transparent px-8 py-4 text-[15px] font-light tracking-wide text-white outline-none placeholder:text-white/30 drop-shadow-sm disabled:opacity-50 transition-opacity duration-500", "relative w-full h-full bg-transparent px-8 text-[15.5px] font-light tracking-wide text-white outline-none placeholder:text-white/30 drop-shadow-sm transition-all duration-500",
isOpen ? "opacity-100" : "opacity-0" isOpen && !isAbsorbing ? "opacity-100 delay-200 z-10" : "opacity-0 w-0 px-0 pointer-events-none -translate-y-2"
)} )}
/> />
<button type="submit" className="hidden">Submit</button> <button type="submit" className="hidden">Submit</button>
</form> </form>
</div> </div>
</div>
); );
}; };