fix(flow): 기획-구현 불일치 정렬
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# 10. Refocus System Spec
|
# 10. Refocus System Spec
|
||||||
|
|
||||||
Last Updated: 2026-03-14
|
Last Updated: 2026-03-15
|
||||||
|
|
||||||
이 문서는 VibeRoom의 `Refocus System`을 제품 대표 경험으로 설계하기 위한 상세 기준 문서다.
|
이 문서는 VibeRoom의 `Refocus System`을 제품 대표 경험으로 설계하기 위한 상세 기준 문서다.
|
||||||
|
|
||||||
@@ -263,9 +263,9 @@ UI:
|
|||||||
|
|
||||||
행동:
|
행동:
|
||||||
|
|
||||||
- 여기까지 끝내기
|
- 여기서 마무리하기
|
||||||
- 다음 목표로 이어가기
|
- 다음 블록 이어가기
|
||||||
- 잠깐 쉬기
|
- 잠시 비우기
|
||||||
|
|
||||||
완료는 celebration보다 **closure quality**가 중요하다.
|
완료는 celebration보다 **closure quality**가 중요하다.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# 12. Core Loop Execution Roadmap
|
# 12. Core Loop Execution Roadmap
|
||||||
|
|
||||||
Last Updated: 2026-03-14
|
Last Updated: 2026-03-15
|
||||||
|
|
||||||
이 문서는 VibeRoom의 핵심 제품 기획을 **어떤 순서로 구현까지 연결할지**를 정의하는 실행 로드맵이다.
|
이 문서는 VibeRoom의 핵심 제품 기획을 **어떤 순서로 구현까지 연결할지**를 정의하는 실행 로드맵이다.
|
||||||
|
|
||||||
@@ -176,6 +176,8 @@ VibeRoom은 아래 방식으로 진행한다.
|
|||||||
|
|
||||||
- 상세 기획 문서 작성 완료
|
- 상세 기획 문서 작성 완료
|
||||||
- 1차 snapshot 구현 완료
|
- 1차 snapshot 구현 완료
|
||||||
|
- `/app -> /stats -> /app` entry flow 구현 완료
|
||||||
|
- `/space` complete 이후 secondary teaser 구현 완료
|
||||||
- 남은 것은 recovery 집계 연결, ritual fit, Free / Pro gating
|
- 남은 것은 recovery 집계 연결, ritual fit, Free / Pro gating
|
||||||
|
|
||||||
문서:
|
문서:
|
||||||
@@ -371,4 +373,4 @@ Away / Return이 끼어들기 전, 다음으로 예정된 축은 아래 두 가
|
|||||||
|
|
||||||
현재 위치:
|
현재 위치:
|
||||||
|
|
||||||
> `3. Break refinement`를 마무리했고, `4. Weekly Review`의 entry flow 구현을 시작했다.
|
> `3. Break refinement`를 마무리했고, `4. Weekly Review`의 entry flow 구현까지 마친 상태다.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# 13. `/space` Intent Card Collapsed / Expanded Spec
|
# 13. `/space` Intent Card Collapsed / Expanded Spec
|
||||||
|
|
||||||
Last Updated: 2026-03-14
|
Last Updated: 2026-03-15
|
||||||
|
|
||||||
이 문서는 `/space` 좌상단 목표 카드의 **collapsed / expanded 구조**를 정의한다.
|
이 문서는 `/space` 좌상단 목표 카드의 **collapsed / expanded 구조**를 정의한다.
|
||||||
|
|
||||||
@@ -120,8 +120,8 @@ Intent Card는 아래 2개 상태만 가진다.
|
|||||||
- decision tray는 반드시 명시적 액션으로만 닫는다
|
- decision tray는 반드시 명시적 액션으로만 닫는다
|
||||||
- `취소`
|
- `취소`
|
||||||
- `적용`
|
- `적용`
|
||||||
- `여기까지 끝내기`
|
- `여기서 마무리하기`
|
||||||
- `잠깐 쉬기`
|
- `잠시 비우기`
|
||||||
- `다음 블록 이어가기`
|
- `다음 블록 이어가기`
|
||||||
- 즉, `/space`에서 가볍게 접히는 것은 `expanded rail`뿐이고, 실질적인 state change layer는 dismissible popover로 취급하지 않는다
|
- 즉, `/space`에서 가볍게 접히는 것은 `expanded rail`뿐이고, 실질적인 state change layer는 dismissible popover로 취급하지 않는다
|
||||||
|
|
||||||
@@ -185,7 +185,7 @@ Intent Card는 아래 2개 상태만 가진다.
|
|||||||
- 우측 정렬된 quiet text action 1개
|
- 우측 정렬된 quiet text action 1개
|
||||||
- `이번 목표 완료`
|
- `이번 목표 완료`
|
||||||
- `다시 방향` 상시 버튼은 두지 않는다
|
- `다시 방향` 상시 버튼은 두지 않는다
|
||||||
- refocus는 goal 클릭을 통해 진입한다
|
- refocus는 expanded 상태의 명시적 `수정` 액션으로만 진입한다
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# 15. `/app -> /stats -> /app` Weekly Review Entry Flow Spec
|
# 15. `/app -> /stats -> /app` Weekly Review Entry Flow Spec
|
||||||
|
|
||||||
Last Updated: 2026-03-14
|
Last Updated: 2026-03-15
|
||||||
|
|
||||||
이 문서는 VibeRoom의 `Weekly Review`를 **어디서, 왜, 어떤 타이밍에 열어야 하는지**를 정의하는 진입 플로우 문서다.
|
이 문서는 VibeRoom의 `Weekly Review`를 **어디서, 왜, 어떤 타이밍에 열어야 하는지**를 정의하는 진입 플로우 문서다.
|
||||||
|
|
||||||
@@ -170,12 +170,12 @@ review가 가장 유의미한 순간도 바로 여기다.
|
|||||||
|
|
||||||
- 첫 1~2회 사용
|
- 첫 1~2회 사용
|
||||||
- 세션 기록이 거의 없는 주
|
- 세션 기록이 거의 없는 주
|
||||||
- current session이 있고 resume가 primary인 상황에서 above-the-fold 경쟁이 심할 때
|
- current session이 있고 review 데이터도 거의 없는 상황
|
||||||
|
|
||||||
이 경우:
|
이 경우:
|
||||||
|
|
||||||
- `/app` 메인 hero는 single-goal commitment에 집중
|
- `/app` 메인 hero는 single-goal commitment에 집중
|
||||||
- review teaser는 숨기거나 below-the-fold에 둔다
|
- resume 상태에서는 hero 아래 큰 teaser 대신, resume card 안의 조용한 secondary entry로 review를 연다
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
# 90. Current State
|
# 90. Current State
|
||||||
|
|
||||||
Last Updated: 2026-03-14
|
Last Updated: 2026-03-15
|
||||||
|
|
||||||
## DONE
|
## DONE
|
||||||
|
|
||||||
- `/app` single-goal commitment gate 재구성:
|
- `/app` single-goal commitment gate 재구성:
|
||||||
- 2-step `goal -> ritual` flow 제거
|
- 2-step `goal -> ritual` flow 제거
|
||||||
- current session이 있으면 `Resume` UI를 우선 노출하고, `/space`로 바로 이어가기만 제안
|
- current session이 있으면 `Resume` UI를 우선 노출하고, `/space`로 바로 이어가기만 제안하되 review entry는 조용한 secondary link로 유지
|
||||||
- current session이 없으면 `goal 1개 + optional microStep 1개 + primary CTA`만 남긴 direct start 구조로 단순화
|
- current session이 없으면 `goal 1개 + optional microStep 1개 + primary CTA`만 남긴 direct start 구조로 단순화
|
||||||
- `환경 세팅`, `블록 정리`, scene/sound/timer 선택을 메인 진입 경로에서 제거
|
- `환경 세팅`, `블록 정리`, scene/sound/timer 선택을 메인 진입 경로에서 제거
|
||||||
- suggestion chip은 planner가 아니라 입력 마찰을 줄이는 용도로만 유지
|
- suggestion chip은 planner가 아니라 입력 마찰을 줄이는 용도로만 유지
|
||||||
@@ -20,12 +20,12 @@ Last Updated: 2026-03-14
|
|||||||
- 한 번에 하나의 recovery tray만 열리도록 hierarchy를 고정
|
- 한 번에 하나의 recovery tray만 열리도록 hierarchy를 고정
|
||||||
- `/space` Refocus System slice 2 구현:
|
- `/space` Refocus System slice 2 구현:
|
||||||
- pause prompt의 `이대로 이어가기`가 실제 resume 동작으로 연결
|
- pause prompt의 `이대로 이어가기`가 실제 resume 동작으로 연결
|
||||||
- goal complete tray에 `여기까지 끝내기` 경로 추가
|
- goal complete tray에 `여기서 마무리하기` 경로 추가
|
||||||
- 현재 세션을 다음 목표 입력 없이도 정상 완료 처리할 수 있게 연결
|
- 현재 세션을 다음 목표 입력 없이도 정상 완료 처리할 수 있게 연결
|
||||||
- goal complete / rest / next-goal의 세 분기가 UI와 동작 모두에서 분리됨
|
- goal complete / rest / next-goal의 세 분기가 UI와 동작 모두에서 분리됨
|
||||||
- `/space` Refocus System slice 3 구현:
|
- `/space` Refocus System slice 3 구현:
|
||||||
- goal complete tray가 초기부터 input form을 강요하지 않도록 progressive disclosure 구조로 변경
|
- goal complete tray가 초기부터 input form을 강요하지 않도록 progressive disclosure 구조로 변경
|
||||||
- `여기까지 끝내기 / 잠깐 쉬기 / 다음 목표 이어가기`를 먼저 제안하고, 다음 목표 입력은 선택 시에만 펼쳐지게 정리
|
- `여기서 마무리하기 / 잠시 비우기 / 다음 목표 이어가기`를 먼저 제안하고, 다음 목표 입력은 선택 시에만 펼쳐지게 정리
|
||||||
- next-beat prompt에 현재 goal 문맥을 함께 보여주도록 보강
|
- next-beat prompt에 현재 goal 문맥을 함께 보여주도록 보강
|
||||||
- `/space` Refocus System slice 4 구현:
|
- `/space` Refocus System slice 4 구현:
|
||||||
- pause / next-beat / complete / refocus tray의 glass material, hairline, spacing을 공통 규칙으로 정리
|
- pause / next-beat / complete / refocus tray의 glass material, hairline, spacing을 공통 규칙으로 정리
|
||||||
@@ -37,7 +37,7 @@ Last Updated: 2026-03-14
|
|||||||
- 짧은 탭 전환에는 반응하지 않도록 hidden threshold를 둠
|
- 짧은 탭 전환에는 반응하지 않도록 hidden threshold를 둠
|
||||||
- 돌아왔을 때 focus가 아직 running이면 `Return` tray에서 `이어서 하기 / 한 조각 다시 잡기`를 제안
|
- 돌아왔을 때 focus가 아직 running이면 `Return` tray에서 `이어서 하기 / 한 조각 다시 잡기`를 제안
|
||||||
- 자리를 비운 사이 focus가 끝나 break phase가 되었으면 standard break 대신 `Return` tray를 먼저 띄움
|
- 자리를 비운 사이 focus가 끝나 break phase가 되었으면 standard break 대신 `Return` tray를 먼저 띄움
|
||||||
- 이 경우 `지금부터 쉬기 / 다음 목표 이어가기 / 한 조각 다시 잡기`를 선택할 수 있음
|
- 이 경우 `쉬기 이어가기 / 다음 목표 이어가기 / 한 조각 다시 잡기`를 선택할 수 있음
|
||||||
- `다음 목표 이어가기`는 `Goal Complete` next view로 바로 연결됨
|
- `다음 목표 이어가기`는 `Goal Complete` next view로 바로 연결됨
|
||||||
- `/space` Pause tray premium polish:
|
- `/space` Pause tray premium polish:
|
||||||
- tray 폭과 열림 높이를 키워 긴 한국어 카피가 잘리지 않게 조정
|
- tray 폭과 열림 높이를 키워 긴 한국어 카피가 잘리지 않게 조정
|
||||||
@@ -45,12 +45,12 @@ Last Updated: 2026-03-14
|
|||||||
- option row spacing, radius, chevron 위치를 보정해 급조된 버튼 묶음 느낌을 완화
|
- option row spacing, radius, chevron 위치를 보정해 급조된 버튼 묶음 느낌을 완화
|
||||||
- `/space` Pause / Break / Return tone 분리 1차 구현:
|
- `/space` Pause / Break / Return tone 분리 1차 구현:
|
||||||
- `Return(focus)`와 `Return(break)`가 같은 tray처럼 보이지 않도록 break tray에 emerald tint release tone 도입
|
- `Return(focus)`와 `Return(break)`가 같은 tray처럼 보이지 않도록 break tray에 emerald tint release tone 도입
|
||||||
- `Goal Complete`의 `잠깐 쉬기` 선택도 같은 break 계열 material로 연결
|
- `Goal Complete`의 `잠시 비우기` 선택도 같은 break 계열 material로 연결
|
||||||
- timer HUD는 break phase에서 더 가벼운 emerald 계열 glass로 보정해 focus/pause와 구분되게 조정
|
- timer HUD는 break phase에서 더 가벼운 emerald 계열 glass로 보정해 focus/pause와 구분되게 조정
|
||||||
- `/space` Pause / Break / Return copy + interaction polish:
|
- `/space` Pause / Break / Return copy + interaction polish:
|
||||||
- `Pause`는 `멈춘 이유` 대신 `다시 시작할 한 줄`을 중심으로 카피를 다시 정리
|
- `Pause`는 `멈춘 이유` 대신 `다시 시작할 한 줄`을 중심으로 카피를 다시 정리
|
||||||
- `Return(focus)`는 `멈춘 자리에서 이어가기`, `Return(break)`는 `지금부터 쉬기 / 다음 블록 이어가기` 중심으로 재서술
|
- `Return(focus)`는 `멈춘 자리에서 이어가기`, `Return(break)`는 `쉬기 이어가기 / 다음 블록 이어가기` 중심으로 재서술
|
||||||
- `Goal Complete`는 `다음 블록 이어가기 / 잠깐 쉬기 / 여기까지 끝내기` 순의 선택 tray를 먼저 보여주고, 다음 블록 입력은 이후 단계에서만 열리게 정리
|
- `Goal Complete`는 `다음 블록 이어가기 / 잠시 비우기 / 여기서 마무리하기` 순의 선택 tray를 먼저 보여주고, 다음 블록 입력은 이후 단계에서만 열리게 정리
|
||||||
- choice/next view의 헤더와 설명도 각각 다른 감정 상태에 맞춰 분리
|
- choice/next view의 헤더와 설명도 각각 다른 감정 상태에 맞춰 분리
|
||||||
- `/space` Pause / Break / Return motion polish 1차 구현:
|
- `/space` Pause / Break / Return motion polish 1차 구현:
|
||||||
- `Pause` tray는 빠르게 다시 붙잡는 recovery reveal로 조정
|
- `Pause` tray는 빠르게 다시 붙잡는 recovery reveal로 조정
|
||||||
@@ -127,7 +127,7 @@ Last Updated: 2026-03-14
|
|||||||
- `/app` teaser와 review return hint도 Pro에서 더 구체적인 next-session handoff 톤으로 표시된다
|
- `/app` teaser와 review return hint도 Pro에서 더 구체적인 next-session handoff 톤으로 표시된다
|
||||||
- `/space` secondary review teaser 4차 연결:
|
- `/space` secondary review teaser 4차 연결:
|
||||||
- goal complete로 setup 상태로 돌아왔을 때만 setup drawer 아래에 low-emphasis review teaser가 보인다
|
- goal complete로 setup 상태로 돌아왔을 때만 setup drawer 아래에 low-emphasis review teaser가 보인다
|
||||||
- teaser는 `주간 review 보기`로 `/stats?review=weekly&origin=space-complete`를 연다
|
- teaser는 `주간 review 보기`로 `/stats`를 열고, 방금 끝낸 흐름 반영을 과장하지 않는 카피만 사용한다
|
||||||
- 다시 시작하거나 dismiss하면 사라지며, live execution 중에는 보이지 않는다
|
- 다시 시작하거나 dismiss하면 사라지며, live execution 중에는 보이지 않는다
|
||||||
- paywall / plan / landing 메시지 재정렬:
|
- paywall / plan / landing 메시지 재정렬:
|
||||||
- paywall 가치 포인트를 multi-queue, rituals, weekly review 중심으로 재작성
|
- paywall 가치 포인트를 multi-queue, rituals, weekly review 중심으로 재작성
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Session Brief
|
# Session Brief
|
||||||
|
|
||||||
Last Updated: 2026-03-14
|
Last Updated: 2026-03-15
|
||||||
|
|
||||||
세션 시작 시 항상 읽는 초소형 스냅샷 문서.
|
세션 시작 시 항상 읽는 초소형 스냅샷 문서.
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ Last Updated: 2026-03-14
|
|||||||
- microStep 완료 후에는 checklist가 아니라 `다음 한 조각이 있나요?` prompt로만 이어진다.
|
- microStep 완료 후에는 checklist가 아니라 `다음 한 조각이 있나요?` prompt로만 이어진다.
|
||||||
- recovery UI는 `paused / refocus / next-beat / complete` 중 하나만 열리도록 단일 overlay 상태로 묶였다.
|
- recovery UI는 `paused / refocus / next-beat / complete` 중 하나만 열리도록 단일 overlay 상태로 묶였다.
|
||||||
- `/space` goal complete 종료 경로를 복구했다.
|
- `/space` goal complete 종료 경로를 복구했다.
|
||||||
- `여기까지 끝내기`로 현재 목표를 다음 목표 입력 없이도 정상 완료 처리할 수 있다.
|
- `여기서 마무리하기`로 현재 목표를 다음 목표 입력 없이도 정상 완료 처리할 수 있다.
|
||||||
- pause prompt의 `이대로 이어가기`는 단순 닫기가 아니라 실제 resume으로 연결된다.
|
- pause prompt의 `이대로 이어가기`는 단순 닫기가 아니라 실제 resume으로 연결된다.
|
||||||
- `/space` goal complete / next beat를 덜 form스럽게 정리했다.
|
- `/space` goal complete / next beat를 덜 form스럽게 정리했다.
|
||||||
- goal complete는 처음부터 input을 요구하지 않고, 선택지를 먼저 보여준 뒤 `다음 목표 이어가기`를 선택했을 때만 입력이 열린다.
|
- goal complete는 처음부터 input을 요구하지 않고, 선택지를 먼저 보여준 뒤 `다음 목표 이어가기`를 선택했을 때만 입력이 열린다.
|
||||||
@@ -35,14 +35,14 @@ Last Updated: 2026-03-14
|
|||||||
- `/space` recovery tray material과 선택 위계를 같은 패밀리로 맞추기 시작했다.
|
- `/space` recovery tray material과 선택 위계를 같은 패밀리로 맞추기 시작했다.
|
||||||
- pause / next-beat / complete tray가 공통 dark-glass shell을 공유한다.
|
- pause / next-beat / complete tray가 공통 dark-glass shell을 공유한다.
|
||||||
- inline 링크 중심이던 선택지를 quiet option row 구조로 바꿔, checklist보다 recovery decision처럼 읽히게 정리했다.
|
- inline 링크 중심이던 선택지를 quiet option row 구조로 바꿔, checklist보다 recovery decision처럼 읽히게 정리했다.
|
||||||
- `Goal Complete`는 `여기까지 끝내기 / 잠깐 쉬기 / 다음 목표 이어가기`를 같은 tray 안의 선택 행으로 제시한다.
|
- `Goal Complete`는 `다음 블록 이어가기 / 잠시 비우기 / 여기서 마무리하기`를 같은 tray 안의 선택 행으로 제시한다.
|
||||||
- `Refocus`는 같은 shell 안에서 field / action 톤을 통일해 다른 tray와 같은 제품군처럼 보이게 맞추는 중이다.
|
- `Refocus`는 같은 shell 안에서 field / action 톤을 통일해 다른 tray와 같은 제품군처럼 보이게 맞추는 중이다.
|
||||||
- `/space` Away / Return Recovery를 구현했다.
|
- `/space` Away / Return Recovery를 구현했다.
|
||||||
- `visibilitychange`, `pagehide`, sleep/wake gap 기반 감지를 추가했다.
|
- `visibilitychange`, `pagehide`, sleep/wake gap 기반 감지를 추가했다.
|
||||||
- 짧은 탭 전환에는 반응하지 않도록 hidden threshold를 둬 오탐을 줄였다.
|
- 짧은 탭 전환에는 반응하지 않도록 hidden threshold를 둬 오탐을 줄였다.
|
||||||
- 돌아왔을 때 focus가 계속 running이면 `Return` tray가 `이어서 하기 / 한 조각 다시 잡기`를 제안한다.
|
- 돌아왔을 때 focus가 계속 running이면 `Return` tray가 `이어서 하기 / 한 조각 다시 잡기`를 제안한다.
|
||||||
- 자리를 비운 사이 focus가 끝나 break phase가 되었으면 standard break 대신 `Return` tray가 먼저 뜬다.
|
- 자리를 비운 사이 focus가 끝나 break phase가 되었으면 standard break 대신 `Return` tray가 먼저 뜬다.
|
||||||
- 이 경우 `지금부터 쉬기 / 다음 목표 이어가기 / 한 조각 다시 잡기` 중 하나를 고를 수 있다.
|
- 이 경우 `쉬기 이어가기 / 다음 목표 이어가기 / 한 조각 다시 잡기` 중 하나를 고를 수 있다.
|
||||||
- `다음 목표 이어가기`는 goal complete next view로 바로 연결된다.
|
- `다음 목표 이어가기`는 goal complete next view로 바로 연결된다.
|
||||||
- pause tray의 visual polish를 진행했다.
|
- pause tray의 visual polish를 진행했다.
|
||||||
- tray 폭과 max-height를 늘려 한국어 제목/설명 잘림을 줄였다.
|
- tray 폭과 max-height를 늘려 한국어 제목/설명 잘림을 줄였다.
|
||||||
@@ -50,12 +50,12 @@ Last Updated: 2026-03-14
|
|||||||
- option row의 radius, padding, chevron 정렬을 보정해 더 차분한 recovery panel처럼 읽히게 했다.
|
- option row의 radius, padding, chevron 정렬을 보정해 더 차분한 recovery panel처럼 읽히게 했다.
|
||||||
- `Pause / Break / Return`의 감정 톤 분리를 시작했다.
|
- `Pause / Break / Return`의 감정 톤 분리를 시작했다.
|
||||||
- `Return(break)`은 focus 복귀 tray와 같은 재질을 쓰지 않고, 더 부드러운 emerald tint release tone으로 분리했다.
|
- `Return(break)`은 focus 복귀 tray와 같은 재질을 쓰지 않고, 더 부드러운 emerald tint release tone으로 분리했다.
|
||||||
- `Goal Complete`의 `잠깐 쉬기` 선택도 같은 release tone으로 연결했다.
|
- `Goal Complete`의 `잠시 비우기` 선택도 같은 release tone으로 연결했다.
|
||||||
- timer HUD도 break phase에서는 더 가벼운 emerald 계열 material로 바뀌어 pause/focus와 다르게 읽히도록 정리 중이다.
|
- timer HUD도 break phase에서는 더 가벼운 emerald 계열 material로 바뀌어 pause/focus와 다르게 읽히도록 정리 중이다.
|
||||||
- `Pause / Break / Return`의 카피와 CTA 위계를 2차로 분리했다.
|
- `Pause / Break / Return`의 카피와 CTA 위계를 2차로 분리했다.
|
||||||
- `Pause`는 `멈춘 이유`보다 `다시 시작할 한 줄`에 집중하는 recovery tone으로 다시 썼다.
|
- `Pause`는 `멈춘 이유`보다 `다시 시작할 한 줄`에 집중하는 recovery tone으로 다시 썼다.
|
||||||
- `Return(focus)`는 `이어가기`, `Return(break)`는 `지금부터 쉬기 / 다음 블록 이어가기` 중심으로 문구를 분리했다.
|
- `Return(focus)`는 `이어가기`, `Return(break)`는 `쉬기 이어가기 / 다음 블록 이어가기` 중심으로 문구를 분리했다.
|
||||||
- `Goal Complete`는 `마무리 / 쉬기 / 이어가기`의 선택 tray가 먼저 보이고, 다음 블록 입력은 이후 단계에서만 열리도록 더 선명해졌다.
|
- `Goal Complete`는 `이어가기 / 잠시 비우기 / 마무리하기`의 선택 tray가 먼저 보이고, 다음 블록 입력은 이후 단계에서만 열리도록 더 선명해졌다.
|
||||||
- `Pause / Break / Return`의 motion polish 1차를 반영했다.
|
- `Pause / Break / Return`의 motion polish 1차를 반영했다.
|
||||||
- `Pause`는 빠르게 다시 붙잡는 recovery reveal로,
|
- `Pause`는 빠르게 다시 붙잡는 recovery reveal로,
|
||||||
- `Return(focus)`는 재진입에 맞는 짧은 settle motion으로,
|
- `Return(focus)`는 재진입에 맞는 짧은 settle motion으로,
|
||||||
@@ -86,6 +86,8 @@ Last Updated: 2026-03-14
|
|||||||
- `/app`에서 `/stats`로 들어가는 primary path 1차가 생겼다.
|
- `/app`에서 `/stats`로 들어가는 primary path 1차가 생겼다.
|
||||||
- current session이 없고 최근 7일 데이터가 충분하면 hero 아래에 weekly review teaser가 보인다.
|
- current session이 없고 최근 7일 데이터가 충분하면 hero 아래에 weekly review teaser가 보인다.
|
||||||
- teaser는 `/stats`로 이동시키되, main start CTA보다 낮은 강조로 유지한다.
|
- teaser는 `/stats`로 이동시키되, main start CTA보다 낮은 강조로 유지한다.
|
||||||
|
- `/app` Resume 상태에서도 weekly review entry가 보이게 정리했다.
|
||||||
|
- review primary entry가 active session 상태에서 사라지지 않도록, resume card 안에 조용한 secondary review link를 추가했다.
|
||||||
- `/stats` 마지막 CTA의 `/app` return handoff가 연결됐다.
|
- `/stats` 마지막 CTA의 `/app` return handoff가 연결됐다.
|
||||||
- carry-forward CTA는 `/app?review=weekly&carryHint=...`로 돌아온다.
|
- carry-forward CTA는 `/app?review=weekly&carryHint=...`로 돌아온다.
|
||||||
- `/app`은 review-aware return hint를 먼저 보여주되, goal은 사용자가 직접 입력하게 유지한다.
|
- `/app`은 review-aware return hint를 먼저 보여주되, goal은 사용자가 직접 입력하게 유지한다.
|
||||||
@@ -95,6 +97,7 @@ Last Updated: 2026-03-14
|
|||||||
- `/space` complete 이후 secondary review teaser까지 연결됐다.
|
- `/space` complete 이후 secondary review teaser까지 연결됐다.
|
||||||
- goal complete로 setup 상태로 돌아왔을 때만 setup drawer 아래에 작은 review teaser가 보인다.
|
- goal complete로 setup 상태로 돌아왔을 때만 setup drawer 아래에 작은 review teaser가 보인다.
|
||||||
- full review 강제 이동 없이 `/stats`를 여는 secondary entry로만 동작한다.
|
- full review 강제 이동 없이 `/stats`를 여는 secondary entry로만 동작한다.
|
||||||
|
- 방금 끝낸 흐름을 반영한다고 과장하지 않는 카피로 정리했다.
|
||||||
- 다음 구현은 weekly review의 실제 recovery 집계 연결이다.
|
- 다음 구현은 weekly review의 실제 recovery 집계 연결이다.
|
||||||
- 유료화 포지셔닝을 `Calm Session OS`로 재정의했다.
|
- 유료화 포지셔닝을 `Calm Session OS`로 재정의했다.
|
||||||
- Free는 기본 집중 시작, Pro는 더 잘 이어가기라는 메시지로 정리했다.
|
- Free는 기본 집중 시작, Pro는 더 잘 이어가기라는 메시지로 정리했다.
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export const app = {
|
|||||||
reviewCarryTryClosure: '시작은 있었지만 마무리가 약했어요. 다음 주에는 완료 직전에 다른 블록으로 넘어가지 않는 흐름을 한 번 만들어 보세요.',
|
reviewCarryTryClosure: '시작은 있었지만 마무리가 약했어요. 다음 주에는 완료 직전에 다른 블록으로 넘어가지 않는 흐름을 한 번 만들어 보세요.',
|
||||||
reviewCarryTryStart: '시작 횟수가 적었어요. 다음 주에는 길이를 늘리기보다 첫 세션을 한 번 더 여는 것에 집중해 보세요.',
|
reviewCarryTryStart: '시작 횟수가 적었어요. 다음 주에는 길이를 늘리기보다 첫 세션을 한 번 더 여는 것에 집중해 보세요.',
|
||||||
reviewCarryCta: '이 흐름으로 다음 세션 시작',
|
reviewCarryCta: '이 흐름으로 다음 세션 시작',
|
||||||
reviewCarryCtaPro: '가장 잘 맞은 ritual로 /app 돌아가기',
|
reviewCarryCtaPro: '추천 ritual과 함께 /app 돌아가기',
|
||||||
reviewCarryKeepTitle: '다음 주에 유지할 것',
|
reviewCarryKeepTitle: '다음 주에 유지할 것',
|
||||||
reviewCarryTryTitle: '다음 주에 바꿔볼 것',
|
reviewCarryTryTitle: '다음 주에 바꿔볼 것',
|
||||||
reviewCarryPresetLabel: '추천 ritual',
|
reviewCarryPresetLabel: '추천 ritual',
|
||||||
|
|||||||
@@ -17,10 +17,10 @@ export const space = {
|
|||||||
timerLabel: '타이머',
|
timerLabel: '타이머',
|
||||||
soundLabel: '사운드',
|
soundLabel: '사운드',
|
||||||
reviewTeaserEyebrow: 'Weekly Review',
|
reviewTeaserEyebrow: 'Weekly Review',
|
||||||
reviewTeaserTitle: '방금 끝낸 흐름까지 review에 담아둘까요?',
|
reviewTeaserTitle: '이번 주 review를 다시 볼까요?',
|
||||||
reviewTeaserTitlePro: '방금 끝낸 흐름까지 포함해 이번 주 리듬을 다시 볼까요?',
|
reviewTeaserTitlePro: '이번 주 흐름과 잘 맞았던 ritual을 다시 볼까요?',
|
||||||
reviewTeaserHelper: '지금은 바로 다시 시작해도 괜찮고, 원하면 주간 review를 잠깐 보고 갈 수 있어요.',
|
reviewTeaserHelper: '지금은 바로 다시 시작해도 괜찮고, 원하면 주간 review를 잠깐 보고 갈 수 있어요.',
|
||||||
reviewTeaserHelperPro: '방금 마친 흐름과 가장 잘 맞는 ritual을 같이 보고 다음 세션으로 이어갈 수 있어요.',
|
reviewTeaserHelperPro: '원하면 이번 주 흐름과 추천 ritual을 다시 보고 다음 세션으로 이어갈 수 있어요.',
|
||||||
reviewTeaserCta: '주간 review 보기',
|
reviewTeaserCta: '주간 review 보기',
|
||||||
reviewTeaserDismiss: '나중에',
|
reviewTeaserDismiss: '나중에',
|
||||||
readyHint: '목표를 적으면 시작할 수 있어요.',
|
readyHint: '목표를 적으면 시작할 수 있어요.',
|
||||||
@@ -65,12 +65,12 @@ export const space = {
|
|||||||
returnPromptEyebrow: '다시 돌아왔어요',
|
returnPromptEyebrow: '다시 돌아왔어요',
|
||||||
returnPromptFocusTitle: '흐름은 아직 남아 있어요.',
|
returnPromptFocusTitle: '흐름은 아직 남아 있어요.',
|
||||||
returnPromptFocusDescription: '멈춘 자리에서 바로 이어가거나, 다시 시작할 한 조각만 조용히 다듬을 수 있어요.',
|
returnPromptFocusDescription: '멈춘 자리에서 바로 이어가거나, 다시 시작할 한 조각만 조용히 다듬을 수 있어요.',
|
||||||
returnPromptBreakTitle: '자리를 비운 사이 이 블록이 끝났어요.',
|
returnPromptBreakTitle: '자리를 비운 사이 쉬는 시간이 시작됐어요.',
|
||||||
returnPromptBreakDescription: '지금부터 쉬거나, 다음 블록으로 부드럽게 넘어갈 수 있어요.',
|
returnPromptBreakDescription: 'break를 그대로 이어가거나, 다음 블록으로 부드럽게 넘어갈 수 있어요.',
|
||||||
returnPromptContinue: '멈춘 자리에서 이어가기',
|
returnPromptContinue: '멈춘 자리에서 이어가기',
|
||||||
returnPromptContinueHint: '타이머와 현재 흐름을 그대로 두고 다시 집중으로 복귀합니다.',
|
returnPromptContinueHint: '타이머와 현재 흐름을 그대로 두고 다시 집중으로 복귀합니다.',
|
||||||
returnPromptRest: '지금부터 쉬기',
|
returnPromptRest: '쉬기 이어가기',
|
||||||
returnPromptRestHint: '지금부터 break를 시작한 것처럼 천천히 숨을 고릅니다.',
|
returnPromptRestHint: '이미 시작된 break를 그대로 두고, 조금 더 천천히 숨을 고릅니다.',
|
||||||
returnPromptNext: '다음 블록 이어가기',
|
returnPromptNext: '다음 블록 이어가기',
|
||||||
returnPromptNextHint: '다음 한 조각을 정하고, 같은 흐름 안에서 부드럽게 이어갑니다.',
|
returnPromptNextHint: '다음 한 조각을 정하고, 같은 흐름 안에서 부드럽게 이어갑니다.',
|
||||||
returnPromptRefocus: '한 조각 다시 잡기',
|
returnPromptRefocus: '한 조각 다시 잡기',
|
||||||
@@ -90,8 +90,8 @@ export const space = {
|
|||||||
suggestions: ['리뷰 코멘트 2개 처리', '문서 1문단 다듬기', '이슈 1개 정리', '메일 2개 회신'],
|
suggestions: ['리뷰 코멘트 2개 처리', '문서 1문단 다듬기', '이슈 1개 정리', '메일 2개 회신'],
|
||||||
placeholderFallback: '다음 한 조각을 적어보세요',
|
placeholderFallback: '다음 한 조각을 적어보세요',
|
||||||
placeholderExample: (goal: string) => `예: ${goal}`,
|
placeholderExample: (goal: string) => `예: ${goal}`,
|
||||||
title: '이 블록을 어떻게 닫을까요?',
|
title: '이 블록을 어떻게 이어갈까요?',
|
||||||
description: '지금은 끝내기, 쉬기, 이어가기 중 하나만 고르면 돼요.',
|
description: '다음으로 이어가기, 잠시 비우기, 여기서 마무리하기 중 하나만 고르면 돼요.',
|
||||||
nextTitle: '좋아요. 다음 한 조각만 정해요.',
|
nextTitle: '좋아요. 다음 한 조각만 정해요.',
|
||||||
nextDescription: '너무 크게 잡지 말고, 바로 손을 올릴 한 줄만 남겨요.',
|
nextDescription: '너무 크게 잡지 말고, 바로 손을 올릴 한 줄만 남겨요.',
|
||||||
currentGoalLabel: '방금 끝낸 블록',
|
currentGoalLabel: '방금 끝낸 블록',
|
||||||
@@ -100,10 +100,10 @@ export const space = {
|
|||||||
chooseNextDescription: '다음 한 조각을 정하고 같은 흐름 안에서 계속 갑니다.',
|
chooseNextDescription: '다음 한 조각을 정하고 같은 흐름 안에서 계속 갑니다.',
|
||||||
backButton: '돌아가기',
|
backButton: '돌아가기',
|
||||||
closeAriaLabel: '닫기',
|
closeAriaLabel: '닫기',
|
||||||
finishButton: '여기까지 끝내기',
|
finishButton: '여기서 마무리하기',
|
||||||
finishDescription: '이 블록은 여기서 닫고, 다음 진입은 가볍게 남겨둡니다.',
|
finishDescription: '이 블록은 여기서 닫고, 다음 진입은 가볍게 남겨둡니다.',
|
||||||
restButton: '잠깐 쉬기',
|
restButton: '잠시 비우기',
|
||||||
restDescription: '이 블록은 닫고, 지금부터는 잠깐 쉬는 리듬으로 넘어갑니다.',
|
restDescription: '이 블록은 아직 닫지 않고, 잠깐 멈춘 뒤 돌아오라고 알려드려요.',
|
||||||
confirmButton: '다음 목표로 바로 시작',
|
confirmButton: '다음 목표로 바로 시작',
|
||||||
confirmPending: '시작 중…',
|
confirmPending: '시작 중…',
|
||||||
finishPending: '마무리 중…',
|
finishPending: '마무리 중…',
|
||||||
|
|||||||
@@ -17,6 +17,14 @@ import { cn } from '@/shared/lib/cn';
|
|||||||
const DEFAULT_SCENE_ID = getSceneById('forest')?.id ?? SCENE_THEMES[0].id;
|
const DEFAULT_SCENE_ID = getSceneById('forest')?.id ?? SCENE_THEMES[0].id;
|
||||||
const DEFAULT_SOUND_ID = SOUND_PRESETS.find((preset) => preset.id === 'forest-birds')?.id ?? SOUND_PRESETS[0].id;
|
const DEFAULT_SOUND_ID = SOUND_PRESETS.find((preset) => preset.id === 'forest-birds')?.id ?? SOUND_PRESETS[0].id;
|
||||||
const DEFAULT_TIMER_ID = '50-10';
|
const DEFAULT_TIMER_ID = '50-10';
|
||||||
|
const REVIEW_ENTRY_PRESETS = {
|
||||||
|
'forest-50-10': {
|
||||||
|
sceneId: DEFAULT_SCENE_ID,
|
||||||
|
soundPresetId: DEFAULT_SOUND_ID,
|
||||||
|
timerPresetId: DEFAULT_TIMER_ID,
|
||||||
|
label: '숲 · 50/10 · Forest Birds',
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
const GOAL_SUGGESTIONS = copy.session.goalChips.slice(0, 4);
|
const GOAL_SUGGESTIONS = copy.session.goalChips.slice(0, 4);
|
||||||
|
|
||||||
const entryCopy = {
|
const entryCopy = {
|
||||||
@@ -45,6 +53,9 @@ const entryCopy = {
|
|||||||
reviewTitlePro: '나에게 잘 맞았던 흐름을 다시 보고 갈까요?',
|
reviewTitlePro: '나에게 잘 맞았던 흐름을 다시 보고 갈까요?',
|
||||||
reviewCtaPro: '나에게 맞는 흐름 보기',
|
reviewCtaPro: '나에게 맞는 흐름 보기',
|
||||||
reviewHelperPro: '가장 잘 맞았던 ritual과 carry-forward를 보고 돌아올 수 있어요.',
|
reviewHelperPro: '가장 잘 맞았던 ritual과 carry-forward를 보고 돌아올 수 있어요.',
|
||||||
|
resumeReviewEyebrow: 'Weekly Review',
|
||||||
|
resumeReviewTitle: '잠깐 review를 보고 다시 들어갈 수 있어요.',
|
||||||
|
resumeReviewHelper: '현재 세션은 그대로 두고, 이번 주 흐름만 짧게 확인합니다.',
|
||||||
reviewReturnEyebrow: '방금 본 review 기준',
|
reviewReturnEyebrow: '방금 본 review 기준',
|
||||||
reviewReturnTitleSteady: '이번 주에 잘 맞았던 흐름을 그대로 가져가 보세요.',
|
reviewReturnTitleSteady: '이번 주에 잘 맞았던 흐름을 그대로 가져가 보세요.',
|
||||||
reviewReturnTitleSmaller: '이번엔 목표를 더 작게 잡아보세요.',
|
reviewReturnTitleSmaller: '이번엔 목표를 더 작게 잡아보세요.',
|
||||||
@@ -109,6 +120,15 @@ export const FocusDashboardWidget = () => {
|
|||||||
const { sceneAssetMap } = useMediaCatalog();
|
const { sceneAssetMap } = useMediaCatalog();
|
||||||
const { review, summary: weeklySummary } = useFocusStats();
|
const { review, summary: weeklySummary } = useFocusStats();
|
||||||
|
|
||||||
|
const reviewEntryPreset = searchParams.get('entryPreset');
|
||||||
|
const reviewEntryPresetConfig = useMemo(() => {
|
||||||
|
if (!reviewEntryPreset) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return REVIEW_ENTRY_PRESETS[reviewEntryPreset as keyof typeof REVIEW_ENTRY_PRESETS] ?? null;
|
||||||
|
}, [reviewEntryPreset]);
|
||||||
|
|
||||||
const [goalDraft, setGoalDraft] = useState('');
|
const [goalDraft, setGoalDraft] = useState('');
|
||||||
const [microStepDraft, setMicroStepDraft] = useState('');
|
const [microStepDraft, setMicroStepDraft] = useState('');
|
||||||
const [isStartingSession, setIsStartingSession] = useState(false);
|
const [isStartingSession, setIsStartingSession] = useState(false);
|
||||||
@@ -120,8 +140,8 @@ export const FocusDashboardWidget = () => {
|
|||||||
const goalInputRef = useRef<HTMLInputElement | null>(null);
|
const goalInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
const activeScene = useMemo(() => {
|
const activeScene = useMemo(() => {
|
||||||
return getSceneById(currentSession?.sceneId ?? DEFAULT_SCENE_ID) ?? SCENE_THEMES[0];
|
return getSceneById(currentSession?.sceneId ?? reviewEntryPresetConfig?.sceneId ?? DEFAULT_SCENE_ID) ?? SCENE_THEMES[0];
|
||||||
}, [currentSession?.sceneId]);
|
}, [currentSession?.sceneId, reviewEntryPresetConfig?.sceneId]);
|
||||||
|
|
||||||
const activeRitualMeta = useMemo(() => {
|
const activeRitualMeta = useMemo(() => {
|
||||||
const timerLabel = timerLabelById[currentSession?.timerPresetId ?? DEFAULT_TIMER_ID] ?? '50/10';
|
const timerLabel = timerLabelById[currentSession?.timerPresetId ?? DEFAULT_TIMER_ID] ?? '50/10';
|
||||||
@@ -138,7 +158,6 @@ export const FocusDashboardWidget = () => {
|
|||||||
review.recoveryQuality.availability === 'ready');
|
review.recoveryQuality.availability === 'ready');
|
||||||
const reviewSource = searchParams.get('review');
|
const reviewSource = searchParams.get('review');
|
||||||
const reviewCarryHint = searchParams.get('carryHint');
|
const reviewCarryHint = searchParams.get('carryHint');
|
||||||
const reviewEntryPreset = searchParams.get('entryPreset');
|
|
||||||
const normalizedReviewCarryHint: ReviewCarryHint | null =
|
const normalizedReviewCarryHint: ReviewCarryHint | null =
|
||||||
reviewCarryHint === 'steady' ||
|
reviewCarryHint === 'steady' ||
|
||||||
reviewCarryHint === 'smaller' ||
|
reviewCarryHint === 'smaller' ||
|
||||||
@@ -151,11 +170,12 @@ export const FocusDashboardWidget = () => {
|
|||||||
const reviewReturnCopy =
|
const reviewReturnCopy =
|
||||||
normalizedReviewCarryHint !== null ? reviewCarryCopyByHint[normalizedReviewCarryHint] : null;
|
normalizedReviewCarryHint !== null ? reviewCarryCopyByHint[normalizedReviewCarryHint] : null;
|
||||||
const reviewReturnRitualLabel =
|
const reviewReturnRitualLabel =
|
||||||
isPro && reviewEntryPreset === 'forest-50-10' ? entryCopy.reviewReturnRitualLabel : null;
|
isPro && reviewEntryPresetConfig ? `추천 ritual · ${reviewEntryPresetConfig.label}` : null;
|
||||||
const reviewTeaserTitle = isPro ? entryCopy.reviewTitlePro : entryCopy.reviewTitle;
|
const reviewTeaserTitle = isPro ? entryCopy.reviewTitlePro : entryCopy.reviewTitle;
|
||||||
const reviewTeaserSummary = isPro ? review.carryForward.keepDoing : review.snapshotSummary;
|
const reviewTeaserSummary = isPro ? review.carryForward.keepDoing : review.snapshotSummary;
|
||||||
const reviewTeaserHelper = isPro ? entryCopy.reviewHelperPro : entryCopy.reviewHelper;
|
const reviewTeaserHelper = isPro ? entryCopy.reviewHelperPro : entryCopy.reviewHelper;
|
||||||
const reviewTeaserCta = isPro ? entryCopy.reviewCtaPro : entryCopy.reviewCta;
|
const reviewTeaserCta = isPro ? entryCopy.reviewCtaPro : entryCopy.reviewCta;
|
||||||
|
const entryRitualHint = reviewEntryPresetConfig ? `추천 ritual · ${reviewEntryPresetConfig.label}` : entryCopy.ritualHint;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
@@ -215,9 +235,9 @@ export const FocusDashboardWidget = () => {
|
|||||||
await focusSessionApi.startSession({
|
await focusSessionApi.startSession({
|
||||||
goal: trimmedGoal,
|
goal: trimmedGoal,
|
||||||
microStep: microStepDraft.trim() || null,
|
microStep: microStepDraft.trim() || null,
|
||||||
sceneId: DEFAULT_SCENE_ID,
|
sceneId: reviewEntryPresetConfig?.sceneId ?? DEFAULT_SCENE_ID,
|
||||||
soundPresetId: DEFAULT_SOUND_ID,
|
soundPresetId: reviewEntryPresetConfig?.soundPresetId ?? DEFAULT_SOUND_ID,
|
||||||
timerPresetId: DEFAULT_TIMER_ID,
|
timerPresetId: reviewEntryPresetConfig?.timerPresetId ?? DEFAULT_TIMER_ID,
|
||||||
entryPoint: 'space-setup',
|
entryPoint: 'space-setup',
|
||||||
});
|
});
|
||||||
router.push('/space');
|
router.push('/space');
|
||||||
@@ -235,6 +255,8 @@ export const FocusDashboardWidget = () => {
|
|||||||
|
|
||||||
const shouldShowWeeklyReviewTeaser =
|
const shouldShowWeeklyReviewTeaser =
|
||||||
!isCheckingSession && !currentSession && hasEnoughWeeklyData && !isReviewReturn;
|
!isCheckingSession && !currentSession && hasEnoughWeeklyData && !isReviewReturn;
|
||||||
|
const shouldShowResumeReviewEntry =
|
||||||
|
!isCheckingSession && Boolean(currentSession) && hasEnoughWeeklyData;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative min-h-dvh overflow-hidden bg-slate-950 text-white selection:bg-white/20">
|
<div className="relative min-h-dvh overflow-hidden bg-slate-950 text-white selection:bg-white/20">
|
||||||
@@ -264,37 +286,6 @@ export const FocusDashboardWidget = () => {
|
|||||||
</p>
|
</p>
|
||||||
<p className="text-[15px] text-white/72">세션 상태를 불러오는 중이에요.</p>
|
<p className="text-[15px] text-white/72">세션 상태를 불러오는 중이에요.</p>
|
||||||
</div>
|
</div>
|
||||||
) : currentSession ? (
|
|
||||||
<div className={cn(goalCardClass, 'space-y-5')}>
|
|
||||||
<div className="space-y-3">
|
|
||||||
<p className="text-[11px] font-medium uppercase tracking-[0.18em] text-white/46">
|
|
||||||
{entryCopy.resumeEyebrow}
|
|
||||||
</p>
|
|
||||||
<h1 className="text-[1.8rem] font-light leading-[1.14] tracking-[-0.03em] text-white md:text-[2.2rem]">
|
|
||||||
{currentSession.goal}
|
|
||||||
</h1>
|
|
||||||
<p className="text-sm text-white/68">
|
|
||||||
{currentSession.state === 'paused' ? entryCopy.resumePaused : entryCopy.resumeRunning}
|
|
||||||
</p>
|
|
||||||
{currentSession.microStep ? (
|
|
||||||
<div className="rounded-[1.1rem] border border-white/10 bg-white/[0.04] px-4 py-3">
|
|
||||||
<p className="mb-1 text-[11px] font-medium uppercase tracking-[0.16em] text-white/44">
|
|
||||||
{entryCopy.resumeMicroStepLabel}
|
|
||||||
</p>
|
|
||||||
<p className="text-[15px] text-white/82">{currentSession.microStep}</p>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
||||||
<button type="button" onClick={handleResumeSession} className={primaryButtonClass}>
|
|
||||||
{entryCopy.resumeCta}
|
|
||||||
</button>
|
|
||||||
<p className="text-xs text-white/48 sm:text-right">{activeRitualMeta}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="text-sm text-white/56">{entryCopy.resumeNewGoalHint}</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{reviewReturnCopy ? (
|
{reviewReturnCopy ? (
|
||||||
@@ -314,129 +305,188 @@ export const FocusDashboardWidget = () => {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className={goalCardClass}>
|
{currentSession ? (
|
||||||
<div className="space-y-3 text-center">
|
<div className={cn(goalCardClass, 'space-y-5')}>
|
||||||
<h1 className="text-[2rem] font-light leading-[1.08] tracking-[-0.04em] text-white md:text-[2.9rem]">
|
<div className="space-y-3">
|
||||||
{entryCopy.title}
|
<p className="text-[11px] font-medium uppercase tracking-[0.18em] text-white/46">
|
||||||
</h1>
|
{entryCopy.resumeEyebrow}
|
||||||
<p className="mx-auto max-w-[32rem] text-[15px] leading-6 text-white/70 md:text-base">
|
|
||||||
{entryCopy.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-8 space-y-4">
|
|
||||||
<label className="block">
|
|
||||||
<span className="sr-only">Goal</span>
|
|
||||||
<input
|
|
||||||
ref={goalInputRef}
|
|
||||||
value={goalDraft}
|
|
||||||
onChange={(event) => setGoalDraft(event.target.value)}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.key === 'Enter' && !event.shiftKey) {
|
|
||||||
event.preventDefault();
|
|
||||||
void handleStartSession();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
placeholder={entryCopy.goalPlaceholder}
|
|
||||||
className={cn(
|
|
||||||
inputShellClass,
|
|
||||||
'text-[1.15rem] font-light tracking-[-0.02em] placeholder:text-white/34 md:text-[1.4rem]',
|
|
||||||
)}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label className="block space-y-2">
|
|
||||||
<span className="text-[12px] font-medium uppercase tracking-[0.16em] text-white/46">
|
|
||||||
{entryCopy.microStepLabel}
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
value={microStepDraft}
|
|
||||||
onChange={(event) => setMicroStepDraft(event.target.value)}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.key === 'Enter' && !event.shiftKey) {
|
|
||||||
event.preventDefault();
|
|
||||||
void handleStartSession();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
placeholder={entryCopy.microStepPlaceholder}
|
|
||||||
className={cn(inputShellClass, 'text-[0.98rem] placeholder:text-white/30')}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<p className="text-sm text-white/48">{entryCopy.microStepHelper}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-6 flex flex-wrap gap-2.5">
|
|
||||||
{GOAL_SUGGESTIONS.map((suggestion) => {
|
|
||||||
const isActive = trimmedGoal === suggestion.label;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={suggestion.id}
|
|
||||||
type="button"
|
|
||||||
onClick={() => handleSelectSuggestion(suggestion.label)}
|
|
||||||
className={cn(
|
|
||||||
'rounded-full border px-3.5 py-1.5 text-sm transition',
|
|
||||||
isActive
|
|
||||||
? 'border-white/32 bg-white/14 text-white'
|
|
||||||
: 'border-white/14 bg-white/[0.04] text-white/72 hover:border-white/22 hover:text-white',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{suggestion.label}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-8 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
void handleStartSession();
|
|
||||||
}}
|
|
||||||
disabled={!canStart}
|
|
||||||
className={primaryButtonClass}
|
|
||||||
>
|
|
||||||
{isStartingSession ? entryCopy.startLoading : entryCopy.startNow}
|
|
||||||
</button>
|
|
||||||
<div className="space-y-1 text-left sm:text-right">
|
|
||||||
<p className="text-xs font-medium uppercase tracking-[0.16em] text-white/44">
|
|
||||||
{entryCopy.ritualHint}
|
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-white/52">{entryCopy.ritualHelper}</p>
|
<h1 className="text-[1.8rem] font-light leading-[1.14] tracking-[-0.03em] text-white md:text-[2.2rem]">
|
||||||
|
{currentSession.goal}
|
||||||
|
</h1>
|
||||||
|
<p className="text-sm text-white/68">
|
||||||
|
{currentSession.state === 'paused' ? entryCopy.resumePaused : entryCopy.resumeRunning}
|
||||||
|
</p>
|
||||||
|
{currentSession.microStep ? (
|
||||||
|
<div className="rounded-[1.1rem] border border-white/10 bg-white/[0.04] px-4 py-3">
|
||||||
|
<p className="mb-1 text-[11px] font-medium uppercase tracking-[0.16em] text-white/44">
|
||||||
|
{entryCopy.resumeMicroStepLabel}
|
||||||
|
</p>
|
||||||
|
<p className="text-[15px] text-white/82">{currentSession.microStep}</p>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<button type="button" onClick={handleResumeSession} className={primaryButtonClass}>
|
||||||
|
{entryCopy.resumeCta}
|
||||||
|
</button>
|
||||||
|
<p className="text-xs text-white/48 sm:text-right">{activeRitualMeta}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-sm text-white/56">{entryCopy.resumeNewGoalHint}</p>
|
||||||
|
|
||||||
|
{shouldShowResumeReviewEntry ? (
|
||||||
|
<Link
|
||||||
|
href="/stats"
|
||||||
|
className="block rounded-[1.35rem] border border-white/10 bg-[#0f1115]/12 px-4 py-3 backdrop-blur-lg transition hover:bg-[#0f1115]/18"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="text-[11px] font-medium uppercase tracking-[0.16em] text-white/42">
|
||||||
|
{entryCopy.resumeReviewEyebrow}
|
||||||
|
</p>
|
||||||
|
<p className="mt-2 text-[0.96rem] font-medium tracking-[-0.02em] text-white/88">
|
||||||
|
{entryCopy.resumeReviewTitle}
|
||||||
|
</p>
|
||||||
|
<p className="mt-2 max-w-[30rem] text-[12px] leading-[1.6] text-white/60">
|
||||||
|
{isPro ? review.carryForward.keepDoing : entryCopy.resumeReviewHelper}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<span className="inline-flex shrink-0 items-center text-[12px] font-medium tracking-[0.04em] text-white/72">
|
||||||
|
{reviewTeaserCta}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
{sessionLookupError ? (
|
<>
|
||||||
<p className="mt-5 text-sm text-amber-100/80">{entryCopy.loadFailed}</p>
|
<div className={goalCardClass}>
|
||||||
) : null}
|
<div className="space-y-3 text-center">
|
||||||
</div>
|
<h1 className="text-[2rem] font-light leading-[1.08] tracking-[-0.04em] text-white md:text-[2.9rem]">
|
||||||
|
{entryCopy.title}
|
||||||
{shouldShowWeeklyReviewTeaser ? (
|
</h1>
|
||||||
<Link
|
<p className="mx-auto max-w-[32rem] text-[15px] leading-6 text-white/70 md:text-base">
|
||||||
href="/stats"
|
{entryCopy.description}
|
||||||
className="block rounded-[1.6rem] border border-white/10 bg-[#0f1115]/18 px-5 py-4 backdrop-blur-lg transition hover:bg-[#0f1115]/24"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
|
|
||||||
<div className="min-w-0">
|
|
||||||
<p className="text-[11px] font-medium uppercase tracking-[0.16em] text-white/42">
|
|
||||||
{entryCopy.reviewEyebrow}
|
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-2 text-[1rem] font-medium tracking-[-0.02em] text-white/88">
|
|
||||||
{reviewTeaserTitle}
|
|
||||||
</p>
|
|
||||||
<p className="mt-2 max-w-[34rem] text-[13px] leading-[1.6] text-white/62">
|
|
||||||
{reviewTeaserSummary}
|
|
||||||
</p>
|
|
||||||
<p className="mt-2 text-[12px] text-white/44">{reviewTeaserHelper}</p>
|
|
||||||
</div>
|
</div>
|
||||||
<span className="inline-flex shrink-0 items-center text-[12px] font-medium tracking-[0.04em] text-white/74">
|
|
||||||
{reviewTeaserCta}
|
<div className="mt-8 space-y-4">
|
||||||
</span>
|
<label className="block">
|
||||||
|
<span className="sr-only">Goal</span>
|
||||||
|
<input
|
||||||
|
ref={goalInputRef}
|
||||||
|
value={goalDraft}
|
||||||
|
onChange={(event) => setGoalDraft(event.target.value)}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === 'Enter' && !event.shiftKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
void handleStartSession();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder={entryCopy.goalPlaceholder}
|
||||||
|
className={cn(
|
||||||
|
inputShellClass,
|
||||||
|
'text-[1.15rem] font-light tracking-[-0.02em] placeholder:text-white/34 md:text-[1.4rem]',
|
||||||
|
)}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className="block space-y-2">
|
||||||
|
<span className="text-[12px] font-medium uppercase tracking-[0.16em] text-white/46">
|
||||||
|
{entryCopy.microStepLabel}
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
value={microStepDraft}
|
||||||
|
onChange={(event) => setMicroStepDraft(event.target.value)}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === 'Enter' && !event.shiftKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
void handleStartSession();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder={entryCopy.microStepPlaceholder}
|
||||||
|
className={cn(inputShellClass, 'text-[0.98rem] placeholder:text-white/30')}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<p className="text-sm text-white/48">{entryCopy.microStepHelper}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 flex flex-wrap gap-2.5">
|
||||||
|
{GOAL_SUGGESTIONS.map((suggestion) => {
|
||||||
|
const isActive = trimmedGoal === suggestion.label;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={suggestion.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleSelectSuggestion(suggestion.label)}
|
||||||
|
className={cn(
|
||||||
|
'rounded-full border px-3.5 py-1.5 text-sm transition',
|
||||||
|
isActive
|
||||||
|
? 'border-white/32 bg-white/14 text-white'
|
||||||
|
: 'border-white/14 bg-white/[0.04] text-white/72 hover:border-white/22 hover:text-white',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{suggestion.label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
void handleStartSession();
|
||||||
|
}}
|
||||||
|
disabled={!canStart}
|
||||||
|
className={primaryButtonClass}
|
||||||
|
>
|
||||||
|
{isStartingSession ? entryCopy.startLoading : entryCopy.startNow}
|
||||||
|
</button>
|
||||||
|
<div className="space-y-1 text-left sm:text-right">
|
||||||
|
<p className="text-xs font-medium uppercase tracking-[0.16em] text-white/44">
|
||||||
|
{entryRitualHint}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-white/52">{entryCopy.ritualHelper}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{sessionLookupError ? (
|
||||||
|
<p className="mt-5 text-sm text-amber-100/80">{entryCopy.loadFailed}</p>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
|
||||||
) : null}
|
{shouldShowWeeklyReviewTeaser ? (
|
||||||
|
<Link
|
||||||
|
href="/stats"
|
||||||
|
className="block rounded-[1.6rem] border border-white/10 bg-[#0f1115]/18 px-5 py-4 backdrop-blur-lg transition hover:bg-[#0f1115]/24"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="text-[11px] font-medium uppercase tracking-[0.16em] text-white/42">
|
||||||
|
{entryCopy.reviewEyebrow}
|
||||||
|
</p>
|
||||||
|
<p className="mt-2 text-[1rem] font-medium tracking-[-0.02em] text-white/88">
|
||||||
|
{reviewTeaserTitle}
|
||||||
|
</p>
|
||||||
|
<p className="mt-2 max-w-[34rem] text-[13px] leading-[1.6] text-white/62">
|
||||||
|
{reviewTeaserSummary}
|
||||||
|
</p>
|
||||||
|
<p className="mt-2 text-[12px] text-white/44">{reviewTeaserHelper}</p>
|
||||||
|
</div>
|
||||||
|
<span className="inline-flex shrink-0 items-center text-[12px] font-medium tracking-[0.04em] text-white/74">
|
||||||
|
{reviewTeaserCta}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ export const SpaceFocusHudWidget = ({
|
|||||||
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 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 normalizedMicroStep = microStep?.trim() ? microStep.trim() : null;
|
||||||
@@ -147,6 +148,12 @@ export const SpaceFocusHudWidget = ({
|
|||||||
hasActiveSession &&
|
hasActiveSession &&
|
||||||
overlay === 'none'
|
overlay === 'none'
|
||||||
) {
|
) {
|
||||||
|
if (suppressNextPausePromptRef.current) {
|
||||||
|
suppressNextPausePromptRef.current = false;
|
||||||
|
pausePlaybackStateRef.current = playbackState;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setIntentError(null);
|
setIntentError(null);
|
||||||
setOverlay('paused');
|
setOverlay('paused');
|
||||||
onStatusMessage({
|
onStatusMessage({
|
||||||
@@ -292,7 +299,6 @@ export const SpaceFocusHudWidget = ({
|
|||||||
}}
|
}}
|
||||||
onRest={() => {
|
onRest={() => {
|
||||||
handleDismissReturnPrompt();
|
handleDismissReturnPrompt();
|
||||||
onStatusMessage({ message: copy.space.focusHud.restReminder });
|
|
||||||
}}
|
}}
|
||||||
onNextGoal={() => {
|
onNextGoal={() => {
|
||||||
handleDismissReturnPrompt();
|
handleDismissReturnPrompt();
|
||||||
@@ -352,6 +358,8 @@ export const SpaceFocusHudWidget = ({
|
|||||||
onFinish={() => Promise.resolve(onGoalFinish())}
|
onFinish={() => Promise.resolve(onGoalFinish())}
|
||||||
onRest={() => {
|
onRest={() => {
|
||||||
setOverlay('none');
|
setOverlay('none');
|
||||||
|
suppressNextPausePromptRef.current = true;
|
||||||
|
onPauseRequested?.();
|
||||||
|
|
||||||
if (restReminderTimerRef.current) {
|
if (restReminderTimerRef.current) {
|
||||||
window.clearTimeout(restReminderTimerRef.current);
|
window.clearTimeout(restReminderTimerRef.current);
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ export const SpaceWorkspaceWidget = () => {
|
|||||||
summary: isPro
|
summary: isPro
|
||||||
? review.carryForward.keepDoing
|
? review.carryForward.keepDoing
|
||||||
: copy.space.setup.reviewTeaserHelper,
|
: copy.space.setup.reviewTeaserHelper,
|
||||||
ctaHref: "/stats?review=weekly&origin=space-complete",
|
ctaHref: "/stats",
|
||||||
ctaLabel: copy.space.setup.reviewTeaserCta,
|
ctaLabel: copy.space.setup.reviewTeaserCta,
|
||||||
onDismiss: () => setShowReviewTeaserAfterComplete(false),
|
onDismiss: () => setShowReviewTeaserAfterComplete(false),
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user