Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions docs/frontend-redesign.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@
- [x] `/` 精简 landing,`/notebooks` 承接登录后笔记本库
- [x] `dashboard`、`library` 保留 redirect
- [x] 「新建笔记本」panel 复用 `CreateNotePanel`
- [ ] 确认 `FluidBG`/`ParticleBG` 无引用后删除
- [x] 确认 `FluidBG`/`ParticleBG` 无引用后删除

**P1 · 三栏工作台** ✅ 主体完成
- [x] `notes/[id]/page.tsx` 收敛到 `NoteWorkspace`
Expand All @@ -97,8 +97,8 @@
- [x] 公开导航不再暴露 `/mm-ablation` 内部实验页入口
- [x] 移动端 `ChatPanel` 回到内容流,避免 sticky 遮挡正文
- [ ] 浏览器 smoke:视频播放、seek、分享、导出、QA 轮询、书签同步
- [ ] a11y:三栏 landmark、章节列表 `aria-current`、Lighthouse a11y ≥ 90
- [ ] 删除确认无引用的旧视觉组件和 `AskBar`
- [ ] a11y:三栏 landmark、章节列表 `aria-current`、Lighthouse a11y ≥ 90(章节 `aria-current` 已补齐)
- [x] 删除确认无引用的旧视觉组件和 `AskBar`
- 验收:`npx tsc --noEmit` 通过;浏览器 smoke 记录写入 `docs/memory/`

## 4.1 2026-06-25 二次升级计划:电影感首页 + 品牌系统 + 沉浸式登录
Expand Down Expand Up @@ -141,6 +141,7 @@
- 修复移动端内容优先级:视频不能长期挤占首屏,笔记内容和提问入口要更早出现。
- 问答入口避免遮挡正文,移动端改成稳定的底部输入或内容流内卡片。
- 章节抽屉补齐焦点管理、`aria-modal` 语义和关闭后的焦点恢复。
- 章节列表使用 `aria-current="location"` 标记当前主章节和当前子章节,帮助读屏器理解播放位置。
- 公开导航不暴露内部实验页,`/mm-ablation` 保留为直接访问或开发入口。(已完成)
- 继续清理 lint、无效 eslint-disable、废弃组件引用和图片加载 warning。

Expand Down
2 changes: 1 addition & 1 deletion web/components/ChapterNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export default function ChapterNav({ chapters, currentIdx, currentTime, onSeek }
key={i}
ref={active ? activeRef : undefined}
onClick={() => onSeek(ch.start)}
aria-current={active ? "true" : undefined}
aria-current={active ? "location" : undefined}
whileTap={{ scale: 0.97 }}
whileHover={{ y: -1 }}
className={`relative shrink-0 flex min-h-11 items-center gap-2 pl-2 pr-3 py-2 rounded-2xl text-xs
Expand Down
3 changes: 2 additions & 1 deletion web/components/ChapterRail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export default function ChapterRail({
<button
ref={active ? activeRef : undefined}
onClick={() => onSeek(ch.start)}
aria-current={active ? "true" : undefined}
aria-current={active ? "location" : undefined}
className={`relative w-full overflow-hidden rounded-xl px-3 py-2 text-left transition-colors
${showProgress ? "pr-9" : ""}
${active
Expand Down Expand Up @@ -107,6 +107,7 @@ export default function ChapterRail({
<button
key={si}
onClick={() => onSeek(sub.start)}
aria-current={subActive ? "location" : undefined}
className={`rounded-lg px-2 py-1.5 text-left text-xs leading-snug transition-colors
${subActive
? "text-[var(--accent)]"
Expand Down
91 changes: 91 additions & 0 deletions web/components/__tests__/ChapterNavigation.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { cleanup, render, screen, within } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";

import ChapterNav from "../ChapterNav";
import ChapterRail from "../ChapterRail";
import type { Chapter } from "@/lib/types";

vi.mock("framer-motion", () => ({
motion: {
button: ({
children,
whileHover: _whileHover,
whileTap: _whileTap,
...props
}: React.ButtonHTMLAttributes<HTMLButtonElement> & {
whileHover?: unknown;
whileTap?: unknown;
}) => {
void _whileHover;
void _whileTap;
return <button {...props}>{children}</button>;
},
},
}));

const chapters: Chapter[] = [
{
title: "Intro",
start: 0,
end: 60,
indices: [],
children: [
{ title: "Opening", start: 0, end: 20, indices: [] },
{ title: "Setup", start: 20, end: 60, indices: [] },
],
},
{ title: "Deep dive", start: 60, end: 120, indices: [] },
];

describe("chapter navigation accessibility", () => {
afterEach(() => {
cleanup();
});

it("marks the active vertical chapter and child as the current location", () => {
window.HTMLElement.prototype.scrollIntoView = vi.fn();

render(
<ChapterRail
chapters={chapters}
currentIdx={0}
currentTime={25}
onSeek={() => {}}
/>
);

expect(screen.getByRole("button", { name: /00:00Intro/ })).toHaveAttribute(
"aria-current",
"location"
);
expect(screen.getByRole("button", { name: /00:20Setup/ })).toHaveAttribute(
"aria-current",
"location"
);
expect(screen.getByRole("button", { name: /01:00Deep dive/ })).not.toHaveAttribute(
"aria-current"
);
});

it("marks the active horizontal chapter as the current location", () => {
window.HTMLElement.prototype.scrollIntoView = vi.fn();

render(
<ChapterNav
chapters={chapters}
currentIdx={1}
currentTime={80}
onSeek={() => {}}
/>
);

const nav = screen.getByRole("navigation", { name: "章节" });
expect(within(nav).getByRole("button", { name: /Deep dive/ })).toHaveAttribute(
"aria-current",
"location"
);
expect(within(nav).getByRole("button", { name: /Intro/ })).not.toHaveAttribute(
"aria-current"
);
});
});
Loading