/* ============================================================
ROOY · 03b 滑行中 · 自由模式
───────────────────────────────────────────────────────────
需求:地图全屏 + 当前滑行的轻反馈,ROOY 偷懒中不主动出声,
用户上拉 sheet 才看到详细。
结构:basemap → peek sheet(170px:scope + 3 stat + ROOY 一行)
→ 上拉 expanded(550px:速度曲线 / 能力 / 左右脚 / 累计)。
stop button 浮在 peek 顶 14px 上方,tap 进 03d 暂停态;结束走 03d
的两步确认(继续 / 结束 pill),不再长按直结束。
italic anchor:ROOY 反馈内联数字(0.08,标识"重心切换偏快")。
accent:ROOY 绿(peek 内 RooyLine dot 一次)。
Anti-pattern:peek↔expanded 之间**不**画 hairline 分隔——hairline
只用于列表 cell 之间,区块切换靠 spacing。
============================================================ */
function SkiScreenB({ onNav, defaultExpanded = false, paused = false }) {
const [expanded, setExpanded] = React.useState(defaultExpanded);
const sheetH = expanded ? 550 : 170;
return (
{/* basemap — 全屏铺底;trail 曲线与高度标记不变。底色是冷调
雪面浅灰蓝(one-off,basemap 不属于 surface token 体系),
与 ink 冷灰 / data 蓝 / rooy 青同一色温. */}
{paused ? (
/* 03d · 暂停态 · 居中 pill pair(继续 + 结束)。pill 高 56
= 03b 圆形 stop 同直径,视觉重量一致。移植自 zhutty ski-b。 */
) : (
/* 03b · 滑行中 · 右下 56pt 圆形 stop。tap → 暂停 push 03d。
固定在 peek 高度上沿 14px,展开时 sheet 升上来会盖住它(用户
故意要的效果——展开就是在看详细数据,不需要再看 stop 入口)。 */
)}
{/* Sheet —— 圆角对称 B.sheetL(ski 常驻 panel 档)*/}
{/* drag handle —— 紧凑行 (03c 同款) */}
onNav && onNav(t)}/>
);
}
/* SheetBody —— peek (caption + 3 stat + ROOY) → divider → expanded 4 段。
peek 顺序按"数据 → 教练"读:caption 告诉你这是哪趟、数字摆出来、
ROOY 在下面给一句话 verdict。展开后再细看四块。 */
function SheetBody({ expanded }) {
return (
<>
{/* ── PEEK · 1. trip scope — "label ⇄ trip name" space-between,
让 "刚刚那趟" 视觉上读作 scope label,不像一句小标题. */}
刚刚那趟
Furi 蓝道
{/* ── PEEK · 2. 3 stat — flex 1 等宽 + 列间 vertical hairline.
跟 03c peek 一致, 让"刚刚那趟"的三个核心数据像 dashboard
一样分列读. */}
{/* ── PEEK · 3. ROOY 反馈 — dot inline 跟文字中部对齐 ── */}
右弯入弯快了 0.08s,比昨天稳。
{/* ── EXPANDED · 3 段,平铺无卡 ─────────────────
顺序:速度曲线 → 左右脚 → 今日累计。
peek↔expanded 不用 hairline,第一段 first padding-top 做气口. */}
{/* 左右脚 —— bar + 50% 中线 reference 让"偏左/偏右"秒读,
比纯数字更直观。fill 弱化到 ink3 (原 ink2 太重),中线
ink2 作为强 reference 锚 50%. "偏左 4" 作为 sub 是 ROOY
主动 verdict,取代了平淡的 "重心分配". */}
{/* "今日累计" 是另一个时间尺度——前 3 段都是本趟,这段是当日。
时间尺度切换靠加大 spacing 表达(gap=40)。
4 项用 2x2 grid,让每项有呼吸——一行 4 项太挤. */}
>
);
}
/* Section —— 平铺段,靠 spacing + caption 自立;不用白卡。
caption 是 T.eyebrow + ink3,副 sub 是 T.footnote + ink4。
first:peek↔expanded 不用 hairline 时,第一段顶气口拉大成 32。
gap:标记时间尺度切换的段(本趟 → 今日),顶气口拉大成 40. */
function Section({ label, sub, first, last, gap, children }) {
const top = first ? 32 : gap ? 40 : 22;
return (
{label}
{sub && {sub}}
{children}
);
}
/* StatCol —— peek 的 3 列数字。label footnote 普通中文小字(不再
uppercase),自然左对齐让三个 stat 跟 ski (03) 的 Stat 一致。 */
function StatCol({ label, value, unit }) {
return (
);
}
/* DayStat —— 今日累计 inline 项:label 灰小 + 数字 size 18 + unit 灰小。
靠 parent flex 的 space-between 分散,无需 dot 分隔符. */
function DayStat({ l, v, u }) {
return (
{l}
{v}
{u && {u}}
);
}
/* ── 速度曲线 sparkline —— 单色 data 折线 + dot-grid 底。
数据按"弯"分组:8 段 turns,每段是一个弯的 speed samples。弯之
间用 dashed vertical line 标分界——让 sub "8 弯" 字面可见,
而不只是"一根曲线"。不高亮最快弯,所有弯平权. */
function SpeedSparkline() {
const turns = [
[18, 28, 36, 30, 20, 14, 12],
[22, 36, 50, 55, 46, 30, 20, 16, 18],
[28, 46, 62, 68, 58, 40, 28, 22, 30],
[38, 48, 50, 40, 22, 14, 12],
[26, 46, 64, 72, 60, 40, 28, 24, 30],
[36, 42, 45, 36, 22, 16, 14],
[28, 44, 58, 60, 50, 32, 22, 18],
[24, 28, 32, 26, 22, 20],
];
const speeds = turns.flat();
// 边界点索引(每个弯起始位置,含尾边界)→ 内部分割线落在
// (boundary - 0.5) 处,画在两个弯之间.
const boundaries = turns.reduce(
(acc, t) => [...acc, acc[acc.length - 1] + t.length],
[0],
).slice(1, -1); // 去掉首尾,只留 7 条内部分隔
const min = Math.min(...speeds), max = Math.max(...speeds);
const range = max - min || 1;
const w = 326, h = 80, padTop = 14, padBot = 10, padX = 6;
const yRange = h - padTop - padBot;
const xRange = w - padX * 2;
const xAt = (i) => padX + (i / (speeds.length - 1)) * xRange;
const pts = speeds.map((v, i) => ({
x: xAt(i),
y: padTop + (1 - (v - min) / range) * yRange,
}));
const path = pts.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x.toFixed(2)} ${p.y.toFixed(2)}`).join(' ');
const last = pts[pts.length - 1];
return (
);
}
Object.assign(window, { SkiScreenB });