use super::*;
pub(crate) fn app() -> VirtualNode {
let route_signal: Signal<String> = App::use_signal(Router::current_route);
let drawer_open: Signal<bool> = App::use_signal(|| false);
let locale_menu_open: Signal<bool> = App::use_signal(|| false);
let collapsed: Signal<Vec<String>> = App::use_signal(Vec::new);
let mobile_signal: Signal<bool> = UseEuvLayout::use_resize();
UseEuvLayout::use_safe_area_fix();
let theme_state: ThemeState = ThemeState::use_theme_state(mobile_signal);
let theme_signal: Signal<String> = theme_state.get_theme();
let root_class_signal: Signal<String> = theme_state.get_root_class();
Router::use_hash_change(route_signal);
Router::use_overlay_history(drawer_open, mobile_signal);
use_anchor_scroll(route_signal);
html! {
div {
key: locale_of(&parse_route(&route_signal.get()).0).prefix
style: "display: contents"
if { mobile_signal.get() } {
docs_mobile_shell {
route_signal
theme_signal
root_class_signal
drawer_open
locale_menu_open
collapsed
}
} else {
docs_desktop_shell {
route_signal
theme_signal
root_class_signal
drawer_open
locale_menu_open
collapsed
}
}
}
}
}
#[component]
pub(crate) fn docs_desktop_shell(node: VirtualNode<DocsShellProps>) -> VirtualNode {
let DocsShellProps {
route_signal,
theme_signal,
root_class_signal,
drawer_open: _,
locale_menu_open,
collapsed,
}: DocsShellProps = node.try_get_props().unwrap_or_default();
let (path, _anchor) = parse_route(&route_signal.get());
let locale: &DocsLocale = locale_of(&path);
html! {
div {
class: root_class_signal
nav {
class: c_app_nav()
brand_header(locale)
locale_row_node(route_signal, locale_menu_open)
p {
class: c_nav_section_label()
{
section_label(locale)
}
}
div {
class: c_nav_items_scroll()
euv_sidebar {
route_signal
collapsed
items: locale.sidebar
}
}
div {
class: c_nav_theme_toggle()
button {
class: c_nav_theme_button()
title: "Toggle theme"
onclick: ThemeState::toggle(theme_signal)
theme_icon_node(theme_signal)
}
}
nav_footer_node(github_link(locale))
}
main {
class: c_app_main()
style: "user-select: text"
docs_main {
route_signal
}
}
}
}
}
#[component]
pub(crate) fn docs_mobile_shell(node: VirtualNode<DocsShellProps>) -> VirtualNode {
let DocsShellProps {
route_signal,
theme_signal,
root_class_signal,
drawer_open,
locale_menu_open,
collapsed,
}: DocsShellProps = node.try_get_props().unwrap_or_default();
let (path, _anchor) = parse_route(&route_signal.get());
let locale: &DocsLocale = locale_of(&path);
html! {
div {
class: root_class_signal
header {
class: c_mobile_header()
div {
class: c_mobile_header_left()
button {
class: if { drawer_open } {
c_mobile_menu_button_active()
} else {
c_mobile_menu_button()
}
onclick: UseEuvLayout::use_drawer_toggle(drawer_open)
"☰"
}
brand_logo_row(locale)
}
button {
class: c_mobile_theme_button()
title: "Toggle theme"
onclick: ThemeState::toggle(theme_signal)
theme_icon_node(theme_signal)
}
}
main {
class: c_mobile_main()
style: "user-select: text"
docs_main {
route_signal
}
}
div {
class: if { drawer_open } {
c_mobile_overlay().to_string()
} else {
format!("{} {}", c_mobile_overlay().get_name(), c_mobile_overlay_hidden().get_name())
}
onclick: close_drawer(drawer_open)
}
nav {
class: if { drawer_open } {
c_mobile_nav_drawer().to_string()
} else {
format!("{} {}", c_mobile_nav_drawer().get_name(), c_mobile_nav_drawer_closed().get_name())
}
div {
class: c_mobile_nav_drawer_header()
brand_logo_row(locale)
button {
class: c_mobile_drawer_close_button()
onclick: close_drawer(drawer_open)
"✕"
}
}
locale_row_node(route_signal, locale_menu_open)
p {
class: c_nav_section_label()
{
section_label(locale)
}
}
div {
class: c_nav_items_scroll()
euv_sidebar {
route_signal
collapsed
items: locale.sidebar
on_navigate: drawer_navigate()
}
}
nav_footer_node(github_link(locale))
}
}
}
}
fn brand_header(locale: &'static DocsLocale) -> VirtualNode {
let site: &DocsSite = &crate::generated::SITE;
let title: &str = if locale.title.is_empty() {
site.title
} else {
locale.title
};
html! {
a {
class: c_nav_header()
href: format!("#{}", locale.prefix)
onclick: Router::link_handler(locale.prefix)
euv_logo {
variant: LogoButtonVariant::Nav
}
span {
class: c_nav_brand_title()
{
title
}
}
}
}
}
fn brand_logo_row(locale: &'static DocsLocale) -> VirtualNode {
let site: &DocsSite = &crate::generated::SITE;
let title: &str = if locale.title.is_empty() {
site.title
} else {
locale.title
};
html! {
a {
class: c_mobile_header_logo()
href: format!("#{}", locale.prefix)
onclick: Router::link_handler(locale.prefix)
euv_logo {
variant: LogoButtonVariant::Nav
}
span {
class: c_nav_brand_title()
{
title
}
}
}
}
}
fn theme_icon_node(theme_signal: Signal<String>) -> VirtualNode {
html! {
div {
class: if { theme_signal.get() == THEME_DARK } {
c_theme_icon_sun()
} else {
c_theme_icon_moon()
}
}
}
}
fn nav_footer_node(github: Option<&'static str>) -> VirtualNode {
let Some(url) = github else {
return html! {
""
};
};
html! {
a {
class: c_nav_footer()
href: url
target: "_blank"
onclick: Router::external_link_handler(url)
div {
class: c_nav_footer_divider()
}
span {
class: c_nav_footer_text()
"Built with "
span {
class: c_nav_footer_brand()
"Euv & Wasm"
}
}
}
}
}
fn locale_row_node(route_signal: Signal<String>, locale_menu_open: Signal<bool>) -> VirtualNode {
let site: &DocsSite = &crate::generated::SITE;
if site.locales.len() <= 1 {
return html! {
""
};
}
let (path, _anchor) = parse_route(&route_signal.get());
let current_label: &str = locale_of(&path).label;
let items: Vec<EuvDropdownItem> = site
.locales
.iter()
.map(|target: &DocsLocale| EuvDropdownItem {
label: target.label,
value: target.prefix,
})
.collect();
html! {
div {
class: c_nav_locale_row()
euv_dropdown {
open: locale_menu_open
items: items
on_select: switch_locale(route_signal, locale_menu_open)
button {
class: c_nav_theme_button()
title: "Language"
onclick: toggle_menu(locale_menu_open)
{
current_label
}
}
}
}
}
}
fn section_label(locale: &'static DocsLocale) -> &'static str {
locale
.navbar
.iter()
.find(|item: &&EuvNavbarItem| !item.link.starts_with("http") && item.link != locale.prefix)
.map(|item: &EuvNavbarItem| item.text)
.unwrap_or("Docs")
}
fn github_link(locale: &'static DocsLocale) -> Option<&'static str> {
locale
.navbar
.iter()
.find(|item: &&EuvNavbarItem| item.link.starts_with("http"))
.map(|item: &EuvNavbarItem| item.link)
}
fn toggle_menu(menu_open: Signal<bool>) -> Option<Rc<dyn Fn(Event)>> {
Some(Rc::new(move |_| menu_open.set(!menu_open.get())))
}
fn switch_locale(
route_signal: Signal<String>,
menu_open: Signal<bool>,
) -> Option<Rc<dyn Fn(&'static str)>> {
Some(Rc::new(move |prefix: &'static str| {
let site: &DocsSite = &crate::generated::SITE;
let Some(target) = site.locales.iter().find(|locale| locale.prefix == prefix) else {
return;
};
let (path, _anchor) = parse_route(&route_signal.get());
menu_open.set(false);
if let Some(window) = web_sys::window() {
let location: Location = window.location();
let route: String = route_in_locale(&path, target);
let _: Result<(), JsValue> = location.set_hash(&route);
let _: Result<(), JsValue> = location.reload();
}
}))
}
fn close_drawer(drawer_open: Signal<bool>) -> Option<Rc<dyn Fn(Event)>> {
Some(Rc::new(move |_| {
Router::overlay_stack_close();
drawer_open.set(false);
}))
}
fn drawer_navigate() -> Option<Rc<dyn Fn(&'static str)>> {
Some(Rc::new(|link: &'static str| {
Router::overlay_back(Some(link.to_string()));
}))
}
fn use_anchor_scroll(route_signal: Signal<String>) {
let handler = move || {
let raw: String = route_signal.get();
let (_path, anchor) = parse_route(&raw);
schedule_scroll(anchor);
};
handler();
route_signal.subscribe(handler);
}
fn schedule_scroll(anchor: Option<String>) {
let Some(window) = web_sys::window() else {
return;
};
let callback: Closure<dyn FnMut()> = Closure::once(move || {
let Some(window) = web_sys::window() else {
return;
};
let scrolled: bool = anchor
.and_then(|id| window.document().and_then(|doc| doc.get_element_by_id(&id)))
.map(|element| {
element.scroll_into_view();
})
.is_some();
if !scrolled {
window.scroll_to_with_x_and_y(0.0, 0.0);
if let Some(doc) = window.document() {
for selector in ["[class*=c_app_main]", "[class*=c_mobile_main]"] {
if let Ok(Some(main)) = doc.query_selector(selector) {
main.set_scroll_top(0);
}
}
}
}
});
let _: Result<i32, JsValue> = window.set_timeout_with_callback_and_timeout_and_arguments_0(
callback.as_ref().unchecked_ref(),
100,
);
callback.forget();
}