diff --git a/docs/README.md b/docs/README.md
index d791599..2e09957 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -77,8 +77,10 @@ review 구조와 BM 연결을 볼 때 읽는다.
구현 규칙이나 구조를 볼 때 읽는다.
+- [08_premium_uiux_guideline.md](./foundation/08_premium_uiux_guideline.md) `source-of-truth`
+ - 세계 최고급(LifeAt, Portal 수준) UI/UX 톤앤매너, 글래스모피즘, 모션 등 프리미엄 디자인 절대 원칙
- [01_ui_guidelines.md](./foundation/01_ui_guidelines.md) `source-of-truth`
- - UI 톤, CTA 위계, premium 품질 기준
+ - UI 톤, CTA 위계, premium 품질 기준 (일반 가이드)
- [02_arch_fsd_rules.md](./foundation/02_arch_fsd_rules.md) `source-of-truth`
- FSD/레이어 구조 규칙
- [03_routes_map.md](./foundation/03_routes_map.md) `source-of-truth`
diff --git a/docs/foundation/08_premium_uiux_guideline.md b/docs/foundation/08_premium_uiux_guideline.md
new file mode 100644
index 0000000..94e987b
--- /dev/null
+++ b/docs/foundation/08_premium_uiux_guideline.md
@@ -0,0 +1,122 @@
+# Premium Immersive UI/UX Guidelines (VibeRoom Core)
+
+이 문서는 VibeRoom 프로젝트가 LifeAt, Portal, Focusmate 등 **세계 최고급의 프리미엄 UI/UX 경험**을 일관되게 유지하기 위해 작성된 절대적인 디자인 헌장입니다.
+어떤 에이전트, 어떤 개발자가 코드를 작성하든 화면을 추가하거나 수정할 때 이 가이드라인을 반드시 숙지하고 엄격하게 준수해야 합니다.
+
+---
+
+## 1. 핵심 철학 (Core Philosophy)
+
+### 1-1. 무대 우선주의 (Stage-first & Immersive)
+- **절대 원칙:** 사용자가 선택한 **배경(Atmosphere/Scene) 자체가 곧 앱의 정체성이자 무대**입니다.
+- 배경을 가리거나 시야를 방해하는 거대한 대시보드 형태의 레이아웃(Split-screen, 거대한 Solid Card Grid)은 절대 금지합니다.
+- UI는 무대 위에 떠 있는 얇고 투명한 유리 조각(Glass)처럼 존재해야 하며, 화면의 여백(White Space)을 극대화하여 공간감을 제공해야 합니다.
+
+### 1-2. 중앙 집중의 의식 (Minimal Central Ritual)
+- 사용자가 수행해야 할 가장 중요한 단 하나의 핵심 액션(예: "무엇에 집중할 것인가?")은 **항상 화면의 정중앙에 거대하고 우아하게 배치**합니다.
+- 텍스트 입력창은 투명하게(`bg-transparent`), 폰트는 크고 얇게(`text-4xl font-light tracking-tight`) 유지하여 단순한 '입력'이 아닌 '의식(Ritual)'처럼 느껴지게 합니다.
+
+### 1-3. 압도적인 글래스모피즘 (Premium Glassmorphism)
+- 단순히 투명도를 낮추는 것이 아닙니다. 뒤의 빛과 배경이 은은하게 굴절되는 진짜 유리의 질감을 구현해야 합니다.
+- **필수 속성:** `backdrop-blur-xl` 또는 `backdrop-blur-2xl`을 반드시 사용합니다.
+- **테두리와 명암:** 투박한 solid border 대신, `border-white/5` ~ `border-white/10` 수준의 매우 얇고 투명한 테두리를 사용합니다. 깊이감을 위해 다중 그림자(`shadow-2xl` 등)와 미세한 그라데이션(`bg-[linear-gradient(...)]`)을 조합합니다.
+
+---
+
+## 2. 레이아웃 & 컴포넌트 배치 원칙 (Layout & Placement)
+
+### 2-1. 절대 위치(Absolute) 사용의 엄격한 제한
+- UI 요소가 창 크기 조절(Resizing) 시 중앙의 핵심 컨텐츠(Main Ritual)를 가리거나 겹치는 현상(Overlap)은 치명적인 결함입니다.
+- **해결책:** 좌/우측에 둥둥 떠 있는(Floating) 위젯 형태를 구현할 때, 단순히 `absolute top-x left-y`로 띄워두지 마십시오. 창이 작아져도 겹치지 않게 하려면 중앙 컨테이너의 흐름(Inline Flex) 내부에 배치하거나, 화면 크기에 따른 철저한 미디어 쿼리 제어를 통해 **어떤 해상도에서도 메인 텍스트 영역을 침범하지 않도록 보장**해야 합니다.
+
+### 2-2. 보조 위젯의 극단적 미니멀리즘 (Subtle Accessories)
+- 메인 액션이 아닌 모든 정보(예: Weekly Review, Error Message, 힌트 등)는 **크기를 최소화하고 시각적 대비를 낮춥니다.**
+- 정보 텍스트는 `text-[12px]` 또는 `text-[13px]`, 라벨이나 뱃지는 `text-[9px] ~ text-[10px]`에 두꺼운 자간(`tracking-[0.25em]`)과 대문자(`uppercase`) 조합을 사용하여 명품 브랜드의 타이포그래피처럼 디자인합니다.
+- 예: 투박한 'Weekly Review' 카드 ❌ -> 한 줄의 세련된 'Smart Hint Pill' 형태 ⭕
+
+### 2-3. 가장자리 도킹 (Edge Docking)
+- 선택형 리스트(Atmosphere 선택 등)는 화면을 덮는 Grid 대신 **화면 최하단에 스와이프 가능한 가로 독(Carousel Dock)** 형태로 배치합니다.
+- 독 내부의 아이템이 확대(Scale)되거나 애니메이션 될 때, 스크롤 컨테이너의 영역이 좁아 카드가 잘려 보이는 현상(Clipping)이 발생하지 않도록 **컨테이너 자체에 충분한 상하 여백(`py-8` 등)**을 확보해야 합니다.
+
+---
+
+## 3. 애니메이션 및 상호작용 (Motion & Interaction)
+
+### 3-1. 부드럽고 웅장한 진입 (Stately Entrance)
+- 뚝 떨어지거나 딱딱하게 나타나는 화면 전환은 금지합니다.
+- 프리미엄 서비스 특유의 '서서히 떠오르는' 모션을 위해 커스텀 Keyframe(`fade-in-up`, `fade-in`)과 섬세한 이징 커브(`cubic-bezier(0.16, 1, 0.3, 1)`)를 적용합니다.
+- `animation-delay`를 활용하여 헤더 -> 중앙 텍스트 -> 하단 독 순서로 물결치듯 순차적으로 나타나는 시퀀스를 구성합니다.
+
+### 3-2. Hover 및 포커스 상태 (Fluid Feedback)
+- 카드 호버 시 단순히 색만 변하는 것이 아니라, 부드러운 스케일 업(`hover:scale-105 ~ 110`)과 함께 그림자가 깊어지고(`hover:shadow-2xl`) 내부 텍스트 및 오버레이의 명도가 미세하게 조절되어야 합니다.
+- 버튼의 경우 누를 때 미세하게 작아지는 햅틱 피드백(`active:scale-[0.98]`)을 적용하여 쫀득한 터치감을 줍니다.
+
+---
+
+## 4. 타이포그래피 및 카피 (Typography & Copy)
+
+### 4-1. 소음 줄이기 (Reduce Visual Noise)
+- 중요하지 않은 부가 설명(Helper Text)은 `text-white/40` ~ `text-white/60` 정도로 투명도를 과감히 낮춰 시야에서 멀어지게 합니다.
+- 강렬한 Primary Color(파란색, 빨간색 등)는 에러나 꼭 필요한 CTA에만 극도로 제한적으로 사용하고, 기본적으로는 **무채색(흰색, 검은색)의 투명도 조절만으로 위계를 표현**합니다.
+
+### 4-2. 프리미엄 카피라이팅 (Tone & Manner)
+- 기능적인 설명보다 감성적이고 몰입을 돕는 문구를 사용합니다.
+- "시간을 입력하세요" ❌ -> "What will you focus on?" / "의식을 시작합니다" ⭕
+- 업그레이드 등 상업적 CTA도 "결제하기"보다 작고 섬세한 캡슐 버튼(`Upgrade →`)으로 디자인하여 브랜드의 우아함을 지킵니다.
+
+## 5. AI 에이전트를 위한 Tailwind CSS 프롬프팅 & 코드 패턴 (Crucial for AI)
+
+AI 에이전트(Codex, Cursor, Cline 등)는 "프리미엄하게 만들어줘"라는 추상적인 지시를 이해하지 못하고 평범한 UI(`bg-gray-100 rounded-md` 등)를 생성하는 경향이 있습니다. AI에게 작업을 지시할 때는 아래의 **명확한 Tailwind 유틸리티 패턴(Snippet)** 을 그대로 복사해서 사용하라고 지시해야 합니다.
+
+### 5-1. Floating Smart Pill (보조 정보 위젯)
+AI가 투박한 Card를 만들지 못하게 하고, 이 코드를 복사하라고 지시하세요.
+```tsx
+// DO: 아주 작고 은은하게 떠 있는 스마트 필
+
+
+
+ 내용 텍스트
+
+
+
+// DON'T: AI가 자주 실수하는 투박한 솔리드 카드
+
내용 텍스트
+```
+
+### 5-2. 메인 입력창 (Ritual Input)
+AI가 흔한 폼 인풋(`border rounded px-4`)을 만들지 못하게 하세요.
+```tsx
+// DO: 거대하고 얇고 투명한 의식적 텍스트
+
+
+// DON'T: 일반적인 대시보드 폼
+
+```
+
+### 5-3. 프리미엄 버튼 (Primary Action)
+```tsx
+// DO: 빛나는 투명 테두리와 쫀득한 햅틱 모션을 가진 버튼
+
+```
+
+### 5-4. 그라데이션 오버레이 (배경 어둡게 하기)
+이미지 위에서 글씨가 잘 보이게 하려면 단순 `bg-black/50` 대신 깊이감 있는 그라데이션을 써야 합니다.
+```tsx
+// DO: 복합 그라데이션 (중앙은 비우고 테두리만 어둡게)
+
+
+```
+
+---
+
+## 6. QA 체크리스트 (커밋 전 반드시 확인)
+
+1. **Overlap Check:** 브라우저 창을 최소 크기(모바일/작은 데스크탑)로 줄였을 때 위젯이나 텍스트가 겹치거나 잘리는 곳이 단 한 곳이라도 있는가? (있다면 즉시 수정)
+2. **Clipping Check:** 리스트 내의 아이템에 마우스를 올렸을 때(hover:scale) 아이템의 상하좌우 테두리가 부모 컨테이너에 의해 잘려 나가는 현상이 없는가?
+3. **Hierarchy Check:** 화면 내에서 가장 눈에 띄는 것이 "현재 사용자가 해야 할 단 하나의 액션"인가? 부가 정보가 너무 커서 메인 액션을 압도하지 않는가?
+4. **Motion Check:** 화면 진입 시 모든 요소가 우아하고 부드럽게 등장하는가? 깜빡이거나 투박하게 나타나는 요소는 없는가?
+5. **Glass Check:** 모든 팝오버, 시트, 위젯이 뒷 배경을 우아하게 투영(backdrop-blur)하고 있으며, 테두리가 지나치게 두껍지 않은가?
diff --git a/package-lock.json b/package-lock.json
index 9287e10..7d37025 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,6 +15,7 @@
"react-apple-signin-auth": "^1.1.2",
"react-dom": "19.2.3",
"react-facebook-login": "^4.1.1",
+ "tailwindcss-animate": "^1.0.7",
"zustand": "^5.0.11"
},
"devDependencies": {
@@ -6167,6 +6168,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/tailwindcss-animate": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz",
+ "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "tailwindcss": ">=3.0.0 || insiders"
+ }
+ },
"node_modules/tapable": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
diff --git a/package.json b/package.json
index 0ac2723..30cbbd3 100644
--- a/package.json
+++ b/package.json
@@ -18,6 +18,7 @@
"react-apple-signin-auth": "^1.1.2",
"react-dom": "19.2.3",
"react-facebook-login": "^4.1.1",
+ "tailwindcss-animate": "^1.0.7",
"zustand": "^5.0.11"
},
"devDependencies": {
diff --git a/src/app/globals.css b/src/app/globals.css
index d5d5112..eb30978 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -1,4 +1,5 @@
@import "tailwindcss";
+@plugin "tailwindcss-animate";
@theme {
/* Noto Sans 다국어 폰트 적용 (next/font/google 변수) */
@@ -81,6 +82,42 @@ body {
}
}
+@keyframes fade-in-up {
+ 0% {
+ opacity: 0;
+ transform: translateY(20px);
+ }
+ 100% {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+@keyframes fade-in {
+ 0% {
+ opacity: 0;
+ }
+ 100% {
+ opacity: 1;
+ }
+}
+
+.animate-fade-in-up {
+ animation: fade-in-up 0.8s cubic-bezier(0.16, 1, 0.3, 1) forwards;
+}
+
+.animate-fade-in {
+ animation: fade-in 0.8s cubic-bezier(0.16, 1, 0.3, 1) forwards;
+}
+
+.delay-150 {
+ animation-delay: 150ms;
+}
+
+.delay-300 {
+ animation-delay: 300ms;
+}
+
.scrollbar-none {
-ms-overflow-style: none;
scrollbar-width: none;
diff --git a/src/widgets/focus-dashboard/ui/AppAtmosphereEntryShell.tsx b/src/widgets/focus-dashboard/ui/AppAtmosphereEntryShell.tsx
index 38f017c..8c4daca 100644
--- a/src/widgets/focus-dashboard/ui/AppAtmosphereEntryShell.tsx
+++ b/src/widgets/focus-dashboard/ui/AppAtmosphereEntryShell.tsx
@@ -5,15 +5,6 @@ import { getSceneCardPhotoUrl } from '@/entities/scene';
import { cn } from '@/shared/lib/cn';
import type { AtmosphereOption } from '../model/atmosphereEntry';
-const stageShellClass =
- 'relative overflow-hidden rounded-[2.35rem] border border-white/12 bg-[linear-gradient(160deg,rgba(9,13,20,0.54)_0%,rgba(9,13,20,0.2)_52%,rgba(9,13,20,0.48)_100%)] shadow-[0_26px_90px_rgba(3,7,18,0.34)] backdrop-blur-[26px]';
-const fieldShellClass =
- 'w-full rounded-[1.45rem] border border-white/12 bg-[linear-gradient(180deg,rgba(255,255,255,0.09)_0%,rgba(255,255,255,0.05)_100%)] px-5 py-4 text-white outline-none transition focus:border-white/24 focus:bg-white/[0.1]';
-const reviewDockClass =
- 'group relative overflow-hidden rounded-[1.65rem] border border-white/12 bg-[linear-gradient(145deg,rgba(255,255,255,0.09)_0%,rgba(255,255,255,0.04)_100%)] px-5 py-4 backdrop-blur-xl transition hover:border-white/18 hover:bg-white/[0.1]';
-const primaryButtonClass =
- 'inline-flex min-h-[3.65rem] items-center justify-center rounded-full border border-white/14 bg-white/[0.16] px-6 text-[15px] font-medium tracking-[-0.01em] text-white shadow-[0_10px_22px_rgba(5,10,20,0.24)] transition hover:bg-white/[0.2] active:scale-[0.99] disabled:cursor-not-allowed disabled:opacity-50';
-
interface AppAtmosphereEntryShellProps {
canStart: boolean;
durationDraft: string;
@@ -25,9 +16,9 @@ interface AppAtmosphereEntryShellProps {
goalInputRef: RefObject;
goalPlaceholder: string;
isStartingSession: boolean;
- reviewEntry?: ReactNode;
+ topAccessory?: ReactNode;
+ errorAccessory?: ReactNode;
selectedAtmosphere: AtmosphereOption;
- sessionLookupError?: string | null;
startButtonLabel: string;
startButtonLoadingLabel: string;
atmosphereOptions: AtmosphereOption[];
@@ -44,21 +35,17 @@ export const AppAtmosphereEntryShell = ({
canStart,
durationDraft,
durationHelper,
- durationInputLabel,
- durationPlaceholder,
durationSuggestions,
goalDraft,
goalInputRef,
goalPlaceholder,
isStartingSession,
- reviewEntry,
+ topAccessory,
+ errorAccessory,
selectedAtmosphere,
- sessionLookupError,
startButtonLabel,
startButtonLoadingLabel,
atmosphereOptions,
- atmosphereTitle,
- atmosphereBody,
onDurationChange,
onGoalChange,
onSelectAtmosphere,
@@ -66,313 +53,144 @@ export const AppAtmosphereEntryShell = ({
onStartSession,
}: AppAtmosphereEntryShellProps) => {
return (
-
-
-
-
-
+
+ {/* Main Focus Entry Ritual */}
+
+ {/* Inline Accessories (No overlap guarantee) */}
+
);
};
diff --git a/src/widgets/focus-dashboard/ui/FocusDashboardWidget.tsx b/src/widgets/focus-dashboard/ui/FocusDashboardWidget.tsx
index 0827b0f..a4cf99c 100644
--- a/src/widgets/focus-dashboard/ui/FocusDashboardWidget.tsx
+++ b/src/widgets/focus-dashboard/ui/FocusDashboardWidget.tsx
@@ -8,7 +8,6 @@ import { usePlanTier } from '@/entities/plan';
import { getSceneById, SCENE_THEMES } from '@/entities/scene';
import { SOUND_PRESETS } from '@/entities/session';
import { PaywallSheetContent } from '@/features/paywall-sheet';
-import { PlanPill } from '@/features/plan-pill';
import { focusSessionApi, type FocusSession } from '@/features/focus-session/api/focusSessionApi';
import { useFocusStats, type ReviewCarryHint } from '@/features/stats';
import { copy } from '@/shared/i18n';
@@ -42,40 +41,37 @@ const DEFAULT_ATMOSPHERE =
const entryCopy = {
eyebrow: 'VibeRoom',
- goalPlaceholder: '예: 제안서 첫 문단만 다듬기',
- durationLabel: '예상 시간(분)',
- durationPlaceholder: '예: 70',
- durationHelper: '이 목표를 끝내는 데 걸릴 것 같은 시간을 적어요.',
- startNow: '이 분위기로 들어가기',
- startLoading: '입장 준비 중...',
- atmosphereTitle: '어떤 분위기에서 들어갈까요?',
+ goalPlaceholder: 'e.g. Write the first draft',
+ durationLabel: 'Estimated Time',
+ durationPlaceholder: 'e.g. 70',
+ durationHelper: 'Set a realistic time to accomplish this goal.',
+ startNow: 'Begin Session',
+ startLoading: 'Entering...',
+ atmosphereTitle: 'Atmosphere',
atmosphereBody:
- '배경과 사운드는 하나의 atmosphere로 움직입니다. 지금 할 일의 온도에 맞는 분위기 하나만 고르면 바로 들어갈 수 있어요.',
- loadFailed: '세션 상태를 불러오지 못했어요. 새로 시작은 계속 할 수 있어요.',
+ 'Background and sound play together. Choose an atmosphere to dive into deep focus.',
+ loadFailed: 'Failed to load session state. You can still start a new one.',
reviewEyebrow: 'Weekly Review',
- reviewTitle: '이번 주 review를 잠깐 보고 갈까요?',
- reviewCta: '주간 review 보기',
- reviewHelper: '다음 세션 전에 가볍게 보고 갈 수 있어요.',
- reviewTitlePro: '나에게 잘 맞았던 흐름을 다시 보고 갈까요?',
- reviewCtaPro: '나에게 맞는 흐름 보기',
- reviewHelperPro: '가장 잘 맞았던 ritual과 carry-forward를 보고 돌아올 수 있어요.',
- reviewReturnEyebrow: '방금 본 review 기준',
- reviewReturnTitleSteady: '이번 주에 잘 맞았던 흐름을 그대로 가져가 보세요.',
- reviewReturnTitleSmaller: '이번엔 목표를 더 작게 잡아보세요.',
- reviewReturnTitleClosure: '이번엔 어디서 닫을지 먼저 정해보세요.',
- reviewReturnTitleStart: '이번 주는 시작 횟수 하나를 더 만드는 게 먼저예요.',
- reviewReturnBodySteady: 'goal은 직접 정하되, 지금처럼 가볍게 들어가는 리듬을 유지해 보세요.',
- reviewReturnBodySmaller: '길이를 늘리기보다, 더 작은 goal과 더 구체적인 첫 한 조각으로 시작하면 이어가기 쉬워져요.',
- reviewReturnBodyClosure: '큰 흐름보다 지금 블록을 어디서 마무리할지 먼저 떠올리면 끝까지 가져가기 쉬워져요.',
- reviewReturnBodyStart: '길이를 늘리기보다, 아주 작은 goal로 이번 주 첫 세션 하나를 더 여는 데 집중해 보세요.',
- reviewReturnRitualLabel: '추천 atmosphere · 숲 · Forest Birds',
+ reviewTitle: 'Take a quick look at your weekly review?',
+ reviewCta: 'View Review',
+ reviewHelper: 'A brief look back before you start.',
+ reviewTitlePro: 'Revisit a flow that worked well for you?',
+ reviewCtaPro: 'View My Flow',
+ reviewHelperPro: 'Check your best rituals and carry-forwards.',
+ reviewReturnEyebrow: 'From your recent review',
+ reviewReturnTitleSteady: 'Keep the rhythm that worked well this week.',
+ reviewReturnTitleSmaller: 'Try setting a smaller goal this time.',
+ reviewReturnTitleClosure: 'Decide where to wrap up before you start.',
+ reviewReturnTitleStart: 'Your focus right now: just start one more session.',
+ reviewReturnBodySteady: 'Set your own goal, but maintain the light entry rhythm.',
+ reviewReturnBodySmaller: 'Instead of extending time, a smaller goal makes it easier to keep going.',
+ reviewReturnBodyClosure: 'Think about where to close the block first to finish strong.',
+ reviewReturnBodyStart: 'Just aim to open one more short session to build momentum.',
+ reviewReturnRitualLabel: 'Recommended Ritual · Forest · Forest Birds',
paywallLead: 'Calm Session OS PRO',
- paywallBody: 'Pro는 더 빠른 ritual과 더 깊은 review로 시작과 복귀를 가볍게 만듭니다.',
+ paywallBody: 'Pro enables faster rituals and deeper reviews for seamless entry and return.',
};
-const goalCardClass =
- 'w-full rounded-[2.2rem] border border-white/12 bg-[linear-gradient(160deg,rgba(9,13,20,0.52)_0%,rgba(9,13,20,0.24)_52%,rgba(9,13,20,0.5)_100%)] px-6 py-6 shadow-[0_26px_90px_rgba(3,7,18,0.34)] backdrop-blur-[24px] md:px-8 md:py-8';
-
const reviewCarryCopyByHint: Record<
ReviewCarryHint,
{ title: string; body: string }
@@ -103,7 +99,7 @@ export const FocusDashboardWidget = () => {
const searchParams = useSearchParams();
const { plan, isPro, setPlan } = usePlanTier();
const { sceneAssetMap } = useMediaCatalog();
- const { review, summary: weeklySummary } = useFocusStats();
+ const { summary: weeklySummary } = useFocusStats();
const reviewEntryPreset = searchParams.get('entryPreset');
const reviewEntryPresetConfig = useMemo(() => {
@@ -181,12 +177,9 @@ export const FocusDashboardWidget = () => {
const reviewReturnRitualLabel =
isPro && reviewEntryPresetConfig ? `추천 ritual · ${reviewEntryPresetConfig.label}` : null;
const reviewTeaserTitle = isPro ? entryCopy.reviewTitlePro : entryCopy.reviewTitle;
- const reviewTeaserSummary = isPro ? review.carryForward.keepDoing : review.snapshotSummary;
- const reviewTeaserHelper = isPro ? entryCopy.reviewHelperPro : entryCopy.reviewHelper;
- const reviewTeaserCta = isPro ? entryCopy.reviewCtaPro : entryCopy.reviewCta;
const durationHelper =
parsedDurationMinutes === null
- ? '이 목표를 끝내는 데 걸릴 것 같은 시간을 분 단위로 적어주세요.'
+ ? 'Please enter the estimated duration in minutes.'
: entryCopy.durationHelper;
const hasCurrentSession = Boolean(currentSession);
@@ -303,120 +296,133 @@ export const FocusDashboardWidget = () => {
!isCheckingSession && !currentSession && hasEnoughWeeklyData && !isReviewReturn;
return (
-