/* ============================================================
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 (
);
}
/* 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' }}>
最近一次
还未开始
);
}
/* 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 指数
›
);
}
/* 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 ────────────────── */}
onNav && onNav('settings')} style={{ marginTop: 12 }}/>
onNav && onNav(tab)}/>
);
}
Object.assign(window, {
HomeScreenFirstRun, SegmentedProgress, SeasonStreak, TickUp,
GhostGem, EmptySkyline, SeasonCardEmpty, IQHeroEmpty, LearningCardEmpty,
});