feat: expose stable journey intent signals
This commit is contained in:
@@ -11,6 +11,7 @@ import type { SectionId } from '@/content/types';
|
||||
import type { NavigationState } from '@/journey/navigation-controller';
|
||||
import {
|
||||
activationLineDocumentY,
|
||||
deriveSectionActivationSnapshot,
|
||||
deriveSectionState,
|
||||
isAtDocumentEnd,
|
||||
selectActiveSection,
|
||||
@@ -30,13 +31,26 @@ const initialJourney = (activeSectionId: SectionId): JourneySnapshot => ({
|
||||
driveSpeed: 0.18,
|
||||
...deriveSectionState(activeSectionId),
|
||||
sign: null,
|
||||
sectionActivation: {
|
||||
layoutRevision: 0,
|
||||
activeHeadingOffsetVh: 0,
|
||||
nextHeadingOffsetVh: null,
|
||||
},
|
||||
});
|
||||
|
||||
export interface JourneySectionsBinding {
|
||||
readonly snapshot: Readonly<Ref<JourneySnapshot>>;
|
||||
requestFrame(): void;
|
||||
subscribeFrame(
|
||||
listener: (now: number, snapshot: JourneySnapshot) => boolean,
|
||||
): () => void;
|
||||
}
|
||||
|
||||
export function useJourneySections(
|
||||
canonicalIds: readonly SectionId[],
|
||||
navigationState: Readonly<Ref<NavigationState>>,
|
||||
motionAllowed?: Readonly<Ref<boolean>>,
|
||||
): Readonly<Ref<JourneySnapshot>> {
|
||||
): JourneySectionsBinding {
|
||||
const firstId = canonicalIds[0];
|
||||
if (firstId === undefined) {
|
||||
throw new Error('At least one canonical journey section is required.');
|
||||
@@ -54,6 +68,10 @@ export function useJourneySections(
|
||||
}));
|
||||
let requestJourneyFrame = (): void => {};
|
||||
let teardown = (): void => {};
|
||||
const frameSubscribers = new Set<(
|
||||
now: number,
|
||||
snapshot: JourneySnapshot,
|
||||
) => boolean>();
|
||||
|
||||
const stopNavigationWatch = watch(
|
||||
() => navigationState.value,
|
||||
@@ -67,6 +85,7 @@ export function useJourneySections(
|
||||
const visualViewport = window.visualViewport;
|
||||
let disposed = false;
|
||||
let structureDirty = true;
|
||||
let layoutRevision = 0;
|
||||
let previousScrollY = drive.snapshot.value.scrollY;
|
||||
let previousActiveId = firstId;
|
||||
let headings: readonly SectionHeadingGeometry[] = [];
|
||||
@@ -86,6 +105,7 @@ export function useJourneySections(
|
||||
if (disposed) return false;
|
||||
if (structureDirty) {
|
||||
headings = readHeadings(driveSnapshot.scrollY);
|
||||
layoutRevision += 1;
|
||||
structureDirty = false;
|
||||
}
|
||||
if (headings.length === 0) return false;
|
||||
@@ -118,6 +138,13 @@ export function useJourneySections(
|
||||
? null
|
||||
: (candidateHeading.top - activationY) / viewportHeight;
|
||||
const sectionState = deriveSectionState(activeSectionId);
|
||||
const sectionActivation = deriveSectionActivationSnapshot({
|
||||
headings,
|
||||
activeSectionId,
|
||||
activationY,
|
||||
viewportHeight,
|
||||
layoutRevision,
|
||||
});
|
||||
const sign = signController.sample({
|
||||
now,
|
||||
source: navigationState.value.source,
|
||||
@@ -139,11 +166,17 @@ export function useJourneySections(
|
||||
...driveSnapshot,
|
||||
routeProgress: atDocumentEnd ? 1 : driveSnapshot.routeProgress,
|
||||
...sectionState,
|
||||
sectionActivation,
|
||||
sign,
|
||||
};
|
||||
previousScrollY = driveSnapshot.scrollY;
|
||||
previousActiveId = activeSectionId;
|
||||
return signController.needsFrame();
|
||||
const currentSnapshot = snapshot.value;
|
||||
let keepRunning = signController.needsFrame();
|
||||
for (const subscriber of [...frameSubscribers]) {
|
||||
keepRunning = subscriber(now, currentSnapshot) || keepRunning;
|
||||
}
|
||||
return keepRunning;
|
||||
};
|
||||
|
||||
const removeFrameSubscriber = drive.subscribeFrame(measure);
|
||||
@@ -199,6 +232,7 @@ export function useJourneySections(
|
||||
document.removeEventListener('visibilitychange', handleVisibility, PASSIVE_LISTENER);
|
||||
resizeObserver.disconnect();
|
||||
signController.reset();
|
||||
frameSubscribers.clear();
|
||||
};
|
||||
});
|
||||
|
||||
@@ -208,5 +242,12 @@ export function useJourneySections(
|
||||
teardown();
|
||||
});
|
||||
|
||||
return readonly(snapshot);
|
||||
return {
|
||||
snapshot: readonly(snapshot),
|
||||
requestFrame: () => requestJourneyFrame(),
|
||||
subscribeFrame(listener) {
|
||||
frameSubscribers.add(listener);
|
||||
return () => frameSubscribers.delete(listener);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ export function useKeyboardNavigation(
|
||||
const state = shallowRef<NavigationState>({
|
||||
epoch: 0,
|
||||
source: "initial",
|
||||
phase: "idle",
|
||||
destinationId: null,
|
||||
signDestinationId: null,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user