Skip to content

Commit cf51e04

Browse files
fix(story): remove stage bar and retune sync
1 parent 0173175 commit cf51e04

4 files changed

Lines changed: 62 additions & 103 deletions

File tree

index.html

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -207,34 +207,6 @@ <h2>Europe is not collapsing. Listed-market power moved elsewhere.</h2>
207207
</div>
208208

209209
<section class="map-panel story-stage" aria-label="Interactive story stage">
210-
<div class="section-kicker section-kicker--panel">
211-
<p class="eyebrow">Evidence explorer</p>
212-
<h2>Compare the story from four angles</h2>
213-
<p>
214-
Start with GDP to see that Europe is not standing still, then
215-
switch to ETF Price, Market Cap, and ACWI composition to see where
216-
the gap opens. Use countries for concrete examples, or regions for
217-
the broader pattern.
218-
</p>
219-
<div class="stage-guide" aria-label="Evidence graph steps">
220-
<button type="button" data-stage-jump="stage-map">
221-
<span>01</span>
222-
Map / scatter
223-
</button>
224-
<button type="button" data-stage-jump="stage-evolution">
225-
<span>02</span>
226-
2008 break
227-
</button>
228-
<button type="button" data-stage-jump="stage-trajectory">
229-
<span>03</span>
230-
Trajectory
231-
</button>
232-
<button type="button" data-stage-jump="stage-acwi">
233-
<span>04</span>
234-
ACWI sectors
235-
</button>
236-
</div>
237-
</div>
238210

239211
<section id="stage-map" class="evidence-step evidence-step--map" data-stage-step="stage-map" aria-label="Map and scatter evidence">
240212
<div class="control-bar">

src/story-controller.js

Lines changed: 61 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
const ACTIVE_CLASS = "story-step--active";
77
const STAGE_ACTIVE_CLASS = "evidence-step--active";
8+
const STORY_TRIGGER_RATIO = 0.7;
89

910
/**
1011
* Connects story steps and preset buttons to a chart preset callback.
@@ -18,6 +19,8 @@ export function createStoryController({ applyPreset }) {
1819
const stage = document.querySelector(".story-stage");
1920
const stageSteps = [...document.querySelectorAll("[data-stage-step]")];
2021
const stageJumpButtons = [...document.querySelectorAll("[data-stage-jump]")];
22+
let activeScrollPresetId = "";
23+
let scrollFrame = 0;
2124

2225
/**
2326
* Applies a preset and updates the active visual state.
@@ -54,24 +57,18 @@ export function createStoryController({ applyPreset }) {
5457
}
5558

5659
if (!("IntersectionObserver" in window)) {
57-
return { destroy() {} };
60+
syncActiveStoryStep();
61+
window.addEventListener("scroll", queueStorySync, { passive: true });
62+
window.addEventListener("resize", queueStorySync);
63+
return {
64+
destroy() {
65+
if (scrollFrame) window.cancelAnimationFrame(scrollFrame);
66+
window.removeEventListener("scroll", queueStorySync);
67+
window.removeEventListener("resize", queueStorySync);
68+
},
69+
};
5870
}
5971

60-
const observer = new IntersectionObserver(
61-
(entries) => {
62-
const visible = entries
63-
.filter((entry) => entry.isIntersecting)
64-
.sort((a, b) => b.intersectionRatio - a.intersectionRatio)[0];
65-
const presetId = visible?.target?.dataset?.storyPreset;
66-
if (presetId) activatePreset(presetId);
67-
},
68-
{
69-
root: null,
70-
rootMargin: "-28% 0px -44% 0px",
71-
threshold: [0.25, 0.5, 0.75],
72-
},
73-
);
74-
7572
const stageObserver =
7673
stage instanceof HTMLElement
7774
? new IntersectionObserver(
@@ -96,8 +93,10 @@ export function createStoryController({ applyPreset }) {
9693
)
9794
: null;
9895

99-
for (const step of steps) observer.observe(step);
10096
if (steps[0]?.dataset.storyPreset) activatePreset(steps[0].dataset.storyPreset);
97+
syncActiveStoryStep();
98+
window.addEventListener("scroll", queueStorySync, { passive: true });
99+
window.addEventListener("resize", queueStorySync);
101100
for (const step of stageSteps) stageObserver?.observe(step);
102101
if (stageSteps[0]?.dataset.stageStep) {
103102
stageSteps[0].classList.add(STAGE_ACTIVE_CLASS);
@@ -108,10 +107,53 @@ export function createStoryController({ applyPreset }) {
108107

109108
return {
110109
destroy() {
111-
observer.disconnect();
110+
if (scrollFrame) window.cancelAnimationFrame(scrollFrame);
111+
window.removeEventListener("scroll", queueStorySync);
112+
window.removeEventListener("resize", queueStorySync);
112113
stageObserver?.disconnect();
113114
},
114115
};
116+
117+
function queueStorySync() {
118+
if (scrollFrame) return;
119+
scrollFrame = window.requestAnimationFrame(() => {
120+
scrollFrame = 0;
121+
syncActiveStoryStep();
122+
});
123+
}
124+
125+
function syncActiveStoryStep() {
126+
const nextStep = getActiveStoryStep(steps);
127+
const presetId = nextStep?.dataset?.storyPreset;
128+
if (!presetId || presetId === activeScrollPresetId) return;
129+
activeScrollPresetId = presetId;
130+
activatePreset(presetId);
131+
}
132+
}
133+
134+
/**
135+
* Picks the story step whose vertical span currently contains the activation
136+
* line. This fires when the top of a new section crosses the chosen band,
137+
* which behaves well even when sections are taller than the viewport.
138+
*
139+
* @param {HTMLElement[]} steps Story steps ordered in document flow.
140+
* @returns {HTMLElement | undefined} Active step.
141+
*/
142+
function getActiveStoryStep(steps) {
143+
const triggerY = window.innerHeight * STORY_TRIGGER_RATIO;
144+
let fallback = steps[0];
145+
146+
for (const step of steps) {
147+
const rect = step.getBoundingClientRect();
148+
if (rect.bottom <= 0) {
149+
fallback = step;
150+
continue;
151+
}
152+
if (rect.top <= triggerY && rect.bottom > triggerY) return step;
153+
if (rect.top > triggerY) return fallback ?? step;
154+
}
155+
156+
return fallback;
115157
}
116158

117159
/**
@@ -123,6 +165,6 @@ export function createStoryController({ applyPreset }) {
123165
*/
124166
function getStageHeaderOffset(stage) {
125167
const header = stage.querySelector(".section-kicker--panel");
126-
if (!(header instanceof HTMLElement)) return 160;
168+
if (!(header instanceof HTMLElement)) return 18;
127169
return header.offsetHeight + 28;
128170
}

src/story-state.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,6 @@ function getStageFocusTarget(stage, treemapPanel, focus) {
139139
*/
140140
function getStageHeaderOffset(stage) {
141141
const header = stage.querySelector(".section-kicker--panel");
142-
if (!(header instanceof HTMLElement)) return 160;
142+
if (!(header instanceof HTMLElement)) return 18;
143143
return header.offsetHeight + 28;
144144
}

src/story.css

Lines changed: 0 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -353,56 +353,6 @@
353353
overscroll-behavior: contain;
354354
}
355355

356-
.story-stage .section-kicker--panel {
357-
position: sticky;
358-
top: 0;
359-
z-index: 4;
360-
backdrop-filter: blur(22px) saturate(140%);
361-
-webkit-backdrop-filter: blur(22px) saturate(140%);
362-
background: rgba(9, 13, 23, 0.9);
363-
}
364-
365-
.stage-guide {
366-
display: flex;
367-
flex-wrap: wrap;
368-
gap: 8px;
369-
margin-top: 16px;
370-
}
371-
372-
.stage-guide button {
373-
display: inline-flex;
374-
align-items: center;
375-
gap: 8px;
376-
min-height: 34px;
377-
padding: 7px 11px;
378-
border: 1px solid var(--hairline);
379-
border-radius: var(--radius-pill);
380-
background: rgba(255, 255, 255, 0.035);
381-
color: var(--text-secondary);
382-
font-size: 0.74rem;
383-
font-weight: 800;
384-
transition:
385-
border-color 180ms var(--ease-emphasized),
386-
background 180ms var(--ease-emphasized),
387-
color 180ms var(--ease-emphasized);
388-
}
389-
390-
.stage-guide button span {
391-
color: var(--text-tertiary);
392-
font-variant-numeric: tabular-nums;
393-
}
394-
395-
.stage-guide button:hover,
396-
.stage-guide button[data-active="true"] {
397-
border-color: rgba(111, 161, 255, 0.34);
398-
background: rgba(111, 161, 255, 0.12);
399-
color: var(--text-primary);
400-
}
401-
402-
.stage-guide button[hidden] {
403-
display: none;
404-
}
405-
406356
.evidence-step {
407357
position: relative;
408358
margin: 18px;
@@ -798,11 +748,6 @@ footer {
798748
.story-stat-row {
799749
margin-top: 18px;
800750
}
801-
802-
.section-kicker--panel {
803-
padding: 18px;
804-
}
805-
806751
.chart-note {
807752
margin: -4px 16px 16px;
808753
}

0 commit comments

Comments
 (0)