commit a5d74574ba56d110512f0b3dd72a64c29e53f966
parent ff21f1ec6d7445c813153de6683e1bdb007d6afe
Author: triesap <triesap@radroots.dev>
Date: Thu, 22 Jan 2026 00:33:22 +0000
app: stabilize dialog presence lifecycle
- move presence wrapper inside portal mount
- store dialog open as ReadSignal to avoid untracked reads
- derive tracked presence signal for portal rendering
- keep sheet reopen behavior consistent after close
Diffstat:
2 files changed, 58 insertions(+), 11 deletions(-)
diff --git a/app/assets/styles.css b/app/assets/styles.css
@@ -72,12 +72,13 @@
inset: 0;
background: rgba(0, 0, 0, 0.32);
opacity: 0;
- transition: opacity var(--dur-2) var(--ease-ios);
+ animation: overlay-fade-out 200ms ease both;
}
[data-ui="dialog-overlay"][data-state="open"],
[data-ui="sheet-overlay"][data-state="open"] {
opacity: 1;
+ animation: overlay-fade-in 240ms var(--ease-ios) both;
}
[data-ui="sheet"] {
@@ -93,15 +94,17 @@
background: var(--material-regular);
backdrop-filter: blur(18px) saturate(180%);
box-shadow: var(--shadow-sheet);
- transform: translateY(12px);
+ transform: translateY(110%);
opacity: 0;
- transition: transform var(--dur-3) var(--ease-ios), opacity var(--dur-2) ease;
+ animation: sheet-slide-out 260ms ease both;
+ will-change: transform, opacity;
overscroll-behavior: none;
}
[data-ui="sheet"][data-state="open"] {
transform: translateY(0);
opacity: 1;
+ animation: sheet-slide-in 420ms var(--ease-ios) both;
}
[data-ui="sheet-handle"] {
@@ -142,3 +145,47 @@
color: var(--text-secondary);
}
}
+
+@keyframes overlay-fade-in {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
+
+@keyframes overlay-fade-out {
+ from {
+ opacity: 1;
+ }
+ to {
+ opacity: 0;
+ }
+}
+
+@keyframes sheet-slide-in {
+ 0% {
+ transform: translateY(110%);
+ opacity: 0;
+ }
+ 60% {
+ transform: translateY(-2%);
+ opacity: 1;
+ }
+ 100% {
+ transform: translateY(0);
+ opacity: 1;
+ }
+}
+
+@keyframes sheet-slide-out {
+ 0% {
+ transform: translateY(0);
+ opacity: 1;
+ }
+ 100% {
+ transform: translateY(110%);
+ opacity: 0;
+ }
+}
diff --git a/crates/ui-components/src/dialog.rs b/crates/ui-components/src/dialog.rs
@@ -22,7 +22,7 @@ use radroots_app_ui_primitives::{
#[derive(Clone)]
struct RadrootsAppUiDialogContext {
- open: Signal<bool>,
+ open: ReadSignal<bool>,
set_open: Callback<bool>,
dismiss: Callback<RadrootsAppUiDismissableReason>,
modal: bool,
@@ -51,8 +51,8 @@ pub fn RadrootsAppUiDialogRoot(
let open_prop = open;
let is_controlled = open_prop.is_some();
let open_signal = match open_prop {
- Some(open) => open.into(),
- None => open_state.into(),
+ Some(open) => open,
+ None => open_state.read_only(),
};
let on_open_change = on_open_change.clone();
let set_open = Callback::new(move |value| {
@@ -125,14 +125,14 @@ pub fn RadrootsAppUiDialogTrigger(
pub fn RadrootsAppUiDialogPortal(children: ChildrenFn) -> impl IntoView {
let context = use_context::<RadrootsAppUiDialogContext>()
.expect("dialog context");
- let present = context.open;
+ let present = Signal::derive(move || context.open.get());
let children = StoredValue::new(children);
view! {
- <RadrootsAppUiPresence present=present>
- <RadrootsAppUiPortal>
+ <RadrootsAppUiPortal>
+ <RadrootsAppUiPresence present=present>
{(children.get_value())()}
- </RadrootsAppUiPortal>
- </RadrootsAppUiPresence>
+ </RadrootsAppUiPresence>
+ </RadrootsAppUiPortal>
}
}