/* ============================================================
ROOY · HOME · 雪季/学习卡共享段(01q 复用)
───────────────────────────────────────────────────────────
历史:本文件曾是 01e-01h 一族 home 变体的家(IQ 三格 / 01-hero /
淡底块 / 能力提升 hero),用 HomeBody + 可换统计块并排比较。那批
变体(含 01g HomeScreenH)已全部删除;本文件现仅保留 01q
(home-q.jsx)复用的 SeasonCardF / LearningCardF / SeasonStatsOriginal。
屏序(用户口味):雪季置顶 → 能力提升 → 正在学习。
能力提升结构(单 InsetGroup):
① 关键数据 two-up 大数字(iOS Fitness/Health 口径):
· 立刃 84 ↑11 —— 本周期突破(ROOY 判断的对象)
· 压力 71 ↑3 —— 第二正向信号,稳步上行
② ROOY 判断 cell:只说回顾侧结论(立刃稳了),不提下一步。
accent budget:成长块唯一 accent = data 蓝(spark + delta + hero
数字压 ink);rooy = 判断 pip;gold 0。
跨屏数据:立刃 84↑11 / 压力 71↑3 = review.jsx CAPS 前二。
★ 对外导出:SeasonCardF / LearningCardF / SeasonStatsOriginal 供
01k(home-l.jsx)复用——babel 脚本间只有 window 上的名字可见,
HTML 加载顺序:本文件在 home-l.jsx 之前。
依赖 home-v4.jsx helpers(SegmentedProgress / TickUp),加载在前。
============================================================ */
/* CARD_SHADOW — 现已提为共享 token Shadow.cardSoft(纯柔和 drop、无
描边)。保留本地别名让下方引用不变。 */
const CARD_SHADOW = Shadow.cardSoft;
/* HomeBody — 01g 共享屏体。统计块经 SeasonStats prop 注入(现只剩
淡底块 SeasonStatsChip 一种)。 */
function HomeBody({ onNav, SeasonStats }) {
return (
{/* ── Header — 同 01 ───────────────────── */}
onNav && onNav('settings')} style={{ marginTop: 12 }}/>
{/* ── 雪季白卡(置顶)── 3-up 统计 + skyline + 最近一次;整卡 → 09 */}
{/* ── 能力提升(回顾侧)── 当前水平内联行 + 2 关键数据 + ROOY 判断;整组 → 05 */}
onNav && onNav('review')}>
能力提升
onNav && onNav('review')} style={{ cursor: 'pointer' }}>
{/* ① 两个关键数据:Fitness/Health 式 two-up 大数字(卡片开场,
原「当前水平」锚点已移除——那条质化水平去 05 复盘看)。 */}
{/* ② ROOY 判断:只说回顾侧结论,别复读正在学习 */}
{/* ── 正在学习(展望侧)── 课程头 + 4 步状态列表;整组 → 06 */}
onNav && onNav(tab)}/>
);
}
/* KeyStatF — 成长组的单个关键数据 cell(two-up 其一)。
竖排:维度名 + 定性 tag → hero 数字 + delta → sparkline。
数字是主角(N.hero 32 ink 压阵),delta 是成长信号(data 蓝),
spark 给趋势质感。两 cell 等宽 flex,中间一条竖 hair 分隔。 */
function KeyStatF({ name, tag, value, delta, spark, style = {} }) {
return (
{name}
{tag}
{value}
{delta}
);
}
/* StepRowF — 正在学习组的步骤行,三态:
done — ✓(ink3 中性)+ 名 ink3 receded + 尾「已完成」
active — rooy 光点(带 halo) + 名 ink semibold + 极淡 rooy 底色 + 尾描述 ink2
upcoming — 序号(ink4) + 名 ink2 + 尾描述 ink3
左标记 = 同尺寸(18px)圆形图标系统,三态成套而非三种画法:
done — systemFill 浅底圆 + ink3 ✓(中性、轻)
active — rooy 描边环 + rooy 实心圆心(靶心「你在这」,唯一彩色点)
upcoming — hair2 描边空心圆 + 内嵌序号 ink4(未开始、静音)
rooy 只收在这颗 active 图标上;不加整行灰底(那条底色带读着像
pressed 态、反而怪)——当前步靠 rooy 靶心图标 + 名加粗点出即可。 */
function StepRowF({ n, name, trailing, state }) {
const done = state === 'done';
const active = state === 'active';
return (
{done ? (
) : active ? (
) : (
{n}
)}
{name}
{trailing}
);
}
/* SEASON_KMS_F — 同 01d:24 周里程,总和 = 64.8 对齐 history.jsx。 */
const SEASON_KMS_F = [
0, 3.4, 0, 4.1, 6.8, 0, 5.2, 0, 7.6, 0, 0, 2.8,
0, 5.9, 0, 4.6, 0, 6.4, 0, 5.3, 0, 4.3, 0, 8.4,
];
/* SeasonSkylineF — 雪季周里程 ridge。有滑的周 = 实 data 蓝柱;空周 =
浅 data 蓝地面条(同色系低浓度,让整条 skyline 都在蓝色家族里,不
再掺灰)。空周浓度 ~15% 足以读作「地面」又不与实柱抢。 */
const SKYLINE_OFF = 'color-mix(in srgb, var(--r-data) 15%, transparent)';
function SeasonSkylineF({ kms, height = 36 }) {
const max = Math.max(...kms) || 1;
return (
{kms.map((km, i) => {
const on = km > 0;
return (
);
})}
);
}
/* TrendSparkF — 迷你趋势曲线(data 蓝,渐变面积填充 + 平滑曲线 +
端点光晕)。视觉沿用 01 CapSpotlightCard:面积 fill 上 0.18 → 下
0.02 竖向渐变、中点二次贝塞尔平滑、端点 halo 圈。
但 y 轴映射不抄 01——01 各自归一到自身 min/max 会把 ↑3 画得和
↑11 一样陡(视觉撒谎);这里保留固定跨度(≥12 分、中心对齐),
让 立刃(↑11) 明显比 压力(↑3) 陡,two-up 并排时诚实可比。
宽度用 ref 实测(非 preserveAspectRatio 拉伸)——否则端点 halo 圆
会被横向拉成椭圆。React.useId 给 gradient 一个安全的唯一 id。 */
function TrendSparkF({ values, h = 44, color = R.data }) {
const ref = React.useRef(null);
const [w, setW] = React.useState(150);
React.useLayoutEffect(() => {
const el = ref.current;
if (!el) return;
const measure = () => { const cw = el.clientWidth; if (cw) setW(cw); };
measure();
const ro = new ResizeObserver(measure);
ro.observe(el);
return () => ro.disconnect();
}, []);
const min = Math.min(...values), max = Math.max(...values);
const span = Math.max(max - min, 12);
const mid = (min + max) / 2;
const padY = 7, padX = 3;
const pts = values.map((v, i) => ({
x: padX + (i / (values.length - 1)) * (w - padX * 2),
y: padY + (1 - ((v - mid) / span + 0.5)) * (h - padY * 2),
}));
// 中点二次贝塞尔平滑(同 01)
let path = `M ${pts[0].x} ${pts[0].y}`;
if (pts.length === 2) {
path += ` L ${pts[1].x} ${pts[1].y}`;
} else {
const m1x = (pts[0].x + pts[1].x) / 2, m1y = (pts[0].y + pts[1].y) / 2;
path += ` L ${m1x} ${m1y}`;
for (let i = 1; i < pts.length - 1; i++) {
const mx = (pts[i].x + pts[i + 1].x) / 2, my = (pts[i].y + pts[i + 1].y) / 2;
path += ` Q ${pts[i].x} ${pts[i].y}, ${mx} ${my}`;
}
path += ` L ${pts[pts.length - 1].x} ${pts[pts.length - 1].y}`;
}
const last = pts[pts.length - 1];
const gid = 'sparkf-' + React.useId().replace(/[:]/g, '');
return (
);
}
/* SeasonStatsChip(01g)— 雪季卡统计块:三格 space-between,ROOY 指数
包在一枚极淡 rooy 底色圆角块里(highlighted tile),和左边两个中性
统计分层。屏体其余部分共享 HomeBody。 */
function SeasonStatsChip() {
return (
);
}
/* ── 可复用卡片段(本文件 HomeBody 01g + home-l.jsx 01k 共用,零重复) ── */
/* SeasonCardF — 雪季卡:SectionTitle + 白卡(统计块 + skyline + 最近
一次)。统计块可换(Stats prop)。 */
function SeasonCardF({ onNav, Stats }) {
return (
onNav && onNav('history')}>
雪季
onNav && onNav('history')} style={{ padding: '16px 20px', cursor: 'pointer' }}>
最近一次
3 月 30 日 · Zermatt · 8.4 km
);
}
/* LearningCardF — 正在学习卡:课程头 + 4 步状态列表。 */
function LearningCardF({ onNav }) {
return (
onNav && onNav('course')}>正在学习
onNav && onNav('course')} style={{ cursor: 'pointer' }}>
);
}
/* SeasonStatsOriginal — 雪季卡「原始无 IQ」三格(滑行 / 里程 / 垂降),
space-between 排版(content-width、间距均匀)。01k 雪季卡复用。 */
function SeasonStatsOriginal() {
return (
} u="天"/>
);
}
/* —— IQ 拍平组件(原 home-n.jsx;01m 屏废弃后组件归入 home-f 共享库)——
home-q / home-q-en 复用。字体走 CSS 变量 var(--r-iq-font/-weight/
-style),Tweaks「IQ 字体」下拉全局切换。 */
/* IQFlat — 拍平的 ROOY 指数 数字。纯色 rooy、无任何打光/描边,字体族
与字重/斜体全部读 CSS 变量(Tweaks 可切)。静态值(不
count-up)——比字体用,动画留待方案定稿。不设 tabular-nums:Satoshi
本地字重的 tnum 变体「1」画法与默认比例数字不同(多一横底座),
会破坏字形;数字是静态值,不需要等宽保稳。 */
function IQFlat({ value = 76, size = 150, style = {} }) {
return (
{value}
);
}
/* StatColFlat — 浮卡分项列:数值 ink,delta data 蓝。 */
function StatColFlat({ name, tag, value, delta, style = {} }) {
return (
{name}
{tag}
{value}
{delta}
);
}
/* IQJudgeRow — ROOY 判断行(rooy pip + 结论)。 */
function IQJudgeRow() {
return (
);
}
/* SeasonCardF / LearningCardF / SeasonStatsOriginal + IQ 拍平组件一并导出
—— babel 脚本间只有 window 上的名字可见。 */
Object.assign(window, {
SeasonCardF, LearningCardF, SeasonStatsOriginal,
IQFlat, StatColFlat, IQJudgeRow,
});