refactor(space): 하단 다이내믹 독 형태의 잡념 수집기 UIUX 재설계 및 오류 수정
- 중앙 모놀리스 영역에서 벗어나 화면 최하단의 플로팅 바 형식으로 UI 전면 개편 - 마우스 호버 시 알약(Pill) 형태로 변하고, 활성화 시 거대한 입력창으로 몰핑되는 부드러운 트랜지션 적용 - backdrop-filter 팝인 버그 수정을 위한 transition 개별 요소 적용 - Cmd+K (Mac) 및 Ctrl+K (Windows) 단축키 인식 오류 수정 및 전역 이벤트 리스너 리팩토링 - 마우스 호버 시 뷰가 부르르 떨리는 현상(Jittering)을 가상 요소(before) 히트박스로 원천 차단
This commit is contained in:
@@ -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")} />
|
||||||
|
|||||||
@@ -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]",
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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(
|
|
||||||
"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)]",
|
|
||||||
isOpen || isAbsorbing
|
|
||||||
? "scale-110"
|
|
||||||
: "hover:scale-110"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{/* Glowing Aura (Only visible on hover/active) */}
|
|
||||||
<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)]",
|
|
||||||
(isOpen || isAbsorbing) ? "opacity-100 scale-125 bg-white/30" : "opacity-0 group-hover:opacity-100 group-hover:scale-150"
|
|
||||||
)} />
|
|
||||||
{/* 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">
|
|
||||||
Brain Dump
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Input Field */}
|
|
||||||
<div
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"pointer-events-auto origin-top-right transition-all duration-700 ease-[cubic-bezier(0.16,1,0.3,1)] transform-gpu",
|
"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",
|
||||||
|
// The Hitbox Extender: prevents jitter when animating upwards by ensuring the mouse never leaves the element
|
||||||
|
"before:absolute before:-inset-y-10 before:-inset-x-20 before:z-[-1]",
|
||||||
|
// Morphing Dimensions Geometry (The Dynamic Line -> The Pill -> The Input)
|
||||||
isOpen
|
isOpen
|
||||||
? "scale-100 translate-y-0"
|
? "w-[30rem] h-14 rounded-full translate-y-0"
|
||||||
: "scale-75 -translate-y-6 pointer-events-none",
|
: "w-[12rem] h-[4px] rounded-full hover:w-[16rem] hover:h-10 hover:-translate-y-2 opacity-60 hover:opacity-100",
|
||||||
isAbsorbing && "scale-50 translate-x-12 -translate-y-12 blur-xl" // Extreme ethereal sucking animation
|
// 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);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<form onSubmit={handleSubmit} className="relative w-[24rem]">
|
{/* Idle Hint Text (Only visible on hover when closed) */}
|
||||||
{/* Ethereal Volumetric Glass Container */}
|
<div className={cn(
|
||||||
<div
|
"absolute inset-0 flex items-center justify-center pointer-events-none transition-all duration-500",
|
||||||
className={cn(
|
(!isOpen && !isAbsorbing) ? "opacity-0 scale-95 group-hover:opacity-100 group-hover:scale-100" : "opacity-0 scale-105 hidden"
|
||||||
"absolute inset-0 rounded-full transition-all duration-700 ease-[cubic-bezier(0.16,1,0.3,1)] transform-gpu",
|
)}>
|
||||||
isOpen
|
<p className="text-[12px] font-medium tracking-wide text-white/50 flex items-center gap-2">
|
||||||
? "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]"
|
<kbd className="font-sans px-2 py-0.5 rounded-md bg-white/10 text-white/70 text-[10px]">Ctrl / ⌘</kbd>
|
||||||
: "bg-transparent backdrop-blur-none border-transparent shadow-none"
|
<kbd className="font-sans px-1.5 py-0.5 rounded-md bg-white/10 text-white/70 text-[10px]">K</kbd>
|
||||||
)}
|
<span className="ml-1">Brain Dump</span>
|
||||||
/>
|
</p>
|
||||||
<input
|
</div>
|
||||||
ref={inputRef}
|
|
||||||
value={draft}
|
{/* Morphing Input Field */}
|
||||||
onChange={(e) => setDraft(e.target.value)}
|
<input
|
||||||
disabled={isAbsorbing}
|
ref={inputRef}
|
||||||
placeholder="What's distracting you?"
|
value={draft}
|
||||||
className={cn(
|
onChange={(e) => setDraft(e.target.value)}
|
||||||
"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",
|
disabled={isAbsorbing || !isOpen}
|
||||||
isOpen ? "opacity-100" : "opacity-0"
|
placeholder="Empty your mind..."
|
||||||
)}
|
className={cn(
|
||||||
/>
|
"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",
|
||||||
<button type="submit" className="hidden">Submit</button>
|
isOpen && !isAbsorbing ? "opacity-100 delay-200 z-10" : "opacity-0 w-0 px-0 pointer-events-none -translate-y-2"
|
||||||
</form>
|
)}
|
||||||
</div>
|
/>
|
||||||
|
<button type="submit" className="hidden">Submit</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user