Skip to content

Commit c91592a

Browse files
committed
feat: implement ScrollObserver island for scroll-reveal animations
Add IntersectionObserver-based island that watches all .reveal, .reveal-left, .reveal-right, and .reveal-scale elements and adds .visible class when they enter the viewport (10% threshold). This was the missing piece — the ScrollReveal component set opacity:0 on elements but the observer to add .visible was noted as "TODO Chunk 6" and never implemented, making all landing page content invisible.
1 parent 8281439 commit c91592a

3 files changed

Lines changed: 53 additions & 1 deletion

File tree

src/app.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use leptos_router::{
99
path,
1010
};
1111

12-
use crate::components::{footer::Footer, nav::Nav};
12+
use crate::components::{footer::Footer, nav::Nav, scroll_observer::ScrollObserver};
1313
use crate::pages::{docs::DocsPage, landing::Landing, not_found::NotFound};
1414

1515
#[component]
@@ -24,6 +24,7 @@ pub fn App() -> impl IntoView {
2424
<Route path=path!("/docs/:slug") view=DocsPage/>
2525
</Routes>
2626
<Footer/>
27+
<ScrollObserver/>
2728
</Router>
2829
}
2930
}

src/components/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@ pub mod doc_toc;
99
pub mod footer;
1010
pub mod nav;
1111
pub mod pipeline_demo;
12+
pub mod scroll_observer;
1213
pub mod scroll_reveal;
1314
pub mod theme_toggle;

src/components/scroll_observer.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// SPDX-FileCopyrightText: 2026 Sephyi <me@sephy.io>
2+
//
3+
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
4+
5+
use leptos::prelude::*;
6+
use wasm_bindgen::prelude::*;
7+
8+
/// Single island that observes all `.reveal` / `.reveal-left` / `.reveal-right` /
9+
/// `.reveal-scale` elements and adds `.visible` when they enter the viewport.
10+
/// Place once in the app shell — it covers the entire page.
11+
#[island]
12+
pub fn ScrollObserver() -> impl IntoView {
13+
Effect::new(move || {
14+
let window = web_sys::window().unwrap();
15+
let document = window.document().unwrap();
16+
17+
let callback = Closure::<dyn Fn(js_sys::Array, web_sys::IntersectionObserver)>::new(
18+
move |entries: js_sys::Array, _observer: web_sys::IntersectionObserver| {
19+
for entry in entries.iter() {
20+
let entry: web_sys::IntersectionObserverEntry = entry.unchecked_into();
21+
if entry.is_intersecting() {
22+
let target = entry.target();
23+
let _ = target.class_list().add_1("visible");
24+
}
25+
}
26+
},
27+
);
28+
29+
let options = web_sys::IntersectionObserverInit::new();
30+
options.set_threshold(&JsValue::from_f64(0.1));
31+
32+
let observer = web_sys::IntersectionObserver::new_with_options(
33+
callback.as_ref().unchecked_ref(),
34+
&options,
35+
)
36+
.unwrap();
37+
38+
let selectors = ".reveal, .reveal-left, .reveal-right, .reveal-scale";
39+
let elements = document.query_selector_all(selectors).unwrap();
40+
for i in 0..elements.length() {
41+
if let Some(el) = elements.get(i) {
42+
observer.observe(el.unchecked_ref());
43+
}
44+
}
45+
46+
callback.forget();
47+
});
48+
49+
view! {}
50+
}

0 commit comments

Comments
 (0)