diff --git a/docs/13_space_intent_card_collapsed_expanded_spec.md b/docs/13_space_intent_card_collapsed_expanded_spec.md new file mode 100644 index 0000000..ffa576a --- /dev/null +++ b/docs/13_space_intent_card_collapsed_expanded_spec.md @@ -0,0 +1,256 @@ +# 13. `/space` Intent Card Collapsed / Expanded Spec + +Last Updated: 2026-03-14 + +이 문서는 `/space` 좌상단 목표 카드의 **collapsed / expanded 구조**를 정의한다. + +목적은 단순하다. + +- goal은 항상 보여야 한다 +- 하지만 배경을 계속 크게 가리면 안 된다 +- 평소에는 조용한 `anchor`처럼 남고, 필요할 때만 `card`처럼 확장돼야 한다 + +관련 문서: + +- `10_refocus_system_spec.md` +- `11_away_return_recovery_spec.md` +- `12_core_loop_execution_roadmap.md` +- `../../current_context.md` + +--- + +## 1. 왜 바꾸는가 + +현재 구조의 문제는 이것이다. + +- goal, microStep, 액션이 항상 한 카드 안에 모두 보인다 +- 읽힘은 확보하지만, 배경의 존재감을 과하게 가져간다 +- `/space`의 주인공이 scene이어야 하는데, 좌상단 카드가 먼저 읽힌다 + +즉 문제는 glass material 자체보다 **항상 큰 상태로 떠 있는 면적**이다. + +premium ambient UI에서는: + +- 정보는 항상 남아야 하지만 +- 존재감은 항상 같을 필요가 없다 + +그래서 목표 카드는 `persistent presence`는 유지하고, `persistent emphasis`는 버려야 한다. + +--- + +## 2. 한 줄 정의 + +> `/space`의 목표 카드는 평소에는 얇은 glass rail로 남고, 사용자가 실제로 결정을 내려야 할 때만 확장된다. + +--- + +## 3. 상태 정의 + +Intent Card는 아래 2개 상태만 가진다. + +### 3.1 Collapsed + +기본 상태. + +보이는 것: + +- goal 1줄 +- 작은 expand affordance 1개 + +보이지 않는 것: + +- microStep 상세 +- 완료 액션 +- recovery footer + +역할: + +- 지금 어떤 일 위에 있는지 잃지 않게 하는 기준점 + +감정: + +- panel이 아니라 anchor + +### 3.2 Expanded + +필요할 때만 열리는 상태. + +보이는 것: + +- goal +- microStep 또는 비어 있을 때의 조용한 helper line +- `이번 목표 완료` +- microStep 완료 체크 + +역할: + +- 지금 이 세션을 어떻게 계속할지 결정하게 하는 짧은 제어 면 + +감정: + +- 툴바가 아니라, 잠깐 열리는 soft control surface + +--- + +## 4. 상태 전환 규칙 + +### Expanded로 전환되는 경우 + +- desktop에서 hover +- focus 진입 +- expand affordance 클릭/tap + +### Collapsed로 돌아가는 경우 + +- pointer leave +- focus out +- expand affordance 재클릭 +- recovery tray가 열릴 때 + +중요: + +- `pause / return / next-beat / complete / refocus` tray가 열려 있는 동안에는 목표 카드는 강제로 collapsed 상태를 유지한다 +- tray가 이미 의사결정 레이어이기 때문에, base card까지 expanded 상태면 배경을 이중으로 가리게 된다 + +--- + +## 5. 시각 구조 + +### Collapsed rail + +- 폭: 약 `18rem ~ 19rem` +- 높이: 약 `48px ~ 52px` +- radius: `18px` 전후 +- glass: + - 더 얇고 더 투명 + - large panel처럼 보이면 안 된다 +- goal: + - 1줄 truncate + - medium weight +- affordance: + - 우측에 작은 chevron / expand control + - pill/button처럼 보이면 안 된다 + +### Expanded card + +- 폭: 약 `21rem ~ 23rem` +- 높이: 내용에 따라 자연스럽게 +- radius: `22px ~ 24px` +- glass: + - collapsed보다 한 단계 더 선명한 재질 + - 하지만 tray만큼 무겁지는 않다 +- 내부 구조: + 1. goal row + 2. microStep row + 3. quiet footer action + +--- + +## 6. 정보 구조 + +### Goal row + +- 좌측: goal 1줄 +- 우측: expand/collapse affordance +- goal 텍스트는 expanded 상태에서 클릭 시 refocus 진입 +- collapsed 상태에서는 goal 클릭이 바로 refocus를 여는 대신, rail 자체의 anchor로만 동작해도 괜찮다 + +### MicroStep row + +- expanded 상태에서만 노출 +- 왼쪽: completion glyph +- 오른쪽: microStep text +- microStep이 없으면: + - helper 1줄만 노출 + - planner-like placeholder 금지 + +### Footer action + +- expanded 상태에서만 노출 +- 우측 정렬된 quiet text action 1개 + - `이번 목표 완료` +- `다시 방향` 상시 버튼은 두지 않는다 +- refocus는 goal 클릭을 통해 진입한다 + +--- + +## 7. interaction 원칙 + +### Goal 수정 + +- expanded 상태에서 goal 텍스트 클릭 -> refocus +- collapsed 상태에서는 goal 클릭으로 바로 열지 않거나, 제품 감각을 해치지 않는 범위에서만 허용한다 +- 핵심은 `expanded`가 먼저 읽히고, `edit`는 그다음이어야 한다 + +### microStep 완료 + +- expanded 상태에서만 완료 glyph가 노출된다 +- collapsed 상태에서는 실수 클릭을 막기 위해 숨긴다 + +### 목표 완료 + +- expanded 상태에서만 footer action 노출 +- collapsed 상태에서는 보이지 않는다 + +--- + +## 8. motion 원칙 + +### Collapsed -> Expanded + +- 넓어짐 + 내부 row fade-in +- card가 “튀어나오는” 느낌보다, rail이 조용히 열리는 느낌이어야 한다 +- duration은 짧고 부드럽게 + +### Expanded -> Collapsed + +- 내부 row가 먼저 약해지고 +- 폭이 줄어든다 +- 닫힘은 더 조용해야 한다 + +### tray open 시 + +- intent card는 먼저 collapsed +- 그 아래 tray가 열림 +- 즉 `card 확장`과 `tray 확장`은 동시에 크게 보이면 안 된다 + +--- + +## 9. 금지사항 + +- 상시 expanded 금지 +- goal + microStep + footer action 항상 노출 금지 +- Now chip 재도입 금지 +- toolbar/pill/button cluster처럼 보이게 만들기 금지 +- 배경보다 card가 먼저 읽히게 만들기 금지 + +--- + +## 10. 구현 기준 + +필수 변경 파일: + +- `src/widgets/space-focus-hud/ui/IntentCapsule.tsx` +- `src/widgets/space-focus-hud/ui/SpaceFocusHudWidget.tsx` + +가능한 보조 변경: + +- `src/shared/i18n/messages/space.ts` +- `src/widgets/space-focus-hud/ui/overlayStyles.ts` + +구현 포인트: + +- `IntentCapsule` 내부에 local expanded state 추가 +- hover / focus / toggle button 기반 전환 +- `showActions === false` 또는 overlay open 상태에서는 강제 collapsed +- microStep / footer action은 expanded에서만 렌더 + +--- + +## 11. 완료 기준 + +- idle 상태에서 상단 goal UI가 배경을 덜 가린다 +- expanded 상태에서만 microStep과 완료 액션이 보인다 +- recovery tray가 열릴 때 base card는 과하게 커져 있지 않다 +- bright / dark scene 모두에서 읽히지만 scene이 먼저 읽힌다 +- `/space`가 productivity dashboard처럼 보이지 않는다 diff --git a/docs/90_current_state.md b/docs/90_current_state.md index 141cb7b..7bd825e 100644 --- a/docs/90_current_state.md +++ b/docs/90_current_state.md @@ -56,6 +56,10 @@ Last Updated: 2026-03-14 - `Pause` tray는 빠르게 다시 붙잡는 recovery reveal로 조정 - `Return(focus)`는 짧은 re-entry settle motion으로, `Return(break)`는 더 느슨한 release reveal로 분리 - `Goal Complete`도 같은 recovery family 안에서 가장 느린 closure motion을 가지도록 조정 +- `/space` intent HUD collapsed / expanded 재설계: + - 상시 큰 goal 카드 대신 idle에서는 goal 1줄과 작은 expand affordance만 남는 collapsed glass rail 구조로 변경 + - hover / focus / toggle에서만 expanded card로 열리며, 이때만 microStep과 `이번 목표 완료` 액션이 노출됨 + - recovery tray(`pause / return / next-beat / complete / refocus`)가 열릴 때는 base card가 강제로 collapsed 상태를 유지하도록 정리 - Focus Entry Surface / Execution Surface 재정의: - `/app`을 planning home이 아니라 hero-first focus entry surface로 재구성 diff --git a/docs/session_brief.md b/docs/session_brief.md index f958771..5e129b1 100644 --- a/docs/session_brief.md +++ b/docs/session_brief.md @@ -60,6 +60,10 @@ Last Updated: 2026-03-14 - `Pause`는 빠르게 다시 붙잡는 recovery reveal로, - `Return(focus)`는 재진입에 맞는 짧은 settle motion으로, - `Return(break)`와 `Goal Complete`는 더 느슨한 release/closure reveal로 분리했다. +- `/space` 목표 카드를 collapsed / expanded 구조로 재설계했다. + - idle에서는 goal 1줄만 남는 얇은 glass rail로 줄였다. + - microStep과 `이번 목표 완료`는 expanded 상태에서만 드러난다. + - recovery tray가 열리면 base card는 자동으로 collapsed 상태를 유지한다. - `/app`을 single-goal commitment gate로 다시 줄였다. - 2-step ritual setup을 제거했다. - current session이 있으면 `Resume` UI만 보여주고, `/space`로 이어가기만 제안한다. diff --git a/src/shared/i18n/messages/space.ts b/src/shared/i18n/messages/space.ts index a0f9aa2..a581e25 100644 --- a/src/shared/i18n/messages/space.ts +++ b/src/shared/i18n/messages/space.ts @@ -38,6 +38,8 @@ export const space = { restReminder: '5분이 지났어요. 다음 한 조각으로 돌아와요.', intentLabel: '이번 세션 목표', microStepLabel: '지금 할 한 조각', + intentExpandAriaLabel: '목표 카드 펼치기', + intentCollapseAriaLabel: '목표 카드 접기', refocusButton: '다시 방향', refocusTitle: '다시 방향 잡기', refocusDescription: '딱 한 줄만 다듬고 다시 시작해요.', diff --git a/src/widgets/space-focus-hud/ui/IntentCapsule.tsx b/src/widgets/space-focus-hud/ui/IntentCapsule.tsx index f88dd7b..95e1222 100644 --- a/src/widgets/space-focus-hud/ui/IntentCapsule.tsx +++ b/src/widgets/space-focus-hud/ui/IntentCapsule.tsx @@ -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 ( -
- {normalizedMicroStep} -
-- {copy.space.focusHud.refocusDescription} -
- )} + ) : null} ++ {normalizedMicroStep} +
++ {copy.space.focusHud.refocusDescription} +
+ )} + + {showActions && canComplete ? ( +