refactor(space): focus hud를 inline 구조로 단순화

This commit is contained in:
2026-03-16 15:17:01 +09:00
parent fb2729193f
commit b91fdbcb67
8 changed files with 254 additions and 913 deletions

View File

@@ -0,0 +1,145 @@
'use client';
import { useState, useRef, useEffect } from 'react';
import { cn } from '@/shared/lib/cn';
interface InlineMicrostepProps {
microStep: string | null;
isBusy: boolean;
onUpdate: (nextStep: string | null) => Promise<boolean>;
}
export const InlineMicrostep = ({ microStep, isBusy, onUpdate }: InlineMicrostepProps) => {
const [isEditing, setIsEditing] = useState(false);
const [draft, setDraft] = useState('');
const [isCompleting, setIsCompleting] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const normalizedStep = microStep?.trim() || null;
useEffect(() => {
if (isEditing && inputRef.current) {
inputRef.current.focus();
}
}, [isEditing]);
const startEditing = () => {
if (isBusy || isCompleting) return;
setDraft(normalizedStep ?? '');
setIsEditing(true);
};
const cancelEditing = () => {
setIsEditing(false);
setDraft('');
};
const submitDraft = async () => {
const trimmed = draft.trim();
if (trimmed === normalizedStep) {
cancelEditing();
return;
}
const success = await onUpdate(trimmed || null);
if (success) {
cancelEditing();
}
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
e.preventDefault();
void submitDraft();
} else if (e.key === 'Escape') {
e.preventDefault();
cancelEditing();
}
};
const handleComplete = async () => {
if (isBusy || isCompleting || !normalizedStep) return;
setIsCompleting(true);
// Visual delay for the strikethrough animation before actually clearing it
setTimeout(async () => {
await onUpdate(null);
setIsCompleting(false);
}, 400);
};
if (isEditing) {
return (
<div className="flex w-full max-w-md items-center gap-3 rounded-full border border-white/20 bg-black/40 pl-5 pr-2 py-1.5 shadow-[0_0_30px_rgba(255,255,255,0.1)] backdrop-blur-2xl transition-all duration-300 animate-fade-in-up">
<input
ref={inputRef}
value={draft}
onChange={(e) => setDraft(e.target.value)}
onKeyDown={handleKeyDown}
onBlur={() => void submitDraft()}
disabled={isBusy}
placeholder="Enter next small step..."
className="flex-1 bg-transparent text-[15px] font-medium text-white outline-none placeholder:text-white/30 disabled:opacity-50"
/>
<button
type="button"
onMouseDown={(e) => {
e.preventDefault(); // Prevent blur
void submitDraft();
}}
disabled={isBusy || !draft.trim()}
className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-white text-black transition-transform hover:scale-105 active:scale-95 disabled:opacity-30 disabled:hover:scale-100"
>
<svg viewBox="0 0 16 16" className="h-4 w-4" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
<path d="M3 8.5 6.5 12 13 4" />
</svg>
</button>
</div>
);
}
if (normalizedStep) {
return (
<div
className={cn(
"group/micro flex w-full max-w-md items-center gap-4 rounded-full border border-white/5 bg-white/5 pl-4 pr-6 py-2.5 backdrop-blur-xl transition-all duration-500",
isCompleting ? "opacity-0 scale-95" : "hover:border-white/15 hover:bg-white/10 opacity-100 scale-100"
)}
>
<button
type="button"
onClick={() => void handleComplete()}
disabled={isBusy || isCompleting}
className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-full border border-white/30 text-white/0 transition-all duration-300 hover:bg-white hover:text-black focus:outline-none focus:ring-2 active:scale-90"
>
<svg viewBox="0 0 16 16" className="h-4 w-4" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M4 8.4 6.7 11 12 5.7" />
</svg>
</button>
<button
onClick={startEditing}
disabled={isBusy || isCompleting}
className="flex-1 text-left"
>
<p className={cn(
"text-[16px] text-white/80 transition-all duration-300 group-hover/micro:text-white truncate",
isCompleting && "line-through text-white/30 decoration-white/30"
)}>
{normalizedStep}
</p>
</button>
</div>
);
}
return (
<button
type="button"
onClick={startEditing}
disabled={isBusy}
className="inline-flex items-center gap-2 rounded-full border border-dashed border-white/20 bg-transparent px-6 py-2.5 text-[14px] font-medium text-white/40 transition-all hover:border-white/40 hover:text-white/80 active:scale-95 disabled:opacity-50"
>
<span className="text-lg leading-none mb-0.5">+</span> Add microstep
</button>
);
};

View File

@@ -1,228 +0,0 @@
'use client';
import { useEffect, useRef, useState } from 'react';
import { copy } from '@/shared/i18n';
import { cn } from '@/shared/lib/cn';
interface IntentCapsuleProps {
goal: string;
microStep?: string | null;
canRefocus: boolean;
canComplete?: boolean;
showActions?: boolean;
onOpenRefocus: () => void;
onMicroStepDone: () => void;
onGoalCompleteRequest?: () => void;
}
export const IntentCapsule = ({
goal,
microStep,
canRefocus,
canComplete = false,
showActions = true,
onOpenRefocus,
onMicroStepDone,
onGoalCompleteRequest,
}: IntentCapsuleProps) => {
const [isPinnedExpanded, setPinnedExpanded] = useState(false);
const [isHovered, setHovered] = useState(false);
const [isFocusWithin, setFocusWithin] = useState(false);
const sectionRef = useRef<HTMLElement | null>(null);
const normalizedMicroStep = microStep?.trim() ? microStep.trim() : null;
const isExpanded = showActions && (isPinnedExpanded || isHovered || isFocusWithin);
const canInteract = showActions && canRefocus;
const microGlyphClass =
'inline-flex h-5 w-5 items-center justify-center rounded-full border border-white/18 text-white/62 transition-all duration-200 hover:border-white/32 hover:text-white focus-visible:border-white/32 focus-visible:text-white disabled:cursor-default disabled:opacity-30';
useEffect(() => {
if (!isExpanded || !showActions) {
return;
}
const handlePointerDown = (event: PointerEvent) => {
const target = event.target;
if (!(target instanceof Node)) {
return;
}
if (sectionRef.current?.contains(target)) {
return;
}
setPinnedExpanded(false);
setHovered(false);
setFocusWithin(false);
};
document.addEventListener('pointerdown', handlePointerDown);
return () => {
document.removeEventListener('pointerdown', handlePointerDown);
};
}, [isExpanded, showActions]);
const handleExpand = () => {
if (!showActions) {
return;
}
setPinnedExpanded(true);
};
return (
<div className="pointer-events-none flex w-full">
<section
ref={sectionRef}
className={cn(
'pointer-events-auto relative overflow-hidden border text-white transition-[width,padding,background-color,border-color,box-shadow] duration-200 ease-out',
isExpanded
? 'w-[22rem] max-w-full rounded-[24px] border-white/10 bg-[#0f1115]/24 px-5 py-4 shadow-[0_12px_28px_rgba(2,6,23,0.14)] backdrop-blur-[8px] backdrop-saturate-125'
: 'w-[18.75rem] max-w-full rounded-[20px] border-white/8 bg-[#0f1115]/16 px-4 py-3 shadow-[0_8px_18px_rgba(2,6,23,0.10)] backdrop-blur-[7px] backdrop-saturate-120',
)}
onMouseEnter={() => {
if (showActions) {
setHovered(true);
}
}}
onMouseLeave={() => setHovered(false)}
onFocusCapture={() => {
if (showActions) {
setFocusWithin(true);
}
}}
onBlurCapture={(event) => {
if (!event.currentTarget.contains(event.relatedTarget)) {
setFocusWithin(false);
}
}}
>
<div
aria-hidden
className={cn(
'pointer-events-none absolute inset-0',
isExpanded
? 'rounded-[24px] bg-[linear-gradient(180deg,rgba(255,255,255,0.08)_0%,rgba(255,255,255,0.025)_24%,rgba(255,255,255,0.01)_100%)]'
: 'rounded-[20px] bg-[linear-gradient(180deg,rgba(255,255,255,0.06)_0%,rgba(255,255,255,0.018)_28%,rgba(255,255,255,0.008)_100%)]',
)}
/>
<div
aria-hidden
className={cn(
'pointer-events-none absolute inset-x-0 top-0 h-px',
isExpanded ? 'bg-white/16' : 'bg-white/12',
)}
/>
<div className="relative">
{isExpanded ? (
<div className="flex items-center gap-3">
<div className="min-w-0 flex-1">
<p className="min-w-0 truncate text-[18px] font-medium tracking-tight text-white/96 md:text-[20px]">
{goal}
</p>
</div>
{canInteract ? (
<button
type="button"
onClick={onOpenRefocus}
aria-label={copy.space.focusHud.refocusButton}
className="inline-flex shrink-0 items-center gap-1 rounded-full border border-white/10 bg-black/10 px-2 py-1 text-[11px] font-medium tracking-[0.06em] text-white/54 transition-colors duration-200 hover:text-white/72 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/14"
>
<svg
aria-hidden
viewBox="0 0 16 16"
className="h-3 w-3"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
>
<path d="m10.9 2.6 2.5 2.5" />
<path d="M3.4 12.6 2.6 15l2.4-.8 7.7-7.7-1.6-1.6-7.7 7.7Z" />
</svg>
<span>{copy.space.focusHud.intentEditLabel}</span>
</button>
) : null}
</div>
) : (
<button
type="button"
onClick={handleExpand}
disabled={!showActions}
aria-label={copy.space.focusHud.intentExpandAriaLabel}
className="flex min-w-0 w-full items-center text-left transition-opacity hover:opacity-92 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/14 disabled:cursor-default disabled:hover:opacity-100"
>
<p
className={cn(
'min-w-0 flex-1 truncate font-medium tracking-tight text-white/96 transition-[font-size,line-height] duration-200 ease-out',
'text-[15px] md:text-[16px]',
)}
>
{goal}
</p>
</button>
)}
<div
className={cn(
'overflow-hidden transition-[max-height,opacity,transform,margin] duration-220 ease-out motion-reduce:transition-none',
isExpanded
? 'mt-3 max-h-40 translate-y-0 opacity-100'
: 'max-h-0 -translate-y-1 opacity-0',
)}
aria-hidden={!isExpanded}
>
{normalizedMicroStep ? (
<div className="flex items-center gap-3 border-t border-white/10 pt-3">
<button
type="button"
onClick={showActions ? onMicroStepDone : undefined}
disabled={!showActions}
className={cn(microGlyphClass, 'shrink-0 bg-black/12')}
aria-label={copy.space.focusHud.microStepCompleteAriaLabel}
>
<svg
aria-hidden
viewBox="0 0 16 16"
className="h-3.5 w-3.5"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
>
<path d="M4 8.4 6.7 11 12 5.7" />
</svg>
</button>
<p className="min-w-0 flex-1 truncate text-left text-[14px] text-white/80">
{normalizedMicroStep}
</p>
</div>
) : (
<p className="border-t border-white/10 pt-3 text-[14px] leading-[1.5] text-white/56">
{copy.space.focusHud.refocusDescription}
</p>
)}
{showActions && canComplete ? (
<div className="mt-4 flex items-center justify-end border-t border-white/10 pt-3">
<button
type="button"
onClick={onGoalCompleteRequest}
className="text-[12px] font-medium tracking-[0.08em] text-white/62 underline decoration-white/10 underline-offset-4 transition-colors hover:text-white/84 hover:decoration-white/22"
>
{copy.space.focusHud.completeAction}
</button>
</div>
) : null}
</div>
</div>
</section>
</div>
);
};

View File

@@ -1,129 +0,0 @@
'use client';
import { copy } from '@/shared/i18n';
import { cn } from '@/shared/lib/cn';
import {
HUD_OPTION_CHEVRON,
HUD_OPTION_ROW,
HUD_OPTION_ROW_PRIMARY,
HUD_REVEAL_BASE,
HUD_REVEAL_HIDDEN,
HUD_REVEAL_NEXT_BEAT,
HUD_TRAY_CONTENT,
HUD_TRAY_HAIRLINE,
HUD_TRAY_LAYER,
HUD_TRAY_SHELL,
} from './overlayStyles';
interface NextMicroStepPromptProps {
open: boolean;
goal: string;
isSubmitting: boolean;
error: string | null;
onKeepGoalOnly: () => void;
onDefineNext: () => void;
onFinish: () => void;
}
export const NextMicroStepPrompt = ({
open,
goal,
isSubmitting,
error,
onKeepGoalOnly,
onDefineNext,
onFinish,
}: NextMicroStepPromptProps) => {
const trimmedGoal = goal.trim();
return (
<div
className={cn(
HUD_REVEAL_BASE,
open ? HUD_REVEAL_NEXT_BEAT : HUD_REVEAL_HIDDEN,
)}
aria-hidden={!open}
>
<section className={HUD_TRAY_SHELL}>
<div aria-hidden className={HUD_TRAY_LAYER} />
<div aria-hidden className={HUD_TRAY_HAIRLINE} />
<div className={HUD_TRAY_CONTENT}>
<p className="text-[11px] font-medium tracking-[0.08em] text-white/42">
{copy.space.focusHud.microStepPromptEyebrow}
</p>
<h3 className="mt-1 max-w-[22rem] text-[1rem] font-medium tracking-tight text-white/94">
{copy.space.focusHud.microStepPromptTitle}
</h3>
<p className="mt-1 max-w-[21rem] text-[12px] leading-[1.55] text-white/56">
{copy.space.focusHud.microStepPromptDescription}
</p>
{trimmedGoal ? (
<div className="mt-3 rounded-[16px] border border-white/8 bg-black/10 px-3.5 py-3">
<p className="text-[11px] font-medium uppercase tracking-[0.16em] text-white/36">
{copy.space.focusHud.intentLabel}
</p>
<p className="mt-1 truncate text-[14px] text-white/84">{trimmedGoal}</p>
</div>
) : null}
{error ? (
<p className="mt-3 text-[12px] text-rose-100/86">
{error}
</p>
) : null}
<div className="mt-4 space-y-2.5 border-t border-white/8 pt-4">
<button
type="button"
onClick={onKeepGoalOnly}
disabled={isSubmitting}
className={HUD_OPTION_ROW}
>
<div className="max-w-[20.5rem]">
<p className="text-[13px] font-medium tracking-[0.01em] text-white/78">
{copy.space.focusHud.microStepPromptKeep}
</p>
<p className="mt-1 text-[12px] leading-[1.55] text-white/44">
{copy.space.focusHud.microStepPromptKeepHint}
</p>
</div>
<span aria-hidden className={cn(HUD_OPTION_CHEVRON, 'mt-[2px]')}></span>
</button>
<button
type="button"
onClick={onDefineNext}
disabled={isSubmitting}
className={cn(HUD_OPTION_ROW, HUD_OPTION_ROW_PRIMARY)}
>
<div className="max-w-[20.5rem]">
<p className="text-[13px] font-semibold tracking-[0.01em] text-white/90">
{copy.space.focusHud.microStepPromptDefine}
</p>
<p className="mt-1 text-[12px] leading-[1.55] text-white/48">
{copy.space.focusHud.microStepPromptDefineHint}
</p>
</div>
<span aria-hidden className={cn(HUD_OPTION_CHEVRON, 'mt-[2px]')}></span>
</button>
</div>
<div className="mt-4 border-t border-white/8 pt-3">
<button
type="button"
onClick={onFinish}
disabled={isSubmitting}
className="text-[12px] font-medium tracking-[0.08em] text-white/58 underline decoration-white/12 underline-offset-4 transition-colors hover:text-white/82 hover:decoration-white/24 disabled:cursor-default disabled:text-white/26 disabled:no-underline"
>
{copy.space.focusHud.microStepPromptFinish}
</button>
<p className="mt-1.5 max-w-[20.5rem] text-[12px] leading-[1.55] text-white/42">
{copy.space.focusHud.microStepPromptFinishHint}
</p>
</div>
</div>
</section>
</div>
);
};

View File

@@ -1,111 +0,0 @@
'use client';
import { copy } from '@/shared/i18n';
import { cn } from '@/shared/lib/cn';
import {
HUD_OPTION_CHEVRON,
HUD_OPTION_ROW,
HUD_OPTION_ROW_PRIMARY,
HUD_PAUSE_BODY,
HUD_PAUSE_EYEBROW,
HUD_PAUSE_TITLE,
HUD_REVEAL_BASE,
HUD_REVEAL_HIDDEN,
HUD_REVEAL_PAUSE,
HUD_TRAY_CONTENT,
HUD_TRAY_HAIRLINE,
HUD_TRAY_LAYER,
HUD_TRAY_SHELL,
} from './overlayStyles';
interface PauseRefocusPromptProps {
open: boolean;
isBusy: boolean;
onRefocus: () => void;
onKeepCurrent: () => void;
onFinish: () => void;
}
export const PauseRefocusPrompt = ({
open,
isBusy,
onRefocus,
onKeepCurrent,
onFinish,
}: PauseRefocusPromptProps) => {
return (
<div
className={cn(
HUD_REVEAL_BASE,
open ? HUD_REVEAL_PAUSE : HUD_REVEAL_HIDDEN,
)}
aria-hidden={!open}
>
<section className={HUD_TRAY_SHELL}>
<div aria-hidden className={HUD_TRAY_LAYER} />
<div aria-hidden className={HUD_TRAY_HAIRLINE} />
<div className={HUD_TRAY_CONTENT}>
<p className={HUD_PAUSE_EYEBROW}>
{copy.space.focusHud.pausePromptEyebrow}
</p>
<h3 className={HUD_PAUSE_TITLE}>
{copy.space.focusHud.pausePromptTitle}
</h3>
<p className={HUD_PAUSE_BODY}>
{copy.space.focusHud.pausePromptDescription}
</p>
<div className="mt-5 space-y-2.5 border-t border-white/8 pt-4">
<button
type="button"
onClick={onRefocus}
disabled={isBusy}
className={cn(HUD_OPTION_ROW, HUD_OPTION_ROW_PRIMARY)}
>
<div className="min-w-0 max-w-[20.5rem]">
<p className="text-[14px] font-semibold leading-[1.35] tracking-[-0.01em] text-white/92">
{copy.space.focusHud.pausePromptRefocus}
</p>
<p className="mt-1.5 text-[12px] leading-[1.55] text-white/50">
{copy.space.focusHud.pausePromptRefocusHint}
</p>
</div>
<span aria-hidden className={cn(HUD_OPTION_CHEVRON, 'mt-[2px]')}></span>
</button>
<button
type="button"
onClick={onKeepCurrent}
disabled={isBusy}
className={HUD_OPTION_ROW}
>
<div className="min-w-0 max-w-[20.5rem]">
<p className="text-[14px] font-medium leading-[1.35] tracking-[-0.01em] text-white/82">
{copy.space.focusHud.pausePromptKeep}
</p>
<p className="mt-1.5 text-[12px] leading-[1.55] text-white/46">
{copy.space.focusHud.pausePromptKeepHint}
</p>
</div>
<span aria-hidden className={cn(HUD_OPTION_CHEVRON, 'mt-[2px]')}></span>
</button>
</div>
<div className="mt-4 border-t border-white/8 pt-3">
<button
type="button"
onClick={onFinish}
disabled={isBusy}
className="text-[12px] font-medium tracking-[0.08em] text-white/58 underline decoration-white/12 underline-offset-4 transition-colors hover:text-white/82 hover:decoration-white/24 disabled:cursor-default disabled:text-white/26 disabled:no-underline"
>
{copy.space.focusHud.pausePromptFinish}
</button>
<p className="mt-1.5 max-w-[20.5rem] text-[12px] leading-[1.55] text-white/42">
{copy.space.focusHud.pausePromptFinishHint}
</p>
</div>
</div>
</section>
</div>
);
};

View File

@@ -1,172 +0,0 @@
'use client';
import type { FormEvent } from 'react';
import { useEffect, useRef } from 'react';
import { copy } from '@/shared/i18n';
import { cn } from '@/shared/lib/cn';
import {
HUD_FIELD,
HUD_TEXT_LINK,
HUD_TEXT_LINK_STRONG,
HUD_TRAY_HAIRLINE,
HUD_TRAY_LAYER,
HUD_TRAY_SHELL,
} from './overlayStyles';
interface RefocusSheetProps {
open: boolean;
goalDraft: string;
microStepDraft: string;
autoFocusField: 'goal' | 'microStep';
submitLabel?: string;
isSaving: boolean;
error: string | null;
onGoalChange: (value: string) => void;
onMicroStepChange: (value: string) => void;
onClose: () => void;
onSubmit: () => void;
}
export const RefocusSheet = ({
open,
goalDraft,
microStepDraft,
autoFocusField,
submitLabel,
isSaving,
error,
onGoalChange,
onMicroStepChange,
onClose,
onSubmit,
}: RefocusSheetProps) => {
const goalRef = useRef<HTMLInputElement | null>(null);
const microStepRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
if (!open) {
return;
}
const rafId = window.requestAnimationFrame(() => {
if (autoFocusField === 'microStep') {
microStepRef.current?.focus();
microStepRef.current?.select();
return;
}
goalRef.current?.focus();
goalRef.current?.select();
});
return () => {
window.cancelAnimationFrame(rafId);
};
}, [autoFocusField, open]);
useEffect(() => {
if (!open) {
return;
}
const handleEscape = (event: KeyboardEvent) => {
if (event.key === 'Escape' && !isSaving) {
onClose();
}
};
window.addEventListener('keydown', handleEscape);
return () => {
window.removeEventListener('keydown', handleEscape);
};
}, [isSaving, onClose, open]);
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (isSaving || goalDraft.trim().length === 0) {
return;
}
onSubmit();
};
return (
<div
className={cn(
'pointer-events-none w-full overflow-hidden transition-all duration-300 ease-out motion-reduce:duration-0',
open
? 'max-h-[32rem] translate-y-0 opacity-100'
: 'pointer-events-none max-h-0 -translate-y-2 opacity-0',
)}
aria-hidden={!open}
>
<section className={HUD_TRAY_SHELL}>
<div aria-hidden className={HUD_TRAY_LAYER} />
<div aria-hidden className={HUD_TRAY_HAIRLINE} />
<header className="relative px-5 pt-4">
<div className="min-w-0">
<p className="text-[11px] font-medium tracking-[0.08em] text-white/42"> </p>
<h3 className="mt-1 text-[0.98rem] font-medium tracking-tight text-white/95">
{copy.space.focusHud.refocusTitle}
</h3>
<p className="mt-1 text-[13px] text-white/56">{copy.space.focusHud.refocusDescription}</p>
</div>
</header>
<form className="relative mt-5 space-y-4 px-5 pb-5" onSubmit={handleSubmit}>
<label className="block">
<span className="mb-2 block text-[11px] font-medium uppercase tracking-[0.22em] text-white/44">
{copy.space.focusHud.intentLabel}
</span>
<input
ref={goalRef}
value={goalDraft}
onChange={(event) => onGoalChange(event.target.value)}
placeholder={copy.space.sessionGoal.placeholder}
className={HUD_FIELD}
/>
</label>
<label className="block">
<span className="mb-2 block text-[11px] font-medium uppercase tracking-[0.22em] text-white/44">
{copy.space.focusHud.microStepLabel}
</span>
<input
ref={microStepRef}
value={microStepDraft}
onChange={(event) => onMicroStepChange(event.target.value)}
placeholder={copy.space.sessionGoal.hint}
className={HUD_FIELD}
/>
</label>
{error ? (
<p className="rounded-[18px] border border-rose-300/12 bg-rose-300/8 px-3 py-2 text-[12px] text-rose-100/86">
{error}
</p>
) : null}
<footer className="flex items-center justify-end gap-2 pt-2">
<button
type="button"
onClick={onClose}
disabled={isSaving}
className={HUD_TEXT_LINK}
>
{copy.common.cancel}
</button>
<button
type="submit"
disabled={isSaving || goalDraft.trim().length === 0}
className={HUD_TEXT_LINK_STRONG}
>
{isSaving ? copy.space.focusHud.refocusApplying : submitLabel ?? copy.space.focusHud.refocusApply}
</button>
</footer>
</form>
</section>
</div>
);
};

View File

@@ -1,12 +1,10 @@
import { useCallback, useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { copy } from '@/shared/i18n'; import { copy } from '@/shared/i18n';
import { cn } from '@/shared/lib/cn';
import type { HudStatusLinePayload } from '@/shared/lib/useHudStatusLine'; import type { HudStatusLinePayload } from '@/shared/lib/useHudStatusLine';
import { SpaceTimerHudWidget } from '@/widgets/space-timer-hud'; import { SpaceTimerHudWidget } from '@/widgets/space-timer-hud';
import { GoalCompleteSheet } from './GoalCompleteSheet'; import { GoalCompleteSheet } from './GoalCompleteSheet';
import { IntentCapsule } from './IntentCapsule'; import { InlineMicrostep } from './InlineMicrostep';
import { NextMicroStepPrompt } from './NextMicroStepPrompt';
import { PauseRefocusPrompt } from './PauseRefocusPrompt';
import { RefocusSheet } from './RefocusSheet';
import { ReturnPrompt } from './ReturnPrompt'; import { ReturnPrompt } from './ReturnPrompt';
interface SpaceFocusHudWidgetProps { interface SpaceFocusHudWidgetProps {
@@ -56,25 +54,15 @@ export const SpaceFocusHudWidget = ({
onGoalFinish, onGoalFinish,
onStatusMessage, onStatusMessage,
}: SpaceFocusHudWidgetProps) => { }: SpaceFocusHudWidgetProps) => {
const [overlay, setOverlay] = useState<'none' | 'paused' | 'return' | 'refocus' | 'next-beat' | 'complete'>('none'); const [overlay, setOverlay] = useState<'none' | 'return' | 'complete'>('none');
const [refocusOrigin, setRefocusOrigin] = useState<'manual' | 'pause' | 'next-beat' | 'return'>('manual');
const [completePreferredView, setCompletePreferredView] = useState<'choice' | 'next'>('choice'); const [completePreferredView, setCompletePreferredView] = useState<'choice' | 'next'>('choice');
const [draftGoal, setDraftGoal] = useState('');
const [draftMicroStep, setDraftMicroStep] = useState('');
const [autoFocusField, setAutoFocusField] = useState<'goal' | 'microStep'>('goal');
const [isSavingIntent, setSavingIntent] = useState(false); const [isSavingIntent, setSavingIntent] = useState(false);
const [intentError, setIntentError] = useState<string | null>(null);
const visibleRef = useRef(false); const visibleRef = useRef(false);
const resumePlaybackStateRef = useRef<'running' | 'paused'>(playbackState); const resumePlaybackStateRef = useRef<'running' | 'paused'>(playbackState);
const pausePlaybackStateRef = useRef<'running' | 'paused'>(playbackState);
const suppressNextPausePromptRef = useRef(false);
const restReminderTimerRef = useRef<number | null>(null); const restReminderTimerRef = useRef<number | null>(null);
const normalizedGoal = goal.trim().length > 0 ? goal.trim() : copy.space.focusHud.goalFallback; const normalizedGoal = goal.trim().length > 0 ? goal.trim() : copy.space.focusHud.goalFallback;
const normalizedMicroStep = microStep?.trim() ? microStep.trim() : null;
const isPausedPromptOpen = overlay === 'paused';
const isReturnPromptOpen = overlay === 'return'; const isReturnPromptOpen = overlay === 'return';
const isRefocusOpen = overlay === 'refocus';
const isMicroStepPromptOpen = overlay === 'next-beat';
const isCompleteOpen = overlay === 'complete'; const isCompleteOpen = overlay === 'complete';
const isIntentOverlayOpen = overlay !== 'none'; const isIntentOverlayOpen = overlay !== 'none';
@@ -90,9 +78,7 @@ export const SpaceFocusHudWidget = ({
useEffect(() => { useEffect(() => {
if (!hasActiveSession) { if (!hasActiveSession) {
setOverlay('none'); setOverlay('none');
setIntentError(null);
setSavingIntent(false); setSavingIntent(false);
setRefocusOrigin('manual');
setCompletePreferredView('choice'); setCompletePreferredView('choice');
} }
}, [hasActiveSession]); }, [hasActiveSession]);
@@ -109,7 +95,6 @@ export const SpaceFocusHudWidget = ({
return; return;
} }
setIntentError(null);
setOverlay('return'); setOverlay('return');
}, [overlay, returnPromptMode]); }, [overlay, returnPromptMode]);
@@ -133,172 +118,94 @@ export const SpaceFocusHudWidget = ({
resumePlaybackStateRef.current = playbackState; resumePlaybackStateRef.current = playbackState;
}, [normalizedGoal, onStatusMessage, playbackState]); }, [normalizedGoal, onStatusMessage, playbackState]);
const openRefocus = useCallback((
field: 'goal' | 'microStep' = 'goal',
origin: 'manual' | 'pause' | 'next-beat' | 'return' = 'manual',
) => {
setDraftGoal(goal.trim());
setDraftMicroStep(normalizedMicroStep ?? '');
setAutoFocusField(field);
setIntentError(null);
setRefocusOrigin(origin);
setOverlay('refocus');
}, [goal, normalizedMicroStep]);
useEffect(() => {
if (
pausePlaybackStateRef.current === 'running' &&
playbackState === 'paused' &&
hasActiveSession &&
overlay === 'none'
) {
if (suppressNextPausePromptRef.current) {
suppressNextPausePromptRef.current = false;
pausePlaybackStateRef.current = playbackState;
return;
}
setIntentError(null);
setOverlay('paused');
onStatusMessage({
message: copy.space.focusHud.refocusOpenOnPause,
});
}
pausePlaybackStateRef.current = playbackState;
}, [hasActiveSession, onStatusMessage, overlay, playbackState]);
useEffect(() => {
if (playbackState === 'running' && overlay === 'paused') {
setOverlay('none');
}
}, [overlay, playbackState]);
useEffect(() => {
if (!normalizedMicroStep && overlay === 'next-beat') {
setOverlay('none');
}
}, [normalizedMicroStep, overlay]);
useEffect(() => { useEffect(() => {
if (entryOverlayIntent !== 'resume-refocus' || !hasActiveSession || overlay !== 'none') { if (entryOverlayIntent !== 'resume-refocus' || !hasActiveSession || overlay !== 'none') {
return; return;
} }
// With inline microsteps, we just handle the intent and let the user click if they want.
openRefocus('microStep', 'manual');
onEntryOverlayIntentHandled?.(); onEntryOverlayIntentHandled?.();
}, [entryOverlayIntent, hasActiveSession, onEntryOverlayIntentHandled, openRefocus, overlay]); }, [entryOverlayIntent, hasActiveSession, onEntryOverlayIntentHandled, overlay]);
const handleOpenCompleteSheet = (preferredView: 'choice' | 'next' = 'choice') => { const handleOpenCompleteSheet = (preferredView: 'choice' | 'next' = 'choice') => {
setIntentError(null);
setCompletePreferredView(preferredView); setCompletePreferredView(preferredView);
setOverlay('complete'); setOverlay('complete');
}; };
const handleDismissReturnPrompt = () => { const handleDismissReturnPrompt = () => {
onDismissReturnPrompt?.(); onDismissReturnPrompt?.();
if (overlay === 'return') { if (overlay === 'return') {
setOverlay('none'); setOverlay('none');
} }
}; };
const handleRefocusSubmit = async () => { const handleInlineMicrostepUpdate = async (nextStep: string | null) => {
const trimmedGoal = draftGoal.trim(); if (isSavingIntent) return false;
if (!trimmedGoal || isSavingIntent) {
return;
}
setSavingIntent(true); setSavingIntent(true);
setIntentError(null);
try { try {
const didUpdate = await onIntentUpdate({ const didUpdate = await onIntentUpdate({ microStep: nextStep });
goal: trimmedGoal, if (didUpdate) {
microStep: draftMicroStep.trim() || null, if (nextStep) {
}); onStatusMessage({ message: copy.space.focusHud.refocusSaved });
} else {
if (!didUpdate) { onStatusMessage({ message: copy.space.focusHud.microStepCleared });
setIntentError(copy.space.workspace.intentSyncFailed); }
return;
}
setOverlay('none');
onStatusMessage({
message: copy.space.focusHud.refocusSaved,
});
if (refocusOrigin === 'return') {
onDismissReturnPrompt?.();
}
if (refocusOrigin === 'pause' && playbackState === 'paused') {
onStartRequested?.();
} }
return didUpdate;
} finally { } finally {
setSavingIntent(false); setSavingIntent(false);
} }
}; };
const handleKeepGoalOnly = async () => {
if (isSavingIntent) {
return;
}
setSavingIntent(true);
setIntentError(null);
try {
const didUpdate = await onIntentUpdate({
microStep: null,
});
if (!didUpdate) {
setIntentError(copy.space.workspace.intentSyncFailed);
return;
}
setOverlay('none');
onStatusMessage({
message: copy.space.focusHud.microStepCleared,
});
} finally {
setSavingIntent(false);
}
};
const handleDefineNextMicroStep = () => {
setDraftGoal(goal.trim());
setDraftMicroStep('');
setAutoFocusField('microStep');
setIntentError(null);
setRefocusOrigin('next-beat');
setOverlay('refocus');
};
return ( return (
<> <>
<div className="pointer-events-none fixed left-6 top-6 z-20 w-[min(29rem,calc(100vw-3rem))] md:left-10 md:top-9"> <div className="pointer-events-none fixed inset-0 z-20 flex flex-col items-center justify-center pt-10 pb-32">
<IntentCapsule {/* The Monolith (Central Hub) */}
key={isIntentOverlayOpen ? 'intent-locked' : 'intent-interactive'} <div className={cn(
goal={normalizedGoal} "pointer-events-auto 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)]",
microStep={microStep} isIntentOverlayOpen ? "opacity-0 scale-95 blur-md" : "opacity-100 scale-100 blur-0"
canRefocus={Boolean(hasActiveSession)} )}>
canComplete={hasActiveSession && (sessionPhase === 'focus' || sessionPhase === 'break')} {/* Massive Central Timer */}
showActions={!isIntentOverlayOpen} <div className="relative group cursor-pointer" onClick={() => playbackState === 'running' ? onPauseRequested?.() : onStartRequested?.()}>
onOpenRefocus={() => openRefocus('goal', 'manual')} <p className={cn(
onMicroStepDone={() => { "text-[8rem] md:text-[14rem] font-light tracking-tighter leading-none transition-colors duration-500",
if (!normalizedMicroStep) { sessionPhase === 'break' ? "text-emerald-300 drop-shadow-[0_0_40px_rgba(16,185,129,0.3)]" : "text-white drop-shadow-[0_0_40px_rgba(255,255,255,0.15)]",
openRefocus('microStep', 'next-beat'); playbackState === 'paused' && "opacity-60"
return; )}>
} {timeDisplay}
</p>
</div>
setIntentError(null); {/* Core Intent */}
setOverlay('next-beat'); <div className="mt-8 flex flex-col items-center group w-full">
}} {/* Immutable Goal */}
onGoalCompleteRequest={handleOpenCompleteSheet} <h2 className="text-2xl md:text-4xl font-light tracking-tight text-white/95">
/> {normalizedGoal}
</h2>
{/* Kinetic Inline Microstep */}
<div className="mt-8 flex flex-col items-center w-full max-w-lg min-h-[4rem]">
<InlineMicrostep
microStep={microStep ?? null}
isBusy={isSavingIntent}
onUpdate={handleInlineMicrostepUpdate}
/>
</div>
{hasActiveSession && (sessionPhase === 'focus' || sessionPhase === 'break') && (
<button
type="button"
onClick={() => handleOpenCompleteSheet('choice')}
className="mt-8 text-[11px] font-bold uppercase tracking-[0.25em] text-white/30 transition hover:text-white/70"
>
End Session
</button>
)}
</div>
</div>
</div>
<div className="pointer-events-none fixed inset-0 z-30">
<ReturnPrompt <ReturnPrompt
open={isReturnPromptOpen && Boolean(returnPromptMode)} open={isReturnPromptOpen && Boolean(returnPromptMode)}
mode={returnPromptMode === 'break' ? 'break' : 'focus'} mode={returnPromptMode === 'break' ? 'break' : 'focus'}
@@ -308,7 +215,6 @@ export const SpaceFocusHudWidget = ({
}} }}
onRefocus={() => { onRefocus={() => {
handleDismissReturnPrompt(); handleDismissReturnPrompt();
openRefocus('microStep', 'return');
}} }}
onRest={() => { onRest={() => {
handleDismissReturnPrompt(); handleDismissReturnPrompt();
@@ -322,59 +228,6 @@ export const SpaceFocusHudWidget = ({
handleOpenCompleteSheet('choice'); handleOpenCompleteSheet('choice');
}} }}
/> />
<PauseRefocusPrompt
open={isPausedPromptOpen}
isBusy={isSavingIntent}
onRefocus={() => openRefocus('microStep', 'pause')}
onKeepCurrent={() => {
setOverlay('none');
onStartRequested?.();
}}
onFinish={() => {
setOverlay('none');
handleOpenCompleteSheet('choice');
}}
/>
<RefocusSheet
open={isRefocusOpen}
goalDraft={draftGoal}
microStepDraft={draftMicroStep}
autoFocusField={autoFocusField}
submitLabel={
refocusOrigin === 'pause' && playbackState === 'paused'
? copy.space.focusHud.refocusApplyAndResume
: copy.space.focusHud.refocusApply
}
isSaving={isSavingIntent}
error={intentError}
onGoalChange={setDraftGoal}
onMicroStepChange={setDraftMicroStep}
onClose={() => {
if (isSavingIntent) {
return;
}
setIntentError(null);
setOverlay('none');
}}
onSubmit={() => {
void handleRefocusSubmit();
}}
/>
<NextMicroStepPrompt
open={isMicroStepPromptOpen}
goal={normalizedGoal}
isSubmitting={isSavingIntent}
error={intentError}
onKeepGoalOnly={() => {
void handleKeepGoalOnly();
}}
onDefineNext={handleDefineNextMicroStep}
onFinish={() => {
setIntentError(null);
handleOpenCompleteSheet('choice');
}}
/>
<GoalCompleteSheet <GoalCompleteSheet
open={isCompleteOpen} open={isCompleteOpen}
currentGoal={goal} currentGoal={goal}
@@ -383,7 +236,6 @@ export const SpaceFocusHudWidget = ({
onFinish={() => Promise.resolve(onGoalFinish())} onFinish={() => Promise.resolve(onGoalFinish())}
onRest={() => { onRest={() => {
setOverlay('none'); setOverlay('none');
suppressNextPausePromptRef.current = true;
onPauseRequested?.(); onPauseRequested?.();
if (restReminderTimerRef.current) { if (restReminderTimerRef.current) {
@@ -400,9 +252,8 @@ export const SpaceFocusHudWidget = ({
}} }}
/> />
</div> </div>
<SpaceTimerHudWidget <SpaceTimerHudWidget
timeDisplay={timeDisplay}
isImmersionMode
hasActiveSession={hasActiveSession} hasActiveSession={hasActiveSession}
sessionPhase={sessionPhase} sessionPhase={sessionPhase}
playbackState={playbackState} playbackState={playbackState}
@@ -410,7 +261,6 @@ export const SpaceFocusHudWidget = ({
canStart={canStartSession} canStart={canStartSession}
canPause={canPauseSession} canPause={canPauseSession}
canReset={canRestartSession} canReset={canRestartSession}
className="pr-[4.2rem]"
onStartClick={onStartRequested} onStartClick={onStartRequested}
onPauseClick={onPauseRequested} onPauseClick={onPauseRequested}
onResetClick={onRestartRequested} onResetClick={onRestartRequested}

View File

@@ -9,13 +9,11 @@ import {
} from '@/features/restart-30s'; } from '@/features/restart-30s';
interface SpaceTimerHudWidgetProps { interface SpaceTimerHudWidgetProps {
timeDisplay?: string;
className?: string; className?: string;
hasActiveSession?: boolean; hasActiveSession?: boolean;
sessionPhase?: 'focus' | 'break' | null; sessionPhase?: 'focus' | 'break' | null;
playbackState?: 'running' | 'paused' | null; playbackState?: 'running' | 'paused' | null;
isControlsDisabled?: boolean; isControlsDisabled?: boolean;
isImmersionMode?: boolean;
canStart?: boolean; canStart?: boolean;
canPause?: boolean; canPause?: boolean;
canReset?: boolean; canReset?: boolean;
@@ -27,13 +25,11 @@ interface SpaceTimerHudWidgetProps {
const HUD_ACTIONS = copy.space.timerHud.actions; const HUD_ACTIONS = copy.space.timerHud.actions;
export const SpaceTimerHudWidget = ({ export const SpaceTimerHudWidget = ({
timeDisplay = '25:00',
className, className,
hasActiveSession = false, hasActiveSession = false,
sessionPhase = 'focus', sessionPhase = 'focus',
playbackState = 'paused', playbackState = 'paused',
isControlsDisabled = false, isControlsDisabled = false,
isImmersionMode = false,
canStart = true, canStart = true,
canPause = false, canPause = false,
canReset = false, canReset = false,
@@ -54,45 +50,32 @@ export const SpaceTimerHudWidget = ({
return ( return (
<div <div
className={cn( className={cn(
'pointer-events-none fixed inset-x-0 z-20 flex justify-center px-4 sm:px-6', 'pointer-events-none fixed inset-x-0 z-20 flex justify-center px-4 transition-opacity duration-500',
className, className,
)} )}
style={{ bottom: 'calc(env(safe-area-inset-bottom, 0px) + 2rem)' }} style={{ bottom: 'calc(env(safe-area-inset-bottom, 0px) + 2.5rem)' }}
> >
<div className="relative pointer-events-auto"> <div className="relative pointer-events-auto opacity-40 hover:opacity-100 transition-opacity duration-300">
<section <section
className={cn( className={cn(
'relative z-10 flex h-[3.5rem] items-center justify-between gap-6 overflow-hidden rounded-full px-5 transition-colors', 'relative z-10 flex h-[3.5rem] items-center gap-4 overflow-hidden rounded-full pl-6 pr-4 transition-colors',
isImmersionMode isBreakPhase
? isBreakPhase ? 'border border-emerald-200/10 bg-[rgba(10,20,18,0.3)] backdrop-blur-2xl shadow-2xl'
? 'border border-emerald-200/14 bg-[rgba(10,20,18,0.26)] backdrop-blur-2xl shadow-[0_8px_32px_rgba(0,0,0,0.10)]' : 'border border-white/5 bg-black/40 backdrop-blur-2xl shadow-2xl hover:border-white/10'
: 'border border-white/10 bg-black/20 backdrop-blur-2xl shadow-[0_8px_32px_rgba(0,0,0,0.12)]'
: isBreakPhase
? 'border border-emerald-200/16 bg-[rgba(10,20,18,0.32)] backdrop-blur-2xl shadow-[0_8px_32px_rgba(0,0,0,0.14)]'
: '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"> <div className="flex items-center gap-3 pr-2 border-r border-white/10">
<span <span
className={cn( className={cn(
'w-14 text-right text-[10px] font-bold uppercase tracking-[0.15em] opacity-80', 'text-[10px] font-bold uppercase tracking-[0.2em] opacity-80',
sessionPhase === 'break' ? 'text-emerald-300' : 'text-brand-primary' sessionPhase === 'break' ? 'text-emerald-300' : 'text-white/60'
)} )}
> >
{modeLabel} {modeLabel}
</span> </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>
<div className="flex items-center gap-1.5 pl-2 border-l border-white/10"> <div className="flex items-center gap-1">
{HUD_ACTIONS.map((action) => { {HUD_ACTIONS.map((action) => {
const isStartAction = action.id === 'start'; const isStartAction = action.id === 'start';
const isPauseAction = action.id === 'pause'; const isPauseAction = action.id === 'pause';
@@ -117,20 +100,11 @@ export const SpaceTimerHudWidget = ({
if (isResetAction) onResetClick?.(); if (isResetAction) onResetClick?.();
}} }}
className={cn( 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', 'inline-flex h-9 w-9 items-center justify-center rounded-full text-sm transition-all duration-300 ease-out focus-visible:outline-none focus-visible:ring-2 active:scale-95 disabled:cursor-not-allowed disabled:opacity-20',
isImmersionMode isBreakPhase
? isBreakPhase ? 'text-white/70 hover:bg-emerald-100/10 hover:text-white'
? 'text-white/74 hover:bg-emerald-100/10 hover:text-white' : 'text-white/60 hover:bg-white/10 hover:text-white',
: 'text-white/70 hover:bg-white/10 hover:text-white' isHighlighted
: isBreakPhase
? 'text-white/82 hover:bg-emerald-100/12 hover:text-white'
: 'text-white/80 hover:bg-white/15 hover:text-white',
isStartAction && isHighlighted
? isBreakPhase
? 'bg-emerald-100/10 text-white shadow-sm'
: 'bg-white/10 text-white shadow-sm'
: '',
isPauseAction && isHighlighted
? isBreakPhase ? isBreakPhase
? 'bg-emerald-100/10 text-white shadow-sm' ? 'bg-emerald-100/10 text-white shadow-sm'
: 'bg-white/10 text-white shadow-sm' : 'bg-white/10 text-white shadow-sm'
@@ -144,7 +118,7 @@ export const SpaceTimerHudWidget = ({
})} })}
<Restart30sAction <Restart30sAction
onTrigger={triggerRestart} onTrigger={triggerRestart}
className="h-8 w-8 ml-1" className="h-9 w-9 ml-1"
/> />
</div> </div>
</section> </section>

View File

@@ -59,41 +59,51 @@ export const FocusRightRail = ({
isIdle ? 'opacity-0 translate-x-4 pointer-events-none' : 'opacity-100 translate-x-0', isIdle ? 'opacity-0 translate-x-4 pointer-events-none' : 'opacity-100 translate-x-0',
)} )}
> >
<div className="relative flex flex-col gap-2 rounded-full border border-white/10 bg-black/20 p-2.5 backdrop-blur-2xl shadow-[0_8px_32px_rgba(0,0,0,0.2)]"> <div className="relative flex flex-col items-center gap-3">
{/* Notes Toggle */} {/* Thought Orb (Brain Dump) */}
<div className="relative group"> <div className="relative group mb-4">
<button <button
type="button" type="button"
aria-label={copy.space.toolsDock.notesButton} aria-label={copy.space.toolsDock.notesButton}
onClick={onToggleNotes} onClick={onToggleNotes}
className={cn( className={cn(
"relative inline-flex h-10 w-10 items-center justify-center rounded-full transition-all duration-200", "relative inline-flex h-12 w-12 items-center justify-center rounded-full transition-all duration-500",
openPopover === 'notes' ? "bg-white/20 text-white shadow-inner" : "bg-transparent text-white/70 hover:bg-white/10 hover:text-white" openPopover === 'notes'
? "bg-white text-black shadow-[0_0_40px_rgba(255,255,255,0.4)] scale-110"
: "bg-gradient-to-tr from-white/10 to-white/5 border border-white/10 text-white/90 hover:bg-white/20 hover:scale-105 hover:shadow-[0_0_20px_rgba(255,255,255,0.15)] backdrop-blur-md"
)} )}
> >
{ANCHOR_ICON.notes} <div className={cn("absolute inset-0 rounded-full bg-white/20 blur-md transition-opacity duration-500", openPopover === 'notes' ? "opacity-100" : "opacity-0 group-hover:opacity-50")} />
<svg viewBox="0 0 24 24" className="h-5 w-5 relative z-10" fill="none" stroke="currentColor" strokeWidth="1.5">
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
</button> </button>
{/* Tooltip */} {/* Tooltip */}
<div className="absolute right-full top-1/2 -translate-y-1/2 mr-3 pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity"> <div className="absolute right-full top-1/2 -translate-y-1/2 mr-3 pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity">
<span className="bg-black/60 backdrop-blur-md text-white text-[11px] px-2 py-1 rounded-md whitespace-nowrap"> <span className="bg-white/10 backdrop-blur-md border border-white/10 text-white text-[10px] uppercase tracking-widest px-3 py-1.5 rounded-full whitespace-nowrap shadow-xl">
{copy.space.toolsDock.notesButton} Brain Dump
</span> </span>
</div> </div>
{openPopover === 'notes' ? ( {openPopover === 'notes' ? (
<div className="absolute right-full top-1/2 -translate-y-1/2 mr-4"> <div className="absolute right-full top-1/2 -translate-y-1/2 mr-6 w-[20rem]">
<QuickNotesPopover <div className="overflow-hidden rounded-[2rem] border border-white/10 bg-[linear-gradient(145deg,rgba(255,255,255,0.06)_0%,rgba(255,255,255,0.01)_100%)] p-2 backdrop-blur-3xl shadow-2xl">
noteDraft={noteDraft} <QuickNotesPopover
onDraftChange={onNoteDraftChange} noteDraft={noteDraft}
onDraftEnter={onNoteSubmit} onDraftChange={onNoteDraftChange}
onSubmit={onNoteSubmit} onDraftEnter={onNoteSubmit}
/> onSubmit={onNoteSubmit}
/>
</div>
</div> </div>
) : null} ) : null}
</div> </div>
{/* Standard Tools */}
<div className="relative flex flex-col gap-2 rounded-full border border-white/10 bg-black/20 p-2.5 backdrop-blur-2xl shadow-[0_8px_32px_rgba(0,0,0,0.2)]">
{/* Sound Toggle */} {/* Sound Toggle */}
<div className="relative group"> <div className="relative group">
<button <button
@@ -174,6 +184,8 @@ export const FocusRightRail = ({
</div> </div>
</div> </div>
</div>
</div> </div>
</div> </div>
); );