feat(space): 목표 카드를 collapsed rail로 재설계
This commit is contained in:
@@ -38,6 +38,8 @@ export const space = {
|
||||
restReminder: '5분이 지났어요. 다음 한 조각으로 돌아와요.',
|
||||
intentLabel: '이번 세션 목표',
|
||||
microStepLabel: '지금 할 한 조각',
|
||||
intentExpandAriaLabel: '목표 카드 펼치기',
|
||||
intentCollapseAriaLabel: '목표 카드 접기',
|
||||
refocusButton: '다시 방향',
|
||||
refocusTitle: '다시 방향 잡기',
|
||||
refocusDescription: '딱 한 줄만 다듬고 다시 시작해요.',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { copy } from '@/shared/i18n';
|
||||
import { cn } from '@/shared/lib/cn';
|
||||
|
||||
@@ -24,80 +25,185 @@ export const IntentCapsule = ({
|
||||
onMicroStepDone,
|
||||
onGoalCompleteRequest,
|
||||
}: IntentCapsuleProps) => {
|
||||
const [isPinnedExpanded, setPinnedExpanded] = useState(false);
|
||||
const [isHovered, setHovered] = useState(false);
|
||||
const [isFocusWithin, setFocusWithin] = useState(false);
|
||||
|
||||
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/20 text-white/62 transition-all duration-200 hover:border-white/36 hover:text-white focus-visible:border-white/36 focus-visible:text-white disabled:cursor-default disabled:opacity-30';
|
||||
'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';
|
||||
|
||||
const handleGoalClick = () => {
|
||||
if (!canInteract) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isExpanded) {
|
||||
setPinnedExpanded(true);
|
||||
return;
|
||||
}
|
||||
|
||||
onOpenRefocus();
|
||||
};
|
||||
|
||||
const handleToggleExpanded = () => {
|
||||
if (!showActions) {
|
||||
return;
|
||||
}
|
||||
|
||||
setPinnedExpanded((current) => !current);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="pointer-events-none w-full">
|
||||
<section className="pointer-events-auto relative w-full overflow-hidden rounded-[24px] border border-white/10 bg-[#0f1115]/24 px-5 py-4 text-white shadow-[0_12px_28px_rgba(2,6,23,0.14)] backdrop-blur-[8px] backdrop-saturate-125">
|
||||
<div className="pointer-events-none flex w-full">
|
||||
<section
|
||||
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="pointer-events-none absolute inset-0 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%)]"
|
||||
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="pointer-events-none absolute inset-x-0 top-0 h-px bg-white/16"
|
||||
className={cn(
|
||||
'pointer-events-none absolute inset-x-0 top-0 h-px',
|
||||
isExpanded ? 'bg-white/16' : 'bg-white/12',
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="relative">
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={canRefocus ? onOpenRefocus : undefined}
|
||||
disabled={!canRefocus || !showActions}
|
||||
onClick={handleGoalClick}
|
||||
disabled={!canInteract}
|
||||
aria-label={copy.space.focusHud.refocusButton}
|
||||
className="block min-w-0 w-full max-w-full text-left transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/14 disabled:cursor-default disabled:hover:opacity-100"
|
||||
className="min-w-0 flex-1 text-left transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/14 disabled:cursor-default disabled:hover:opacity-100"
|
||||
>
|
||||
<p className="truncate text-[18px] font-medium tracking-tight text-white/96 md:text-[20px]">
|
||||
<p
|
||||
className={cn(
|
||||
'truncate font-medium tracking-tight text-white/96 transition-[font-size,line-height] duration-200 ease-out',
|
||||
isExpanded ? 'text-[18px] md:text-[20px]' : 'text-[15px] md:text-[16px]',
|
||||
)}
|
||||
>
|
||||
{goal}
|
||||
</p>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{normalizedMicroStep ? (
|
||||
<div className="mt-3 flex items-center gap-3 border-t border-white/10 pt-3">
|
||||
{showActions ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={showActions ? onMicroStepDone : undefined}
|
||||
disabled={!showActions}
|
||||
className={cn(microGlyphClass, 'shrink-0 bg-black/12')}
|
||||
aria-label={copy.space.focusHud.microStepCompleteAriaLabel}
|
||||
onClick={handleToggleExpanded}
|
||||
aria-label={
|
||||
isExpanded
|
||||
? copy.space.focusHud.intentCollapseAriaLabel
|
||||
: copy.space.focusHud.intentExpandAriaLabel
|
||||
}
|
||||
className={cn(
|
||||
'inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full border border-white/8 bg-black/10 text-white/56 transition-all duration-200 hover:border-white/14 hover:bg-black/16 hover:text-white/82 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/12',
|
||||
isExpanded && 'border-white/12 bg-black/14 text-white/82',
|
||||
)}
|
||||
>
|
||||
<svg
|
||||
aria-hidden
|
||||
viewBox="0 0 16 16"
|
||||
className="h-3.5 w-3.5"
|
||||
className={cn(
|
||||
'h-3.5 w-3.5 transition-transform duration-200 ease-out',
|
||||
isExpanded ? 'rotate-180' : 'rotate-0',
|
||||
)}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.6"
|
||||
>
|
||||
<path d="M4 8.4 6.7 11 12 5.7" />
|
||||
<path d="M4.5 6.5 8 10l3.5-3.5" />
|
||||
</svg>
|
||||
</button>
|
||||
<p className="min-w-0 flex-1 truncate text-left text-[14px] text-white/80">
|
||||
{normalizedMicroStep}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<p className="mt-3 border-t border-white/10 pt-3 text-[14px] text-white/56">
|
||||
{copy.space.focusHud.refocusDescription}
|
||||
</p>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{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
|
||||
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>
|
||||
|
||||
@@ -261,6 +261,7 @@ export const SpaceFocusHudWidget = ({
|
||||
<>
|
||||
<div className="pointer-events-none fixed left-6 top-6 z-20 w-[min(29rem,calc(100vw-3rem))] md:left-10 md:top-9">
|
||||
<IntentCapsule
|
||||
key={isIntentOverlayOpen ? 'intent-locked' : 'intent-interactive'}
|
||||
goal={normalizedGoal}
|
||||
microStep={microStep}
|
||||
canRefocus={Boolean(hasActiveSession)}
|
||||
|
||||
Reference in New Issue
Block a user