diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 41bd4c18..5c3168d1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -214,7 +214,7 @@ jobs: cd end2end wasm-pack test --headless --${{ matrix.browser }} - build-example: + build-examples: needs: check-toolchain-is-stable name: Build example runs-on: ${{ matrix.runs-on }} @@ -226,6 +226,7 @@ jobs: - csr-complete - ssr-hydrate-actix - ssr-hydrate-axum + - ssr-islands-axum - system-gtk toolchain: - stable @@ -305,12 +306,9 @@ jobs: - qa - unit-tests - end2end-tests - - build-example + - build-examples - build-book - if: | - '${{ github.event.pull_request.user.login }}' == 'mondeja' || - startsWith(github.ref, 'refs/tags/') || - github.ref == 'refs/heads/master' + if: startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/master' name: Test leptos-fluent-macros release runs-on: ubuntu-latest steps: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 45aff54c..7fc15acd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -36,9 +36,9 @@ repos: - id: clippy alias: clippy-no-features name: clippy-no-features - args: - [ + args: [ --exclude=leptos-fluent-ssr-hydrate-axum-example, + --exclude=leptos-fluent-ssr-islands-axum-example, # TODO: excluded by Leptos bug --workspace, --, -D, @@ -87,6 +87,7 @@ repos: [ "--features=ssr,actix", --exclude=leptos-fluent-ssr-hydrate-axum-example, + --exclude=leptos-fluent-ssr-islands-axum-example, --exclude=leptos-fluent-csr-complete-example, --exclude=leptos-fluent-csr-minimal-example, --workspace, @@ -115,6 +116,7 @@ repos: --exclude=leptos-fluent-ssr-hydrate-actix-example, --exclude=leptos-fluent-csr-complete-example, --exclude=leptos-fluent-csr-minimal-example, + --exclude=leptos-fluent-ssr-islands-axum-example, --workspace, --, -D, diff --git a/Cargo.lock b/Cargo.lock index a28a05b4..1466043c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1784,6 +1784,8 @@ dependencies = [ "leptos_macro", "leptos_reactive", "leptos_server", + "serde", + "serde_json", "server_fn", "tracing", "typed-builder", @@ -1916,6 +1918,27 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "leptos-fluent-ssr-islands-axum-example" +version = "0.1.0" +dependencies = [ + "axum", + "console_error_panic_hook", + "fluent-templates", + "http 1.1.0", + "leptos", + "leptos-fluent", + "leptos_axum", + "leptos_meta", + "leptos_router", + "serde", + "thiserror", + "tokio", + "tower", + "tower-http", + "wasm-bindgen", +] + [[package]] name = "leptos_actix" version = "0.6.14" diff --git a/Cargo.toml b/Cargo.toml index 8f500fd5..13154f2b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "examples/csr-minimal", "examples/ssr-hydrate-actix", "examples/ssr-hydrate-axum", + "examples/ssr-islands-axum", "examples/system-gtk", ] resolver = "2" @@ -17,13 +18,7 @@ edition = "2021" documentation = "https://docs.rs/leptos-fluent" repository = "https://github.com/mondeja/leptos-fluent" homepage = "https://mondeja.github.io/leptos-fluent" -keywords = [ - "leptos", - "fluent", - "i18n", - "localization", - "wasm", -] +keywords = ["leptos", "fluent", "i18n", "localization", "wasm"] [profile.wasm-release] inherits = "release" diff --git a/examples/ssr-islands-axum/Cargo.toml b/examples/ssr-islands-axum/Cargo.toml new file mode 100644 index 00000000..646bc56e --- /dev/null +++ b/examples/ssr-islands-axum/Cargo.toml @@ -0,0 +1,120 @@ +[package] +name = "leptos-fluent-ssr-islands-axum-example" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +axum = { version = "0.7", optional = true } +console_error_panic_hook = "0.1" +leptos = { version = "0.6", features = ["experimental-islands"] } +leptos_axum = { version = "0.6", optional = true, features = [ + "experimental-islands", +] } +leptos_meta = { version = "0.6" } +leptos_router = { version = "0.6" } +tokio = { version = "1", features = ["rt-multi-thread"], optional = true } +tower = { version = "0.4", optional = true, features = ["util"] } +tower-http = { version = "0.5", features = ["fs"], optional = true } +wasm-bindgen = "=0.2.93" +thiserror = "1" +http = "1" +serde = "1" + +leptos-fluent = { path = "../../leptos-fluent" } +fluent-templates = { version = "0.10", default-features = false, features = [ + "macros", + "walkdir", +] } + +[features] +hydrate = [ + "leptos/hydrate", + "leptos_meta/hydrate", + "leptos_router/hydrate", + "leptos-fluent/hydrate", +] +ssr = [ + "dep:axum", + "dep:tokio", + "dep:tower", + "dep:tower-http", + "dep:leptos_axum", + "leptos/ssr", + "leptos_meta/ssr", + "leptos_router/ssr", + "leptos-fluent/ssr", + "leptos-fluent/axum", +] + +[package.metadata.leptos] +# Additional files your application could depends on. +# A change to any file in those directories will trigger a rebuild. +watch-additional-files = ["examples/ssr-islands-axum/locales"] + +# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name +output-name = "leptos-fluent-ssr-islands-axum-example" + +# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup. +site-root = "target/site-islands" + +# The site-root relative folder where all compiled output (JS, WASM and CSS) is written +# Defaults to pkg +site-pkg-dir = "pkg" + +# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to //app.css +style-file = "style/main.css" +# Assets source dir. All files found here will be copied and synchronized to site-root. +# The assets-dir cannot have a sub directory with the same name/path as site-pkg-dir. +# +# Optional. Env: LEPTOS_ASSETS_DIR. +assets-dir = "public" + +# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup. +site-addr = "127.0.0.1:3000" + +# The port to use for automatic reload monitoring +reload-port = 3001 + +# [Optional] Command to use when running end2end tests. It will run in the end2end dir. +# [Windows] for non-WSL use "npx.cmd playwright test" +# This binary name can be checked in Powershell with Get-Command npx +end2end-cmd = "npx playwright test" +end2end-dir = "end2end" + +# The browserlist query used for optimizing the CSS. +browserquery = "defaults" + +# The environment Leptos will run in, usually either "DEV" or "PROD" +env = "DEV" + +# The features to use when compiling the bin target +# +# Optional. Can be over-ridden with the command line parameter --bin-features +bin-features = ["ssr"] + +# If the --no-default-features flag should be used when compiling the bin target +# +# Optional. Defaults to false. +bin-default-features = false + +# The features to use when compiling the lib target +# +# Optional. Can be over-ridden with the command line parameter --lib-features +lib-features = ["hydrate"] + +# If the --no-default-features flag should be used when compiling the lib target +# +# Optional. Defaults to false. +lib-default-features = false + +# The profile to use for the lib target when compiling for release +# +# Optional. Defaults to "release". +lib-profile-release = "wasm-release" + +[package.metadata.cargo-machete] +ignored = ["serde"] # needed by Leptos to pass arguments to islands diff --git a/examples/ssr-islands-axum/README.md b/examples/ssr-islands-axum/README.md new file mode 100644 index 00000000..5321f6f7 --- /dev/null +++ b/examples/ssr-islands-axum/README.md @@ -0,0 +1,21 @@ +# SSR example with Axum using islands for leptos-fluent + +When using the islands feature, the app is rendered as static HTML by default. +To enable interactivity, the `#[island]` macro must be applied selectively. +Typically, with an internationalization framework like `leptos-fluent`, nearly +every part of the website is translated reactively, which would require using +the `#[island]` macro extensively. + +However, this approach conflicts with one of the core principles of islands +architecture: islands should be as minimal and targeted as possible. + +To benefit from smaller WebAssembly (wasm) file sizes, this example opts to keep +all translations server-side, instead of using the dynamic translation updates +provided by `move_tr!`. The page is reloaded after a language change, ensuring +the translations are updated without excessive use of islands. + +To run: + +```sh +cargo leptos watch +``` diff --git a/examples/ssr-islands-axum/locales/client/en/main.ftl b/examples/ssr-islands-axum/locales/client/en/main.ftl new file mode 100644 index 00000000..503c138f --- /dev/null +++ b/examples/ssr-islands-axum/locales/client/en/main.ftl @@ -0,0 +1,2 @@ +home = Home +page-2 = Page 2 diff --git a/examples/ssr-islands-axum/locales/client/es/main.ftl b/examples/ssr-islands-axum/locales/client/es/main.ftl new file mode 100644 index 00000000..751df60e --- /dev/null +++ b/examples/ssr-islands-axum/locales/client/es/main.ftl @@ -0,0 +1,2 @@ +home = Inicio +page-2 = Página 2 diff --git a/examples/ssr-islands-axum/locales/languages.json b/examples/ssr-islands-axum/locales/languages.json new file mode 100644 index 00000000..42a2eae0 --- /dev/null +++ b/examples/ssr-islands-axum/locales/languages.json @@ -0,0 +1,4 @@ +[ + ["en", "EN"], + ["es", "ES"] +] diff --git a/examples/ssr-islands-axum/locales/server/en/main.ftl b/examples/ssr-islands-axum/locales/server/en/main.ftl new file mode 100644 index 00000000..fcd38c83 --- /dev/null +++ b/examples/ssr-islands-axum/locales/server/en/main.ftl @@ -0,0 +1,5 @@ +welcome-to-leptos = Welcome to Leptos +select-language = Select a language +home-title = Welcome to the home page +page-2-title = Welcome to page 2 +click-me = Click me diff --git a/examples/ssr-islands-axum/locales/server/es/main.ftl b/examples/ssr-islands-axum/locales/server/es/main.ftl new file mode 100644 index 00000000..142ec829 --- /dev/null +++ b/examples/ssr-islands-axum/locales/server/es/main.ftl @@ -0,0 +1,5 @@ +welcome-to-leptos = Bienvenido a Leptos +select-language = Selecciona un idioma +home-title = Bienvenido a la página de inicio +page-2-title = Bienvenido a la página 2 +click-me = Haz click diff --git a/examples/ssr-islands-axum/public/favicon.ico b/examples/ssr-islands-axum/public/favicon.ico new file mode 100644 index 00000000..2ba8527c Binary files /dev/null and b/examples/ssr-islands-axum/public/favicon.ico differ diff --git a/examples/ssr-islands-axum/src/app.rs b/examples/ssr-islands-axum/src/app.rs new file mode 100644 index 00000000..82988ab1 --- /dev/null +++ b/examples/ssr-islands-axum/src/app.rs @@ -0,0 +1,283 @@ +use crate::error_template::{AppError, ErrorTemplate}; +use fluent_templates::static_loader; +use leptos::*; +use leptos_fluent::{expect_i18n, leptos_fluent, tr, I18n}; +use leptos_meta::*; +use leptos_router::{Outlet, Route, Router, Routes}; + +static_loader! { + static SERVER_TRANSLATIONS = { + locales: "./locales/server", + fallback_language: "en", + }; +} + +static_loader! { + static CLIENT_TRANSLATIONS = { + locales: "./locales/client", + fallback_language: "en", + }; +} + +#[component] +pub fn App() -> impl IntoView { + // Provides context that manages stylesheets, titles, meta tags, etc. + provide_meta_context(); + provide_i18n_context(); + + view! { + + + + + <Router fallback=|| { + let mut outside_errors = Errors::default(); + outside_errors.insert_with_default_key(AppError::NotFound); + view! { <ErrorTemplate outside_errors /> }.into_view() + }> + <Routes> + <Route path="" view=BodyView> + <Route path="" view=home::View /> + <Route path="/page-2" view=page_2::View /> + </Route> + </Routes> + </Router> + } +} + +pub fn provide_i18n_context() { + leptos_fluent! { + translations: [SERVER_TRANSLATIONS], + languages: "./locales/languages.json", + locales: "./locales/server", + sync_html_tag_lang: true, + sync_html_tag_dir: true, + cookie_name: "lang", + cookie_attrs: "SameSite=Strict; Secure; path=/; max-age=600", + initial_language_from_cookie: true, + url_param: "lang", + initial_language_from_url_param: true, + initial_language_from_url_param_to_cookie: true, + set_language_to_url_param: true, + initial_language_from_accept_language_header: true, + }; +} + +#[component] +pub fn BodyView() -> impl IntoView { + let i18n = expect_i18n(); + + // Reproviding the context after the header makes server + // translations available for `Outlet` + view! { + <header::View /> + {provide_context::<I18n>(i18n)} + <main> + <Outlet /> + </main> + } +} + +mod header { + use leptos::*; + use leptos_fluent::{expect_i18n, leptos_fluent, tr}; + use leptos_router::A; + + #[component] + pub fn View() -> impl IntoView { + view! { + <header> + <Archipelago> + <LargeMenu /> + <MobileMenu /> + </Archipelago> + </header> + } + } + + #[island] + fn Archipelago(children: Children) -> impl IntoView { + leptos_fluent! { + translations: [super::CLIENT_TRANSLATIONS], + languages: "./locales/languages.json", + locales: "./locales/client", + sync_html_tag_lang: true, + sync_html_tag_dir: true, + cookie_name: "lang", + cookie_attrs: "SameSite=Strict; Secure; path=/; max-age=600", + initial_language_from_cookie: true, + set_language_to_cookie: true, + url_param: "lang", + initial_language_from_url_param: true, + initial_language_from_url_param_to_cookie: true, + set_language_to_url_param: true, + }; + + // Children will be executed when the i18n context is ready. + // See https://book.leptos.dev/islands.html#passing-context-between-islands + // + // > That’s why in `HomePage`, I made `let tabs = move ||` a function, and + // > called it like `{tabs()}`: creating the tabs lazily this way meant that + // > the `Tabs` island would already have provided the `selected` context by + // > the time each `Tab` went looking for it. + children() + } + + #[component] + pub fn LargeMenu() -> impl IntoView { + view! { + <div class="header-large-menu"> + <A href="/">{tr!("home")}</A> + <A href="/page-2">{tr!("page-2")}</A> + <LanguageSelector name="desktop".into() /> + </div> + } + } + + #[component] + pub fn MobileMenu() -> impl IntoView { + view! { + <div class="header-mobile-menu"> + <MobileMenuButton> + <MobileMenuPanel> + <LanguageSelector name="mobile".into() /> + </MobileMenuPanel> + </MobileMenuButton> + </div> + } + } + + #[island] + pub fn MobileMenuButton(children: Children) -> impl IntoView { + let (is_mobile_menu_visible, set_mobile_menu_visibility) = + create_signal(false); + provide_context(is_mobile_menu_visible); + + view! { + <button + class="mobile-button" + on:click=move |_| { + set_mobile_menu_visibility.update(|is_visible| *is_visible = !*is_visible); + } + > + + <svg + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="black" + aria-hidden="true" + style="width: 30px; height: 30px" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" + ></path> + </svg> + </button> + <div + class="mobile-menu-panel-hidder" + class:hidden=move || !is_mobile_menu_visible.get() + on:click=move |_| { + set_mobile_menu_visibility.set(false); + } + ></div> + {children()} + } + } + + #[island] + pub fn MobileMenuPanel(children: Children) -> impl IntoView { + let is_mobile_menu_visible = expect_context::<ReadSignal<bool>>(); + + view! { + <div class="mobile-menu-panel" class:hidden=move || !is_mobile_menu_visible.get()> + <a href="/">{tr!("home")}</a> + <a href="/page-2">{tr!("page-2")}</a> + {children()} + </div> + } + } + + #[island] + fn LanguageSelector(name: String) -> impl IntoView { + let i18n = expect_i18n(); + + // A page reload is necessary after changing the language because + // the translations are stored on the server. This ensures that all + // content is updated to reflect the selected language. + view! { + <div class="language-selector"> + {i18n + .languages + .iter() + .map(|lang| { + view! { + <div> + <input + type="radio" + name=format!("language-{}", name) + value=lang + checked=lang.is_active() + on:click=move |_| { + i18n.language.set(lang); + window() + .location() + .reload() + .expect("Failed to reload the page"); + } + /> + + <label>{lang.name}</label> + </div> + } + }) + .collect::<Vec<_>>()} + + </div> + } + } +} + +mod home { + use leptos::*; + use leptos_fluent::tr; + + #[component] + pub fn View() -> impl IntoView { + view! { + <h1>{tr!("home-title")}</h1> + <HomeCounter>{tr!("click-me")}</HomeCounter> + } + } + + #[island] + fn HomeCounter(children: Children) -> impl IntoView { + let (count, set_count) = create_signal(0); + let on_click = move |_| set_count.update(|count| *count += 1); + + view! { <button on:click=on_click>{children()} " - " {count}</button> } + } +} + +mod page_2 { + use leptos::*; + use leptos_fluent::tr; + + #[component] + pub fn View() -> impl IntoView { + view! { + <h1>{tr!("page-2-title")}</h1> + <Page2Counter>{tr!("click-me")}</Page2Counter> + } + } + + #[island] + fn Page2Counter(children: Children) -> impl IntoView { + let (count, set_count) = create_signal(0); + let on_click = move |_| set_count.update(|count| *count += 1); + + view! { <button on:click=on_click>{children()} " - " {count}</button> } + } +} diff --git a/examples/ssr-islands-axum/src/error_template.rs b/examples/ssr-islands-axum/src/error_template.rs new file mode 100644 index 00000000..cd574f1a --- /dev/null +++ b/examples/ssr-islands-axum/src/error_template.rs @@ -0,0 +1,75 @@ +use http::status::StatusCode; +use leptos::*; +use thiserror::Error; + +#[derive(Clone, Debug, Error)] +pub enum AppError { + #[error("Not Found")] + NotFound, +} + +impl AppError { + pub fn status_code(&self) -> StatusCode { + match self { + AppError::NotFound => StatusCode::NOT_FOUND, + } + } +} + +// A basic function to display errors served by the error boundaries. +// Feel free to do more complicated things here than just displaying the error. +#[component] +pub fn ErrorTemplate( + #[prop(optional)] outside_errors: Option<Errors>, + #[prop(optional)] errors: Option<RwSignal<Errors>>, +) -> impl IntoView { + let errors = match outside_errors { + Some(e) => create_rw_signal(e), + None => match errors { + Some(e) => e, + None => panic!("No Errors found and we expected errors!"), + }, + }; + // Get Errors from Signal + let errors = errors.get_untracked(); + + // Downcast lets us take a type that implements `std::error::Error` + let errors: Vec<AppError> = errors + .into_iter() + .filter_map(|(_k, v)| v.downcast_ref::<AppError>().cloned()) + .collect(); + #[allow(clippy::print_stdout)] + { + println!("Errors: {errors:#?}"); + }; + + // Only the response code for the first error is actually sent from the server + // this may be customized by the specific application + #[cfg(feature = "ssr")] + { + use leptos_axum::ResponseOptions; + let response = use_context::<ResponseOptions>(); + if let Some(response) = response { + response.set_status(errors[0].status_code()); + } + } + + view! { + <h1>{if errors.len() > 1 { "Errors" } else { "Error" }}</h1> + <For + // a function that returns the items we're iterating over; a signal is fine + each=move || { errors.clone().into_iter().enumerate() } + // a unique key for each item as a reference + key=|(index, _error)| *index + // renders each item to a view + children=move |error| { + let error_string = error.1.to_string(); + let error_code = error.1.status_code(); + view! { + <h2>{error_code.to_string()}</h2> + <p>"Error: " {error_string}</p> + } + } + /> + } +} diff --git a/examples/ssr-islands-axum/src/fileserv.rs b/examples/ssr-islands-axum/src/fileserv.rs new file mode 100644 index 00000000..7ae81c02 --- /dev/null +++ b/examples/ssr-islands-axum/src/fileserv.rs @@ -0,0 +1,58 @@ +use crate::app::App; +use axum::response::Response as AxumResponse; +use axum::{ + body::Body, + extract::State, + http::{Request, Response, StatusCode}, + response::IntoResponse, +}; +use leptos::*; +use std::convert::Infallible; +use tower::ServiceExt; +use tower_http::services::{fs::ServeFileSystemResponseBody, ServeDir}; + +pub async fn file_and_error_handler( + State(options): State<LeptosOptions>, + req: Request<Body>, +) -> AxumResponse { + let root = options.site_root.clone(); + let (parts, body) = req.into_parts(); + + let mut static_parts = parts.clone(); + static_parts.headers.clear(); + if let Some(encodings) = parts.headers.get("accept-encoding") { + static_parts + .headers + .insert("accept-encoding", encodings.clone()); + } + + let res = get_static_file( + Request::from_parts(static_parts, Body::empty()), + &root, + ) + .await + .unwrap(); + + if res.status() == StatusCode::OK { + res.into_response() + } else { + let handler = + leptos_axum::render_app_to_stream(options.to_owned(), App); + handler(Request::from_parts(parts, body)) + .await + .into_response() + } +} + +async fn get_static_file( + request: Request<Body>, + root: &str, +) -> Result<Response<ServeFileSystemResponseBody>, Infallible> { + // `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot` + // This path is relative to the cargo root + ServeDir::new(root) + .precompressed_gzip() + .precompressed_br() + .oneshot(request) + .await +} diff --git a/examples/ssr-islands-axum/src/lib.rs b/examples/ssr-islands-axum/src/lib.rs new file mode 100644 index 00000000..f1c80ceb --- /dev/null +++ b/examples/ssr-islands-axum/src/lib.rs @@ -0,0 +1,13 @@ +pub mod app; +pub mod error_template; +#[cfg(feature = "ssr")] +pub mod fileserv; + +#[cfg(feature = "hydrate")] +#[wasm_bindgen::prelude::wasm_bindgen] +pub fn hydrate() { + #[allow(unused_imports)] + use crate::app::*; + console_error_panic_hook::set_once(); + leptos::leptos_dom::HydrationCtx::stop_hydrating(); +} diff --git a/examples/ssr-islands-axum/src/main.rs b/examples/ssr-islands-axum/src/main.rs new file mode 100644 index 00000000..011847bc --- /dev/null +++ b/examples/ssr-islands-axum/src/main.rs @@ -0,0 +1,38 @@ +#[cfg(feature = "ssr")] +#[tokio::main] +async fn main() { + use axum::Router; + use leptos::*; + use leptos_axum::{generate_route_list, LeptosRoutes}; + use leptos_fluent_ssr_islands_axum_example::app::*; + use leptos_fluent_ssr_islands_axum_example::fileserv::file_and_error_handler; + + // Setting get_configuration(None) means we'll be using cargo-leptos's env values + // For deployment these variables are: + // <https://github.com/leptos-rs/start-axum#executing-a-server-on-a-remote-machine-without-the-toolchain> + // Alternately a file can be specified such as Some("Cargo.toml") + // The file would need to be included with the executable when moved to deployment + let conf = get_configuration(None).await.unwrap(); + let leptos_options = conf.leptos_options; + let addr = leptos_options.site_addr; + let routes = generate_route_list(App); + + // build our application with a route + let app = Router::new() + .leptos_routes(&leptos_options, routes, App) + .fallback(file_and_error_handler) + .with_state(leptos_options); + + let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); + logging::log!("listening on http://{}", &addr); + axum::serve(listener, app.into_make_service()) + .await + .unwrap(); +} + +#[cfg(not(feature = "ssr"))] +pub fn main() { + // no client-side main function + // unless we want this to work with e.g., Trunk for a purely client-side app + // see lib.rs for hydration function instead +} diff --git a/examples/ssr-islands-axum/style/main.css b/examples/ssr-islands-axum/style/main.css new file mode 100644 index 00000000..a530a13f --- /dev/null +++ b/examples/ssr-islands-axum/style/main.css @@ -0,0 +1,77 @@ +body { + font-family: sans-serif; +} + +header { + position: relative; + z-index: 20; + background-color: bisque; + padding: 10px; +} + +a { + margin-left: 5px; + margin-right: 5px; +} + +.hidden { + display: none !important; +} + +.header-large-menu { + display: none; + justify-content: center; + + @media (min-width: 1024px) { + display: flex; + } +} + +.header-mobile-menu { + display: flex; + justify-content: end; + + @media (min-width: 1024px) { + display: none; + } +} + +.mobile-button { + background: none; + border: none; + cursor: pointer; +} + +.mobile-menu-panel { + display: flex; + flex-direction: column; + background-color: bisque; + position: absolute; + top: 50px; + left: 28px; + right: 28px; + text-align: center; + border: 2px solid black; + border-radius: 5px; + padding-bottom: 5px; + z-index: 20; +} + +.mobile-menu-panel > * { + padding-top: 5px; +} + +.mobile-menu-panel-hidder { + position: fixed; + top: 0; + right: 0; + left: 0; + bottom: 0; + z-index: 10; + background-color: black; + opacity: 50%; +} + +.language-selector { + display: inline-flex; +}