feat: expose stable journey intent signals

This commit is contained in:
2026-09-04 15:25:35 +03:00
parent c73410a956
commit 7632c8b9c0
12 changed files with 197 additions and 9 deletions

View File

@@ -5,11 +5,14 @@ declare const __PORTFOLIO_TEST_BUILD__: boolean;
const TEST_BUILD =
typeof __PORTFOLIO_TEST_BUILD__ !== "undefined" && __PORTFOLIO_TEST_BUILD__;
export type NavigationPhase = "idle" | "travelling" | "settled";
export interface NavigationState {
epoch: number;
source: JourneySource;
destinationId: SectionId | null;
signDestinationId: SectionId | null;
readonly epoch: number;
readonly source: JourneySource;
readonly phase: NavigationPhase;
readonly destinationId: SectionId | null;
readonly signDestinationId: SectionId | null;
}
export interface NavigationRuntime {
@@ -122,6 +125,7 @@ export function createNavigationController(
let currentState: NavigationState = {
epoch: 0,
source: "initial",
phase: "idle",
destinationId: null,
signDestinationId: null,
};
@@ -171,6 +175,7 @@ export function createNavigationController(
currentState = {
epoch: currentState.epoch + 1,
source,
phase: source === "passive-scroll" ? "idle" : "travelling",
destinationId,
signDestinationId,
};
@@ -211,6 +216,8 @@ export function createNavigationController(
)
return;
cancelSettle();
currentState = { ...currentState, phase: "settled" };
emit();
frameId = runtime.requestFrame(() => {
frameId = undefined;
if (
@@ -284,6 +291,14 @@ export function createNavigationController(
const epoch = beginEpoch(source, id);
if (source === "initial") {
runtime.scrollTarget(target, "instant");
if (
currentState.epoch === epoch &&
currentState.source === source &&
currentState.destinationId === id
) {
currentState = { ...currentState, phase: "settled" };
emit();
}
} else {
frameId = runtime.requestFrame(() => {
frameId = undefined;
@@ -293,6 +308,8 @@ export function createNavigationController(
currentState.destinationId === id
) {
runtime.scrollTarget(target, "instant");
currentState = { ...currentState, phase: "settled" };
emit();
}
});
}

View File

@@ -1,5 +1,5 @@
import type { SectionId } from "@/content/types";
import type { EnvironmentChapter } from "./types";
import type { EnvironmentChapter, SectionActivationSnapshot } from "./types";
export interface SectionHeadingGeometry {
id: SectionId;
@@ -12,6 +12,14 @@ export interface ActiveSectionInput {
atDocumentEnd: boolean;
}
export interface SectionActivationInput {
readonly headings: readonly SectionHeadingGeometry[];
readonly activeSectionId: SectionId;
readonly activationY: number;
readonly viewportHeight: number;
readonly layoutRevision: number;
}
export interface DerivedSectionState {
activeSectionId: SectionId;
chapter: EnvironmentChapter;
@@ -56,6 +64,31 @@ export function selectActiveSection(input: ActiveSectionInput): SectionId {
return active;
}
export function deriveSectionActivationSnapshot(
input: SectionActivationInput,
): SectionActivationSnapshot {
const activeIndex = input.headings.findIndex(
(heading) => heading.id === input.activeSectionId,
);
const activeHeading = input.headings[activeIndex];
if (activeHeading === undefined || input.viewportHeight <= 0) {
return {
layoutRevision: input.layoutRevision,
activeHeadingOffsetVh: 0,
nextHeadingOffsetVh: null,
};
}
const nextHeading = input.headings[activeIndex + 1];
return {
layoutRevision: input.layoutRevision,
activeHeadingOffsetVh:
(activeHeading.top - input.activationY) / input.viewportHeight,
nextHeadingOffsetVh: nextHeading === undefined
? null
: (nextHeading.top - input.activationY) / input.viewportHeight,
};
}
const chapters: Readonly<Record<SectionId, EnvironmentChapter>> = {
start: "deep-night",
profile: "deep-night",

View File

@@ -66,9 +66,16 @@ export interface SignController {
needsFrame(): boolean;
}
export interface SectionActivationSnapshot {
readonly layoutRevision: number;
readonly activeHeadingOffsetVh: number;
readonly nextHeadingOffsetVh: number | null;
}
export interface JourneySnapshot extends ScrollDriveSnapshot {
activeSectionId: SectionId;
chapter: EnvironmentChapter;
sign: SignSnapshot | null;
arrived: boolean;
sectionActivation: SectionActivationSnapshot;
}