/* ============================================================ ROOY · HOME helpers + 01a 首次登录空状态 ─────────────────────────────────────────────────────────── Home 共用 helpers(SegmentedProgress / SeasonStreak / TickUp)+ 01a 空状态屏。helpers 全局挂出,供其它 home 文件复用。 01a 设计意图:新用户从 onboarding 出来落到 Home。**照搬 01q (HomeScreenQ,最终定稿)的近白骨架**——header → 雪季 → ROOY 指数 hero → 正在学习,逐块降级到"还没发生": · 雪季卡:同 01q SeasonCardF 结构,统计 «—»、skyline 全空、 最近一次 = 还未开始。 · ROOY 指数 hero:GhostGem(01q HexGemBadge 的「满量程骨架」层 单独拎出来当主角 + 虚线 rim = 待激活),浮白卡单行号召「第一趟 后出指数」。tap → presession(开始第一趟),不是 review。 · 正在学习:onboarding 选的立刃路径,doneCount=0。 营造"框架已就位、等你第一趟点亮",而非"暂无数据"。 ============================================================ */ /* SegmentedProgress — path bar used on Home + Course. Done segments are inked, the current segment is ROOY-color and slightly taller (6 vs 4 pt) so it reads as the active step. */ function SegmentedProgress({ nodes, doneCount }) { return (
{Array.from({ length: nodes }).map((_, i) => { const done = i < doneCount; const cur = i === doneCount; const h = cur ? 6 : 4; const bg = done ? R.ink : cur ? R.rooy : R.hair; return (
); })}
); } /* SeasonStreak — thin strip of weekly cells across the season. 24 weeks (Nov → Apr); filled weeks at 55% white, empty at 10%. Reads as a "you skied here, here, here" rhythm without labels. Pass sessions=new Set() to render an all-empty strip (FirstRun). Defaults assume ink hero surface; on white cards (01b) pass ink-based onColor / offColor tokens. */ const DEFAULT_SESSIONS = new Set([1, 3, 4, 6, 8, 11, 13, 15, 17, 19, 21, 23]); function SeasonStreak({ sessions = DEFAULT_SESSIONS, onColor = 'rgba(255,255,255,0.82)', offColor = 'rgba(255,255,255,0.12)', }) { return (
{Array.from({ length: 24 }).map((_, i) => { const on = sessions.has(i); return (
); })}
); } /* TickUp — count-up animation, used on hero numerals. Plays once per mount over ~900ms with ease-out. Honors prefers- reduced-motion (skips to final value). */ function TickUp({ to, duration = 900 }) { const [n, setN] = React.useState(to); React.useEffect(() => { if (typeof window === 'undefined') return; if (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; let raf; const start = performance.now(); setN(0); const tick = (now) => { const t = Math.min(1, (now - start) / duration); const eased = 1 - Math.pow(1 - t, 3); setN(Math.round(eased * to)); if (t < 1) raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); return () => cancelAnimationFrame(raf); }, [to, duration]); return n; } /* GhostGem — 01a 空状态的「待激活」宝石。直接照搬 01q HexGemBadge 的 「满量程骨架」层(细 rooy rim + 6 轴心连线)+ rooy 辉光底盘——即 01q 里被琢面盖住、只在负空间露出的那层骨架,这里单独拎出来当主角:脚手架 已就位、数据还没填。rim 用虚线(pending 信号),不加琢面/高光(那是 「已有数据」才有的光彩)。concept:六轴框架等你第一趟来点亮。 */ function GhostGem({ size = 116 }) { const uid = React.useId().replace(/[:]/g, ''); const cx = 50, cy = 50, rOut = 47; const ang = (i) => -Math.PI / 2 + i * (Math.PI / 3); const poly = (r) => Array.from({ length: 6 }, (_, i) => [cx + Math.cos(ang(i)) * r, cy + Math.sin(ang(i)) * r].map(n => n.toFixed(2)).join(',')).join(' '); const rim = 'color-mix(in srgb, var(--r-rooy) 42%, transparent)'; const spoke = 'color-mix(in srgb, var(--r-rooy) 26%, transparent)'; return ( {/* rooy 辉光底盘 — 同 01q,浮起感(更弱) */} {/* 满量程骨架:极淡填底 + 虚线 rim(pending)+ 6 轴心连线 */} {Array.from({ length: 6 }, (_, i) => { const a = ang(i); return ; })} ); } /* EmptySkyline — 01a 雪季卡的空 skyline:24 根 hair2 细条(全空周), 对位 01q SeasonSkylineF 的 24 格节奏,但无任何 data 色——读作「地面 已画好、还没滑过」。 */ function EmptySkyline() { return (
{Array.from({ length: 24 }, (_, i) => (
))}
); } /* SeasonCardEmpty — 01a 雪季卡:照搬 01q SeasonCardF 结构(SectionTitle + 白卡 + 3-up 统计 + skyline + 最近一次),每格降级到空态——统计 «—»、 skyline 全空、最近一次 = 还未开始。tap → 09 history。 */ function SeasonCardEmpty({ onNav }) { return ( onNav && onNav('history')}> 雪季
onNav && onNav('history')} style={{ padding: '16px 20px', cursor: 'pointer' }}>
11 月 4 月
最近一次 还未开始
); } /* IQHeroEmpty — 01a 的 ROOY 指数 hero,照搬 01q IQHeroQ 的三段(pill 行 + 主体 + 浮白卡),无数据态: - 主体:GhostGem 居中(占 01q 里 173 数字 + 徽章的视觉位置)+ 下方铭牌 「六项能力 / 第一趟后测出」。 - 浮白卡:单行 ROOY 号召(rooy pip + 一句话),替 01q 的立刃/压力 + 判断行——新用户还没有分项可展示。 tap 整块 → presession(开始第一趟),不是 review(还没得复盘)。 */ function IQHeroEmpty({ onNav }) { return (
onNav && onNav('presession')} style={{ cursor: 'pointer' }}>
ROOY 指数
六项能力
第一趟后测出

第一趟滑行后,ROOY 给你第一份指数

); } /* LearningCardEmpty — 01a 正在学习卡:照搬 01q LearningCardF(SectionTitle + 白卡 + 课程头 + SegmentedProgress + 说明),空态绑定 onboarding 选的 立刃路径,doneCount=0(将开始)。tap → 06 course。 */ function LearningCardEmpty({ onNav }) { return ( onNav && onNav('course')}>正在学习
onNav && onNav('course')} style={{ cursor: 'pointer' }}>
立刃路径 ROOY 推荐

起步时把更多重心放在脚趾,感受雪板的反馈

); } /* HomeScreenFirstRun — 01a 首次登录空状态。 骨架与 01q HomeScreenQ 1:1:header → 雪季 → ROOY 指数 hero → 正在学习, 每块换成空态变体(SeasonCardEmpty / IQHeroEmpty / LearningCardEmpty)。 */ function HomeScreenFirstRun({ onNav }) { return (
{/* ── Header — 同 01q ────────────────── */}

5 月 3 日 · 周五

你好,Yuki

onNav && onNav('settings')} style={{ marginTop: 12 }}/>
onNav && onNav(tab)}/>
); } Object.assign(window, { HomeScreenFirstRun, SegmentedProgress, SeasonStreak, TickUp, GhostGem, EmptySkyline, SeasonCardEmpty, IQHeroEmpty, LearningCardEmpty, });