Compare commits

...

11 commits

Author SHA1 Message Date
luoxiaozero
e18bbff216
feat: update leptos to v0.7.0-beta7 (#272)
Some checks failed
Deploy demo / deploy (push) Has been cancelled
2024-09-29 14:09:30 +08:00
kandrelczyk
b831008e09
Toast dismiss (#270)
* broken toaster

* toast dismiss

* toaster test

* fixed dismiss

* cargo fmt

* dipose signal
2024-09-29 11:22:14 +08:00
kandrelczyk
a03226f202
Toast intent (#269)
* add toast intent support

* toast intent

* toast intent

* fix CI
2024-09-24 07:14:11 +08:00
luoxiaozero
3545744335
feat: When the Field status is Error, the border of the input class component is displayed in red (#268) 2024-09-23 23:37:35 +08:00
luoxiaozero
c87b989c3c
feat: change the any_view prop of dispatch_toast to children (#267) 2024-09-22 18:08:24 +08:00
luoxiaozero
687350102a
feat: adds Label component (#266) 2024-09-20 00:13:40 +08:00
kandrelczyk
282fcd038c
required input (#264)
* required input

* review fixes
2024-09-19 10:58:32 +08:00
luoxiaozero
dfd5ddd328
Feat/overlay drawer modal (#265)
* feat: OverlayDrawer adds modal_type prop

* fix: OverlayDrawer modal_type
2024-09-18 23:44:27 +08:00
luoxiao
2382b8f779 v0.4.0-beta3 2024-09-14 00:05:25 +08:00
luoxiao
32693e8244 feat: update README 2024-09-13 09:01:31 +08:00
luoxiaozero
092581d46d
Feat/tag picker (#260)
* refactor: Listbox and OptionGroup

* feat: adds TagPickerOptionGroup component

* fix: call_on_click_outside_with_list
2024-09-12 22:31:51 +08:00
63 changed files with 877 additions and 206 deletions

View file

@ -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

View file

@ -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" }

View file

@ -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)

View file

@ -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"

View file

@ -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/>

View file

@ -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());
}

View file

@ -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)
}

View file

@ -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",

View file

@ -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 {

View file

@ -1,5 +1,6 @@
use crate::components::{Demo, DemoCode};
use leptos::{ev, prelude::*};
use thaw::*;
use uuid;
demo_markdown::include_md! {}

View file

@ -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>

View file

@ -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",

View file

@ -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)*

View file

@ -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 }

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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()))

View file

@ -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;

View file

@ -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 /> }
}

View file

@ -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);
}
}

View file

@ -0,0 +1,3 @@
pub mod listbox;
pub mod option_group;
mod utils;

View file

@ -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);

View 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>
}
}

View file

@ -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::*;

View file

@ -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

View file

@ -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,
}

View file

@ -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()

View file

@ -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

View file

@ -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);
}

View file

@ -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>
}
}

View 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
View 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
View 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
View file

@ -0,0 +1,3 @@
mod label;
pub use label::*;

View file

@ -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::*;

View file

@ -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)]

View file

@ -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! {

View file

@ -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>

View file

@ -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! {

View file

@ -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` | | |

View file

@ -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::*;

View file

@ -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);

View file

@ -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);

View 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 /> }
}

View file

@ -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(),
}
}
}

View file

@ -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(),

View file

@ -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

View file

@ -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()
}

View file

@ -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);

View file

@ -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>

View file

@ -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;

View file

@ -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>
}
}

View file

@ -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>
}
}

View file

@ -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! {

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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)

View file

@ -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;
}
}