feat: theme

This commit is contained in:
luoxiao 2023-11-01 14:04:12 +08:00
parent c974f80ea5
commit cf18b488db
20 changed files with 295 additions and 133 deletions

View file

@ -5,7 +5,22 @@ use melt_ui::*;
#[component]
pub fn App() -> impl IntoView {
let theme = create_rw_signal(Theme::light());
fn use_query_value(key: &str) -> Option<String> {
let href = window().location().href().ok()?;
let url = Url::try_from(href.as_str()).ok()?;
url.search_params.get(key).cloned()
}
let theme = use_query_value("theme").map_or_else(Theme::light, |name| {
if name == "light" {
Theme::light()
} else if name == "dark" {
Theme::dark()
} else {
Theme::light()
}
});
let theme = create_rw_signal(theme);
provide_context(theme);
view! {
<Provider theme>

View file

@ -1,5 +1,5 @@
use leptos::*;
use leptos_router::{use_navigate, use_query_map, Url};
use leptos_router::{use_navigate, use_query_map};
use melt_ui::*;
#[component]
@ -8,19 +8,6 @@ pub fn Home() -> impl IntoView {
// mobile page
if let Some(path) = query_map.get("path") {
let navigate = use_navigate();
if let Some((_, search)) = path.split_once("?") {
if let Some((key, value)) = search.split_once("=") {
if key == "theme" {
let theme = use_rw_theme();
let theme_name = theme.with_untracked(|theme| theme.name.clone());
if value == "light" && theme_name != "light" {
theme.set(Theme::light())
} else if value == "dark" && theme_name != "dark" {
theme.set(Theme::dark())
}
}
}
}
navigate(path, Default::default());
}
view! {

View file

@ -4,7 +4,7 @@ use melt_ui::{use_theme, Theme};
#[component]
pub fn MobilePage(path: &'static str) -> impl IntoView {
let theme = use_theme(Theme::light);
let src = create_memo(move |_| theme.with(|theme| format!("{path}?theme={}", theme.name)));
let src = create_memo(move |_| theme.with(|theme| format!("{path}&theme={}", theme.name)));
let style = create_memo(move |_| {
theme.with(|theme| {
let mut style = String::from("margin-top: 5vh; width: 350px; height: 680px; border-radius: 16px; box-shadow: 0 6px 16px -9px rgba(0, 0, 0, .08), 0 9px 28px 0 rgba(0, 0, 0, .05), 0 12px 48px 16px rgba(0, 0, 0, .03);");

View file

@ -3,7 +3,7 @@
display: inline-block;
max-height: 200px;
padding: 5px;
background-color: #fff;
background-color: var(--melt-background-color);
border-radius: 3px;
box-sizing: border-box;
box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12),
@ -16,5 +16,5 @@
cursor: pointer;
}
.melt-auto-complete__menu-item:hover {
background-color: #f6f6f7;
background-color: var(--melt-background-color-hover);
}

View file

@ -1,5 +1,10 @@
use crate::{mount_style, teleport::Teleport, utils::maybe_rw_signal::MaybeRwSignal, Input};
mod theme;
use crate::{
mount_style, teleport::Teleport, use_theme, utils::maybe_rw_signal::MaybeRwSignal, Input, Theme,
};
use leptos::*;
pub use theme::AutoCompleteTheme;
#[derive(Clone, PartialEq)]
pub struct AutoCompleteOption {
@ -14,6 +19,22 @@ pub fn AutoComplete(
#[prop(optional, into)] options: MaybeSignal<Vec<AutoCompleteOption>>,
) -> impl IntoView {
mount_style("auto-complete", include_str!("./auto-complete.css"));
let theme = use_theme(Theme::light);
let menu_css_vars = create_memo(move |_| {
let mut css_vars = String::new();
theme.with(|theme| {
css_vars.push_str(&format!(
"--melt-background-color: {};",
theme.select.menu_background_color
));
css_vars.push_str(&format!(
"--melt-background-color-hover: {};",
theme.select.menu_background_color_hover
));
});
css_vars
});
let is_show_menu = create_rw_signal(false);
let auto_complete_ref = create_node_ref::<html::Div>();
let auto_complete_menu_ref = create_node_ref::<html::Div>();
@ -58,7 +79,7 @@ pub fn AutoComplete(
<div
class="melt-auto-complete__menu"
style=move || {
if is_show_menu.get() { None } else { Some("display: none;") }
if is_show_menu.get() { menu_css_vars.get() } else { "display: none;".to_string() }
}
ref=auto_complete_menu_ref

View file

@ -0,0 +1,23 @@
use crate::theme::ThemeMethod;
#[derive(Clone)]
pub struct AutoCompleteTheme {
pub menu_background_color: String,
pub menu_background_color_hover: String,
}
impl ThemeMethod for AutoCompleteTheme {
fn light() -> Self {
Self {
menu_background_color: "#fff".into(),
menu_background_color_hover: "#f3f5f6".into(),
}
}
fn dark() -> Self {
Self {
menu_background_color: "#48484e".into(),
menu_background_color_hover: "#ffffff17".into(),
}
}
}

View file

@ -24,7 +24,7 @@
right: 0;
width: 240px;
padding: 12px;
background-color: #fff;
background-color: var(--melt-background-color);
border-radius: 3px;
box-sizing: border-box;
box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12),
@ -65,7 +65,7 @@
border-radius: 6px;
box-sizing: border-box;
border: 2px solid white;
box-shadow: 0 0 2px 0 rgba(0, 0, 0, .45);
box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.45);
}
.melt-color-picker-slider {

View file

@ -1,14 +1,28 @@
mod color;
mod theme;
use crate::{mount_style, teleport::Teleport, utils::maybe_rw_signal::MaybeRwSignal};
use crate::{
mount_style, teleport::Teleport, use_theme, utils::maybe_rw_signal::MaybeRwSignal, Theme,
};
pub use color::*;
use leptos::leptos_dom::helpers::WindowListenerHandle;
use leptos::*;
pub use theme::ColorPickerTheme;
use wasm_bindgen::__rt::IntoJsResult;
#[component]
pub fn ColorPicker(#[prop(optional, into)] value: MaybeRwSignal<RGBA>) -> impl IntoView {
mount_style("color-picker", include_str!("./color-picker.css"));
let theme = use_theme(Theme::light);
let popover_css_vars = create_memo(move |_| {
theme.with(|theme| {
format!(
"--melt-background-color: {};",
theme.color_picker.popover_background_color
)
})
});
let hue = create_rw_signal(0);
let sv = create_rw_signal((0.0, 0.0));
let label = create_rw_signal(String::new());
@ -95,7 +109,7 @@ pub fn ColorPicker(#[prop(optional, into)] value: MaybeRwSignal<RGBA>) -> impl I
class="melt-color-picker-popover"
ref=popover_ref
style=move || {
if !is_show_popover.get() { Some("display: none") } else { None }
if is_show_popover.get() { popover_css_vars.get() } else { "display: none".to_string() }
}
>

20
src/color_picker/theme.rs Normal file
View file

@ -0,0 +1,20 @@
use crate::theme::ThemeMethod;
#[derive(Clone)]
pub struct ColorPickerTheme {
pub popover_background_color: String,
}
impl ThemeMethod for ColorPickerTheme {
fn light() -> Self {
Self {
popover_background_color: "#fff".into(),
}
}
fn dark() -> Self {
Self {
popover_background_color: "#48484e".into(),
}
}
}

View file

@ -1,9 +1,14 @@
mod theme;
use crate::{
components::*,
icon::*,
use_theme,
utils::{mount_style::mount_style, StoredMaybeSignal},
Theme,
};
use leptos::*;
pub use theme::NavBarTheme;
#[component]
pub fn NavBar(
@ -15,6 +20,15 @@ pub fn NavBar(
#[prop(optional, into)] on_click_right: Option<Callback<ev::MouseEvent>>,
) -> impl IntoView {
mount_style("nav-bar", include_str!("./nav-bar.css"));
let theme = use_theme(Theme::light);
let css_vars = create_memo(move |_| {
theme.with(|theme| {
format!(
"--melt-background-color: {};",
theme.nav_bar.background_color
)
})
});
let title: StoredMaybeSignal<_> = title.into();
let left_text: StoredMaybeSignal<_> = left_text.into();
let right_text: StoredMaybeSignal<_> = right_text.into();
@ -32,7 +46,7 @@ pub fn NavBar(
};
view! {
<div class="melt-nav-bar">
<div class="melt-nav-bar" style=move || css_vars.get()>
<If cond=MaybeSignal::derive(move || left_arrow.get() || !left_text.get().is_empty())>
<Then slot>
<div class="melt-nav-bar__left" on:click=on_click_left>

View file

@ -5,7 +5,7 @@
right: 0;
height: 46px;
line-height: 46px;
background-color: #fff;
background-color: var(--melt-background-color);
}
.melt-nav-bar__center {

View file

@ -0,0 +1,20 @@
use crate::theme::ThemeMethod;
#[derive(Clone)]
pub struct NavBarTheme {
pub background_color: String,
}
impl ThemeMethod for NavBarTheme {
fn light() -> Self {
Self {
background_color: "#fff".into(),
}
}
fn dark() -> Self {
Self {
background_color: "#323233".into(),
}
}
}

View file

@ -1,9 +1,14 @@
mod tabbar_item;
mod theme;
use crate::utils::{maybe_rw_signal::MaybeRwSignal, mount_style::mount_style};
use crate::{
use_theme,
utils::{maybe_rw_signal::MaybeRwSignal, mount_style::mount_style},
Theme,
};
use leptos::*;
pub use tabbar_item::*;
pub use theme::TabbarTheme;
#[component]
pub fn Tabbar(
@ -11,6 +16,15 @@ pub fn Tabbar(
children: Children,
) -> impl IntoView {
mount_style("tabbar", include_str!("./tabbar.css"));
let theme = use_theme(Theme::light);
let css_vars = create_memo(move |_| {
theme.with(|theme| {
format!(
"--melt-background-color: {};",
theme.tabbar.background_color
)
})
});
let tabbar_injection_key = create_rw_signal(TabbarInjectionKey::new(value.get()));
create_effect(move |_| {
@ -29,7 +43,7 @@ pub fn Tabbar(
}
});
provide_context(tabbar_injection_key);
view! { <div class="melt-tabbar">{children()}</div> }
view! { <div class="melt-tabbar" style=move || css_vars.get()>{children()}</div> }
}
#[derive(Clone)]

View file

@ -1,5 +1,5 @@
.melt-tabbar {
background-color: #fff;
background-color: var(--melt-background-color);
position: fixed;
left: 0;
right: 0;
@ -7,4 +7,4 @@
height: 50px;
display: flex;
justify-content: space-around;
}
}

View file

@ -0,0 +1,20 @@
use crate::theme::ThemeMethod;
#[derive(Clone)]
pub struct TabbarTheme {
pub background_color: String,
}
impl ThemeMethod for TabbarTheme {
fn light() -> Self {
Self {
background_color: "#fff".into(),
}
}
fn dark() -> Self {
Self {
background_color: "#323233".into(),
}
}
}

View file

@ -47,16 +47,16 @@ where
css_vars
});
let popover_css_vars = create_memo(move |_| {
let menu_css_vars = create_memo(move |_| {
let mut css_vars = String::new();
theme.with(|theme| {
css_vars.push_str(&format!(
"--melt-background-color: {};",
theme.select.popover_background_color
theme.select.menu_background_color
));
css_vars.push_str(&format!(
"--melt-background-color-hover: {};",
theme.select.popover_background_color_hover
theme.select.menu_background_color_hover
));
css_vars.push_str(&format!("--melt-font-color: {};", theme.select.font_color));
css_vars.push_str(&format!(
@ -67,14 +67,14 @@ where
css_vars
});
let is_show_popover = create_rw_signal(false);
let is_show_menu = create_rw_signal(false);
let trigger_ref = create_node_ref::<html::Div>();
let popover_ref = create_node_ref::<html::Div>();
let show_popover = move |_| {
let menu_ref = create_node_ref::<html::Div>();
let show_menu = move |_| {
let rect = trigger_ref.get().unwrap().get_bounding_client_rect();
is_show_popover.set(true);
if let Some(popover_ref) = popover_ref.get() {
popover_ref
is_show_menu.set(true);
if let Some(menu_ref) = menu_ref.get() {
menu_ref
.style("width", format!("{}px", rect.width()))
.style(
"transform",
@ -95,14 +95,14 @@ where
if current_el == *body {
break;
};
if current_el == ***popover_ref.get().unwrap()
if current_el == ***menu_ref.get().unwrap()
|| current_el == ***trigger_ref.get().unwrap()
{
return;
}
el = current_el.parent_element();
}
is_show_popover.set(false);
is_show_menu.set(false);
});
on_cleanup(move || timer.remove());
@ -116,7 +116,7 @@ where
None => String::new(),
});
view! {
<div class="melt-select" ref=trigger_ref on:click=show_popover style=move || css_vars.get()>
<div class="melt-select" ref=trigger_ref on:click=show_menu style=move || css_vars.get()>
{move || select_option_label.get()}
@ -125,14 +125,14 @@ where
<div
class="melt-select-menu"
style=move || {
if is_show_popover.get() {
popover_css_vars.get()
if is_show_menu.get() {
menu_css_vars.get()
} else {
"display: none;".into()
}
}
ref=popover_ref
ref=menu_ref
>
<For
each=move || options.get()
@ -142,7 +142,7 @@ where
let onclick = move |_| {
let SelectOption { value: item_value, label: _ } = item.get_value();
value.set(Some(item_value));
is_show_popover.set(false);
is_show_menu.set(false);
};
view! {
<div

View file

@ -5,8 +5,8 @@ pub struct SelectTheme {
pub font_color: String,
pub border_color: String,
pub background_color: String,
pub popover_background_color: String,
pub popover_background_color_hover: String,
pub menu_background_color: String,
pub menu_background_color_hover: String,
}
impl ThemeMethod for SelectTheme {
@ -15,8 +15,8 @@ impl ThemeMethod for SelectTheme {
font_color: "#333639".into(),
border_color: "#e0e0e6".into(),
background_color: "#fff".into(),
popover_background_color: "#fff".into(),
popover_background_color_hover: "#f3f5f6".into(),
menu_background_color: "#fff".into(),
menu_background_color_hover: "#f3f5f6".into(),
}
}
@ -25,8 +25,8 @@ impl ThemeMethod for SelectTheme {
font_color: "#ffffffd1".into(),
border_color: "#0000".into(),
background_color: "#ffffff1a".into(),
popover_background_color: "#48484e".into(),
popover_background_color_hover: "#ffffff17".into(),
menu_background_color: "#48484e".into(),
menu_background_color_hover: "#ffffff17".into(),
}
}
}

View file

@ -2,8 +2,10 @@ mod common;
use self::common::CommonTheme;
use crate::{
AlertTheme, AvatarTheme, ButtonTheme, InputTheme, MenuTheme, MessageTheme, SelectTheme,
SkeletionTheme, SliderTheme, SwitchTheme, TableTheme, TagTheme, UploadTheme,
mobile::{NavBarTheme, TabbarTheme},
AlertTheme, AutoCompleteTheme, AvatarTheme, ButtonTheme, ColorPickerTheme, InputTheme,
MenuTheme, MessageTheme, SelectTheme, SkeletionTheme, SliderTheme, SwitchTheme, TableTheme,
TagTheme, UploadTheme,
};
use leptos::*;
@ -29,6 +31,10 @@ pub struct Theme {
pub slider: SliderTheme,
pub switch: SwitchTheme,
pub upload: UploadTheme,
pub nav_bar: NavBarTheme,
pub tabbar: TabbarTheme,
pub auto_complete: AutoCompleteTheme,
pub color_picker: ColorPickerTheme,
}
impl Theme {
@ -49,6 +55,10 @@ impl Theme {
slider: SliderTheme::light(),
switch: SwitchTheme::light(),
upload: UploadTheme::light(),
nav_bar: NavBarTheme::light(),
tabbar: TabbarTheme::light(),
auto_complete: AutoCompleteTheme::light(),
color_picker: ColorPickerTheme::light(),
}
}
pub fn dark() -> Self {
@ -68,6 +78,10 @@ impl Theme {
slider: SliderTheme::dark(),
switch: SwitchTheme::dark(),
upload: UploadTheme::dark(),
nav_bar: NavBarTheme::dark(),
tabbar: TabbarTheme::dark(),
auto_complete: AutoCompleteTheme::dark(),
color_picker: ColorPickerTheme::dark(),
}
}
}

View file

@ -1,89 +1,89 @@
use leptos::StoredValue;
use std::{fmt, future::Future, pin::Pin, rc::Rc};
// use leptos::StoredValue;
// use std::{fmt, future::Future, pin::Pin, rc::Rc};
pub struct AsyncCallback<In: 'static, Out: 'static = ()>(
#[allow(clippy::complexity)] StoredValue<Rc<dyn Fn(In) -> Pin<Box<dyn Future<Output = Out>>>>>,
);
// pub struct AsyncCallback<In: 'static, Out: 'static = ()>(
// #[allow(clippy::complexity)] StoredValue<Rc<dyn Fn(In) -> Pin<Box<dyn Future<Output = Out>>>>>,
// );
impl<In> fmt::Debug for AsyncCallback<In> {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
fmt.write_str("AsyncCallback")
}
}
// impl<In> fmt::Debug for AsyncCallback<In> {
// fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
// fmt.write_str("AsyncCallback")
// }
// }
impl<In, Out> Clone for AsyncCallback<In, Out> {
fn clone(&self) -> Self {
*self
}
}
// impl<In, Out> Clone for AsyncCallback<In, Out> {
// fn clone(&self) -> Self {
// *self
// }
// }
impl<In, Out> Copy for AsyncCallback<In, Out> {}
// impl<In, Out> Copy for AsyncCallback<In, Out> {}
impl<In, Out> AsyncCallback<In, Out> {
pub fn new<F, Fu>(f: F) -> Self
where
F: Fn(In) -> Fu + 'static,
Fu: Future<Output = Out> + 'static,
{
let f = Rc::new(move |input: In| {
let fut = f(input);
Box::pin(fut) as Pin<Box<dyn Future<Output = Out>>>
});
Self(StoredValue::new(f))
}
// impl<In, Out> AsyncCallback<In, Out> {
// pub fn new<F, Fu>(f: F) -> Self
// where
// F: Fn(In) -> Fu + 'static,
// Fu: Future<Output = Out> + 'static,
// {
// let f = Rc::new(move |input: In| {
// let fut = f(input);
// Box::pin(fut) as Pin<Box<dyn Future<Output = Out>>>
// });
// Self(StoredValue::new(f))
// }
pub async fn call(&self, input: In) -> Out {
let f = self.0.get_value();
f(input).await
}
}
// pub async fn call(&self, input: In) -> Out {
// let f = self.0.get_value();
// f(input).await
// }
// }
impl<F, In, Fu, Out> From<F> for AsyncCallback<In, Out>
where
F: Fn(In) -> Fu + 'static,
Fu: Future<Output = Out> + 'static,
{
fn from(f: F) -> AsyncCallback<In, Out> {
AsyncCallback::new(f)
}
}
// impl<F, In, Fu, Out> From<F> for AsyncCallback<In, Out>
// where
// F: Fn(In) -> Fu + 'static,
// Fu: Future<Output = Out> + 'static,
// {
// fn from(f: F) -> AsyncCallback<In, Out> {
// AsyncCallback::new(f)
// }
// }
#[cfg(test)]
mod tests {
use crate::utils::AsyncCallback;
use leptos::create_runtime;
// #[cfg(test)]
// mod tests {
// use crate::utils::AsyncCallback;
// use leptos::create_runtime;
struct NoClone {}
// struct NoClone {}
#[test]
fn clone_async_callback() {
let rt = create_runtime();
let callback = AsyncCallback::new(move |_no_clone: NoClone| async { NoClone {} });
let _cloned = callback.clone();
rt.dispose();
}
// #[test]
// fn clone_async_callback() {
// let rt = create_runtime();
// let callback = AsyncCallback::new(move |_no_clone: NoClone| async { NoClone {} });
// let _cloned = callback.clone();
// rt.dispose();
// }
#[test]
fn async_callback_from() {
let rt = create_runtime();
let _callback: AsyncCallback<(), String> = (|()| async { "test".to_string() }).into();
rt.dispose();
}
// #[test]
// fn async_callback_from() {
// let rt = create_runtime();
// let _callback: AsyncCallback<(), String> = (|()| async { "test".to_string() }).into();
// rt.dispose();
// }
#[test]
fn async_callback_from_html() {
let rt = create_runtime();
use leptos::{
html::{HtmlElement, H1},
*,
};
// #[test]
// fn async_callback_from_html() {
// let rt = create_runtime();
// use leptos::{
// html::{HtmlElement, H1},
// *,
// };
let _callback: AsyncCallback<String, HtmlElement<H1>> = (|x: String| async move {
view! {
<h1>{x}</h1>
}
})
.into();
rt.dispose();
}
}
// let _callback: AsyncCallback<String, HtmlElement<H1>> = (|x: String| async move {
// view! {
// <h1>{x}</h1>
// }
// })
// .into();
// rt.dispose();
// }
// }

View file

@ -1,8 +1,8 @@
mod callback;
// mod callback;
pub mod maybe_rw_signal;
mod maybe_signal_store;
pub mod mount_style;
pub mod signal;
pub use callback::AsyncCallback;
// pub use callback::AsyncCallback;
pub use maybe_signal_store::*;