refactor(space): refine thought orb interaction
This commit is contained in:
@@ -13,6 +13,7 @@ export const ThoughtOrb = ({ isFocusMode, onCaptureThought }: ThoughtOrbProps) =
|
|||||||
const [draft, setDraft] = useState('');
|
const [draft, setDraft] = useState('');
|
||||||
const [isAbsorbing, setIsAbsorbing] = useState(false);
|
const [isAbsorbing, setIsAbsorbing] = useState(false);
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen && inputRef.current) {
|
if (isOpen && inputRef.current) {
|
||||||
@@ -26,12 +27,24 @@ export const ThoughtOrb = ({ isFocusMode, onCaptureThought }: ThoughtOrbProps) =
|
|||||||
const handleEscape = (e: KeyboardEvent) => {
|
const handleEscape = (e: KeyboardEvent) => {
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
setDraft('');
|
// Slightly delay clearing text to allow fade out animation
|
||||||
|
setTimeout(() => setDraft(''), 300);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOutsideClick = (e: MouseEvent) => {
|
||||||
|
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
||||||
|
setIsOpen(false);
|
||||||
|
setTimeout(() => setDraft(''), 300);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('keydown', handleEscape);
|
window.addEventListener('keydown', handleEscape);
|
||||||
return () => window.removeEventListener('keydown', handleEscape);
|
document.addEventListener('mousedown', handleOutsideClick);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('keydown', handleEscape);
|
||||||
|
document.removeEventListener('mousedown', handleOutsideClick);
|
||||||
|
};
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
if (!isFocusMode) return null;
|
if (!isFocusMode) return null;
|
||||||
@@ -56,40 +69,41 @@ export const ThoughtOrb = ({ isFocusMode, onCaptureThought }: ThoughtOrbProps) =
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed top-8 right-8 z-40 flex flex-col items-end gap-3 pointer-events-none">
|
<div ref={containerRef} className="fixed top-8 right-8 z-40 flex flex-col items-end gap-3 pointer-events-none">
|
||||||
<div className="pointer-events-auto relative group">
|
<div className="pointer-events-auto relative group">
|
||||||
{/* The Magnetic Orb */}
|
{/* The Magnetic Orb */}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIsOpen((prev) => !prev)}
|
onClick={() => setIsOpen((prev) => !prev)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative inline-flex h-14 w-14 items-center justify-center rounded-full transition-all duration-700 ease-[cubic-bezier(0.16,1,0.3,1)]",
|
"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
|
isOpen || isAbsorbing
|
||||||
? "scale-110"
|
? "scale-110"
|
||||||
: "hover:scale-110",
|
: "hover:scale-110"
|
||||||
isAbsorbing && "animate-pulse"
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Glowing Aura */}
|
{/* Glowing Aura (Only visible on hover/active) */}
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
"absolute inset-0 rounded-full bg-white/20 blur-xl transition-all duration-700",
|
"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"
|
(isOpen || isAbsorbing) ? "opacity-100 scale-125 bg-white/30" : "opacity-0 group-hover:opacity-100 group-hover:scale-150"
|
||||||
)} />
|
)} />
|
||||||
{/* Solid Core Ring */}
|
{/* Solid Core Ring */}
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
"absolute inset-0 rounded-full border border-white/20 transition-all duration-700",
|
"absolute inset-0 rounded-full border transition-all duration-[800ms] ease-[cubic-bezier(0.16,1,0.3,1)]",
|
||||||
isOpen || isAbsorbing ? "bg-white/10" : "bg-black/20 backdrop-blur-md"
|
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 */}
|
{/* Inner Light Dot (The Silent Anchor) */}
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
"relative z-10 rounded-full transition-all duration-500",
|
"relative z-10 rounded-full transition-all duration-[600ms] ease-[cubic-bezier(0.16,1,0.3,1)]",
|
||||||
isOpen || isAbsorbing ? "h-6 w-6 bg-white shadow-[0_0_20px_rgba(255,255,255,0.8)]" : "h-2 w-2 bg-white/60 group-hover:bg-white group-hover:h-3 group-hover:w-3 group-hover:shadow-[0_0_10px_rgba(255,255,255,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>
|
</button>
|
||||||
|
|
||||||
{/* Tooltip */}
|
{/* Tooltip */}
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
"absolute right-full top-1/2 -translate-y-1/2 mr-6 pointer-events-none transition-all duration-300",
|
"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"
|
(!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">
|
<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">
|
||||||
@@ -101,23 +115,33 @@ export const ThoughtOrb = ({ isFocusMode, onCaptureThought }: ThoughtOrbProps) =
|
|||||||
{/* Input Field */}
|
{/* Input Field */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"pointer-events-auto overflow-hidden transition-all duration-500 ease-[cubic-bezier(0.16,1,0.3,1)] origin-top-right",
|
"pointer-events-auto origin-top-right transition-all duration-700 ease-[cubic-bezier(0.16,1,0.3,1)] transform-gpu",
|
||||||
isOpen
|
isOpen
|
||||||
? "opacity-100 scale-100 translate-y-0"
|
? "scale-100 translate-y-0"
|
||||||
: "opacity-0 scale-95 -translate-y-4 pointer-events-none",
|
: "scale-75 -translate-y-6 pointer-events-none",
|
||||||
isAbsorbing && "scale-50 opacity-0 blur-md translate-x-10 -translate-y-10" // "Sucked into the orb" animation
|
isAbsorbing && "scale-50 translate-x-12 -translate-y-12 blur-xl" // Extreme ethereal sucking animation
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<form onSubmit={handleSubmit} className="relative w-[22rem]">
|
<form onSubmit={handleSubmit} className="relative w-[24rem]">
|
||||||
{/* Premium Smoked Glass Container */}
|
{/* Ethereal Volumetric Glass Container */}
|
||||||
<div className="absolute inset-0 rounded-[2rem] bg-black/40 backdrop-blur-3xl ring-1 ring-white/15 shadow-2xl" />
|
<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}
|
||||||
placeholder="Dump a distracting thought..."
|
placeholder="What's distracting you?"
|
||||||
className="relative z-10 w-full bg-transparent px-6 py-5 text-[15px] font-medium text-white outline-none placeholder:text-white/40 drop-shadow-sm disabled:opacity-50"
|
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",
|
||||||
|
isOpen ? "opacity-100" : "opacity-0"
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<button type="submit" className="hidden">Submit</button>
|
<button type="submit" className="hidden">Submit</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
Reference in New Issue
Block a user