refactor(space): focus hud를 inline 구조로 단순화
This commit is contained in:
145
src/widgets/space-focus-hud/ui/InlineMicrostep.tsx
Normal file
145
src/widgets/space-focus-hud/ui/InlineMicrostep.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user