// profile.jsx — Profile / Settings / Notifications + quick-action flows + edge states const { useState: useStatePr } = React; // ── Generic Ava "offer" screen used by quick actions ─────── function OfferScreen({ icon, badge, tone, ava, quest, primary, secondary, tertiary, back, hint }) { const tn = tone || T.coral; return ( {ava}
{badge}
{quest.title}
}>{primary.label} {secondary && {secondary.label}} {tertiary && {tertiary.label}}
); } function Flow({ flow, go, back }) { if (flow === 'notime') return ( 좋아요. 오늘은 10초만으로 기록을 지킬게요. 부담 없이 가요.} quest={{ title: '서서 숨 고르기 10초', meta: [{ icon: 'clock', label: '10초' }, { icon: 'seat', label: '공간 필요 없음' }, { icon: 'camera_off', label: '카메라 없음' }] }} primary={{ label: '10초 시작', onClick: () => go('player', { quest: { title: '숨 고르기', seconds: 10, steps: ['바르게 서요', '어깨를 내려요', '깊게 호흡해요'], avaLine: '10초도 건강 행동이에요', xp: 5 } }) }} secondary={{ label: '나중에 알려줘', onClick: () => go('flow', { flow: 'later' }) }} tertiary={{ label: '오늘은 건너뛸래요', onClick: () => go('flow', { flow: 'skip' }) }} back={back}/> ); if (flow === 'tired') return ( 밀어붙이지 않는 것도 훈련이에요. 오늘은 회복으로 갈게요.} quest={{ title: '누워서 호흡 30초', meta: [{ icon: 'clock', label: '30초' }, { icon: 'moon', label: '침대에서' }, { icon: 'wind', label: '회복 모드' }] }} primary={{ label: '회복 Quest 시작', onClick: () => go('player', { quest: { title: '회복 호흡', seconds: 30, tone: T.calm, steps: ['편하게 누워요', '4초 들이마셔요', '6초 길게 내쉬어요'], avaLine: '오늘의 목표는 회복이에요', xp: 12 } }) }} secondary={{ label: '10초만 할래요', onClick: () => go('player', { quest: { title: '회복 호흡', seconds: 10, tone: T.calm, steps: ['편하게 누워요', '천천히 호흡해요'], avaLine: '짧아도 충분해요', xp: 5 } }) }} back={back}/> ); if (flow === 'nospace') return ( 공간이 없어도 괜찮아요. 앉아서 할 수 있는 Quest로 바꿀게요.} quest={{ title: '의자에서 등 세우기 20초', meta: [{ icon: 'clock', label: '20초' }, { icon: 'seat', label: '앉아서' }, { icon: 'camera_off', label: '카메라 없음' }] }} primary={{ label: '앉아서 시작', onClick: () => go('player', { quest: { title: '등 세우기', seconds: 20, steps: ['의자에 깊게 앉아요', '등을 곧게 세워요', '어깨를 내려요'], avaLine: '허리가 아니라 배에 힘을', xp: 10 } }) }} secondary={{ label: '호흡 Quest로 변경', onClick: () => go('flow', { flow: 'tired' }) }} back={back}/> ); if (flow === 'swap') return ; if (flow === 'later') return ; if (flow === 'skip') return ; if (flow === 'streakBroken') return ; if (flow === 'healthDenied') return ; return null; } function SwapScreen({ go, back }) { const alts = [ { tone: T.coral, badge: '더 쉽게', icon: 'bolt', title: '어깨 리셋 10초', meta: '10초 · 앉아서', q: { title: '어깨 리셋', seconds: 10, steps: ['어깨를 올려요', '뒤로 돌려요'], avaLine: '짧아도 충분해요', xp: 5 } }, { tone: T.calm, badge: '회복', icon: 'wind', title: '누워서 호흡 30초', meta: '30초 · 침대에서', q: { title: '회복 호흡', seconds: 30, tone: T.calm, steps: ['편하게 누워요', '천천히 호흡해요'], avaLine: '회복도 훈련이에요', xp: 12 } }, { tone: T.amberDark, badge: '수분', icon: 'drop', title: '물 한 잔 마시기', meta: '셀프 체크', q: { selfReport: true, title: '물 한 잔', xp: 8 } }, ]; return ( 오늘 더 잘 맞는 행동으로 바꿔드릴게요. 하나만 고르면 돼요.
{alts.map((a, i) => ( ))}
); } function LaterReminder({ go, back }) { const [pick, setPick] = useStatePr(null); const opts = [['30분 뒤', '30m'], ['저녁에', 'pm'], ['잠들기 전', 'night'], ['내일 다시', 'tmr']]; return (

언제 다시 알려드릴까요?

{opts.map(o => setPick(o[1])}/>)}
go('home')} disabled={!pick}>알림 설정
); } function QuestSkipped({ go, back }) { return (

오늘 Quest를 건너뛰었어요

“괜찮아요. 오늘 컨디션도 기록이에요. 원하면 밤에 10초 Recovery로 Streak를 지킬 수 있어요.”
go('player', { quest: { title: '회복 호흡', seconds: 10, tone: T.calm, steps: ['편하게 누워요', '천천히 호흡해요'], avaLine: '연결을 지켰어요', xp: 5 } })}>Recovery Quest 지금 하기 go('flow', { flow: 'later' })}>밤에 알려줘 go('home')}>오늘은 쉬기
); } function StreakBroken({ go, back }) { return (

Streak가 끊겼어요

“괜찮아요. 기록은 사라진 게 아니라 누적돼 있어요. 오늘부터 새 Streak를 시작할 수 있어요.”
BEST STREAK
6일
누적 XP
320
Streak Repair · 이번 주 1회 가능
어제 놓친 기록을 10초 Recovery로 복구할 수 있어요.
go('player', { quest: { title: '회복 호흡', seconds: 10, tone: T.calm, steps: ['편하게 누워요', '천천히 호흡해요'], avaLine: '복구 완료, 다시 이어가요', xp: 5 } })}>10초 Recovery로 복구 go('player', { quest: { title: '새 시작 호흡', seconds: 10, steps: ['바르게 서요', '깊게 호흡해요'], avaLine: '새 Streak Day 1', xp: 10 } })}>새 Streak 시작
); } function HealthDenied({ go, back }) { return (

Health 연동 없이도
괜찮아요

Quest와 XP는 그대로 기록돼요. 나중에 가치를 느끼면 다시 연결할 수 있어요.

go('home')}>수동으로 계속하기 go('home')}>나중에 연결하기
); } // ── Profile ──────────────────────────────────────────────── function ProfileScreen({ data, go }) { return ( go('home')} right={ }/>
지민
Lv.{data.level} · Daily Mover
{[['🔥', data.streak, '현재 Streak'], ['🏆', 6, 'Best Streak'], ['⚡', data.xp, 'XP']].map((s, i) => (
{s[1]}
{s[2]}
))}
SQUAD
{['민','준','서','+2'].map((m, i) => (
{m}
))}
오늘 3명이 Streak 유지 중
} onClick={() => go('invite')}>친구 초대
); } // ── Settings ─────────────────────────────────────────────── function SettingsRow({ icon, label, detail, onClick, tone = T.ink2, last }) { return ( ); } function Settings({ go, setData, theme, toggleTheme }) { return ( go('home')}/>
멤버십
일반
다크 모드
go('notifications')}/> go('flow', { flow: 'healthDenied' })} last/>
프로토타입 상태 미리보기
{ setData(d => ({ ...d, homeVariant: 'default', completedToday: false })); go('home'); }}/> { setData(d => ({ ...d, homeVariant: 'streakRisk', completedToday: false })); go('home'); }}/> { setData(d => ({ ...d, homeVariant: 'completed', completedToday: true })); go('home'); }}/> { setData(d => ({ ...d, homeVariant: 'return7', completedToday: false })); go('home'); }}/> go('flow', { flow: 'skip' })}/> go('flow', { flow: 'streakBroken' })}/> go('bodycheck')}/> go('energy')}/> go('academyAt', { at: 'failed' })} last/>
); } // ── Notifications ────────────────────────────────────────── function NotifToggle({ label, sub, on, onToggle }) { return (
{label}
{sub &&
{sub}
}
); } function Notifications({ go }) { const [s, setS] = useStatePr({ morning: true, streak: true, squad: false, recap: true, lesson: false }); const t = (k) => setS(p => ({ ...p, [k]: !p[k] })); return ( go('settings')}/> 압박하지 않을게요. 하루 최대 2개, 죄책감 없는 알림만 보내요.
알림 종류
t('morning')}/> t('streak')}/> t('squad')}/> t('recap')}/>
t('lesson')}/>
시간
); } Object.assign(window, { Flow, ProfileScreen, Settings, Notifications });