diff --git a/app/src/domain/icons/page_icons.rs b/app/src/domain/icons/page_icons.rs
index 42c06ee..1f44446 100644
--- a/app/src/domain/icons/page_icons.rs
+++ b/app/src/domain/icons/page_icons.rs
@@ -94,7 +94,7 @@ pub fn PageIcons() -> impl IntoView {
selected_icon_name.set(icon_name);
selected_icon_function.set(Some(icon_func));
- // Click the hidden trigger to let JavaScript handle the drawer opening
+ // Click the hidden trigger inside the Drawer subtree to open it reactively.
let window = window();
if let Some(document) = window.document()
&& let Ok(Some(trigger)) = document.query_selector("[data-name=\"DrawerTrigger\"]")
@@ -294,10 +294,8 @@ pub fn PageIcons() -> impl IntoView {
- // Hidden trigger for JavaScript to wire up
- "Open"
-
+ "Open"
diff --git a/app/src/shell.rs b/app/src/shell.rs
index e50f0b6..b5d7904 100644
--- a/app/src/shell.rs
+++ b/app/src/shell.rs
@@ -138,6 +138,8 @@ pub fn shell(options: LeptosOptions) -> impl IntoView {
+
+
// Load scripts (async for non-blocking parallel download, executes as soon as ready)
diff --git a/app_crates/registry/src/demos/demo_drawer_family.rs b/app_crates/registry/src/demos/demo_drawer_family.rs
index 9f9919a..ca87a23 100644
--- a/app_crates/registry/src/demos/demo_drawer_family.rs
+++ b/app_crates/registry/src/demos/demo_drawer_family.rs
@@ -14,19 +14,19 @@ pub fn DemoDrawerFamily() -> impl IntoView {
"Options"
-
}
}
+
+fn bool_attr(value: bool) -> &'static str {
+ if value { "true" } else { "false" }
+}
+
+fn now_ms() -> f64 {
+ js_sys::Date::now()
+}
+
+fn content_element(ctx: &DrawerContext) -> Option {
+ ctx.content_ref.get_untracked().map(|element| element.unchecked_into::())
+}
+
+fn overlay_element(ctx: &DrawerContext) -> Option {
+ ctx.overlay_ref.get_untracked().map(|element| element.unchecked_into::())
+}
+
+fn query_drawer_wrapper() -> Option {
+ let document = window().document()?;
+ let element = document.query_selector("[data-vaul-drawer-wrapper]").ok()??;
+ element.dyn_into::().ok()
+}
+
+fn measure_drawer_size(drawer: &HtmlElement, position: DrawerPosition) -> f64 {
+ let rect = drawer.get_bounding_client_rect();
+ if is_horizontal(position) { rect.width() } else { rect.height() }
+}
+
+fn is_horizontal(position: DrawerPosition) -> bool {
+ matches!(position, DrawerPosition::Left | DrawerPosition::Right)
+}
+
+fn pointer_axis_value(event: &PointerEvent, position: DrawerPosition) -> f64 {
+ if is_horizontal(position) { f64::from(event.page_x()) } else { f64::from(event.page_y()) }
+}
+
+fn dragging_in_closing_direction(delta: f64, position: DrawerPosition) -> bool {
+ match position {
+ DrawerPosition::Bottom | DrawerPosition::Right => delta > 0.0,
+ DrawerPosition::Left => delta < 0.0,
+ }
+}
+
+fn percentage_dragged(delta: f64, drawer_size: f64) -> f64 {
+ if drawer_size <= 0.0 { 0.0 } else { (delta / drawer_size).clamp(0.0, 1.0) }
+}
+
+fn get_scale() -> f64 {
+ let Some(inner_width) = window().inner_width().ok().and_then(|value| value.as_f64()) else {
+ return 1.0;
+ };
+ (inner_width - WINDOW_TOP_OFFSET) / inner_width
+}
+
+fn dampen_value(value: f64) -> f64 {
+ 8.0 * ((value + 1.0).ln() - 2.0)
+}
+
+fn drag_transform(delta: f64, closing_direction: bool, position: DrawerPosition) -> String {
+ if closing_direction {
+ return if is_horizontal(position) {
+ format!("translate3d({delta}px, 0, 0)")
+ } else {
+ format!("translate3d(0, {delta}px, 0)")
+ };
+ }
+
+ let damped = dampen_value(delta.abs());
+ match position {
+ DrawerPosition::Bottom => format!("translate3d(0, {}px, 0)", -damped),
+ DrawerPosition::Left | DrawerPosition::Right => {
+ let signed = if delta > 0.0 { damped } else { -damped };
+ format!("translate3d({signed}px, 0, 0)")
+ }
+ }
+}
+
+fn should_close_from_drag(delta: f64, velocity: f64, drawer_size: f64, position: DrawerPosition) -> bool {
+ match position {
+ DrawerPosition::Bottom | DrawerPosition::Right => {
+ (velocity > VELOCITY_THRESHOLD || delta / drawer_size >= CLOSE_THRESHOLD) && delta > 0.0
+ }
+ DrawerPosition::Left => {
+ (velocity > VELOCITY_THRESHOLD || delta.abs() / drawer_size >= CLOSE_THRESHOLD) && delta < 0.0
+ }
+ }
+}
+
+fn finish_pointer_drag(
+ ctx: &DrawerContext,
+ event: PointerEvent,
+ position: DrawerPosition,
+ drag_state: DragState,
+) {
+ if !drag_state.is_dragging.get() || !ctx.dismissible.get() {
+ return;
+ }
+
+ drag_state.is_dragging.set(false);
+
+ let Some(drawer) = content_element(ctx) else { return };
+ let overlay = overlay_element(ctx);
+ let delta = drag_state.current_pos.get_untracked() - drag_state.start_pos.get_untracked();
+ let elapsed = (now_ms() - drag_state.drag_start_time.get_untracked()).max(1.0);
+ let velocity = delta.abs() / elapsed;
+
+ let _ = drawer
+ .style()
+ .set_property("transition", &format!("transform 0.5s {TRANSITION_EASING}"));
+
+ if let Some(wrapper) = query_drawer_wrapper() {
+ let _ = wrapper.style().set_property(
+ "transition",
+ &format!("transform 0.5s {TRANSITION_EASING}, border-radius 0.5s {TRANSITION_EASING}"),
+ );
+ }
+
+ if let Some(overlay) = &overlay {
+ let _ = overlay
+ .style()
+ .set_property("transition", &format!("opacity 0.5s {TRANSITION_EASING}"));
+ }
+
+ let size = drag_state.drawer_size.get_untracked().max(1.0);
+ if should_close_from_drag(delta, velocity, size, position) {
+ ctx.close();
+ } else {
+ let _ = drawer.style().set_property("transform", "translate3d(0, 0, 0)");
+
+ if let Some(wrapper) = query_drawer_wrapper() {
+ let scale = get_scale();
+ let _ = wrapper.style().set_property("border-radius", &format!("{BORDER_RADIUS}px"));
+ let _ = wrapper
+ .style()
+ .set_property("transform", &format!("scale({scale}) translate3d(0, {WRAPPER_TRANSLATE_Y}px, 0)"));
+ }
+
+ if let Some(overlay) = overlay {
+ let _ = overlay.style().set_property("opacity", "1");
+ }
+ }
+
+ if drawer.has_pointer_capture(event.pointer_id()) {
+ let _ = drawer.release_pointer_capture(event.pointer_id());
+ }
+}
+
+fn event_target_matches(target: Option, selector: &str) -> bool {
+ target
+ .and_then(|target| target.dyn_into::().ok())
+ .and_then(|element| element.closest(selector).ok().flatten())
+ .is_some()
+}
+
+fn should_drag(
+ target: Option,
+ drawer: &HtmlElement,
+ open_time: RwSignal