Align the notebook browser with the physical LCD, refresh real project captures and navigation, simplify service content, and stabilize hero hydration.
This commit is contained in:
1 parent
e2b20bc699
commit
8b12e51bc5
35 files changed
+2359
-1299
No files matched your search
@@ -1,126 +1,75 @@
|
||||
import { getNotebookFrame } from "./notebookTimeline";
|
||||
import { getNotebookFrame, getNotebookSeekProgress } from "./notebookTimeline";
|
||||
|
||||
describe("getNotebookFrame", () => {
|
||||
it("clamps progress below zero and starts with a closed device", () => {
|
||||
expect(getNotebookFrame(-1, 4)).toMatchObject({
|
||||
phase: "approach",
|
||||
lidAngle: 0,
|
||||
activeTabIndex: -1,
|
||||
cursorVisible: false,
|
||||
});
|
||||
describe("notebook choreography", () => {
|
||||
it("opens and boots before browsing, then blanks the display before closing", () => {
|
||||
const frames = Array.from({ length: 1001 }, (_, index) =>
|
||||
getNotebookFrame(index / 1000, 4),
|
||||
);
|
||||
const phases = [...new Set(frames.map((frame) => frame.phase))];
|
||||
expect(phases).toEqual([
|
||||
"approach",
|
||||
"open",
|
||||
"boot",
|
||||
"cursor-in",
|
||||
"browse",
|
||||
"hold",
|
||||
"close",
|
||||
]);
|
||||
for (const frame of frames) {
|
||||
if (frame.phase === "open") expect(frame.screenOpacity).toBe(0);
|
||||
if (frame.cursorVisible) expect(frame.screenOpacity).toBe(1);
|
||||
if (frame.phase === "close" && frame.lidAngle < 112)
|
||||
expect(frame.screenOpacity).toBe(0);
|
||||
}
|
||||
expect(getNotebookFrame(-1, 4).lidAngle).toBe(0);
|
||||
expect(getNotebookFrame(2, 4).lidAngle).toBe(0);
|
||||
});
|
||||
|
||||
it("clamps progress above one and ends with a closed device", () => {
|
||||
expect(getNotebookFrame(2, 4)).toMatchObject({
|
||||
phase: "close",
|
||||
lidAngle: 0,
|
||||
activeTabIndex: -1,
|
||||
cursorVisible: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps the phase boundaries deterministic", () => {
|
||||
expect(getNotebookFrame(0.1, 4).phase).toBe("open");
|
||||
expect(getNotebookFrame(0.25, 4).phase).toBe("boot");
|
||||
expect(getNotebookFrame(0.34, 4).phase).toBe("cursor-in");
|
||||
expect(getNotebookFrame(0.44, 4).phase).toBe("browse");
|
||||
expect(getNotebookFrame(0.78, 4).phase).toBe("hold");
|
||||
expect(getNotebookFrame(0.87, 4).phase).toBe("close");
|
||||
});
|
||||
|
||||
it("opens the lid before the browser starts", () => {
|
||||
const frame = getNotebookFrame(0.22, 4);
|
||||
|
||||
expect(frame.phase).toBe("open");
|
||||
expect(frame.lidAngle).toBeGreaterThan(0);
|
||||
expect(frame.screenOpacity).toBe(0);
|
||||
});
|
||||
|
||||
it("boots the screen before the cursor enters", () => {
|
||||
expect(getNotebookFrame(0.25, 4).screenOpacity).toBe(0);
|
||||
expect(getNotebookFrame(0.34, 4).screenOpacity).toBe(1);
|
||||
expect(getNotebookFrame(0.4, 4).cursorVisible).toBe(true);
|
||||
});
|
||||
|
||||
it("turns the screen off before the lid closes", () => {
|
||||
const screenOff = getNotebookFrame(0.9, 4);
|
||||
const closing = getNotebookFrame(0.95, 4);
|
||||
|
||||
expect(screenOff).toMatchObject({
|
||||
phase: "close",
|
||||
lidAngle: 112,
|
||||
screenOpacity: 0,
|
||||
});
|
||||
expect(closing.screenOpacity).toBe(0);
|
||||
expect(closing.lidAngle).toBeGreaterThan(0);
|
||||
expect(closing.lidAngle).toBeLessThan(112);
|
||||
});
|
||||
|
||||
it("selects each tab only inside the browse window", () => {
|
||||
expect(getNotebookFrame(0.43, 4).activeTabIndex).toBe(-1);
|
||||
expect(getNotebookFrame(0.48, 4).activeTabIndex).toBe(0);
|
||||
expect(getNotebookFrame(0.57, 4).activeTabIndex).toBe(1);
|
||||
expect(getNotebookFrame(0.67, 4).activeTabIndex).toBe(2);
|
||||
expect(getNotebookFrame(0.76, 4).activeTabIndex).toBe(3);
|
||||
});
|
||||
|
||||
it("holds the final tab until the closing phase", () => {
|
||||
expect(getNotebookFrame(0.84, 4)).toMatchObject({
|
||||
phase: "hold",
|
||||
activeTabIndex: 3,
|
||||
cursorVisible: true,
|
||||
});
|
||||
expect(getNotebookFrame(0.88, 4)).toMatchObject({
|
||||
phase: "close",
|
||||
activeTabIndex: -1,
|
||||
cursorVisible: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("emits a deterministic cursor click pulse for each tab", () => {
|
||||
const first = getNotebookFrame(0.475, 4);
|
||||
const later = getNotebookFrame(0.56, 4);
|
||||
|
||||
expect(first.cursorClicking).toBe(true);
|
||||
expect(later.cursorClicking).toBe(true);
|
||||
expect(first.cursorX).toBeGreaterThanOrEqual(0);
|
||||
expect(first.cursorX).toBeLessThanOrEqual(1);
|
||||
expect(first.cursorY).toBeGreaterThanOrEqual(0);
|
||||
expect(first.cursorY).toBeLessThanOrEqual(1);
|
||||
});
|
||||
|
||||
it("moves the cursor between tab targets instead of teleporting", () => {
|
||||
const previous = getNotebookFrame(0.525, 4);
|
||||
const moving = getNotebookFrame(0.532, 4);
|
||||
const target = getNotebookFrame(0.56, 4);
|
||||
|
||||
expect(moving.cursorX).toBeGreaterThan(previous.cursorX);
|
||||
expect(moving.cursorX).toBeLessThan(target.cursorX);
|
||||
expect(moving.cursorY).toBe(target.cursorY);
|
||||
});
|
||||
|
||||
it("lands the click pulse on the center of the selected tab", () => {
|
||||
for (const progress of [0.475, 0.56, 0.65, 0.735]) {
|
||||
const frame = getNotebookFrame(progress, 4);
|
||||
expect(frame.cursorClicking).toBe(true);
|
||||
expect(frame.cursorX).toBeCloseTo((frame.activeTabIndex + 0.5) / 4, 6);
|
||||
it("keeps the previous project until cursor arrival, including reverse scroll", () => {
|
||||
const frames = Array.from({ length: 10001 }, (_, index) =>
|
||||
getNotebookFrame(index / 10000, 4),
|
||||
);
|
||||
for (let selected = 1; selected < 4; selected += 1) {
|
||||
const arrival = frames.findIndex(
|
||||
(frame) => frame.activeTabIndex === selected,
|
||||
);
|
||||
const before = getNotebookFrame((arrival - 1) / 10000, 4);
|
||||
const atClick = frames[arrival];
|
||||
expect(before.activeTabIndex).toBe(selected - 1);
|
||||
expect(before.cursorClicking).toBe(false);
|
||||
expect(atClick.cursorClicking).toBe(true);
|
||||
expect(atClick.cursorX).toBeCloseTo((selected + 0.5) / 4);
|
||||
const reversed = getNotebookFrame((arrival - 2) / 10000, 4);
|
||||
expect(reversed.activeTabIndex).toBe(selected - 1);
|
||||
expect(reversed.cursorX).toBeLessThan(atClick.cursorX);
|
||||
expect(reversed.cursorX).toBeGreaterThan((selected - 0.5) / 4);
|
||||
}
|
||||
});
|
||||
|
||||
it("tracks the real number of tabs", () => {
|
||||
expect(getNotebookFrame(0.68, 2).cursorX).toBeCloseTo(0.75, 6);
|
||||
expect(getNotebookFrame(0.6, 3).cursorX).toBeCloseTo(0.5, 6);
|
||||
it("seeks directly into a readable hold for the requested project", () => {
|
||||
for (const count of [1, 4]) {
|
||||
for (let index = 0; index < count; index += 1) {
|
||||
const progress = getNotebookSeekProgress(index, count);
|
||||
const frame = getNotebookFrame(progress, count);
|
||||
expect(frame.activeTabIndex).toBe(index);
|
||||
expect(frame.screenOpacity).toBe(1);
|
||||
expect(frame.cursorClicking).toBe(false);
|
||||
expect(getNotebookFrame(progress + 0.03, count).activeTabIndex).toBe(
|
||||
index,
|
||||
);
|
||||
}
|
||||
}
|
||||
expect(
|
||||
getNotebookFrame(getNotebookSeekProgress(99, 4), 4).activeTabIndex,
|
||||
).toBe(3);
|
||||
});
|
||||
|
||||
it("is deterministic when rewound to the same progress", () => {
|
||||
expect(getNotebookFrame(0.61, 4)).toEqual(getNotebookFrame(0.61, 4));
|
||||
});
|
||||
|
||||
it("does not expose an invalid tab when there are no projects", () => {
|
||||
it("does not expose a tab or cursor for empty or invalid project counts", () => {
|
||||
expect(getNotebookFrame(0.6, 0)).toMatchObject({
|
||||
activeTabIndex: -1,
|
||||
cursorVisible: false,
|
||||
cursorClicking: false,
|
||||
});
|
||||
expect(getNotebookFrame(0.6, Number.NaN).activeTabIndex).toBe(-1);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user