From 63db4fa0c7336d96f711a70341529c48a7f0b710 Mon Sep 17 00:00:00 2001 From: Aminu Oluwaseun Joshua Date: Tue, 7 Apr 2026 20:00:28 +0100 Subject: [PATCH 01/14] added new project Signed-off-by: Aminu Oluwaseun Joshua --- data/projects.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/data/projects.json b/data/projects.json index 7eeedd8..5cdf8b7 100644 --- a/data/projects.json +++ b/data/projects.json @@ -63,5 +63,10 @@ "repo_url": "https://github.com/enigma-137/log-analyzer", "banner": "https://opengraph.githubassets.com/1/enigma-137/log-analyzer", "tags": ["cli"] + }, + { + "repo_url": "https://github.com/seun-ja/rpc-agent", + "banner": "https://opengraph.githubassets.com/1/seun-ja/rpc-agent", + "tags": ["rpc", "ai", "distributed_system"] } ] From 7b60c301489769826fe1457e2e6bf1656108abab Mon Sep 17 00:00:00 2001 From: Jonathan Irhodia <30146982+elcharitas@users.noreply.github.com> Date: Sun, 3 May 2026 23:00:35 +0100 Subject: [PATCH 02/14] Add new project entry for 'momenta' in projects.json --- data/projects.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/data/projects.json b/data/projects.json index 7eeedd8..11f3def 100644 --- a/data/projects.json +++ b/data/projects.json @@ -63,5 +63,10 @@ "repo_url": "https://github.com/enigma-137/log-analyzer", "banner": "https://opengraph.githubassets.com/1/enigma-137/log-analyzer", "tags": ["cli"] + }, + { + "repo_url": "https://github.com/elcharitas/momenta", + "banner": "https://opengraph.githubassets.com/1/elcharitas/momenta", + "tags": ["crate", "framework", "web"] } ] From f48705da0bcbf8077f57d38be290fc2ef8a95035 Mon Sep 17 00:00:00 2001 From: Akin Aguda Date: Tue, 14 Jul 2026 13:14:21 -0500 Subject: [PATCH 03/14] WIP on More accurate buttons --- src/components/button/button_backdrop.rs | 136 +++++++++++++++++++++++ src/components/button/mod.rs | 86 +++++++++++++- src/components/button/shaders.rs | 106 ++++++++++++++++++ src/components/community_stats/mod.rs | 26 ++--- src/components/nav/mod.rs | 28 ++--- 5 files changed, 351 insertions(+), 31 deletions(-) create mode 100644 src/components/button/button_backdrop.rs create mode 100644 src/components/button/shaders.rs diff --git a/src/components/button/button_backdrop.rs b/src/components/button/button_backdrop.rs new file mode 100644 index 0000000..bb33816 --- /dev/null +++ b/src/components/button/button_backdrop.rs @@ -0,0 +1,136 @@ +use leptos::{ + html, + prelude::{Get, NodeRef}, + wasm_bindgen::JsCast, +}; + +use web_sys::{ + HtmlCanvasElement, WebGlBuffer, WebGlProgram, WebGlRenderingContext as Gl, WebGlUniformLocation, +}; + +use super::shaders::{FRAGMENT_SHADER, VERTEX_SHADER}; +use crate::utils::{ + resize_canvas_to_display_size::resize_canvas_to_display_size, + webgl::{create_program, create_shader, get_canvas_to_clipspace_projection_matrix, set_quad}, +}; + +#[derive(Clone)] +pub struct ButtonBackdropInstance { + gl: Gl, + canvas: HtmlCanvasElement, + program: WebGlProgram, + vertex_position_attribute_loc: i32, + vertex_position_buffer: WebGlBuffer, + canvas_projection_matrix_uniform_loc: WebGlUniformLocation, + canvas_resolution_uniform_location: WebGlUniformLocation, + extension_dimension_uniform_location: WebGlUniformLocation, +} + +#[derive(Clone)] +pub struct ButtonBackdropBuilder {} + +impl ButtonBackdropBuilder { + pub async fn load() -> Self { + Self {} + } + + pub fn create_backdrop(self, canvas: HtmlCanvasElement) -> ButtonBackdropInstance { + ButtonBackdropInstance::new(canvas) + } +} + +impl ButtonBackdropInstance { + pub fn new(canvas: HtmlCanvasElement) -> Self { + let gl = canvas + .get_context("webgl") + .unwrap() + .unwrap() + .dyn_into::() + .unwrap(); + + let vertex_shader = create_shader(&gl, Gl::VERTEX_SHADER, VERTEX_SHADER).unwrap(); + let fragment_shader = create_shader(&gl, Gl::FRAGMENT_SHADER, FRAGMENT_SHADER).unwrap(); + + let program = create_program(&gl, &vertex_shader, &fragment_shader).unwrap(); + + gl.pixel_storei(Gl::UNPACK_FLIP_Y_WEBGL, 1); + + let canvas_projection_matrix_uniform_loc = gl + .get_uniform_location(&program, "u_canvasProjectionMatrix") + .unwrap(); + + let vertex_position_attribute_loc = gl.get_attrib_location(&program, "a_position"); + + let vertex_position_buffer = gl.create_buffer().unwrap(); + + let canvas_resolution_uniform_location = gl + .get_uniform_location(&program, "u_canvas_resolution") + .unwrap(); + + let extension_dimension_uniform_location = gl + .get_uniform_location(&program, "u_extension_dimension") + .unwrap(); + + gl.use_program(Some(&program)); + + ButtonBackdropInstance { + gl, + canvas, + program, + vertex_position_attribute_loc, + vertex_position_buffer, + canvas_projection_matrix_uniform_loc, + canvas_resolution_uniform_location, + extension_dimension_uniform_location, + } + } + + pub fn render(&self) { + let ButtonBackdropInstance { + gl, + canvas, + program, + vertex_position_attribute_loc, + vertex_position_buffer, + canvas_projection_matrix_uniform_loc, + canvas_resolution_uniform_location, + extension_dimension_uniform_location, + } = self; + + gl.use_program(Some(program)); + + resize_canvas_to_display_size(canvas); + + let canvas_width = canvas.width() as f32; + let canvas_height = canvas.height() as f32; + + gl.uniform_matrix3fv_with_f32_array( + Some(canvas_projection_matrix_uniform_loc), + false, + &get_canvas_to_clipspace_projection_matrix(canvas_width, canvas_height), + ); + + gl.uniform2fv_with_f32_array( + Some(&canvas_resolution_uniform_location), + &[canvas_width, canvas_height], + ); + + gl.uniform1f(Some(&extension_dimension_uniform_location), canvas_height); + + gl.bind_buffer(Gl::ARRAY_BUFFER, Some(vertex_position_buffer)); + set_quad(gl, canvas_width, canvas_height); + gl.enable_vertex_attrib_array(*vertex_position_attribute_loc as u32); + gl.vertex_attrib_pointer_with_i32( + *vertex_position_attribute_loc as u32, + 2, + Gl::FLOAT, + false, + 0, + 0, + ); + + gl.viewport(0, 0, canvas_width as i32, canvas_height as i32); + + gl.draw_arrays(Gl::TRIANGLES, 0, 6); + } +} diff --git a/src/components/button/mod.rs b/src/components/button/mod.rs index e164b15..54f4942 100644 --- a/src/components/button/mod.rs +++ b/src/components/button/mod.rs @@ -1,7 +1,12 @@ -use leptos::{either::Either, ev::MouseEvent, prelude::*}; +mod button_backdrop; +mod shaders; + +use leptos::{either::Either, ev::MouseEvent, html, prelude::*}; use tailwind_fuse::*; -use crate::icons::right_arrow::RightArrow; +use crate::{ + components::button::button_backdrop::ButtonBackdropBuilder, icons::right_arrow::RightArrow, +}; pub enum ButtonUsecase { Button { @@ -46,7 +51,7 @@ pub enum ButtonSizeVariants { #[derive(TwClass)] #[tw( - class = "inline-flex gap-x-1 cursor-pointer items-center font-medium overflow-hidden duration-300" + class = "inline-flex relative gap-x-1 cursor-pointer items-center font-medium overflow-visible duration-300" )] struct ButtonVariants { size: ButtonSizeVariants, @@ -62,6 +67,10 @@ pub fn Button( #[prop(default = "")] class: &'static str, #[prop(optional)] icon: Option, ) -> impl IntoView { + let canvas_ref: NodeRef = NodeRef::new(); + let button_ref: NodeRef = NodeRef::new(); + let link_ref: NodeRef = NodeRef::new(); + let class = ButtonVariants { size, color }.with_class(class); let icon_el = match icon { @@ -79,9 +88,78 @@ pub fn Button( "" }; + let maybe_backdrop_builder = LocalResource::new(move || ButtonBackdropBuilder::load()); + + let (extension_dimension, set_extension_dimension) = signal::>(None); + + let (is_hovering, set_is_hovering) = signal(false); + + // Effect::new(move || { + // match use_as.clone() { + // ButtonUsecase::Button { on_click: _ } => { + // set_extension_dimension( + // button_ref + // .get() + // .map_or(None, |button| Some(button.client_height())), + // ); + // } + // ButtonUsecase::Link { href: _ } => { + // set_extension_dimension( + // link_ref + // .get() + // .map_or(None, |link| Some(link.client_height())), + // ); + // } + // }; + // }); + + Effect::new(move || { + set_extension_dimension( + button_ref + .get() + .map_or(None, |button| Some(button.client_height())), + ); + }); + + Effect::new(move || { + if let Some(canvas) = canvas_ref.get() { + if let Some(backdrop_builder) = maybe_backdrop_builder.get() { + let backdrop = backdrop_builder.create_backdrop(canvas); + backdrop.render(); + } + } + }); + match use_as { ButtonUsecase::Button { on_click } => Either::Left(view! { - + }), ButtonUsecase::Link { href } => Either::Right(view! { {children()}{icon_el} diff --git a/src/components/button/shaders.rs b/src/components/button/shaders.rs new file mode 100644 index 0000000..5a12212 --- /dev/null +++ b/src/components/button/shaders.rs @@ -0,0 +1,106 @@ +pub const VERTEX_SHADER: &str = r#" + attribute vec2 a_position; + + uniform mat3 u_canvasProjectionMatrix; + + void main() { + vec2 position = (u_canvasProjectionMatrix * vec3(a_position, 1)).xy; + gl_Position = vec4(position, 0, 1); + } +"#; + +pub const FRAGMENT_SHADER: &str = r#" + precision mediump float; + + uniform vec2 u_canvas_resolution; + uniform float u_extension_dimension; + + float inverseLerp(float v, float minValue, float maxValue) { + return (v - minValue) / (maxValue - minValue); + } + + float remap(float v, float inMin, float inMax, float outMin, float outMax) { + float t = inverseLerp(v, inMin, inMax); + return mix(outMin, outMax, t); + } + + // First arg is the coord, second is the dimensions from center and third is the radius of the corners + float sdRoundBox( vec2 p, vec2 b, float r ) + { + vec2 q = abs(p) - b + r; + return length(max(q,0.0)) + min(max(q.x,q.y),0.0) - r; + } + + float circleSdf(vec2 pos, float r) + { + return length(pos) - r; + } + + vec4 paintSdf(vec4 color, float sdf) + { + vec4 clear = vec4(0.0); + return mix(color, clear, smoothstep(-1.0, 1.0, sdf)); //Smoothstep for some small anti aliasing + } + + // This moves the SDF, assuming the origin to be at the bottom left + // + // Also, accoding to what I know now, (I am still building intuition for this) to "move" and SDF to position (x,y), you figure out what operation would + // normally move it from its desired position (when you want to take it to), to the canvas origin (at the bottom left) and then apply that. + // For instance, if you want to draw a circle at the center of the canvas, you pinpoint the center of that circle (the circle's position) and then figure + // out what operation would move that circle to the bottom left. That operaiton is what moves the sdf to that center of the canvas (weird I know xD) + vec2 placeSdf(vec2 desiredPosition, vec2 coord) { + return desiredPosition - coord; + } + + // exponential + float smin( float a, float b, float k ) + { + k *= 1.0; + float r = exp2(-a/k) + exp2(-b/k); + return -k*log2(r); + } + + void main() { + vec4 clear = vec4(0.0); + vec4 buttonColor = vec4(0.0, 1.0, 1.0, 1.0); + + vec4 color = clear; + + float extension = u_extension_dimension; + + vec2 coord = gl_FragCoord.xy; + + // Write a nice utility to draw the SDF in the scene (use pixel values) + + // Background dimensions without the extension + vec2 buttonBg = u_canvas_resolution - vec2(extension, 0.0); + + // Move it to the middle of the button BG from the left side of the canvas + vec2 buttonPosition = placeSdf( + buttonBg * 0.5, + coord + ); + + float pill = sdRoundBox( + buttonPosition, + buttonBg * 0.5, + u_canvas_resolution.y * 0.5 + ); + + // Temporary Move. Move the circle to the rightmost part of the canvas + vec2 circlePosition = placeSdf( + ((u_canvas_resolution) - vec2(10.0, 0.0)) - (extension * 0.5), + coord + ); + + float circle = circleSdf(circlePosition, extension * 0.5); + + // Draw Circle + + float pillAndCircle = smin(pill, circle, 9.0); + + color += paintSdf(buttonColor, pillAndCircle); + + gl_FragColor = color; + } +"#; diff --git a/src/components/community_stats/mod.rs b/src/components/community_stats/mod.rs index 3a79646..77cde8e 100644 --- a/src/components/community_stats/mod.rs +++ b/src/components/community_stats/mod.rs @@ -38,19 +38,19 @@ pub fn CommunityStats() -> impl IntoView { "opacity-0 translate-y-8 duration-500 mt-6", (section_in_view(), "opacity-100 translate-y-0 delay-300") ))> - + // diff --git a/src/components/nav/mod.rs b/src/components/nav/mod.rs index 84a5ee0..100c7fe 100644 --- a/src/components/nav/mod.rs +++ b/src/components/nav/mod.rs @@ -68,20 +68,20 @@ pub fn Nav() -> impl IntoView { ).collect_view() }
  • - + //
  • From 9b734fecb68ca5b0d9e7bda9d09a83f591723af8 Mon Sep 17 00:00:00 2001 From: Akin Aguda Date: Thu, 16 Jul 2026 16:19:51 -0500 Subject: [PATCH 04/14] V1 of Button animation as designed --- Cargo.lock | 7 + Cargo.toml | 1 + .../articles_section/articles/article_card.rs | 2 +- src/components/button/button_backdrop.rs | 53 +++- src/components/button/mod.rs | 256 +++++++++++++----- src/components/button/shaders.rs | 36 ++- src/components/cards_list/mod.rs | 25 +- src/components/community_stats/mod.rs | 27 +- .../events_section/events/event_card.rs | 3 +- src/components/hero_section/mod.rs | 2 +- src/components/nav/mod.rs | 31 +-- src/utils/clamp.rs | 5 - src/utils/mod.rs | 1 - src/utils/render_loop.rs | 13 + 14 files changed, 325 insertions(+), 137 deletions(-) delete mode 100644 src/utils/clamp.rs diff --git a/Cargo.lock b/Cargo.lock index 31130a0..b5d8a93 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2114,6 +2114,7 @@ dependencies = [ "leptos_router", "serde", "serde_json", + "simple-bezier-easing", "stylance", "tailwind_fuse", "wasm-bindgen", @@ -2359,6 +2360,12 @@ dependencies = [ "libc", ] +[[package]] +name = "simple-bezier-easing" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29f783d6e697cb9df4af34e64d6bc72e859d84a8b4a7b9024a9500ef9e364379" + [[package]] name = "siphasher" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index 667a268..5f3e284 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ serde = "1.0.228" serde_json = "1.0" chrono = {version = "0.4.42", features = ["serde"]} leptos-use = "0.16.3" +simple-bezier-easing = "0.1.1" [dependencies.web-sys] version = "0.3.76" diff --git a/src/components/articles_section/articles/article_card.rs b/src/components/articles_section/articles/article_card.rs index 7bd033f..4803410 100644 --- a/src/components/articles_section/articles/article_card.rs +++ b/src/components/articles_section/articles/article_card.rs @@ -45,7 +45,7 @@ pub fn ArticleCard( }), ButtonUsecase::Link { href } => Either::Right(view! { - {children()}{icon_el} + + + {move || extension_dimension().map(move |ext| { + view! { + + } + })} + + + + {children()} + + + {icon_el} + }), } } diff --git a/src/components/button/shaders.rs b/src/components/button/shaders.rs index 5a12212..5d50f48 100644 --- a/src/components/button/shaders.rs +++ b/src/components/button/shaders.rs @@ -14,14 +14,12 @@ pub const FRAGMENT_SHADER: &str = r#" uniform vec2 u_canvas_resolution; uniform float u_extension_dimension; + uniform float u_progression; + uniform vec4 u_base_color; + uniform vec4 u_hover_color; - float inverseLerp(float v, float minValue, float maxValue) { - return (v - minValue) / (maxValue - minValue); - } - - float remap(float v, float inMin, float inMax, float outMin, float outMax) { - float t = inverseLerp(v, inMin, inMax); - return mix(outMin, outMax, t); + vec3 srgbToLinear(vec3 c) { + return pow(c, vec3(2.2)); // or the exact piecewise sRGB curve if you want precision } // First arg is the coord, second is the dimensions from center and third is the radius of the corners @@ -61,8 +59,12 @@ pub const FRAGMENT_SHADER: &str = r#" } void main() { - vec4 clear = vec4(0.0); - vec4 buttonColor = vec4(0.0, 1.0, 1.0, 1.0); + vec4 clear = vec4(srgbToLinear(vec3(0.0)).xyz, 0.0); + vec4 buttonColor = mix( + vec4(srgbToLinear(vec3(u_base_color.xyz) / 225.0).xyz, u_base_color.a), + vec4(srgbToLinear(vec3(u_hover_color.xyz) / 225.0).xyz, u_hover_color.a), + u_progression + ); vec4 color = clear; @@ -87,18 +89,26 @@ pub const FRAGMENT_SHADER: &str = r#" u_canvas_resolution.y * 0.5 ); - // Temporary Move. Move the circle to the rightmost part of the canvas + // Place Circle flush with right side of pill + vec2 posCircleWithPillBorder = buttonBg - vec2(extension * 0.5); + + // Start the circle from within the pill vec2 circlePosition = placeSdf( - ((u_canvas_resolution) - vec2(10.0, 0.0)) - (extension * 0.5), + posCircleWithPillBorder + vec2(mix(-0.5 * extension, extension, u_progression), 0.0), // Lerp position coord ); - float circle = circleSdf(circlePosition, extension * 0.5); + // Multiplying by some scaling constant to make it slightly smaller and look nicer (preference) + float circleRadius = ((extension - 5.0) * 0.5) * 0.9; + + float circle = circleSdf(circlePosition, mix(circleRadius * 0.5, circleRadius, u_progression)); // Draw Circle - float pillAndCircle = smin(pill, circle, 9.0); + float pillAndCircle = smin(pill, circle, 10.0); + // color += paintSdf(buttonColor, pill); + // color += paintSdf(vec4(1.0, 0.0, 0.0, 1.0), circle); color += paintSdf(buttonColor, pillAndCircle); gl_FragColor = color; diff --git a/src/components/cards_list/mod.rs b/src/components/cards_list/mod.rs index c42925d..1b37643 100644 --- a/src/components/cards_list/mod.rs +++ b/src/components/cards_list/mod.rs @@ -109,14 +109,14 @@ where tags_set }); - let selected_tags: RwSignal> = RwSignal::new(tags.get()); + let (selected_tags, set_selected_tags) = signal::>(tags.get_untracked()); let filtered_data = move || { cards_data_memo() .iter() .filter(|d| { let tags = d.get_tags(); - tags.iter().any(|tag| selected_tags.read().contains(tag)) + tags.iter().any(|tag| selected_tags.get().contains(tag)) }) .cloned() .collect::>() @@ -167,7 +167,7 @@ where (count, (count * multiplier) as usize) }); - let (items_count, set_items_count) = signal(count_data().1); + let (items_count, set_items_count) = signal(count_data.get_untracked().1); let PaginationData { current_page, @@ -185,7 +185,7 @@ where let has_more_to_show = move || items_count() < filtered_data().len(); - Effect::new(move || selected_tags.set(tags())); + Effect::new(move || set_selected_tags(tags())); Effect::new(move || set_items_count(count_data().1)); @@ -228,13 +228,14 @@ where (selected_tags.get().contains(&tag), "opacity-100") )) on:click=move |_| { - selected_tags.update(|set| { - if set.contains(&tag) { - set.remove(&tag); - } else { - set.insert(tag); - } - }); + let mut set = selected_tags.get(); + if set.contains(&tag) { + set.remove(&tag); + } else { + set.insert(tag); + } + set_selected_tags(set); + } > {format!("{}", tag)} @@ -271,7 +272,7 @@ where ))> + diff --git a/src/components/events_section/events/event_card.rs b/src/components/events_section/events/event_card.rs index 78da301..f8434a7 100644 --- a/src/components/events_section/events/event_card.rs +++ b/src/components/events_section/events/event_card.rs @@ -55,8 +55,7 @@ pub fn EventCard(
    - +
    diff --git a/src/utils/clamp.rs b/src/utils/clamp.rs deleted file mode 100644 index 09a4cb8..0000000 --- a/src/utils/clamp.rs +++ /dev/null @@ -1,5 +0,0 @@ -use web_sys::js_sys::Math::{max, min}; - -pub fn clamp(minimum: f64, maximum: f64, value: f64) -> f64 { - max(minimum, min(value, maximum)) -} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 61a24e6..98ca283 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,4 +1,3 @@ -pub mod clamp; pub mod cn; pub mod get_color_pair; pub mod get_pagination_page_list; diff --git a/src/utils/render_loop.rs b/src/utils/render_loop.rs index 6d8eb6d..0aef482 100644 --- a/src/utils/render_loop.rs +++ b/src/utils/render_loop.rs @@ -14,3 +14,16 @@ impl RenderLoop { } } } + +impl Drop for RenderLoop { + fn drop(&mut self) { + if let Some(animation_id) = self.animation_id { + let window = + web_sys::window().expect("Failed to get window when cleaning up animation loop"); + + window + .cancel_animation_frame(animation_id) + .expect("Cannot Cancel Animation Frame"); + } + } +} From 953ed2114902751a341de33cbfb45169a52930de Mon Sep 17 00:00:00 2001 From: Akin Aguda Date: Thu, 16 Jul 2026 17:09:14 -0500 Subject: [PATCH 05/14] Added useful comment to shader --- src/components/button/shaders.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/button/shaders.rs b/src/components/button/shaders.rs index 5d50f48..ded4997 100644 --- a/src/components/button/shaders.rs +++ b/src/components/button/shaders.rs @@ -107,6 +107,7 @@ pub const FRAGMENT_SHADER: &str = r#" float pillAndCircle = smin(pill, circle, 10.0); + // Leaving these comments here because they are useful for debugging // color += paintSdf(buttonColor, pill); // color += paintSdf(vec4(1.0, 0.0, 0.0, 1.0), circle); color += paintSdf(buttonColor, pillAndCircle); From 0507db58aeec647c3573759612c0c1ec36f43317 Mon Sep 17 00:00:00 2001 From: Akin Aguda Date: Thu, 16 Jul 2026 17:11:59 -0500 Subject: [PATCH 06/14] some tweaks --- src/components/button/mod.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/button/mod.rs b/src/components/button/mod.rs index 60ac56e..f6dbadf 100644 --- a/src/components/button/mod.rs +++ b/src/components/button/mod.rs @@ -200,6 +200,9 @@ pub fn Button( let use_as_clone = use_as.clone(); + // TODO - This should respond to resizing. We might also benefit from making the backdrop + // struct also get it's extension from here + Effect::new(move || { if icon.is_none() || matches!(size, ButtonSizeVariants::Thin) From 63ef50b2fd423615470df678e53e5a6aa9b7c224 Mon Sep 17 00:00:00 2001 From: Akin Aguda Date: Thu, 16 Jul 2026 17:29:11 -0500 Subject: [PATCH 07/14] Fixed Grey button not showing correctly --- src/components/button/mod.rs | 4 ++-- src/components/button/shaders.rs | 9 ++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/components/button/mod.rs b/src/components/button/mod.rs index f6dbadf..918e5e4 100644 --- a/src/components/button/mod.rs +++ b/src/components/button/mod.rs @@ -53,11 +53,11 @@ impl ButtonColorVariants { pub fn get_state_rgbs(&self) -> StateRgbs { match self { ButtonColorVariants::Black => StateRgbs { - base: [42.0, 42.0, 42.0, 1.0], + base: [10.0, 10.0, 10.0, 1.0], hover: [38.0, 38.0, 38.0, 1.0], }, ButtonColorVariants::Grey => StateRgbs { - base: [42.0, 42.0, 42.0, 1.0], + base: [62.0, 62.0, 62.0, 1.0], // TODO - Not the same as bg-grey-30. Fix hover: [79.0, 79.0, 79.0, 1.0], }, ButtonColorVariants::White => StateRgbs { diff --git a/src/components/button/shaders.rs b/src/components/button/shaders.rs index ded4997..ac2406e 100644 --- a/src/components/button/shaders.rs +++ b/src/components/button/shaders.rs @@ -60,11 +60,10 @@ pub const FRAGMENT_SHADER: &str = r#" void main() { vec4 clear = vec4(srgbToLinear(vec3(0.0)).xyz, 0.0); - vec4 buttonColor = mix( - vec4(srgbToLinear(vec3(u_base_color.xyz) / 225.0).xyz, u_base_color.a), - vec4(srgbToLinear(vec3(u_hover_color.xyz) / 225.0).xyz, u_hover_color.a), - u_progression - ); + vec4 baseColor = vec4(srgbToLinear(vec3(u_base_color.xyz) / 225.0).xyz, u_base_color.a); + vec4 hoverColor = vec4(srgbToLinear(vec3(u_hover_color.xyz) / 225.0).xyz, u_hover_color.a); + + vec4 buttonColor = mix(baseColor, hoverColor, u_progression); vec4 color = clear; From 23a08214f8fc065f744dad9ed00fed39f5e02212 Mon Sep 17 00:00:00 2001 From: Akin Aguda Date: Thu, 16 Jul 2026 17:32:27 -0500 Subject: [PATCH 08/14] Resolved issues found during build by linter --- src/components/button/button_backdrop.rs | 6 +++--- src/components/button/mod.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/button/button_backdrop.rs b/src/components/button/button_backdrop.rs index 4e24927..56532d7 100644 --- a/src/components/button/button_backdrop.rs +++ b/src/components/button/button_backdrop.rs @@ -138,13 +138,13 @@ impl ButtonBackdropInstance { ); gl.uniform2fv_with_f32_array( - Some(&canvas_resolution_uniform_location), + Some(canvas_resolution_uniform_location), &[canvas_width, canvas_height], ); - gl.uniform1f(Some(&extension_dimension_uniform_location), canvas_height); + gl.uniform1f(Some(extension_dimension_uniform_location), canvas_height); - gl.uniform1f(Some(&progression_uniform_location), progression); + gl.uniform1f(Some(progression_uniform_location), progression); gl.bind_buffer(Gl::ARRAY_BUFFER, Some(vertex_position_buffer)); set_quad(gl, canvas_width, canvas_height); diff --git a/src/components/button/mod.rs b/src/components/button/mod.rs index 918e5e4..d4e3d09 100644 --- a/src/components/button/mod.rs +++ b/src/components/button/mod.rs @@ -189,7 +189,7 @@ pub fn Button( "" }; - let maybe_backdrop_builder = LocalResource::new(move || ButtonBackdropBuilder::load()); + let maybe_backdrop_builder = LocalResource::new(ButtonBackdropBuilder::load); let (extension_dimension, set_extension_dimension) = signal::>(None); From 1eb45209c49de9de557000f8836f182900b943d4 Mon Sep 17 00:00:00 2001 From: Akin Aguda Date: Thu, 16 Jul 2026 20:43:27 -0500 Subject: [PATCH 09/14] Fixed issues form merged contribs --- data/projects.json | 2 +- src/components/cards_list/mod.rs | 2 +- src/types/projects.rs | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/data/projects.json b/data/projects.json index 73bdd79..61fe3c8 100644 --- a/data/projects.json +++ b/data/projects.json @@ -72,6 +72,6 @@ { "repo_url": "https://github.com/elcharitas/momenta", "banner": "https://opengraph.githubassets.com/1/elcharitas/momenta", - "tags": ["crate", "framework", "web"] + "tags": ["crate", "web"] } ] diff --git a/src/components/cards_list/mod.rs b/src/components/cards_list/mod.rs index 1b37643..9055061 100644 --- a/src/components/cards_list/mod.rs +++ b/src/components/cards_list/mod.rs @@ -206,7 +206,7 @@ where
    {move || diff --git a/src/types/projects.rs b/src/types/projects.rs index 63f9ec9..16a5ead 100644 --- a/src/types/projects.rs +++ b/src/types/projects.rs @@ -16,6 +16,8 @@ pub enum ProjectTags { RateLimiting, Template, Cli, + Rpc, + Web, } impl fmt::Display for ProjectTags { @@ -31,6 +33,8 @@ impl fmt::Display for ProjectTags { ProjectTags::RateLimiting => "Rate Limiting", ProjectTags::Template => "Template", ProjectTags::Cli => "CLI", + ProjectTags::Rpc => "RPC", + ProjectTags::Web => "Web", }; write!(f, "{}", s) } From c0876c2ef3884e767cc9fe1835a9661612fdcad7 Mon Sep 17 00:00:00 2001 From: Akin Aguda Date: Fri, 17 Jul 2026 11:13:49 -0500 Subject: [PATCH 10/14] When button now leaves view, it cancels animation --- src/components/button/mod.rs | 176 ++++++++++++++++++++++++++++++----- src/utils/render_loop.rs | 17 ++-- 2 files changed, 160 insertions(+), 33 deletions(-) diff --git a/src/components/button/mod.rs b/src/components/button/mod.rs index d4e3d09..3f64b5e 100644 --- a/src/components/button/mod.rs +++ b/src/components/button/mod.rs @@ -15,6 +15,7 @@ use crate::{ components::button::button_backdrop::{ ButtonBackdropBuilder, ButtonBackdropCfg, ButtonBackdropInstance, }, + hooks::use_in_view::{use_in_view, ElementVisibilityData, InViewOptions}, icons::right_arrow::RightArrow, utils::render_loop::RenderLoop, }; @@ -104,21 +105,49 @@ enum AnimationDirection { const TOTAL_ANIMATION_DURATION_MS: f64 = 500.0; -// My animaiton loop should pass in a value to my render funciton that ping pongs between 0 and 1 depending on the direction +type RenderLoopPtr = Rc>; + +/// Public handles to a running loop. These live *beside* the RenderLoop, not inside it, so +/// calling one only borrows the loop's RefCell fresh — nothing else is holding it at the time. +/// (Putting these on RenderLoop itself deadlocks: reaching the field needs a borrow, and the +/// handle then wants borrow_mut on the same cell.) +struct AnimationControls { + wake: Box, + sleep: Box, + // Tears the loop down when the component drops this (via the StoredValue). Replaces + // on_cleanup, whose Send + Sync bound an Rc/RefCell can't satisfy. + _handle: LoopHandle, +} + +/// Owns the loop and cancels + frees it on drop. +struct LoopHandle(RenderLoopPtr); + +impl Drop for LoopHandle { + fn drop(&mut self) { + let mut render_loop = self.0.borrow_mut(); + render_loop.cancel(); + // Drop the Closure so its captured Rc> clone is released — otherwise + // the RenderLoop <-> Closure cycle keeps the loop alive forever. + render_loop.closure = None; + } +} + +// Animaiton loop should pass in a value to render funciton that ping pongs between 0 and 1 depending on the direction // of the animation. Frowards goes from 0 -> 1 and backwards goes from 1 -> 0. The idea is that its basically just playing a // predefined animation but giving us a normalised time value between 0 and 1 -fn start_animaiton_loop(backdrop: ButtonBackdropInstance, direction: Rc>) { - let render_loop: Rc> = Rc::new(RefCell::new(RenderLoop::default())); - let window = web_sys::window().unwrap(); - let time = Cell::new(Date::now()); +fn create_render_loop( + backdrop: ButtonBackdropInstance, + direction: Rc>, +) -> AnimationControls { + let render_loop: RenderLoopPtr = Rc::new(RefCell::new(RenderLoop::default())); let timing = bezier(0.42, 0.0, 0.58, 1.0).unwrap(); + let time: Rc> = Rc::new(Cell::new(Date::now())); + let progression: Rc> = Rc::new(Cell::new(0.0)); + let running: Rc> = Rc::new(Cell::new(false)); - let progression = Cell::new(0.0); - - let closure: Closure = { - let window = web_sys::window().unwrap(); - let render_loop = render_loop.clone(); - Closure::wrap(Box::new(move |_| { + let render_fn: Rc = { + let (progression, time, direction) = (progression.clone(), time.clone(), direction.clone()); + Rc::new(move || { let now = Date::now(); let dt = now - time.get(); let multiplier = match direction.get() { @@ -128,16 +157,23 @@ fn start_animaiton_loop(backdrop: ButtonBackdropInstance, direction: Rc = { + let window = web_sys::window().unwrap(); + let (render_loop, render_fn) = (render_loop.clone(), render_fn.clone()); + + Closure::wrap(Box::new(move |_| { + render_fn(); let mut render_loop = render_loop.borrow_mut(); render_loop.animation_id = render_loop.closure.as_ref().map(|closure| { @@ -148,15 +184,69 @@ fn start_animaiton_loop(backdrop: ButtonBackdropInstance, direction: Rc = { + let window = web_sys::window().unwrap(); + let (render_loop, progression, time, running) = ( + render_loop.clone(), + progression.clone(), + time.clone(), + running.clone(), + ); + + Box::new(move || { + // Guard first: a redundant wake must not reset progression mid-animation or start a + // parallel rAF chain (which would run the animation at double speed). + if running.replace(true) { + return; + } + + // Fresh start from 0; reset the clock so the first frame doesn't see a stale dt. + progression.set(0.0); + time.set(Date::now()); + + let mut render_loop_borrow = render_loop.borrow_mut(); + if let Some(closure) = &render_loop_borrow.closure { + render_loop_borrow.animation_id = Some( + window + .request_animation_frame(closure.as_ref().unchecked_ref()) + .expect("cannot set animation frame"), + ); + } + }) + }; + + let sleep: Box = { + let (render_loop, progression, time, direction, running, render_fn) = ( + render_loop.clone(), + progression.clone(), + time.clone(), + direction.clone(), + running.clone(), + render_fn.clone(), + ); + + Box::new(move || { + running.set(false); + progression.set(0.0); + time.set(Date::now()); + direction.set(AnimationDirection::Backwards); + + // Paint the resting frame once so the canvas isn't left mid-animation / blank. + render_fn(); + + render_loop.borrow().cancel(); + }) + }; - render_loop.closure = Some(closure) + // No on_cleanup: teardown rides on LoopHandle's Drop, which runs when the StoredValue holding + // these controls is disposed on unmount (or replaced if this effect re-runs). + AnimationControls { + wake, + sleep, + _handle: LoopHandle(render_loop), + } } #[component] @@ -172,6 +262,10 @@ pub fn Button( let button_ref: NodeRef = NodeRef::new(); let link_ref: NodeRef = NodeRef::new(); + // StoredValue is Copy, so the effects below capture it directly — no Rc>> + // to clone around. new_local because the boxed closures aren't Send/Sync. + let controls = StoredValue::new_local(None::); + let class = ButtonVariants { size, color }.with_class(class); let icon_el = match icon { @@ -200,6 +294,16 @@ pub fn Button( let use_as_clone = use_as.clone(); + let ElementVisibilityData { + in_view: canvas_in_view, + } = use_in_view( + canvas_ref, + Some(InViewOptions { + trigger_once: Some(false), + ..Default::default() + }), + ); + // TODO - This should respond to resizing. We might also benefit from making the backdrop // struct also get it's extension from here @@ -229,12 +333,23 @@ pub fn Button( } }); + // Build the loop once canvas + backdrop builder are ready, apply the current visibility state + // immediately (untracked, so this effect stays keyed to canvas/builder only), then store it. + // The wake guard makes the initial wake safe even if the visibility effect also fires true. Effect::new(move || { if let Some(canvas) = canvas_ref.get() { if let Some(backdrop_builder) = maybe_backdrop_builder.get() { let backdrop = backdrop_builder.create_backdrop(canvas, ButtonBackdropCfg { color }); - start_animaiton_loop(backdrop, animation_direction.clone()); + let ctrls = create_render_loop(backdrop, animation_direction.clone()); + + if canvas_in_view.get_untracked() { + (ctrls.wake)(); + } else { + (ctrls.sleep)(); + } + + controls.set_value(Some(ctrls)); } } }); @@ -247,6 +362,21 @@ pub fn Button( } }); + // Drive wake/sleep off visibility. Read the signal FIRST and unconditionally, or the effect + // registers no dependency on its early-exit run and never fires again. + Effect::new(move || { + let in_view = canvas_in_view.get(); + controls.with_value(|c| { + if let Some(c) = c { + if in_view { + (c.wake)(); + } else { + (c.sleep)(); + } + } + }); + }); + match use_as { ButtonUsecase::Button { on_click } => Either::Left(view! { + + +
    } From f63ad674a03c43126499c456930080504afdc8da Mon Sep 17 00:00:00 2001 From: Akin Aguda Date: Sat, 18 Jul 2026 08:38:50 -0500 Subject: [PATCH 13/14] Fix/Updated rustup version in Dockerfile --- Dockerfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index be66a80..d2499dd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM rust:1.92.0-alpine3.23 AS base +FROM rust:1.97.1-alpine3.23 AS base RUN apk add --no-cache \ bash \ @@ -14,14 +14,14 @@ RUN apk add --no-cache \ perl \ python3 \ cmake - + COPY rust-toolchain.toml ./ RUN rustup show # Install all Rust tools once in base RUN cargo install cargo-binstall -RUN cargo install cargo-chef +RUN cargo install cargo-chef RUN npm install -g sass -RUN cargo install stylance-cli +RUN cargo install stylance-cli RUN cargo binstall cargo-leptos -y RUN cargo install -f wasm-bindgen-cli --version 0.2.105 RUN rustup target add wasm32-unknown-unknown From 4cc09c8bc0f98b709cab70b21487f02fc16331a1 Mon Sep 17 00:00:00 2001 From: Akin Aguda Date: Sat, 18 Jul 2026 08:57:36 -0500 Subject: [PATCH 14/14] Fixed the Dockerfile --- Dockerfile | 42 ++++++++++++++++-------------------------- 1 file changed, 16 insertions(+), 26 deletions(-) diff --git a/Dockerfile b/Dockerfile index d2499dd..49dbba4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,4 @@ +###### Base stage — image toolchain (1.97.1) builds the tools ###### FROM rust:1.97.1-alpine3.23 AS base RUN apk add --no-cache \ @@ -15,73 +16,62 @@ RUN apk add --no-cache \ python3 \ cmake -COPY rust-toolchain.toml ./ -RUN rustup show -# Install all Rust tools once in base -RUN cargo install cargo-binstall -RUN cargo install cargo-chef +# IMPORTANT: install tooling BEFORE rust-toolchain.toml is copied in. +# Otherwise the pinned nightly overrides the image toolchain and tool +# dependencies (kstring, vergen, ...) fail their rustc version checks. +RUN cargo install --locked cargo-binstall +RUN cargo binstall -y --locked cargo-chef stylance-cli cargo-leptos +RUN cargo binstall -y wasm-bindgen-cli --version 0.2.126 RUN npm install -g sass -RUN cargo install stylance-cli -RUN cargo binstall cargo-leptos -y -RUN cargo install -f wasm-bindgen-cli --version 0.2.105 -RUN rustup target add wasm32-unknown-unknown + +# Now pin the project toolchain and add the wasm target to *it* +COPY rust-toolchain.toml ./ +RUN rustup show \ + && rustup target add wasm32-unknown-unknown WORKDIR /work -###### Planner stage #### +###### Planner stage ###### FROM base AS planner - -# Only copy dependency files, NOT source code COPY . . RUN cargo chef prepare --recipe-path recipe.json -###### Chef stage - cook dependencies #### +###### Chef stage — cook dependencies ###### FROM base AS chef - COPY --from=planner /work/recipe.json recipe.json RUN cargo chef cook --release --recipe-path recipe.json ###### Builder stage ###### FROM base AS builder - WORKDIR /work -# Copy cooked dependencies +# Reuse cooked dependency artifacts COPY --from=chef /work/target target COPY --from=chef /usr/local/cargo /usr/local/cargo -# Now copy source code COPY . . -# Run stylance and build RUN stylance . RUN cargo leptos build --release -vv -##### Production runner ##### +###### Production runner ###### FROM debian:bookworm-slim AS runner - WORKDIR /app -# Install runtime dependencies RUN apt-get update -y \ && apt-get install -y --no-install-recommends openssl ca-certificates \ && apt-get autoremove -y \ && apt-get clean -y \ && rm -rf /var/lib/apt/lists/* -# Copy only what's needed for runtime COPY --from=builder /work/data /app/data COPY --from=builder /work/target/release/rust-nigeria-website /app/ COPY --from=builder /work/target/site /app/site COPY --from=builder /work/Cargo.toml /app/ - - - ENV RUST_LOG="debug" ENV LEPTOS_SITE_ADDR="0.0.0.0:8080" ENV LEPTOS_SITE_ROOT=./site EXPOSE 8080 - CMD ["/app/rust-nigeria-website"]