mirror of
https://github.com/adoyle0/thaw.git
synced 2025-01-22 22:09:22 -05:00
Compare commits
11 commits
e9d1630473
...
e18bbff216
Author | SHA1 | Date | |
---|---|---|---|
|
e18bbff216 | ||
|
b831008e09 | ||
|
a03226f202 | ||
|
3545744335 | ||
|
c87b989c3c | ||
|
687350102a | ||
|
282fcd038c | ||
|
dfd5ddd328 | ||
|
2382b8f779 | ||
|
32693e8244 | ||
|
092581d46d |
63 changed files with 877 additions and 206 deletions
21
CHANGELOG.md
21
CHANGELOG.md
|
@ -1,3 +1,24 @@
|
|||
## [0.4.0-beta3](https://github.com/thaw-ui/thaw/compare/v0.4.0-beta2...v0.4.0-beta3) (2024-09-13)
|
||||
|
||||
### Features
|
||||
|
||||
* Adss `Flex` component.
|
||||
* Adss `TagPicker` component.
|
||||
* `Spinner` adds children prop.
|
||||
|
||||
### Bug Fixs
|
||||
|
||||
* `AnchorLink` outline.
|
||||
* `SliderLabel` position.
|
||||
* `NavDrawer` selected category value error.
|
||||
* Adjust the z-index of the Binder popup.
|
||||
* Binder component display position.
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
* Change Tag's closable to dismissible.
|
||||
* Update leptos to v0.7.0-beta5.
|
||||
|
||||
## [0.3.4](https://github.com/thaw-ui/thaw/compare/v0.3.3...v0.3.4) (2024-09-11)
|
||||
|
||||
### Features
|
||||
|
|
14
Cargo.toml
14
Cargo.toml
|
@ -11,11 +11,11 @@ members = [
|
|||
exclude = ["examples"]
|
||||
|
||||
[workspace.dependencies]
|
||||
thaw = { version = "0.4.0-beta2", path = "./thaw" }
|
||||
thaw_components = { version = "0.2.0-beta2", path = "./thaw_components" }
|
||||
thaw_macro = { version = "0.1.0-beta2", path = "./thaw_macro" }
|
||||
thaw_utils = { version = "0.1.0-beta2", path = "./thaw_utils" }
|
||||
thaw = { version = "0.4.0-beta3", path = "./thaw" }
|
||||
thaw_components = { version = "0.2.0-beta3", path = "./thaw_components" }
|
||||
thaw_macro = { version = "0.1.0-beta3", path = "./thaw_macro" }
|
||||
thaw_utils = { version = "0.1.0-beta3", path = "./thaw_utils" }
|
||||
|
||||
leptos = { version = "0.7.0-beta5" }
|
||||
leptos_meta = { version = "0.7.0-beta5" }
|
||||
leptos_router = { version = "0.7.0-beta5" }
|
||||
leptos = { version = "0.7.0-beta7" }
|
||||
leptos_meta = { version = "0.7.0-beta7" }
|
||||
leptos_router = { version = "0.7.0-beta7" }
|
||||
|
|
|
@ -4,6 +4,12 @@
|
|||
<h1 align="center">Thaw UI</h1>
|
||||
<p align="center">An easy to use leptos component library</p>
|
||||
|
||||
## The main branch is currently incompatible with Leptos-v0.6, For Lepots-v0.6 you can use Thaw-v0.3
|
||||
|
||||
v0.3 branch: https://github.com/thaw-ui/thaw/tree/thaw-v0.3
|
||||
|
||||
v0.3 docs: https://thaw-85fsrigp0-thaw.vercel.app
|
||||
|
||||
## Documentation & Community
|
||||
|
||||
[https://thawui.vercel.app](https://thawui.vercel.app)
|
||||
|
|
|
@ -18,6 +18,7 @@ chrono = "0.4.38"
|
|||
cfg-if = "1.0.0"
|
||||
# leptos-use = "0.10.10"
|
||||
send_wrapper = "0.6"
|
||||
uuid = { version = "1.10.0", features = ["v4", "js"] }
|
||||
console_error_panic_hook = "0.1.7"
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
|
|
|
@ -84,6 +84,7 @@ fn TheRouter() -> impl IntoView {
|
|||
<Route path=path!("/icon") view=IconMdPage/>
|
||||
<Route path=path!("/image") view=ImageMdPage/>
|
||||
<Route path=path!("/input") view=InputMdPage/>
|
||||
<Route path=path!("/label") view=LabelMdPage/>
|
||||
<Route path=path!("/layout") view=LayoutMdPage/>
|
||||
<Route path=path!("/link") view=LinkMdPage/>
|
||||
<Route path=path!("/loading-bar") view=LoadingBarMdPage/>
|
||||
|
|
|
@ -79,7 +79,9 @@ pub fn SiteHeader() -> impl IntoView {
|
|||
{
|
||||
use leptos::ev;
|
||||
let handle = window_event_listener(ev::keydown, move |e| {
|
||||
if js_sys::Reflect::has(&e, &js_sys::wasm_bindgen::JsValue::from_str("key")).unwrap_or_default() {
|
||||
if js_sys::Reflect::has(&e, &js_sys::wasm_bindgen::JsValue::from_str("key"))
|
||||
.unwrap_or_default()
|
||||
{
|
||||
let key = e.key();
|
||||
if key == *"/" {
|
||||
if let Some(auto_complete_ref) = auto_complete_ref.get_untracked() {
|
||||
|
@ -88,7 +90,6 @@ pub fn SiteHeader() -> impl IntoView {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
on_cleanup(move || handle.remove());
|
||||
}
|
||||
|
|
|
@ -8,6 +8,6 @@ use leptos::prelude::*;
|
|||
fn main() {
|
||||
let _ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
|
||||
mount_to_body(App)
|
||||
}
|
||||
|
|
|
@ -95,11 +95,11 @@ pub(crate) struct NavItemOption {
|
|||
}
|
||||
|
||||
trait VecIntoView {
|
||||
fn into_view(self) -> Vec<AnyView<Dom>>;
|
||||
fn into_view(self) -> Vec<AnyView>;
|
||||
}
|
||||
|
||||
impl VecIntoView for Vec<NavItemOption> {
|
||||
fn into_view(self) -> Vec<AnyView<Dom>> {
|
||||
fn into_view(self) -> Vec<AnyView> {
|
||||
let mut iter = self.into_iter().peekable();
|
||||
let mut views = vec![];
|
||||
while let Some(item) = iter.next() {
|
||||
|
@ -297,6 +297,11 @@ pub(crate) fn gen_nav_data() -> Vec<NavGroupOption> {
|
|||
value: "/components/input",
|
||||
label: "Input",
|
||||
},
|
||||
NavItemOption {
|
||||
group: None,
|
||||
value: "/components/label",
|
||||
label: "Label",
|
||||
},
|
||||
NavItemOption {
|
||||
group: None,
|
||||
value: "/components/layout",
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
use crate::components::SiteHeader;
|
||||
use leptos::prelude::*;
|
||||
use leptos_meta::Style;
|
||||
use leptos_router::hooks::{use_navigate, use_query_map};
|
||||
use thaw::*;
|
||||
use leptos_meta::Style;
|
||||
|
||||
#[component]
|
||||
pub fn Home() -> impl IntoView {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use crate::components::{Demo, DemoCode};
|
||||
use leptos::{ev, prelude::*};
|
||||
use thaw::*;
|
||||
use uuid;
|
||||
|
||||
demo_markdown::include_md! {}
|
||||
|
|
|
@ -8,7 +8,9 @@ cargo add thaw --features=csr
|
|||
|
||||
<MessageBar intent=MessageBarIntent::Warning>
|
||||
<MessageBarBody>
|
||||
"If you are using the nightly feature in Leptos, please enable Thaw's nightly as well."
|
||||
<div style="white-space: normal">
|
||||
"If you are using the nightly feature in Leptos, please enable Thaw's nightly as well."
|
||||
</div>
|
||||
</MessageBarBody>
|
||||
</MessageBar>
|
||||
|
||||
|
|
|
@ -47,6 +47,7 @@ pub fn include_md(_token_stream: proc_macro::TokenStream) -> proc_macro::TokenSt
|
|||
"IconMdPage" => "../../thaw/src/icon/docs/mod.md",
|
||||
"ImageMdPage" => "../../thaw/src/image/docs/mod.md",
|
||||
"InputMdPage" => "../../thaw/src/input/docs/mod.md",
|
||||
"LabelMdPage" => "../../thaw/src/label/docs/mod.md",
|
||||
"LayoutMdPage" => "../../thaw/src/layout/docs/mod.md",
|
||||
"LinkMdPage" => "../../thaw/src/link/docs/mod.md",
|
||||
"LoadingBarMdPage" => "../../thaw/src/loading_bar/docs/mod.md",
|
||||
|
|
|
@ -158,7 +158,7 @@ fn iter_nodes<'a>(
|
|||
NodeValue::Superscript => quote!("Superscript todo!!!"),
|
||||
NodeValue::Link(node_link) => {
|
||||
let NodeLink { url, title } = node_link;
|
||||
|
||||
|
||||
quote!(
|
||||
<Link href=#url attr:title=#title>
|
||||
#(#children)*
|
||||
|
|
|
@ -9,10 +9,10 @@ crate-type = ["cdylib", "rlib"]
|
|||
[dependencies]
|
||||
axum = { version = "0.7", optional = true }
|
||||
console_error_panic_hook = "0.1"
|
||||
leptos = { version = "0.7.0-beta5", features = ["experimental-islands"] }
|
||||
leptos_axum = { version = "0.7.0-beta5", optional = true }
|
||||
leptos_meta = { version = "0.7.0-beta5" }
|
||||
leptos_router = { version = "0.7.0-beta5" }
|
||||
leptos = { version = "0.7.0-beta7", features = ["experimental-islands"] }
|
||||
leptos_axum = { version = "0.7.0-beta7", optional = true }
|
||||
leptos_meta = { version = "0.7.0-beta7" }
|
||||
leptos_router = { version = "0.7.0-beta7" }
|
||||
tokio = { version = "1", features = ["rt-multi-thread"], optional = true }
|
||||
tower = { version = "0.4", optional = true }
|
||||
tower-http = { version = "0.5", features = ["fs"], optional = true }
|
||||
|
|
|
@ -9,10 +9,10 @@ crate-type = ["cdylib", "rlib"]
|
|||
[dependencies]
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
console_error_panic_hook = "0.1"
|
||||
leptos = { version = "0.7.0-beta5" }
|
||||
leptos_axum = { version = "0.7.0-beta5", optional = true }
|
||||
leptos_meta = { version = "0.7.0-beta5" }
|
||||
leptos_router = { version = "0.7.0-beta5" }
|
||||
leptos = { version = "0.7.0-beta7" }
|
||||
leptos_axum = { version = "0.7.0-beta7", optional = true }
|
||||
leptos_meta = { version = "0.7.0-beta7" }
|
||||
leptos_router = { version = "0.7.0-beta7" }
|
||||
tokio = { version = "1", features = ["rt-multi-thread"], optional = true }
|
||||
tower = { version = "0.5.0", optional = true }
|
||||
tower-http = { version = "0.5", features = ["fs"], optional = true }
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 5.5 KiB |
|
@ -1,11 +1,10 @@
|
|||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle cx="12" cy="12" r="12" fill="#0078ff" />
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 325 325">
|
||||
<path
|
||||
d="M1116.79,462.73l-2-7.25-.65-2-1.43-4.14-2.12-5.4-2.88-6.28-2.27-4.34-4.3-7-1.82-2.63-1.36-1.84-5.72-6.83-6.77-6.62-1.27-1.09-1.54-1.28-7.45-5.46-5.41-3.34-4.53-2.45L1055,390.15l-.65-.25-.25-.1-.56-.21-11-3.58-4.11-1.08-6.65-1.5-2-.39-7-1.21-6.54-.89-2.91-.34-6.22-.59-5.24-.4-5.51-.31-4.92-.21-4.57-.15-6.67-.14-4.43-.05-4.35,0-7,0h-8.67l-7,0-4.35,0-4.44.05-6.67.14-4.57.15-4.92.21-5.51.31-5.24.4-6.21.59-2.91.34-6.55.89-7,1.21-2,.39-6.66,1.5-4.1,1.08-11,3.58-.55.21-.25.1-.66.25-10.23,4.57-4.53,2.45L845,400.51,837.51,406,836,407.25l-1.27,1.09L827.93,415l-5.72,6.83-1.35,1.84L819,426.26l-4.29,7-2.28,4.34-2.88,6.28-2.12,5.4L806,453.44l-.65,2-2,7.25L802.13,468l-.27,1.24-.38,1.9-1.71,10.3-.47,3.62-.51,4.62-.54,6-.36,5.17-.3,5.83-.16,4-.16,6-.11,6.26,0,3.27,0,6.29v15.82l0,6.29,0,3.28.11,6.25.16,6,.16,4,.3,5.83.36,5.16.54,6,.51,4.62.47,3.62,1.71,10.31.38,1.89.27,1.25,1.23,5.28,2,7.25.65,2,1.43,4.15,2.12,5.4,2.88,6.28,2.28,4.33,4.29,7,1.83,2.62,1.35,1.84,5.72,6.83,6.77,6.62,1.27,1.09,1.54,1.28,7.45,5.47,5.42,3.33,4.53,2.45,10.23,4.57.66.26.25.09.55.21,11,3.58,4.1,1.08,6.66,1.5,2,.39,7,1.21,6.55.9,2.91.33,6.21.6,5.24.39,5.51.32,4.92.21,4.57.14,6.67.14,4.44,0,4.35,0,7,0h8.67l7,0,4.35,0,4.43,0,6.67-.14,4.57-.14,4.92-.21,5.51-.32,5.24-.39,6.22-.6,2.91-.33,6.54-.9,7-1.21,2-.39,6.65-1.5,4.11-1.08,11-3.58.56-.21.25-.09.65-.26,10.24-4.57,4.53-2.45,5.41-3.33,7.45-5.47,1.54-1.28,1.27-1.09,6.77-6.62,5.72-6.83,1.36-1.84,1.82-2.62,4.3-7,2.27-4.33,2.88-6.28,2.12-5.4,1.43-4.15.65-2,2-7.25,1.24-5.28.26-1.25.38-1.89,1.71-10.31.47-3.62.51-4.62.55-6,.35-5.16.31-5.83.15-4,.17-6,.1-6.25,0-3.28,0-6.29V532.55l0-6.29,0-3.27-.1-6.26-.17-6-.15-4-.31-5.83-.35-5.17-.55-6-.51-4.62-.47-3.62-1.71-10.3-.38-1.9L1118,468Z"
|
||||
transform="translate(-797.09 -378.7)"/>
|
||||
<path
|
||||
d="M21 11h-3.17l2.54-2.54a.996.996 0 0 0 0-1.41c-.39-.39-1.03-.39-1.42 0L15 11h-2V9l3.95-3.95c.39-.39.39-1.03 0-1.42a.996.996 0 0 0-1.41 0L13 6.17V3c0-.55-.45-1-1-1s-1 .45-1 1v3.17L8.46 3.63a.996.996 0 0 0-1.41 0c-.39.39-.39 1.03 0 1.42L11 9v2H9L5.05 7.05c-.39-.39-1.03-.39-1.42 0a.996.996 0 0 0 0 1.41L6.17 11H3c-.55 0-1 .45-1 1s.45 1 1 1h3.17l-2.54 2.54a.996.996 0 0 0 0 1.41c.39.39 1.03.39 1.42 0L9 13h2v2l-3.95 3.95c-.39.39-.39 1.03 0 1.42c.39.39 1.02.39 1.41 0L11 17.83V21c0 .55.45 1 1 1s1-.45 1-1v-3.17l2.54 2.54c.39.39 1.02.39 1.41 0c.39-.39.39-1.03 0-1.42L13 15v-2h2l3.95 3.95c.39.39 1.03.39 1.42 0a.996.996 0 0 0 0-1.41L17.83 13H21c.55 0 1-.45 1-1s-.45-1-1-1z"
|
||||
fill="#fff">
|
||||
fill="#fff" transform="translate(8 8) scale(13)">
|
||||
</path>
|
||||
</svg>
|
Before Width: | Height: | Size: 897 B After Width: | Height: | Size: 2.6 KiB |
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "thaw"
|
||||
version = "0.4.0-beta2"
|
||||
version = "0.4.0-beta3"
|
||||
edition = "2021"
|
||||
keywords = ["web", "leptos", "ui", "thaw", "component"]
|
||||
readme = "../README.md"
|
||||
|
|
|
@ -226,10 +226,10 @@ pub(crate) fn now_date() -> NaiveDate {
|
|||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CalendarChildrenFn(Arc<dyn Fn(&NaiveDate) -> AnyView<Dom> + Send + Sync>);
|
||||
pub struct CalendarChildrenFn(Arc<dyn Fn(&NaiveDate) -> AnyView + Send + Sync>);
|
||||
|
||||
impl Deref for CalendarChildrenFn {
|
||||
type Target = Arc<dyn Fn(&NaiveDate) -> AnyView<Dom> + Send + Sync>;
|
||||
type Target = Arc<dyn Fn(&NaiveDate) -> AnyView + Send + Sync>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
|
@ -239,7 +239,7 @@ impl Deref for CalendarChildrenFn {
|
|||
impl<F, C> From<F> for CalendarChildrenFn
|
||||
where
|
||||
F: Fn(&NaiveDate) -> C + Send + Sync + 'static,
|
||||
C: RenderHtml<Dom> + Send + 'static,
|
||||
C: RenderHtml + Send + 'static,
|
||||
{
|
||||
fn from(f: F) -> Self {
|
||||
Self(Arc::new(move |date| f(date).into_any()))
|
||||
|
|
|
@ -115,21 +115,6 @@
|
|||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.thaw-combobox__listbox {
|
||||
row-gap: var(--spacingHorizontalXXS);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 160px;
|
||||
max-height: 80vh;
|
||||
background-color: var(--colorNeutralBackground1);
|
||||
padding: var(--spacingHorizontalXS);
|
||||
outline: 1px solid var(--colorTransparentStroke);
|
||||
border-radius: var(--borderRadiusMedium);
|
||||
box-shadow: var(--shadow16);
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.thaw-combobox-option {
|
||||
column-gap: var(--spacingHorizontalXS);
|
||||
position: relative;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use super::option_group::OptionGroup;
|
||||
use leptos::prelude::*;
|
||||
use thaw_utils::{class_list, mount_style};
|
||||
|
||||
#[component]
|
||||
pub fn ComboboxOptionGroup(
|
||||
|
@ -9,17 +9,5 @@ pub fn ComboboxOptionGroup(
|
|||
label: String,
|
||||
children: Children,
|
||||
) -> impl IntoView {
|
||||
mount_style(
|
||||
"combobox-option-group",
|
||||
include_str!("./combobox-option-group.css"),
|
||||
);
|
||||
|
||||
view! {
|
||||
<div role="group" class=class_list!["thaw-combobox-option-group", class]>
|
||||
<span role="presentation" class="thaw-combobox-option-group__label">
|
||||
{label}
|
||||
</span>
|
||||
{children()}
|
||||
</div>
|
||||
}
|
||||
view! { <OptionGroup class_prefix="thaw-combobox-option-group" class label children /> }
|
||||
}
|
||||
|
|
|
@ -1,3 +1,18 @@
|
|||
.thaw-listbox {
|
||||
row-gap: var(--spacingHorizontalXXS);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 160px;
|
||||
max-height: 80vh;
|
||||
background-color: var(--colorNeutralBackground1);
|
||||
padding: var(--spacingHorizontalXS);
|
||||
outline: 1px solid var(--colorTransparentStroke);
|
||||
border-radius: var(--borderRadiusMedium);
|
||||
box-shadow: var(--shadow16);
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.thaw-listbox.fade-in-scale-up-transition-leave-active {
|
||||
transform-origin: inherit;
|
||||
transition: opacity 0.2s cubic-bezier(0.4, 0, 1, 1),
|
||||
|
@ -20,4 +35,4 @@
|
|||
.thaw-listbox.fade-in-scale-up-transition-enter-to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
3
thaw/src/combobox/common/mod.rs
Normal file
3
thaw/src/combobox/common/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
pub mod listbox;
|
||||
pub mod option_group;
|
||||
mod utils;
|
|
@ -1,10 +1,10 @@
|
|||
.thaw-combobox-option-group {
|
||||
.thaw-option-group {
|
||||
display: flex;
|
||||
row-gap: var(--spacingHorizontalXXS);
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.thaw-combobox-option-group__label {
|
||||
.thaw-option-group__label {
|
||||
display: block;
|
||||
color: var(--colorNeutralForeground3);
|
||||
font-weight: var(--fontWeightSemibold);
|
||||
|
@ -14,7 +14,7 @@
|
|||
border-radius: var(--borderRadiusMedium);
|
||||
}
|
||||
|
||||
.thaw-combobox-option-group:not(:last-child)::after {
|
||||
.thaw-option-group:not(:last-child)::after {
|
||||
content: "";
|
||||
display: block;
|
||||
margin: 0 calc(var(--spacingHorizontalXS) * -1) var(--spacingVerticalXS);
|
25
thaw/src/combobox/common/option_group.rs
Normal file
25
thaw/src/combobox/common/option_group.rs
Normal file
|
@ -0,0 +1,25 @@
|
|||
use leptos::prelude::*;
|
||||
use thaw_utils::{class_list, mount_style};
|
||||
|
||||
#[component]
|
||||
pub fn OptionGroup(
|
||||
class_prefix: &'static str,
|
||||
class: MaybeProp<String>,
|
||||
/// Label of the group.
|
||||
label: String,
|
||||
children: Children,
|
||||
) -> impl IntoView {
|
||||
mount_style("option-group", include_str!("./option-group.css"));
|
||||
|
||||
view! {
|
||||
<div role="group" class=class_list!["thaw-option-group", class_prefix, class]>
|
||||
<span
|
||||
role="presentation"
|
||||
class=format!("thaw-option-group__label {class_prefix}__label")
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
{children()}
|
||||
</div>
|
||||
}
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
mod combobox;
|
||||
mod combobox_option;
|
||||
mod combobox_option_group;
|
||||
pub(crate) mod listbox;
|
||||
mod utils;
|
||||
mod common;
|
||||
|
||||
pub use combobox::*;
|
||||
pub use combobox_option::*;
|
||||
pub use combobox_option_group::*;
|
||||
pub(crate) use common::*;
|
||||
|
|
|
@ -39,6 +39,34 @@ view! {
|
|||
}
|
||||
```
|
||||
|
||||
### Overlay No Modal
|
||||
|
||||
```rust demo
|
||||
let open = RwSignal::new(false);
|
||||
|
||||
view! {
|
||||
<Button on_click=move |_| open.update(|open| *open = !*open)>"Toggle"</Button>
|
||||
<OverlayDrawer open modal_type=DrawerModalType::NonModal>
|
||||
<DrawerHeader>
|
||||
<DrawerHeaderTitle>
|
||||
<DrawerHeaderTitleAction slot>
|
||||
<Button
|
||||
appearance=ButtonAppearance::Subtle
|
||||
on_click=move |_| open.set(false)
|
||||
>
|
||||
"x"
|
||||
</Button>
|
||||
</DrawerHeaderTitleAction>
|
||||
"Default Drawer"
|
||||
</DrawerHeaderTitle>
|
||||
</DrawerHeader>
|
||||
<DrawerBody>
|
||||
<p>"Drawer content"</p>
|
||||
</DrawerBody>
|
||||
</OverlayDrawer>
|
||||
}
|
||||
```
|
||||
|
||||
### Inline
|
||||
|
||||
```rust demo
|
||||
|
@ -151,6 +179,7 @@ view! {
|
|||
| close_on_esc | `bool` | `false` | Whether to close drawer on Esc is pressed. |
|
||||
| position | `MaybeSignal<DrawerPosition>` | `DrawerPlacement::Left` | Position of the drawer. |
|
||||
| size | `MaybeSignal<DrawerSize>` | `DrawerSize::Small` | Size of the drawer. |
|
||||
| modal_type | `DrawerModalType` | `DrawerModalType::Modal` | Dialog variations. |
|
||||
| children | `Children` | | |
|
||||
|
||||
### InlineDrawer Props
|
||||
|
|
|
@ -54,3 +54,14 @@ impl DrawerSize {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
pub enum DrawerModalType {
|
||||
/// When this type of dialog is open,
|
||||
/// the rest of the page is dimmed out and cannot be interacted with.
|
||||
#[default]
|
||||
Modal,
|
||||
/// When a non-modal dialog is open,
|
||||
/// the rest of the page is not dimmed out and users can interact with the rest of the page.
|
||||
NonModal,
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use super::{DrawerPosition, DrawerSize};
|
||||
use super::{DrawerModalType, DrawerPosition, DrawerSize};
|
||||
use crate::ConfigInjection;
|
||||
use leptos::{ev, html, prelude::*};
|
||||
use leptos::{either::Either, ev, html, prelude::*};
|
||||
use thaw_components::{CSSTransition, FocusTrap, Teleport};
|
||||
use thaw_utils::{class_list, mount_style, use_lock_html_scroll, Model};
|
||||
|
||||
|
@ -22,6 +22,9 @@ pub fn OverlayDrawer(
|
|||
/// Size of the drawer.
|
||||
#[prop(optional, into)]
|
||||
size: MaybeSignal<DrawerSize>,
|
||||
/// Dialog variations.
|
||||
#[prop(optional, into)]
|
||||
modal_type: DrawerModalType,
|
||||
children: Children,
|
||||
) -> impl IntoView {
|
||||
mount_style("drawer", include_str!("./drawer.css"));
|
||||
|
@ -60,20 +63,28 @@ pub fn OverlayDrawer(
|
|||
class=class_list!["thaw-config-provider thaw-overlay-drawer-container", class]
|
||||
data-thaw-id=config_provider.id()
|
||||
>
|
||||
<CSSTransition
|
||||
node_ref=mask_ref
|
||||
appear=open.get_untracked()
|
||||
show=open.signal()
|
||||
name="fade-in-transition"
|
||||
let:display
|
||||
>
|
||||
<div
|
||||
class="thaw-overlay-drawer__backdrop"
|
||||
style=move || display.get().unwrap_or_default()
|
||||
on:click=on_mask_click
|
||||
node_ref=mask_ref
|
||||
></div>
|
||||
</CSSTransition>
|
||||
{if modal_type == DrawerModalType::Modal {
|
||||
Either::Left(
|
||||
view! {
|
||||
<CSSTransition
|
||||
node_ref=mask_ref
|
||||
appear=open.get_untracked()
|
||||
show=open.signal()
|
||||
name="fade-in-transition"
|
||||
let:display
|
||||
>
|
||||
<div
|
||||
class="thaw-overlay-drawer__backdrop"
|
||||
style=move || display.get().unwrap_or_default()
|
||||
on:click=on_mask_click
|
||||
node_ref=mask_ref
|
||||
></div>
|
||||
</CSSTransition>
|
||||
},
|
||||
)
|
||||
} else {
|
||||
Either::Right(())
|
||||
}}
|
||||
<CSSTransition
|
||||
node_ref=drawer_ref
|
||||
appear=open_drawer.get_untracked()
|
||||
|
|
|
@ -117,6 +117,35 @@ view! {
|
|||
}
|
||||
```
|
||||
|
||||
### Required
|
||||
|
||||
```rust demo
|
||||
view! {
|
||||
<form>
|
||||
<FieldContextProvider>
|
||||
<Field label="Username" name="username" required=true>
|
||||
<Input rules=vec![InputRule::required(true.into())]/>
|
||||
</Field>
|
||||
<div style="margin-top: 8px">
|
||||
<Button
|
||||
button_type=ButtonType::Submit
|
||||
on_click={
|
||||
let field_context = FieldContextInjection::expect_context();
|
||||
move |e: ev::MouseEvent| {
|
||||
if !field_context.validate() {
|
||||
e.prevent_default();
|
||||
}
|
||||
}
|
||||
}
|
||||
>
|
||||
"Submit"
|
||||
</Button>
|
||||
</div>
|
||||
</FieldContextProvider>
|
||||
</form>
|
||||
}
|
||||
```
|
||||
|
||||
### Field Props
|
||||
|
||||
| Name | Type | Default | Desciption |
|
||||
|
@ -125,6 +154,7 @@ view! {
|
|||
| label | `MaybeProp<String>` | `Default::default()` | The label associated with the field. |
|
||||
| name | `MaybeProp<String>` | `Default::default()` | A string specifying a name for the input control. This name is submitted along with the control's value when the form data is submitted. |
|
||||
| orientation | `MaybeSignal<FieldOrientation>` | `FieldOrientation::Vertical` | The orientation of the label relative to the field component. |
|
||||
| required | `MaybeSignal<bool>` | `false` | If set to true this field will be marked as required. |
|
||||
| children | `Children` | | |
|
||||
|
||||
### FieldContextProvider Props
|
||||
|
|
|
@ -6,10 +6,6 @@
|
|||
margin-bottom: var(--spacingVerticalXXS);
|
||||
padding-bottom: var(--spacingVerticalXXS);
|
||||
padding-top: var(--spacingVerticalXXS);
|
||||
line-height: var(--lineHeightBase300);
|
||||
font-size: var(--fontSizeBase300);
|
||||
font-family: var(--fontFamilyBase);
|
||||
color: var(--colorNeutralForeground1);
|
||||
}
|
||||
|
||||
.thaw-field--horizontal {
|
||||
|
@ -41,6 +37,10 @@
|
|||
color: var(--colorPaletteRedForeground1);
|
||||
}
|
||||
|
||||
.thaw-field--error .thaw-spin-button:not(:focus-within),
|
||||
.thaw-field--error .thaw-select__select:not(:focus-within),
|
||||
.thaw-field--error .thaw-combobox:not(:focus-within),
|
||||
.thaw-field--error .thaw-textarea:not(:focus-within),
|
||||
.thaw-field--error .thaw-input:not(:focus-within) {
|
||||
border-color: var(--colorPaletteRedBorder2);
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use crate::Label;
|
||||
use leptos::{context::Provider, either::EitherOf3, prelude::*};
|
||||
use thaw_components::OptionComp;
|
||||
use thaw_utils::{class_list, mount_style};
|
||||
|
@ -16,6 +17,9 @@ pub fn Field(
|
|||
/// The orientation of the label relative to the field component.
|
||||
#[prop(optional, into)]
|
||||
orientation: MaybeSignal<FieldOrientation>,
|
||||
///Is this input field required
|
||||
#[prop(optional, into)]
|
||||
required: MaybeSignal<bool>,
|
||||
children: Children,
|
||||
) -> impl IntoView {
|
||||
mount_style("field", include_str!("./field.css"));
|
||||
|
@ -44,9 +48,13 @@ pub fn Field(
|
|||
move || {
|
||||
view! {
|
||||
<OptionComp value=label.get() let:label>
|
||||
<label class="thaw-field__label" for=id.get_value()>
|
||||
<Label
|
||||
class="thaw-field__label"
|
||||
required=required
|
||||
attr:r#for=id.get_value()
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
</Label>
|
||||
</OptionComp>
|
||||
}
|
||||
}
|
||||
|
|
57
thaw/src/label/docs/mod.md
Normal file
57
thaw/src/label/docs/mod.md
Normal file
|
@ -0,0 +1,57 @@
|
|||
# Label
|
||||
|
||||
```rust demo
|
||||
view! {
|
||||
<Label>"This is a label"</Label>
|
||||
}
|
||||
```
|
||||
|
||||
### Size
|
||||
|
||||
```rust demo
|
||||
view! {
|
||||
<Flex>
|
||||
<Label size=LabelSize::Small>"Small"</Label>
|
||||
<Label>"Medium"</Label>
|
||||
<Label size=LabelSize::Large>"Large"</Label>
|
||||
</Flex>
|
||||
}
|
||||
```
|
||||
|
||||
### Weight
|
||||
|
||||
```rust demo
|
||||
view! {
|
||||
<Flex>
|
||||
<Label>"Label"</Label>
|
||||
<Label weight=LabelWeight::Semibold>"Strong label"</Label>
|
||||
</Flex>
|
||||
}
|
||||
```
|
||||
|
||||
### Disabled
|
||||
|
||||
```rust demo
|
||||
view! {
|
||||
<Label required=true disabled=true>"Required label"</Label>
|
||||
}
|
||||
```
|
||||
|
||||
### Required
|
||||
|
||||
```rust demo
|
||||
view! {
|
||||
<Label required=true>"Required label"</Label>
|
||||
}
|
||||
```
|
||||
|
||||
### Label Props
|
||||
|
||||
| Name | Type | Default | Desciption |
|
||||
| --- | --- | --- | --- |
|
||||
| class | `MaybeProp<String>` | `Default::default()` | |
|
||||
| size | `MaybeSignal<LabelSize>` | `LabelSize::Medium` | A label supports different sizes. |
|
||||
| weight | `MaybeSignal<LabelWeight>` | `LabelWeight::Regular` | A label supports regular and semibold fontweight. |
|
||||
| disabled | `MaybeSignal<bool>` | `false` | Renders the label as disabled. |
|
||||
| required | `MaybeSignal<bool>` | `false` | Displays an indicator that the label is for a required field. |
|
||||
| children | `Children` | | |
|
34
thaw/src/label/label.css
Normal file
34
thaw/src/label/label.css
Normal file
|
@ -0,0 +1,34 @@
|
|||
.thaw-label {
|
||||
line-height: var(--lineHeightBase300);
|
||||
font-size: var(--fontSizeBase300);
|
||||
font-family: var(--fontFamilyBase);
|
||||
color: var(--colorNeutralForeground1);
|
||||
}
|
||||
|
||||
.thaw-label__required {
|
||||
padding-left: var(--spacingHorizontalXS);
|
||||
color: var(--colorPaletteRedForeground3);
|
||||
}
|
||||
|
||||
.thaw-label--disabled {
|
||||
color: var(--colorNeutralForegroundDisabled);
|
||||
}
|
||||
|
||||
.thaw-label--disabled .thaw-label__required {
|
||||
color: var(--colorNeutralForegroundDisabled);
|
||||
}
|
||||
|
||||
.thaw-label--small {
|
||||
font-size: var(--fontSizeBase200);
|
||||
line-height: var(--lineHeightBase200);
|
||||
}
|
||||
|
||||
.thaw-label--large {
|
||||
font-weight: var(--fontWeightSemibold);
|
||||
font-size: var(--fontSizeBase400);
|
||||
line-height: var(--lineHeightBase400);
|
||||
}
|
||||
|
||||
.thaw-label--semibold {
|
||||
font-weight: var(--fontWeightSemibold);
|
||||
}
|
75
thaw/src/label/label.rs
Normal file
75
thaw/src/label/label.rs
Normal file
|
@ -0,0 +1,75 @@
|
|||
use leptos::prelude::*;
|
||||
use thaw_components::{If, Then};
|
||||
use thaw_utils::{class_list, mount_style};
|
||||
|
||||
#[component]
|
||||
pub fn Label(
|
||||
#[prop(optional, into)] class: MaybeProp<String>,
|
||||
/// A label supports different sizes.
|
||||
#[prop(optional, into)]
|
||||
size: MaybeSignal<LabelSize>,
|
||||
/// A label supports regular and semibold fontweight.
|
||||
#[prop(optional, into)]
|
||||
weight: MaybeSignal<LabelWeight>,
|
||||
/// Displays an indicator that the label is for a required field.
|
||||
#[prop(optional, into)]
|
||||
required: MaybeSignal<bool>,
|
||||
/// Renders the label as disabled.
|
||||
#[prop(optional, into)]
|
||||
disabled: MaybeSignal<bool>,
|
||||
children: Children,
|
||||
) -> impl IntoView {
|
||||
mount_style("label", include_str!("./label.css"));
|
||||
|
||||
view! {
|
||||
<label class=class_list![
|
||||
"thaw-label",
|
||||
("thaw-label--disabled", move || disabled.get()),
|
||||
move || format!("thaw-label--{}", size.get().as_str()),
|
||||
move || format!("thaw-label--{}", weight.get().as_str()),
|
||||
class
|
||||
]>
|
||||
{children()} <If cond=required>
|
||||
<Then slot>
|
||||
<span aria-hidden="true" class="thaw-label__required">
|
||||
"*"
|
||||
</span>
|
||||
</Then>
|
||||
</If>
|
||||
</label>
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub enum LabelSize {
|
||||
Small,
|
||||
#[default]
|
||||
Medium,
|
||||
Large,
|
||||
}
|
||||
|
||||
impl LabelSize {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Small => "small",
|
||||
Self::Medium => "medium",
|
||||
Self::Large => "large",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub enum LabelWeight {
|
||||
#[default]
|
||||
Regular,
|
||||
Semibold,
|
||||
}
|
||||
|
||||
impl LabelWeight {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Regular => "regular",
|
||||
Self::Semibold => "semibold",
|
||||
}
|
||||
}
|
||||
}
|
3
thaw/src/label/mod.rs
Normal file
3
thaw/src/label/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
mod label;
|
||||
|
||||
pub use label::*;
|
|
@ -24,6 +24,7 @@ mod grid;
|
|||
mod icon;
|
||||
mod image;
|
||||
mod input;
|
||||
mod label;
|
||||
mod layout;
|
||||
mod link;
|
||||
mod loading_bar;
|
||||
|
@ -79,6 +80,7 @@ pub use grid::*;
|
|||
pub use icon::*;
|
||||
pub use image::*;
|
||||
pub use input::*;
|
||||
pub use label::*;
|
||||
pub use layout::*;
|
||||
pub use link::*;
|
||||
pub use loading_bar::*;
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
mod loading_bar_provider;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
pub use loading_bar_provider::*;
|
||||
use tachys::renderer::DomRenderer;
|
||||
|
||||
use crate::ConfigInjection;
|
||||
use leptos::{html, prelude::*};
|
||||
use std::sync::Arc;
|
||||
use thaw_utils::{mount_style, ComponentRef};
|
||||
|
||||
#[derive(Clone)]
|
||||
|
|
|
@ -5,13 +5,13 @@ let toaster = ToasterInjection::expect_context();
|
|||
|
||||
let on_select = move |key: String| {
|
||||
leptos::logging::warn!("{}", key);
|
||||
toaster.dispatch_toast(view! {
|
||||
toaster.dispatch_toast(move || view! {
|
||||
<Toast>
|
||||
<ToastBody>
|
||||
"key"
|
||||
</ToastBody>
|
||||
</Toast>
|
||||
}.into_any(), Default::default());
|
||||
}, Default::default());
|
||||
};
|
||||
|
||||
view! {
|
||||
|
|
|
@ -1,5 +1,13 @@
|
|||
# Space
|
||||
|
||||
<MessageBar intent=MessageBarIntent::Warning>
|
||||
<MessageBarBody>
|
||||
<div style="white-space: normal">
|
||||
"The Space component may be removed in future versions. It is recommended to use the Flex component."
|
||||
</div>
|
||||
</MessageBarBody>
|
||||
</MessageBar>
|
||||
|
||||
```rust demo
|
||||
view! {
|
||||
<Space>
|
||||
|
|
|
@ -38,14 +38,14 @@ view! {
|
|||
let toaster = ToasterInjection::expect_context();
|
||||
|
||||
let on_dismiss = move |_| {
|
||||
toaster.dispatch_toast(view! {
|
||||
toaster.dispatch_toast(move || view! {
|
||||
<Toast>
|
||||
<ToastTitle>"Tag"</ToastTitle>
|
||||
<ToastBody>
|
||||
"Tag dismiss"
|
||||
</ToastBody>
|
||||
</Toast>
|
||||
}.into_any(), Default::default());
|
||||
}, Default::default());
|
||||
};
|
||||
|
||||
view! {
|
||||
|
|
|
@ -37,6 +37,76 @@ view! {
|
|||
}
|
||||
```
|
||||
|
||||
### Grouped
|
||||
|
||||
```rust demo
|
||||
use leptos::either::Either;
|
||||
let selected_options = RwSignal::new(vec![]);
|
||||
let land = vec!["Cat", "Dog", "Ferret", "Hamster"];
|
||||
let water = vec!["Fish", "Jellyfish", "Octopus", "Seal"];
|
||||
|
||||
view! {
|
||||
<TagPicker selected_options>
|
||||
<TagPickerControl slot>
|
||||
<TagPickerGroup>
|
||||
{move || {
|
||||
selected_options.get().into_iter().map(|option| view!{
|
||||
<Tag value=option.clone()>
|
||||
{option}
|
||||
</Tag>
|
||||
}).collect_view()
|
||||
}}
|
||||
</TagPickerGroup>
|
||||
<TagPickerInput />
|
||||
</TagPickerControl>
|
||||
{move || {
|
||||
selected_options.with(|selected_options| {
|
||||
let land_view = land.iter().filter_map(|option| {
|
||||
if selected_options.iter().any(|o| o == option) {
|
||||
return None
|
||||
} else {
|
||||
Some(view! {
|
||||
<TagPickerOption value=option.clone() text=option.clone() />
|
||||
})
|
||||
}
|
||||
}).collect_view();
|
||||
if land_view.is_empty() {
|
||||
Either::Left(())
|
||||
} else {
|
||||
Either::Right(view! {
|
||||
<TagPickerOptionGroup label="Land">
|
||||
{land_view}
|
||||
</TagPickerOptionGroup>
|
||||
})
|
||||
}
|
||||
})
|
||||
}}
|
||||
{move || {
|
||||
selected_options.with(|selected_options| {
|
||||
let water_view = water.iter().filter_map(|option| {
|
||||
if selected_options.iter().any(|o| o == option) {
|
||||
return None
|
||||
} else {
|
||||
Some(view! {
|
||||
<TagPickerOption value=option.clone() text=option.clone() />
|
||||
})
|
||||
}
|
||||
}).collect_view();
|
||||
if water_view.is_empty() {
|
||||
Either::Left(())
|
||||
} else {
|
||||
Either::Right(view! {
|
||||
<TagPickerOptionGroup label="Sea">
|
||||
{water_view}
|
||||
</TagPickerOptionGroup>
|
||||
})
|
||||
}
|
||||
})
|
||||
}}
|
||||
</TagPicker>
|
||||
}
|
||||
```
|
||||
|
||||
### TagPicker Props
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|
@ -68,3 +138,11 @@ view! {
|
|||
| value | `String` | | Defines a unique identifier for the option. |
|
||||
| text | `String` | | An optional override the string value of the Option's display text, defaulting to the Option's child content. |
|
||||
| children | `Option<Children>` | `None` | |
|
||||
|
||||
### TagPickerOptionGroup Props
|
||||
|
||||
| Name | Type | Default | Desciption |
|
||||
| -------- | ------------------- | -------------------- | ------------------- |
|
||||
| class | `MaybeProp<String>` | `Default::default()` | |
|
||||
| label | `String` | | Label of the group. |
|
||||
| children | `Children` | | |
|
||||
|
|
|
@ -2,8 +2,10 @@ mod tag_picker;
|
|||
mod tag_picker_group;
|
||||
mod tag_picker_input;
|
||||
mod tag_picker_option;
|
||||
mod tag_picker_option_group;
|
||||
|
||||
pub use tag_picker::*;
|
||||
pub use tag_picker_group::*;
|
||||
pub use tag_picker_input::*;
|
||||
pub use tag_picker_option::*;
|
||||
pub use tag_picker_option_group::*;
|
||||
|
|
|
@ -82,21 +82,6 @@
|
|||
outline-style: none;
|
||||
}
|
||||
|
||||
.thaw-tag-picker__listbox {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: var(--spacingHorizontalXXS);
|
||||
overflow-y: auto;
|
||||
min-width: 160px;
|
||||
max-height: 80vh;
|
||||
background-color: var(--colorNeutralBackground1);
|
||||
padding: var(--spacingHorizontalXS);
|
||||
outline: 1px solid var(--colorTransparentStroke);
|
||||
border-radius: var(--borderRadiusMedium);
|
||||
box-shadow: var(--shadow16);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.thaw-tag-picker-option {
|
||||
grid-template-columns: auto 1fr;
|
||||
column-gap: var(--spacingHorizontalXS);
|
||||
|
|
|
@ -6,7 +6,7 @@ use crate::{
|
|||
use leptos::{context::Provider, ev, html, prelude::*};
|
||||
use std::collections::HashMap;
|
||||
use thaw_components::{Binder, Follower, FollowerPlacement, FollowerWidth};
|
||||
use thaw_utils::{call_on_click_outside, class_list, mount_style, BoxCallback, Model};
|
||||
use thaw_utils::{call_on_click_outside_with_list, class_list, mount_style, BoxCallback, Model};
|
||||
|
||||
#[component]
|
||||
pub fn TagPicker(
|
||||
|
@ -55,8 +55,8 @@ pub fn TagPicker(
|
|||
}
|
||||
is_show_listbox.update(|show| *show = !*show);
|
||||
};
|
||||
call_on_click_outside(
|
||||
trigger_ref,
|
||||
call_on_click_outside_with_list(
|
||||
vec![trigger_ref, listbox_ref],
|
||||
{
|
||||
move || {
|
||||
is_show_listbox.set(false);
|
||||
|
|
13
thaw/src/tag_picker/tag_picker_option_group.rs
Normal file
13
thaw/src/tag_picker/tag_picker_option_group.rs
Normal file
|
@ -0,0 +1,13 @@
|
|||
use crate::option_group::OptionGroup;
|
||||
use leptos::prelude::*;
|
||||
|
||||
#[component]
|
||||
pub fn TagPickerOptionGroup(
|
||||
#[prop(optional, into)] class: MaybeProp<String>,
|
||||
/// Label of the group.
|
||||
#[prop(into)]
|
||||
label: String,
|
||||
children: Children,
|
||||
) -> impl IntoView {
|
||||
view! { <OptionGroup class_prefix="thaw-tag-picker-option-group" class label children /> }
|
||||
}
|
|
@ -119,6 +119,7 @@ pub struct ColorTheme {
|
|||
pub shadow4: String,
|
||||
pub shadow8: String,
|
||||
pub shadow16: String,
|
||||
pub shadow64: String,
|
||||
}
|
||||
|
||||
impl ColorTheme {
|
||||
|
@ -242,6 +243,7 @@ impl ColorTheme {
|
|||
shadow4: "0 0 2px rgba(0,0,0,0.12), 0 2px 4px rgba(0,0,0,0.14)".into(),
|
||||
shadow8: "0 0 2px rgba(0,0,0,0.12), 0 4px 8px rgba(0,0,0,0.14)".into(),
|
||||
shadow16: "0 0 2px rgba(0,0,0,0.12), 0 8px 16px rgba(0,0,0,0.14)".into(),
|
||||
shadow64: "0 0 8px rgba(0,0,0,0.12), 0 32px 64px rgba(0,0,0,0.14)".into(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -365,6 +367,7 @@ impl ColorTheme {
|
|||
shadow4: "0 0 2px rgba(0,0,0,0.24), 0 2px 4px rgba(0,0,0,0.28)".into(),
|
||||
shadow8: "0 0 2px rgba(0,0,0,0.24), 0 4px 8px rgba(0,0,0,0.28)".into(),
|
||||
shadow16: "0 0 2px rgba(0,0,0,0.24), 0 8px 16px rgba(0,0,0,0.28)".into(),
|
||||
shadow64: "0 0 8px rgba(0,0,0,0.24), 0 32px 64px rgba(0,0,0,0.28)".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -59,6 +59,7 @@ pub struct CommonTheme {
|
|||
pub duration_ultra_fast: String,
|
||||
pub duration_faster: String,
|
||||
pub duration_normal: String,
|
||||
pub duration_gentle: String,
|
||||
pub duration_slow: String,
|
||||
pub curve_accelerate_mid: String,
|
||||
pub curve_decelerate_max: String,
|
||||
|
@ -126,6 +127,7 @@ impl CommonTheme {
|
|||
duration_ultra_fast: "50ms".into(),
|
||||
duration_faster: "100ms".into(),
|
||||
duration_normal: "200ms".into(),
|
||||
duration_gentle: "250ms".into(),
|
||||
duration_slow: "300ms".into(),
|
||||
curve_accelerate_mid: "cubic-bezier(1,0,1,1)".into(),
|
||||
curve_decelerate_max: "cubic-bezier(0.1,0.9,0.2,1)".into(),
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
let toaster = ToasterInjection::expect_context();
|
||||
|
||||
let on_click = move |_| {
|
||||
toaster.dispatch_toast(view! {
|
||||
toaster.dispatch_toast(move || view! {
|
||||
<Toast>
|
||||
<ToastTitle>"Email sent"</ToastTitle>
|
||||
<ToastBody>
|
||||
|
@ -19,7 +19,7 @@ let on_click = move |_| {
|
|||
// <Link>Action</Link>
|
||||
</ToastFooter>
|
||||
</Toast>
|
||||
}.into_any(), Default::default());
|
||||
}, Default::default());
|
||||
};
|
||||
|
||||
view! {
|
||||
|
@ -33,7 +33,7 @@ view! {
|
|||
let toaster = ToasterInjection::expect_context();
|
||||
|
||||
fn dispatch_toast(toaster: ToasterInjection, position: ToastPosition) {
|
||||
toaster.dispatch_toast(view! {
|
||||
toaster.dispatch_toast(move || view! {
|
||||
<Toast>
|
||||
<ToastTitle>"Email sent"</ToastTitle>
|
||||
<ToastBody>
|
||||
|
@ -48,7 +48,7 @@ fn dispatch_toast(toaster: ToasterInjection, position: ToastPosition) {
|
|||
// <Link>Action</Link>
|
||||
</ToastFooter>
|
||||
</Toast>
|
||||
}.into_any(), ToastOptions::default().with_position(position));
|
||||
}, ToastOptions::default().with_position(position));
|
||||
};
|
||||
|
||||
view! {
|
||||
|
@ -63,11 +63,99 @@ view! {
|
|||
}
|
||||
```
|
||||
|
||||
### Toast Intent
|
||||
|
||||
```rust demo
|
||||
let toaster = ToasterInjection::expect_context();
|
||||
|
||||
fn dispatch_toast(toaster: ToasterInjection, intent: ToastIntent) {
|
||||
toaster.dispatch_toast(move || view! {
|
||||
<Toast>
|
||||
<ToastTitle>"Email sent"</ToastTitle>
|
||||
<ToastBody>
|
||||
"This is a toast body"
|
||||
<ToastBodySubtitle slot>
|
||||
"Subtitle"
|
||||
</ToastBodySubtitle>
|
||||
</ToastBody>
|
||||
<ToastFooter>
|
||||
"Footer"
|
||||
</ToastFooter>
|
||||
</Toast>
|
||||
}, ToastOptions::default().with_intent(intent));
|
||||
};
|
||||
|
||||
view! {
|
||||
<Space>
|
||||
<Button on_click=move |_| dispatch_toast(toaster, ToastIntent::Info)>"Info"</Button>
|
||||
<Button on_click=move |_| dispatch_toast(toaster, ToastIntent::Success)>"Success"</Button>
|
||||
<Button on_click=move |_| dispatch_toast(toaster, ToastIntent::Warning)>"Warning"</Button>
|
||||
<Button on_click=move |_| dispatch_toast(toaster, ToastIntent::Error)>"Error"</Button>
|
||||
</Space>
|
||||
}
|
||||
```
|
||||
|
||||
### Dismiss Toast
|
||||
|
||||
```rust demo
|
||||
let toaster = ToasterInjection::expect_context();
|
||||
let id = uuid::Uuid::new_v4();
|
||||
|
||||
|
||||
let dispatch = move |_| {
|
||||
toaster.dispatch_toast(move || view! {
|
||||
<Toast>
|
||||
<ToastTitle>"Email sent"</ToastTitle>
|
||||
<ToastBody>
|
||||
"This is a toast body"
|
||||
<ToastBodySubtitle slot>
|
||||
"Subtitle"
|
||||
</ToastBodySubtitle>
|
||||
</ToastBody>
|
||||
</Toast>
|
||||
},ToastOptions::default().with_id(id));
|
||||
};
|
||||
|
||||
let dismiss = move |_| {
|
||||
toaster.dismiss_toast(id);
|
||||
};
|
||||
|
||||
view! {
|
||||
<Button on_click=dispatch>"Show toast"</Button>
|
||||
<Button on_click=dismiss>"Hide toast"</Button>
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Toast Title Media
|
||||
|
||||
```rust demo
|
||||
let toaster = ToasterInjection::expect_context();
|
||||
|
||||
let on_click = move |_| {
|
||||
toaster.dispatch_toast(move || view! {
|
||||
<Toast>
|
||||
<ToastTitle>
|
||||
"Loading"
|
||||
<ToastTitleMedia slot>
|
||||
<Spinner size=SpinnerSize::Tiny/>
|
||||
</ToastTitleMedia>
|
||||
</ToastTitle>
|
||||
</Toast>
|
||||
}, Default::default());
|
||||
};
|
||||
|
||||
view! {
|
||||
<Button on_click=on_click>"Make toast"</Button>
|
||||
}
|
||||
```
|
||||
|
||||
### ToasterProvider Props
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| -------- | --------------- | -------------------------- | ------------------------------------- |
|
||||
| position | `ToastPosition` | `ToastPosition::BottomEnd` | The position the toast should render. |
|
||||
| intent | `ToastIntent ` | `ToastPosition::Info` | The intent of the toast. |
|
||||
| children | `Children` | | |
|
||||
|
||||
### ToastOptions Props
|
||||
|
@ -76,7 +164,7 @@ view! {
|
|||
| ------------- | --------------------------------------- | ------------------------------------- |
|
||||
| with_position | `Fn(mut self, position: ToastPosition)` | The position the toast should render. |
|
||||
| with_timeout | `Fn(mut self, timeout: Duration)` | Auto dismiss timeout in milliseconds. |
|
||||
| with_intent | `Fn(mut self, intent: ToastIntent)` | Intent. |
|
||||
| with_intent | `Fn(mut self, intent: ToastIntent)` | The intent of the toast. |
|
||||
|
||||
### Toast & ToastFooter Props
|
||||
|
||||
|
|
|
@ -13,22 +13,26 @@ pub use toaster_provider::*;
|
|||
|
||||
use leptos::prelude::*;
|
||||
use std::sync::mpsc::{channel, Receiver, Sender, TryIter};
|
||||
use tachys::view::any_view::AnyView;
|
||||
use wasm_bindgen::UnwrapThrowExt;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ToasterInjection {
|
||||
sender: StoredValue<Sender<(AnyView<Dom>, ToastOptions)>>,
|
||||
sender: StoredValue<Sender<ToasterMessage>>,
|
||||
trigger: StoredValue<ArcTrigger>,
|
||||
}
|
||||
|
||||
enum ToasterMessage {
|
||||
Dispatch(Children, ToastOptions),
|
||||
Dismiss(uuid::Uuid),
|
||||
}
|
||||
|
||||
impl ToasterInjection {
|
||||
pub fn expect_context() -> Self {
|
||||
expect_context()
|
||||
}
|
||||
|
||||
pub fn channel() -> (Self, ToasterReceiver) {
|
||||
let (sender, receiver) = channel::<(AnyView<Dom>, ToastOptions)>();
|
||||
let (sender, receiver) = channel::<ToasterMessage>();
|
||||
let trigger = ArcTrigger::new();
|
||||
|
||||
(
|
||||
|
@ -40,24 +44,43 @@ impl ToasterInjection {
|
|||
)
|
||||
}
|
||||
|
||||
pub fn dispatch_toast(&self, any_view: AnyView<Dom>, options: ToastOptions) {
|
||||
self.sender
|
||||
.with_value(|sender| sender.send((any_view, options)).unwrap_throw());
|
||||
pub fn dismiss_toast(&self, toast_id: uuid::Uuid) {
|
||||
self.sender.with_value(|sender| {
|
||||
sender
|
||||
.send(ToasterMessage::Dismiss(toast_id))
|
||||
.unwrap_throw()
|
||||
});
|
||||
self.trigger.with_value(|trigger| trigger.notify());
|
||||
}
|
||||
|
||||
pub fn dispatch_toast<C, IV>(&self, children: C, options: ToastOptions)
|
||||
where
|
||||
C: FnOnce() -> IV + Send + 'static,
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
self.sender.with_value(|sender| {
|
||||
sender
|
||||
.send(ToasterMessage::Dispatch(
|
||||
Box::new(move || children().into_any()),
|
||||
options,
|
||||
))
|
||||
.unwrap_throw()
|
||||
});
|
||||
self.trigger.with_value(|trigger| trigger.notify());
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ToasterReceiver {
|
||||
receiver: Receiver<(AnyView<Dom>, ToastOptions)>,
|
||||
receiver: Receiver<ToasterMessage>,
|
||||
trigger: ArcTrigger,
|
||||
}
|
||||
|
||||
impl ToasterReceiver {
|
||||
pub fn new(receiver: Receiver<(AnyView<Dom>, ToastOptions)>, trigger: ArcTrigger) -> Self {
|
||||
fn new(receiver: Receiver<ToasterMessage>, trigger: ArcTrigger) -> Self {
|
||||
Self { receiver, trigger }
|
||||
}
|
||||
|
||||
pub fn try_recv(&self) -> TryIter<'_, (AnyView<Dom>, ToastOptions)> {
|
||||
fn try_recv(&self) -> TryIter<'_, ToasterMessage> {
|
||||
self.trigger.track();
|
||||
self.receiver.try_iter()
|
||||
}
|
||||
|
|
|
@ -34,9 +34,10 @@ impl ToastPosition {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub enum ToastIntent {
|
||||
Success,
|
||||
#[default]
|
||||
Info,
|
||||
Warning,
|
||||
Error,
|
||||
|
@ -62,6 +63,12 @@ impl Default for ToastOptions {
|
|||
}
|
||||
|
||||
impl ToastOptions {
|
||||
/// The id that will be assigned to this toast.
|
||||
pub fn with_id(mut self, id: uuid::Uuid) -> Self {
|
||||
self.id = id;
|
||||
self
|
||||
}
|
||||
|
||||
/// The position the toast should render.
|
||||
pub fn with_position(mut self, position: ToastPosition) -> Self {
|
||||
self.position = Some(position);
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
use leptos::{either::Either, prelude::*};
|
||||
use thaw_components::OptionComp;
|
||||
use thaw_utils::class_list;
|
||||
|
||||
use crate::ToastIntent;
|
||||
|
||||
#[component]
|
||||
pub fn ToastTitle(
|
||||
|
@ -7,27 +10,63 @@ pub fn ToastTitle(
|
|||
children: Children,
|
||||
#[prop(optional)] toast_title_action: Option<ToastTitleAction>,
|
||||
) -> impl IntoView {
|
||||
let intent: ToastIntent = expect_context();
|
||||
|
||||
view! {
|
||||
<div class="thaw-toast-title__media">
|
||||
<div class=class_list![
|
||||
"thaw-toast-title__media", format!("thaw-toast-title__{:?}", intent).to_lowercase()
|
||||
]>
|
||||
{if let Some(media) = toast_title_media {
|
||||
Either::Left((media.children)())
|
||||
} else {
|
||||
Either::Right(
|
||||
view! {
|
||||
<svg
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
d="M18 10a8 8 0 1 0-16 0 8 8 0 0 0 16 0ZM9.5 8.91a.5.5 0 0 1 1 0V13.6a.5.5 0 0 1-1 0V8.9Zm-.25-2.16a.75.75 0 1 1 1.5 0 .75.75 0 0 1-1.5 0Z"
|
||||
{
|
||||
Either::Right(
|
||||
view! {
|
||||
<svg
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
},
|
||||
)
|
||||
aria-hidden="true"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
{match intent {
|
||||
ToastIntent::Info => {
|
||||
view! {
|
||||
<path
|
||||
d="M18 10a8 8 0 1 0-16 0 8 8 0 0 0 16 0ZM9.5 8.91a.5.5 0 0 1 1 0V13.6a.5.5 0 0 1-1 0V8.9Zm-.25-2.16a.75.75 0 1 1 1.5 0 .75.75 0 0 1-1.5 0Z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
}.into_any()
|
||||
},
|
||||
ToastIntent::Success => {
|
||||
view! {
|
||||
<path
|
||||
d="M10 2a8 8 0 1 1 0 16 8 8 0 0 1 0-16Zm3.36 5.65a.5.5 0 0 0-.64-.06l-.07.06L9 11.3 7.35 9.65l-.07-.06a.5.5 0 0 0-.7.7l.07.07 2 2 .07.06c.17.11.4.11.56 0l.07-.06 4-4 .07-.08a.5.5 0 0 0-.06-.63Z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
}.into_any()
|
||||
},
|
||||
ToastIntent::Warning => {
|
||||
view! {
|
||||
<path
|
||||
d="M8.68 2.79a1.5 1.5 0 0 1 2.64 0l6.5 12A1.5 1.5 0 0 1 16.5 17h-13a1.5 1.5 0 0 1-1.32-2.21l6.5-12ZM10.5 7.5a.5.5 0 0 0-1 0v4a.5.5 0 0 0 1 0v-4Zm.25 6.25a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
}.into_any()
|
||||
},
|
||||
ToastIntent::Error => {
|
||||
view! {
|
||||
<path
|
||||
d="M10 2a8 8 0 1 1 0 16 8 8 0 0 1 0-16ZM7.8 7.11a.5.5 0 0 0-.63.06l-.06.07a.5.5 0 0 0 .06.64L9.3 10l-2.12 2.12-.06.07a.5.5 0 0 0 .06.64l.07.06c.2.13.47.11.64-.06L10 10.7l2.12 2.12.07.06c.2.13.46.11.64-.06l.06-.07a.5.5 0 0 0-.06-.64L10.7 10l2.12-2.12.06-.07a.5.5 0 0 0-.06-.64l-.07-.06a.5.5 0 0 0-.64.06L10 9.3 7.88 7.17l-.07-.06Z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
}.into_any()
|
||||
}
|
||||
}}
|
||||
</svg>
|
||||
},
|
||||
)
|
||||
}
|
||||
}}
|
||||
</div>
|
||||
<div class="thaw-toast-title">{children()}</div>
|
||||
|
|
|
@ -108,10 +108,25 @@ div.thaw-toaster-wrapper {
|
|||
grid-column-end: 2;
|
||||
padding-right: 8px;
|
||||
font-size: 16px;
|
||||
color: var(--colorNeutralForeground1);
|
||||
color: var(--colorNeutralForeground2);
|
||||
}
|
||||
|
||||
.thaw-toast-title__info {
|
||||
color: var(--colorNeutralForeground2);
|
||||
}
|
||||
|
||||
.thaw-toast-title__success {
|
||||
color: var(--colorStatusSuccessForeground1);
|
||||
}
|
||||
|
||||
.thaw-toast-title__warning {
|
||||
color: var(--colorStatusWarningForeground3);
|
||||
}
|
||||
|
||||
.thaw-toast-title__error {
|
||||
color: var(--colorStatusDangerForeground1);
|
||||
}
|
||||
|
||||
.thaw-toast-title__media > svg {
|
||||
display: inline;
|
||||
line-height: 0;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use super::{ToastOptions, ToastPosition, ToasterReceiver};
|
||||
use crate::ConfigInjection;
|
||||
use leptos::{either::Either, html, prelude::*, tachys::view::any_view::AnyView};
|
||||
use super::{ToastIntent, ToastOptions, ToastPosition, ToasterReceiver};
|
||||
use crate::{toast::ToasterMessage, ConfigInjection};
|
||||
use leptos::{context::Provider, either::Either, html, prelude::*};
|
||||
use send_wrapper::SendWrapper;
|
||||
use std::{collections::HashMap, time::Duration};
|
||||
use thaw_components::{CSSTransition, Teleport};
|
||||
|
@ -11,6 +11,7 @@ use wasm_bindgen::UnwrapThrowExt;
|
|||
pub fn Toaster(
|
||||
receiver: ToasterReceiver,
|
||||
#[prop(optional)] position: ToastPosition,
|
||||
#[prop(optional)] intent: ToastIntent,
|
||||
#[prop(default = Duration::from_secs(3))] timeout: Duration,
|
||||
) -> impl IntoView {
|
||||
mount_style("toaster", include_str!("./toaster.css"));
|
||||
|
@ -21,9 +22,11 @@ pub fn Toaster(
|
|||
let bottom_id_list = RwSignal::<Vec<uuid::Uuid>>::new(Default::default());
|
||||
let bottom_start_id_list = RwSignal::<Vec<uuid::Uuid>>::new(Default::default());
|
||||
let bottom_end_id_list = RwSignal::<Vec<uuid::Uuid>>::new(Default::default());
|
||||
let toasts = StoredValue::<HashMap<uuid::Uuid, (SendWrapper<AnyView<Dom>>, ToastOptions)>>::new(
|
||||
Default::default(),
|
||||
);
|
||||
let toasts = StoredValue::<
|
||||
HashMap<uuid::Uuid, (SendWrapper<Children>, ToastOptions, RwSignal<bool>)>,
|
||||
>::new(Default::default());
|
||||
let toast_show_list =
|
||||
StoredValue::<HashMap<uuid::Uuid, RwSignal<bool>>>::new(Default::default());
|
||||
|
||||
let id_list = move |position: &ToastPosition| match position {
|
||||
ToastPosition::Top => top_id_list,
|
||||
|
@ -34,23 +37,38 @@ pub fn Toaster(
|
|||
ToastPosition::BottomEnd => bottom_end_id_list,
|
||||
};
|
||||
|
||||
let owner = Owner::current().unwrap();
|
||||
Effect::new(move |_| {
|
||||
for (view, mut options) in receiver.try_recv() {
|
||||
if options.position.is_none() {
|
||||
options.position = Some(position);
|
||||
}
|
||||
if options.timeout.is_none() {
|
||||
options.timeout = Some(timeout);
|
||||
}
|
||||
for message in receiver.try_recv() {
|
||||
match message {
|
||||
ToasterMessage::Dispatch(view, mut options) => {
|
||||
if options.position.is_none() {
|
||||
options.position = Some(position);
|
||||
}
|
||||
if options.timeout.is_none() {
|
||||
options.timeout = Some(timeout);
|
||||
}
|
||||
if options.intent.is_none() {
|
||||
options.intent = Some(intent);
|
||||
}
|
||||
|
||||
let list = id_list(&options.position.unwrap_throw());
|
||||
let id = options.id;
|
||||
toasts.update_value(|map| {
|
||||
map.insert(id, (SendWrapper::new(view), options));
|
||||
});
|
||||
list.update(|list| {
|
||||
list.push(id);
|
||||
});
|
||||
let list = id_list(&options.position.unwrap_throw());
|
||||
let id = options.id;
|
||||
let is_show = owner.with(|| RwSignal::new(true));
|
||||
toasts.update_value(|map| {
|
||||
map.insert(id, (SendWrapper::new(view), options, is_show));
|
||||
});
|
||||
toast_show_list.update_value(|map| {
|
||||
map.insert(id, is_show);
|
||||
});
|
||||
list.update(|list| {
|
||||
list.push(id);
|
||||
});
|
||||
}
|
||||
ToasterMessage::Dismiss(toast_id) => {
|
||||
toast_show_list.with_value(|map| map.get(&toast_id).unwrap_throw().set(false));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -62,6 +80,12 @@ pub fn Toaster(
|
|||
};
|
||||
list.remove(index);
|
||||
});
|
||||
let is_show = toast_show_list
|
||||
.try_update_value(|map| map.remove(&id))
|
||||
.flatten();
|
||||
if let Some(is_show) = is_show {
|
||||
is_show.dispose();
|
||||
}
|
||||
}));
|
||||
|
||||
view! {
|
||||
|
@ -72,12 +96,19 @@ pub fn Toaster(
|
|||
>
|
||||
<div class="thaw-toaster thaw-toaster--top">
|
||||
<For each=move || top_id_list.get() key=|id| id.clone() let:id>
|
||||
{if let Some((view, options)) = toasts
|
||||
{if let Some((view, options, is_show)) = toasts
|
||||
.try_update_value(|map| { map.remove(&id) })
|
||||
.flatten()
|
||||
{
|
||||
Either::Left(
|
||||
view! { <ToasterContainer on_close view=view.take() options /> },
|
||||
view! {
|
||||
<ToasterContainer
|
||||
on_close
|
||||
children=view.take()
|
||||
options
|
||||
is_show
|
||||
/>
|
||||
},
|
||||
)
|
||||
} else {
|
||||
Either::Right(())
|
||||
|
@ -86,12 +117,19 @@ pub fn Toaster(
|
|||
</div>
|
||||
<div class="thaw-toaster thaw-toaster--top-start">
|
||||
<For each=move || top_start_id_list.get() key=|id| id.clone() let:id>
|
||||
{if let Some((view, options)) = toasts
|
||||
{if let Some((view, options, is_show)) = toasts
|
||||
.try_update_value(|map| { map.remove(&id) })
|
||||
.flatten()
|
||||
{
|
||||
Either::Left(
|
||||
view! { <ToasterContainer on_close view=view.take() options /> },
|
||||
view! {
|
||||
<ToasterContainer
|
||||
on_close
|
||||
children=view.take()
|
||||
options
|
||||
is_show
|
||||
/>
|
||||
},
|
||||
)
|
||||
} else {
|
||||
Either::Right(())
|
||||
|
@ -100,12 +138,19 @@ pub fn Toaster(
|
|||
</div>
|
||||
<div class="thaw-toaster thaw-toaster--top-end">
|
||||
<For each=move || top_end_id_list.get() key=|id| id.clone() let:id>
|
||||
{if let Some((view, options)) = toasts
|
||||
{if let Some((view, options, is_show)) = toasts
|
||||
.try_update_value(|map| { map.remove(&id) })
|
||||
.flatten()
|
||||
{
|
||||
Either::Left(
|
||||
view! { <ToasterContainer on_close view=view.take() options /> },
|
||||
view! {
|
||||
<ToasterContainer
|
||||
on_close
|
||||
children=view.take()
|
||||
options
|
||||
is_show
|
||||
/>
|
||||
},
|
||||
)
|
||||
} else {
|
||||
Either::Right(())
|
||||
|
@ -114,12 +159,19 @@ pub fn Toaster(
|
|||
</div>
|
||||
<div class="thaw-toaster thaw-toaster--bottom">
|
||||
<For each=move || bottom_id_list.get() key=|id| id.clone() let:id>
|
||||
{if let Some((view, options)) = toasts
|
||||
{if let Some((view, options, is_show)) = toasts
|
||||
.try_update_value(|map| { map.remove(&id) })
|
||||
.flatten()
|
||||
{
|
||||
Either::Left(
|
||||
view! { <ToasterContainer on_close view=view.take() options /> },
|
||||
view! {
|
||||
<ToasterContainer
|
||||
on_close
|
||||
children=view.take()
|
||||
options
|
||||
is_show
|
||||
/>
|
||||
},
|
||||
)
|
||||
} else {
|
||||
Either::Right(())
|
||||
|
@ -128,12 +180,19 @@ pub fn Toaster(
|
|||
</div>
|
||||
<div class="thaw-toaster thaw-toaster--bottom-start">
|
||||
<For each=move || bottom_start_id_list.get() key=|id| id.clone() let:id>
|
||||
{if let Some((view, options)) = toasts
|
||||
{if let Some((view, options, is_show)) = toasts
|
||||
.try_update_value(|map| { map.remove(&id) })
|
||||
.flatten()
|
||||
{
|
||||
Either::Left(
|
||||
view! { <ToasterContainer on_close view=view.take() options /> },
|
||||
view! {
|
||||
<ToasterContainer
|
||||
on_close
|
||||
children=view.take()
|
||||
options
|
||||
is_show
|
||||
/>
|
||||
},
|
||||
)
|
||||
} else {
|
||||
Either::Right(())
|
||||
|
@ -142,12 +201,19 @@ pub fn Toaster(
|
|||
</div>
|
||||
<div class="thaw-toaster thaw-toaster--bottom-end">
|
||||
<For each=move || bottom_end_id_list.get() key=|id| id.clone() let:id>
|
||||
{if let Some((view, options)) = toasts
|
||||
{if let Some((view, options, is_show)) = toasts
|
||||
.try_update_value(|map| { map.remove(&id) })
|
||||
.flatten()
|
||||
{
|
||||
Either::Left(
|
||||
view! { <ToasterContainer on_close view=view.take() options /> },
|
||||
view! {
|
||||
<ToasterContainer
|
||||
on_close
|
||||
children=view.take()
|
||||
options
|
||||
is_show
|
||||
/>
|
||||
},
|
||||
)
|
||||
} else {
|
||||
Either::Right(())
|
||||
|
@ -161,20 +227,23 @@ pub fn Toaster(
|
|||
|
||||
#[component]
|
||||
fn ToasterContainer(
|
||||
view: AnyView<Dom>,
|
||||
options: ToastOptions,
|
||||
#[prop(into)] on_close: StoredValue<ArcTwoCallback<uuid::Uuid, ToastPosition>>,
|
||||
children: Children,
|
||||
is_show: RwSignal<bool>,
|
||||
) -> impl IntoView {
|
||||
let container_ref = NodeRef::<html::Div>::new();
|
||||
let is_show = RwSignal::new(true);
|
||||
let ToastOptions {
|
||||
id,
|
||||
timeout,
|
||||
position,
|
||||
intent,
|
||||
..
|
||||
} = options;
|
||||
|
||||
let timeout = timeout.unwrap_throw();
|
||||
let position = position.unwrap_throw();
|
||||
let intent = intent.unwrap_throw();
|
||||
|
||||
if !timeout.is_zero() {
|
||||
set_timeout(
|
||||
|
@ -209,9 +278,11 @@ fn ToasterContainer(
|
|||
on_after_leave=on_after_leave
|
||||
let:_
|
||||
>
|
||||
<div class="thaw-toaster-container" node_ref=container_ref>
|
||||
{view}
|
||||
</div>
|
||||
<Provider value=intent>
|
||||
<div class="thaw-toaster-container" node_ref=container_ref>
|
||||
{children()}
|
||||
</div>
|
||||
</Provider>
|
||||
</CSSTransition>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use super::{toaster::Toaster, ToastPosition, ToasterInjection};
|
||||
use super::{toaster::Toaster, ToastIntent, ToastPosition, ToasterInjection};
|
||||
use leptos::{context::Provider, prelude::*};
|
||||
|
||||
#[component]
|
||||
|
@ -6,11 +6,14 @@ pub fn ToasterProvider(
|
|||
/// The position the toast should render.
|
||||
#[prop(optional)]
|
||||
position: ToastPosition,
|
||||
/// The intent of the toasts
|
||||
#[prop(optional)]
|
||||
intent: ToastIntent,
|
||||
children: Children,
|
||||
) -> impl IntoView {
|
||||
let (injection, receiver) = ToasterInjection::channel();
|
||||
view! {
|
||||
<Toaster receiver position />
|
||||
<Toaster receiver position intent />
|
||||
<Provider value=injection>{children()}</Provider>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,13 +28,13 @@ let toaster = ToasterInjection::expect_context();
|
|||
|
||||
let custom_request = move |file_list: FileList| {
|
||||
let len = file_list.length();
|
||||
toaster.dispatch_toast(view! {
|
||||
toaster.dispatch_toast(move || view! {
|
||||
<Toast>
|
||||
<ToastBody>
|
||||
{format!("Number of uploaded files: {len}")}
|
||||
</ToastBody>
|
||||
</Toast>
|
||||
}.into_any(), Default::default());
|
||||
}, Default::default());
|
||||
};
|
||||
|
||||
view! {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "thaw_components"
|
||||
version = "0.2.0-beta2"
|
||||
version = "0.2.0-beta3"
|
||||
edition = "2021"
|
||||
keywords = ["leptos", "thaw", "components"]
|
||||
readme = "../README.md"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "thaw_macro"
|
||||
version = "0.1.0-beta2"
|
||||
version = "0.1.0-beta3"
|
||||
edition = "2021"
|
||||
keywords = ["leptos", "thaw", "macro"]
|
||||
readme = "../README.md"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "thaw_utils"
|
||||
version = "0.1.0-beta2"
|
||||
version = "0.1.0-beta3"
|
||||
edition = "2021"
|
||||
keywords = ["leptos", "thaw", "utils"]
|
||||
readme = "../README.md"
|
||||
|
|
|
@ -6,7 +6,7 @@ use leptos::{
|
|||
use leptos::{
|
||||
prelude::{Oco, RenderEffect, RwSignal},
|
||||
reactive_graph::traits::{Update, With, WithUntracked},
|
||||
tachys::renderer::DomRenderer,
|
||||
tachys::renderer::{types, Rndr},
|
||||
};
|
||||
use std::collections::HashSet;
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
|
@ -163,12 +163,9 @@ impl ClassList {
|
|||
}
|
||||
}
|
||||
|
||||
impl<R> leptos::tachys::html::class::IntoClass<R> for ClassList
|
||||
where
|
||||
R: DomRenderer,
|
||||
{
|
||||
impl leptos::tachys::html::class::IntoClass for ClassList {
|
||||
type AsyncOutput = Self;
|
||||
type State = RenderEffect<(R::Element, String)>;
|
||||
type State = RenderEffect<(types::Element, String)>;
|
||||
type Cloneable = Self;
|
||||
type CloneableOwned = Self;
|
||||
|
||||
|
@ -193,7 +190,7 @@ where
|
|||
self.write_class_string(class);
|
||||
}
|
||||
|
||||
fn hydrate<const FROM_SERVER: bool>(self, el: &R::Element) -> Self::State {
|
||||
fn hydrate<const FROM_SERVER: bool>(self, el: &types::Element) -> Self::State {
|
||||
let el = el.to_owned();
|
||||
RenderEffect::new(move |prev| {
|
||||
let mut class = String::new();
|
||||
|
@ -202,7 +199,7 @@ where
|
|||
if let Some(state) = prev {
|
||||
let (el, prev_class) = state;
|
||||
if class != prev_class {
|
||||
R::set_attribute(&el, "class", &class);
|
||||
Rndr::set_attribute(&el, "class", &class);
|
||||
(el, class)
|
||||
} else {
|
||||
(el, prev_class)
|
||||
|
@ -210,7 +207,7 @@ where
|
|||
} else {
|
||||
if !class.is_empty() {
|
||||
if !FROM_SERVER {
|
||||
R::set_attribute(&el, "class", &class);
|
||||
Rndr::set_attribute(&el, "class", &class);
|
||||
}
|
||||
}
|
||||
(el.clone(), class)
|
||||
|
@ -218,7 +215,7 @@ where
|
|||
})
|
||||
}
|
||||
|
||||
fn build(self, el: &R::Element) -> Self::State {
|
||||
fn build(self, el: &types::Element) -> Self::State {
|
||||
let el = el.to_owned();
|
||||
RenderEffect::new(move |prev| {
|
||||
let mut class = String::new();
|
||||
|
@ -226,14 +223,14 @@ where
|
|||
if let Some(state) = prev {
|
||||
let (el, prev_class) = state;
|
||||
if class != prev_class {
|
||||
R::set_attribute(&el, "class", &class);
|
||||
Rndr::set_attribute(&el, "class", &class);
|
||||
(el, class)
|
||||
} else {
|
||||
(el, prev_class)
|
||||
}
|
||||
} else {
|
||||
if !class.is_empty() {
|
||||
R::set_attribute(&el, "class", &class);
|
||||
Rndr::set_attribute(&el, "class", &class);
|
||||
}
|
||||
(el.clone(), class)
|
||||
}
|
||||
|
@ -249,7 +246,7 @@ where
|
|||
self.write_class_string(&mut class);
|
||||
let (el, prev_class) = state;
|
||||
if class != *prev_class {
|
||||
R::set_attribute(&el, "class", &class);
|
||||
Rndr::set_attribute(&el, "class", &class);
|
||||
(el, class)
|
||||
} else {
|
||||
(el, prev_class)
|
||||
|
|
|
@ -21,3 +21,28 @@ pub fn call_on_click_outside(element: NodeRef<Div>, on_click: BoxCallback) {
|
|||
let _ = on_click;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn call_on_click_outside_with_list(refs: Vec<NodeRef<Div>>, on_click: BoxCallback) {
|
||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
||||
{
|
||||
let handle = window_event_listener(::leptos::ev::click, move |ev| {
|
||||
let composed_path = ev.composed_path();
|
||||
if refs.iter().any(|r| {
|
||||
if let Some(el) = r.get_untracked() {
|
||||
composed_path.includes(&el, 0)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}) {
|
||||
return;
|
||||
}
|
||||
on_click();
|
||||
});
|
||||
on_cleanup(move || handle.remove());
|
||||
}
|
||||
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
|
||||
{
|
||||
let _ = refs;
|
||||
let _ = on_click;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue