yggdrasil/components/
footer.rs1use crate::api::settings::get_site_settings;
7#[cfg(target_arch = "wasm32")]
8use crate::hooks::event_listener::use_event_listener;
9use dioxus::prelude::*;
10
11#[component]
19#[allow(unused_mut)]
20pub fn Footer() -> Element {
21 let mut visible = use_signal(|| false);
22
23 let settings_res = use_server_future(get_site_settings)?;
27 let github_url: Option<String> = settings_res
28 .read()
29 .as_ref()
30 .and_then(|r| r.as_ref().ok())
31 .map(|s| s.github_url.clone())
32 .filter(|u| !u.is_empty());
33
34 let mut sync_visible = move || {
37 #[cfg(target_arch = "wasm32")]
38 {
39 if let Some(w) = web_sys::window() {
40 let threshold = w
41 .inner_height()
42 .ok()
43 .and_then(|h| h.as_f64())
44 .unwrap_or(0.0);
45 let scroll_y = w.scroll_y().unwrap_or(0.0);
46 visible.set(scroll_y > threshold);
47 }
48 }
49 };
50
51 #[cfg(target_arch = "wasm32")]
56 use_event_listener(
57 web_sys::window,
58 "scroll",
59 sync_visible,
61 );
62
63 use_effect(move || {
65 sync_visible();
66 });
67
68 rsx! {
69 FooterView {
70 github_url,
71 top_visible: visible(),
72 on_top: move |_| scroll_to_top(),
73 }
74 }
75}
76
77#[component]
79pub(crate) fn FooterView(
80 github_url: Option<String>,
81 top_visible: bool,
82 on_top: EventHandler<()>,
83 #[props(default = true)] page_shell: bool,
84 #[props(default)] inline_top: bool,
85) -> Element {
86 let btn_class = if inline_top {
87 "w-9 h-9 rounded-full bg-paper-entry border border-paper-border shadow-sm flex items-center justify-center cursor-pointer text-paper-secondary hover:text-paper-accent"
88 } else if top_visible {
89 "fixed bottom-16 right-8 z-50 w-10 h-10 rounded-full bg-paper-entry border border-paper-border shadow-sm flex items-center justify-center cursor-pointer transition-all duration-300 text-paper-secondary hover:text-paper-accent opacity-100 translate-y-0"
90 } else {
91 "fixed bottom-16 right-8 z-50 w-10 h-10 rounded-full bg-paper-entry border border-paper-border shadow-sm flex items-center justify-center cursor-pointer transition-all duration-300 text-paper-secondary hover:text-paper-accent opacity-0 translate-y-2 pointer-events-none"
92 };
93 rsx! {
94 footer { "data-vt-shell": page_shell.then_some("frontend-footer"), class: "w-full border-t border-paper-border mt-auto",
95 div { class: "max-w-4xl mx-auto px-6 py-5 flex items-center justify-between text-sm text-paper-secondary",
96 span { "© 2026 Yggdrasil" }
97 if let Some(url) = github_url.as_ref() {
98 a {
99 class: "inline-flex items-center justify-center w-8 h-8 rounded-full text-paper-secondary hover:text-paper-primary hover:bg-paper-entry transition-colors",
100 href: "{url}",
101 target: "_blank",
102 rel: "noopener noreferrer",
103 aria_label: "GitHub",
104 title: "GitHub",
105 svg {
106 xmlns: "http://www.w3.org/2000/svg",
107 width: "20",
108 height: "20",
109 view_box: "0 0 24 24",
110 fill: "currentColor",
111 path {
112 fill_rule: "evenodd",
113 clip_rule: "evenodd",
114 d: "M12.026 2c-5.509 0-9.974 4.465-9.974 9.974 0 4.406 2.857 8.145 6.821 9.465.499.09.679-.217.679-.481 0-.237-.008-.865-.011-1.696-2.775.602-3.361-1.338-3.361-1.338-.452-1.152-1.107-1.459-1.107-1.459-.905-.619.069-.605.069-.605 1.002.07 1.527 1.028 1.527 1.028.89 1.524 2.336 1.084 2.902.829.091-.645.351-1.085.635-1.334-2.214-.251-4.542-1.107-4.542-4.93 0-1.087.389-1.979 1.024-2.675-.101-.253-.446-1.268.099-2.64 0 0 .837-.269 2.742 1.021a9.582 9.582 0 0 1 2.496-.336 9.554 9.554 0 0 1 2.496.336c1.906-1.291 2.742-1.021 2.742-1.021.545 1.372.203 2.387.099 2.64.64.696 1.024 1.587 1.024 2.675 0 3.833-2.33 4.675-4.552 4.922.355.308.675.916.675 1.846 0 1.334-.012 2.41-.012 2.737 0 .267.178.577.687.479C19.146 20.115 22 16.379 22 11.974 22 6.465 17.535 2 12.026 2z",
115 }
116 }
117 }
118 }
119 if inline_top {
120 button {
121 class: "{btn_class}",
122 r#type: "button",
123 aria_label: "go to top",
124 title: "回到演示顶部",
125 onclick: move |_| on_top.call(()),
126 TopIcon {}
127 }
128 }
129 }
130 }
131 if !inline_top {
132 a {
133 class: "{btn_class}",
134 href: "#top",
135 aria_label: "go to top",
136 title: "Go to Top (Alt + G)",
137 accesskey: "g",
138 onclick: move |event| {
139 event.prevent_default();
140 on_top.call(());
141 },
142 TopIcon {}
143 }
144 }
145 }
146}
147
148#[component]
149fn TopIcon() -> Element {
150 rsx! {
151 svg {
152 xmlns: "http://www.w3.org/2000/svg",
153 height: "24px",
154 view_box: "0 -960 960 960",
155 width: "24px",
156 fill: "currentColor",
157 path { d: "m296-224-56-56 240-240 240 240-56 56-184-183-184 183Zm0-240-56-56 240-240 240 240-56 56-184-183-184 183Z" }
158 }
159 }
160}
161
162fn scroll_to_top() {
166 #[cfg(target_arch = "wasm32")]
167 {
168 if let Some(window) = web_sys::window() {
169 let options = web_sys::ScrollToOptions::new();
170 options.set_top(0.0);
171 options.set_behavior(web_sys::ScrollBehavior::Smooth);
172 window.scroll_to_with_scroll_to_options(&options);
173
174 if let Ok(history) = window.history() {
175 let _ = history.replace_state_with_url(&wasm_bindgen::JsValue::NULL, "", Some(" "));
176 }
177 }
178 }
179}