Merge pull request #225 from thaw-ui/thaw/fluent

Thaw/fluent
This commit is contained in:
luoxiaozero 2024-08-11 16:10:33 +08:00 committed by GitHub
commit d9afe2a730
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
361 changed files with 13030 additions and 10273 deletions

View file

@ -1,9 +1,21 @@
[workspace]
resolver = "2"
members = ["thaw", "thaw_components", "thaw_utils", "demo", "demo_markdown"]
members = [
"thaw",
"thaw_components",
"thaw_macro",
"thaw_utils",
"demo",
"demo_markdown",
]
exclude = ["examples"]
[workspace.dependencies]
thaw = { version = "0.3.3", path = "./thaw" }
thaw_components = { version = "0.1.3", path = "./thaw_components" }
thaw_utils = { version = "0.0.5", path = "./thaw_utils" }
thaw = { version = "0.4.0-alpha", path = "./thaw" }
thaw_components = { version = "0.2.0-alpha", path = "./thaw_components" }
thaw_macro = { version = "0.1.0-alpha", path = "./thaw_macro" }
thaw_utils = { version = "0.1.0-alpha", path = "./thaw_utils" }
leptos = { git = "https://github.com/leptos-rs/leptos", rev = "867036559489e86c1f25cdf7133d803d88b0579b" }
leptos_meta = { git = "https://github.com/leptos-rs/leptos", rev = "867036559489e86c1f25cdf7133d803d88b0579b" }
leptos_router = { git = "https://github.com/leptos-rs/leptos", rev = "867036559489e86c1f25cdf7133d803d88b0579b" }

View file

@ -6,20 +6,21 @@
## Documentation & Community
[https://thawui.vercel.app](https://thawui.vercel.app)
[https://next-thawui.vercel.app](https://next-thawui.vercel.app)
[Discord](https://discord.gg/YPxuprzu6M)
[Discord](https://discord.com/channels/1031524867910148188/1270735289437913108)
## Leptos compatibility
| Crate version | Compatible Leptos version |
| --------------------------------------------------------------- | ------------------------- |
| 0.1 | 0.5 |
| 0.2 / 0.3 | 0.6 |
| [thaw/fluent](https://github.com/thaw-ui/thaw/tree/thaw/fluent) | 0.7 |
| Crate version | Compatible Leptos version |
| -------------------------------------------------------------------- | ------------------------- |
| 0.2 / 0.3 | 0.6 |
| 0.4([thaw/fluent](https://github.com/thaw-ui/thaw/tree/thaw/fluent)) | 0.7 |
## Resources
[Fluent UI](https://react.fluentui.dev)
[Pigment](https://github.com/kobaltedev/pigment)
[Naive UI](https://github.com/tusen-ai/naive-ui)

View file

@ -7,34 +7,33 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
leptos = { version = "0.6.10" }
leptos_meta = { version = "0.6.10" }
leptos_router = { version = "0.6.10" }
leptos_devtools = { version = "0.0.1", optional = true }
leptos = { workspace = true }
leptos_meta = { workspace = true }
leptos_router = { workspace = true }
thaw = { path = "../thaw" }
demo_markdown = { path = "../demo_markdown" }
icondata = "0.3.0"
palette = "0.7.4"
chrono = "0.4.33"
cfg-if = "1.0.0"
# leptos-use = "0.10.10"
send_wrapper = "0.6"
console_error_panic_hook = "0.1.7"
console_log = "1"
log = "0.4"
[features]
default = ["csr"]
tracing = ["leptos/tracing"]
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr", "thaw/csr"]
csr = ["leptos/csr", "thaw/csr"]
ssr = ["leptos/ssr", "leptos_meta/ssr", "leptos_router/ssr", "thaw/ssr"]
hydrate = [
"leptos/hydrate",
"leptos_meta/hydrate",
"leptos_router/hydrate",
"thaw/hydrate",
]
nightly = ["leptos/nightly", "leptos_meta/nightly", "leptos_router/nightly"]
hydrate = ["leptos/hydrate", "thaw/hydrate"]
nightly = ["leptos/nightly", "leptos_router/nightly", "thaw/nightly"]
# https://benw.is/posts/how-i-improved-my-rust-compile-times-by-seventy-five-percent#optimization-level
[profile.dev]
opt-level = 1
# [profile.dev]
# opt-level = 1
[profile.dev.package."*"]
opt-level = 3
# [profile.dev.package."*"]
# opt-level = 3

View file

@ -1,7 +1,7 @@
[build]
target = "index.html"
# public_url = "/thaw/"
# release = true
release = true
# filehash = false
[watch]

View file

@ -1,127 +1,123 @@
use crate::pages::*;
use leptos::*;
use leptos::{prelude::*, reactive_graph::wrappers::write::SignalSetter};
use leptos_meta::provide_meta_context;
use leptos_router::*;
use leptos_router::{
components::{ParentRoute, Route, Router, Routes},
path,
};
// use leptos_use::{
// storage::use_local_storage,
// utils::{FromToStringCodec, StringCodec},
// };
use thaw::*;
#[component]
pub fn App() -> impl IntoView {
let is_routing = create_rw_signal(false);
provide_meta_context();
// let (read_theme, _, _) = use_local_storage::<String, FromToStringCodec>("theme");
// let theme = RwSignal::new(Theme::from(read_theme.get_untracked()));
view! {
<ConfigProvider>
<ToasterProvider>
<LoadingBarProvider>
<TheRouter />
</LoadingBarProvider>
</ToasterProvider>
</ConfigProvider>
}
}
#[component]
fn TheRouter() -> impl IntoView {
let loading_bar = LoadingBarInjection::expect_use();
let is_routing = RwSignal::new(false);
let set_is_routing = SignalSetter::map(move |is_routing_data| {
is_routing.set(is_routing_data);
});
provide_meta_context();
Effect::watch(
move || is_routing.get(),
move |is_routing, _, _| {
if *is_routing {
loading_bar.start();
} else {
loading_bar.finish();
}
},
false,
);
view! {
<Router set_is_routing>
<TheProvider>
<TheRouter is_routing/>
</TheProvider>
<Routes fallback=|| "404">
<Route path=path!("/") view=Home/>
<ParentRoute path=path!("/guide") view=ComponentsPage>
<Route path=path!("/installation") view=InstallationMdPage/>
<Route path=path!("/server-sider-rendering") view=ServerSiderRenderingMdPage/>
<Route path=path!("/development/components") view=DevelopmentComponentsMdPage/>
</ParentRoute>
<ParentRoute path=path!("/components") view=ComponentsPage>
{
view! {
<Route path=path!("/accordion") view=AccordionMdPage/>
<Route path=path!("/anchor") view=AnchorMdPage/>
<Route path=path!("/auto-complete") view=AutoCompleteMdPage/>
<Route path=path!("/avatar") view=AvatarMdPage/>
<Route path=path!("/back-top") view=BackTopMdPage/>
<Route path=path!("/badge") view=BadgeMdPage/>
<Route path=path!("/breadcrumb") view=BreadcrumbMdPage/>
<Route path=path!("/button") view=ButtonMdPage/>
<Route path=path!("/calendar") view=CalendarMdPage/>
<Route path=path!("/card") view=CardMdPage/>
<Route path=path!("/checkbox") view=CheckboxMdPage/>
<Route path=path!("/color-picker") view=ColorPickerMdPage/>
<Route path=path!("/combobox") view=ComboboxMdPage/>
<Route path=path!("/config-provider") view=ConfigProviderMdPage/>
}
}
{
view! {
<Route path=path!("date-picker") view=DatePickerMdPage/>
<Route path=path!("/dialog") view=DialogMdPage/>
<Route path=path!("/divider") view=DividerMdPage/>
<Route path=path!("/drawer") view=DrawerMdPage/>
<Route path=path!("/menu") view=MenuMdPage/>
<Route path=path!("/grid") view=GridMdPage/>
<Route path=path!("/icon") view=IconMdPage/>
<Route path=path!("/image") view=ImageMdPage/>
<Route path=path!("/input") view=InputMdPage/>
<Route path=path!("/layout") view=LayoutMdPage/>
<Route path=path!("/loading-bar") view=LoadingBarMdPage/>
<Route path=path!("/message-bar") view=MessageBarMdPage/>
<Route path=path!("/nav") view=NavMdPage/>
<Route path=path!("/pagination") view=PaginationMdPage/>
<Route path=path!("/popover") view=PopoverMdPage/>
<Route path=path!("/progress-bar") view=ProgressBarMdPage/>
}
}
{
view! {
<Route path=path!("/radio") view=RadioMdPage/>
<Route path=path!("/scrollbar") view=ScrollbarMdPage/>
<Route path=path!("/skeleton") view=SkeletonMdPage/>
<Route path=path!("/slider") view=SliderMdPage/>
<Route path=path!("/space") view=SpaceMdPage/>
<Route path=path!("/spin-button") view=SpinButtonMdPage/>
<Route path=path!("/spinner") view=SpinnerMdPage/>
<Route path=path!("/switch") view=SwitchMdPage/>
<Route path=path!("/tab-list") view=TabListMdPage/>
<Route path=path!("/table") view=TableMdPage/>
<Route path=path!("/tag") view=TagMdPage/>
<Route path=path!("/text") view=TextMdPage/>
<Route path=path!("/textarea") view=TextareaMdPage/>
<Route path=path!("/time-picker") view=TimePickerMdPage/>
<Route path=path!("/toast") view=ToastMdPage />
<Route path=path!("/upload") view=UploadMdPage/>
}
}
</ParentRoute>
</Routes>
</Router>
}
}
#[component]
fn TheRouter(is_routing: RwSignal<bool>) -> impl IntoView {
let loading_bar = use_loading_bar();
_ = is_routing.watch(move |is_routing| {
if *is_routing {
loading_bar.start();
} else {
loading_bar.finish();
}
});
view! {
<Routes>
<Route path="/" view=Home/>
<Route path="/guide" view=GuidePage>
<Route path="/installation" view=InstallationMdPage/>
<Route path="/usage" view=UsageMdPage/>
<Route path="/server-sider-rendering" view=ServerSiderRenderingMdPage/>
<Route path="/development/guide" view=DevelopmentGuideMdPage/>
<Route path="/development/components" view=DevelopmentComponentsMdPage/>
</Route>
<Route path="/components" view=ComponentsPage>
<Route path="/tabbar" view=TabbarPage/>
<Route path="/nav-bar" view=NavBarPage/>
<Route path="/toast" view=ToastPage/>
<Route path="/alert" view=AlertMdPage/>
<Route path="/anchor" view=AnchorMdPage/>
<Route path="/auto-complete" view=AutoCompleteMdPage/>
<Route path="/avatar" view=AvatarMdPage/>
<Route path="/back-top" view=BackTopMdPage/>
<Route path="/badge" view=BadgeMdPage/>
<Route path="/breadcrumb" view=BreadcrumbMdPage/>
<Route path="/button" view=ButtonMdPage/>
<Route path="/calendar" view=CalendarMdPage/>
<Route path="/card" view=CardMdPage/>
<Route path="/checkbox" view=CheckboxMdPage/>
<Route path="/collapse" view=CollapseMdPage/>
<Route path="/color-picker" view=ColorPickerMdPage/>
<Route path="/date-picker" view=DatePickerMdPage/>
<Route path="/divider" view=DividerMdPage/>
<Route path="/drawer" view=DrawerMdPage/>
<Route path="/dropdown" view=DropdownMdPage/>
<Route path="/grid" view=GridMdPage/>
<Route path="/icon" view=IconMdPage/>
<Route path="/image" view=ImageMdPage/>
<Route path="/input" view=InputMdPage/>
<Route path="/input-number" view=InputNumberMdPage/>
<Route path="/layout" view=LayoutMdPage/>
<Route path="/loading-bar" view=LoadingBarMdPage/>
<Route path="/menu" view=MenuMdPage/>
<Route path="/message" view=MessageMdPage/>
<Route path="/modal" view=ModalMdPage/>
<Route path="/pagination" view=PaginationMdPage/>
<Route path="/popover" view=PopoverMdPage/>
<Route path="/progress" view=ProgressMdPage/>
<Route path="/radio" view=RadioMdPage/>
<Route path="/scrollbar" view=ScrollbarMdPage/>
<Route path="/select" view=SelectMdPage/>
<Route path="/skeleton" view=SkeletonMdPage/>
<Route path="/slider" view=SliderMdPage/>
<Route path="/space" view=SpaceMdPage/>
<Route path="/spinner" view=SpinnerMdPage/>
<Route path="/switch" view=SwitchMdPage/>
<Route path="/table" view=TableMdPage/>
<Route path="/tabs" view=TabsMdPage/>
<Route path="/tag" view=TagMdPage/>
<Route path="/theme" view=ThemeMdPage/>
<Route path="/time-picker" view=TimePickerMdPage/>
<Route path="/typography" view=TypographyMdPage/>
<Route path="/upload" view=UploadMdPage/>
</Route>
<Route path="/mobile/tabbar" view=TabbarDemoPage/>
<Route path="/mobile/nav-bar" view=NavBarDemoPage/>
<Route path="/mobile/toast" view=ToastDemoPage/>
</Routes>
}
}
#[component]
fn TheProvider(children: Children) -> impl IntoView {
fn use_query_value(key: &str) -> Option<String> {
let query_map = use_query_map();
query_map.with_untracked(|query| query.get(key).cloned())
}
let theme = use_query_value("theme").map_or_else(Theme::light, |name| {
if name == "light" {
Theme::light()
} else if name == "dark" {
Theme::dark()
} else {
Theme::light()
}
});
let theme = create_rw_signal(theme);
view! {
<ThemeProvider theme>
<GlobalStyle/>
<MessageProvider>
<LoadingBarProvider>{children()}</LoadingBarProvider>
</MessageProvider>
</ThemeProvider>
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

View file

@ -1,4 +1,4 @@
use leptos::*;
use leptos::prelude::*;
use leptos_meta::Style;
use thaw::*;
@ -6,16 +6,17 @@ use thaw::*;
pub struct DemoCode {
#[prop(default = true)]
is_highlight: bool,
children: Children,
#[prop(into)]
text: String,
}
#[component]
pub fn Demo(demo_code: DemoCode, #[prop(optional)] children: Option<Children>) -> impl IntoView {
let theme = use_theme(Theme::light);
let theme = Theme::use_theme(Theme::light);
let css_vars = Memo::new(move |_| {
let mut css_vars = String::new();
theme.with(|theme| {
if theme.common.color_scheme == "dark" {
if theme.color.color_scheme == "dark" {
css_vars.push_str("--demo-color: #ffffff60;");
css_vars.push_str("--demo-color-hover: #ffffffe0;");
css_vars.push_str("--demo-border-color: #383f52;");
@ -23,10 +24,7 @@ pub fn Demo(demo_code: DemoCode, #[prop(optional)] children: Option<Children>) -
} else {
css_vars.push_str("--demo-color: #00000060;");
css_vars.push_str("--demo-color-hover: #000000e0;");
css_vars.push_str(&format!(
"--demo-border-color: {};",
theme.common.border_color
));
css_vars.push_str(&format!("--demo-border-color: var(--colorNeutralStroke2);",));
css_vars.push_str("--demo-background-color: #f9fafb;");
}
});
@ -34,26 +32,12 @@ pub fn Demo(demo_code: DemoCode, #[prop(optional)] children: Option<Children>) -
});
let code_class = Memo::new(move |_| {
theme.with(|theme| {
format!(
"demo-demo__code color-scheme--{}",
theme.common.color_scheme
)
})
theme.with(|theme| format!("demo-demo__code color-scheme--{}", theme.color.color_scheme))
});
let is_show_code = RwSignal::new(children.is_none());
let is_highlight = demo_code.is_highlight;
let frag = (demo_code.children)();
let mut html = String::new();
for node in frag.nodes {
match node {
View::Text(text) => html.push_str(&text.content),
_ => {
leptos::logging::warn!("Only text nodes are supported as children of <DemoCode />.")
}
}
}
let html = demo_code.text;
view! {
<Style id="leptos-thaw-syntect-css">
@ -68,7 +52,7 @@ pub fn Demo(demo_code: DemoCode, #[prop(optional)] children: Option<Children>) -
view! {
<div class="demo-demo__view">{children()}</div>
<div class="demo-demo__toolbar" class=("demo-demo__toolbar--code", move || !is_show_code.get())>
<Popover tooltip=true>
<Popover appearance=PopoverAppearance::Inverted>
<PopoverTrigger slot>
<span on:click=move |_| is_show_code.update(|show| *show = !*show) class="demo-demo__toolbar-btn">
{
@ -99,7 +83,7 @@ pub fn Demo(demo_code: DemoCode, #[prop(optional)] children: Option<Children>) -
None
}
}
<div class=move || code_class.get() style:display=move || (!is_show_code.get()).then_some("none")>
<div class=move || code_class.get() style:display=move || (!is_show_code.get()).then_some("none").unwrap_or_default()>
{
if is_highlight {
view! {

View file

@ -1,13 +1,16 @@
use super::switch_version::SwitchVersion;
use leptos::*;
use leptos::{ev::MouseEvent, prelude::*};
use leptos_meta::Style;
use leptos_router::{use_location, use_navigate};
use leptos_router::hooks::use_navigate;
// use leptos_use::{storage::use_local_storage, utils::FromToStringCodec};
use thaw::*;
#[component]
pub fn SiteHeader() -> impl IntoView {
let theme = use_rw_theme();
let theme_name = create_memo(move |_| {
let navigate = use_navigate();
let navigate_signal = RwSignal::new(use_navigate());
let theme = Theme::use_rw_theme();
let theme_name = Memo::new(move |_| {
theme.with(|theme| {
if theme.name == *"light" {
"Dark".to_string()
@ -16,19 +19,21 @@ pub fn SiteHeader() -> impl IntoView {
}
})
});
let change_theme = Callback::new(move |_| {
// let (_, write_theme, _) = use_local_storage::<String, FromToStringCodec>("theme");
let change_theme = move |_| {
if theme_name.get_untracked() == "Light" {
theme.set(Theme::light());
// write_theme.set("light".to_string());
} else {
theme.set(Theme::dark());
// write_theme.set("dark".to_string());
}
});
let style = create_memo(move |_| {
theme.with(|theme| format!("border-bottom: 1px solid {}", theme.common.border_color))
});
let search_value = create_rw_signal(String::new());
let search_all_options = store_value(gen_search_all_options());
let search_options = create_memo(move |_| {
};
let search_value = RwSignal::new(String::new());
let search_all_options = StoredValue::new(gen_search_all_options());
let search_options = Memo::new(move |_| {
let search_value = search_value.get();
if search_value.is_empty() {
return vec![];
@ -62,23 +67,29 @@ pub fn SiteHeader() -> impl IntoView {
.collect()
})
});
let on_search_select = move |path: String| {
let navigate = use_navigate();
navigate(&path, Default::default());
};
let auto_complete_ref = create_component_ref::<AutoCompleteRef>();
let handle = window_event_listener(ev::keydown, move |event| {
let key = event.key();
if key == *"/" {
if let Some(auto_complete_ref) = auto_complete_ref.get_untracked() {
event.prevent_default();
auto_complete_ref.focus();
}
let on_search_select = {
let navigate = navigate.clone();
move |path: String| {
navigate(&path, Default::default());
}
});
on_cleanup(move || handle.remove());
};
let auto_complete_ref = ComponentRef::<AutoCompleteRef>::new();
#[cfg(any(feature = "csr", feature = "hydrate"))]
{
use leptos::ev;
let handle = window_event_listener(ev::keydown, move |event| {
let key = event.key();
if key == *"/" {
if let Some(auto_complete_ref) = auto_complete_ref.get_untracked() {
event.prevent_default();
auto_complete_ref.focus();
}
}
});
on_cleanup(move || handle.remove());
}
let menu_value = use_menu_value(change_theme);
// let menu_value = use_menu_value(change_theme);
view! {
<Style id="demo-header">
"
@ -88,6 +99,9 @@ pub fn SiteHeader() -> impl IntoView {
align-items: center;
justify-content: space-between;
padding: 0 20px;
z-index: 1000;
position: relative;
border-bottom: 1px solid var(--colorNeutralStroke2);
}
.demo-name {
cursor: pointer;
@ -100,8 +114,8 @@ pub fn SiteHeader() -> impl IntoView {
.demo-header__menu-mobile {
display: none !important;
}
.demo-header__menu-popover-mobile {
padding: 0;
.demo-header__right-btn .thaw-select {
width: 60px;
}
@media screen and (max-width: 600px) {
.demo-header {
@ -121,87 +135,103 @@ pub fn SiteHeader() -> impl IntoView {
}
"
</Style>
<LayoutHeader class="demo-header" style>
<Space
on:click=move |_| {
let navigate = use_navigate();
<LayoutHeader attr:class=("demo-header", true)>
<Space on:click=move |_| {
navigate("/", Default::default());
}>
<img src="/logo.svg" style="width: 36px"/>
<div class="demo-name">
"Thaw UI"
</div>
<div class="demo-name">"Thaw UI"</div>
</Space>
<Space>
<Space align=SpaceAlign::Center>
<AutoComplete
value=search_value
placeholder="Type '/' to search"
options=search_options
clear_after_select=true
blur_after_select=true
on_select=on_search_select
comp_ref=auto_complete_ref
>
<For each=move || search_options.get() key=|option| option.label.clone() let:option>
<AutoCompleteOption value=option.value>{option.label}</AutoCompleteOption>
</For>
<AutoCompletePrefix slot>
<Icon icon=icondata::AiSearchOutlined style="font-size: 18px; color: var(--thaw-placeholder-color);"/>
<Icon
icon=icondata::AiSearchOutlined
style="font-size: 18px; color: var(--thaw-placeholder-color);"
/>
</AutoCompletePrefix>
</AutoComplete>
<Popover placement=PopoverPlacement::BottomEnd class="demo-header__menu-popover-mobile">
<PopoverTrigger slot class="demo-header__menu-mobile">
<Button
variant=ButtonVariant::Text
<Menu
position=MenuPosition::BottomEnd
on_select=move |value : String| match value.as_str() {
"Dark" => change_theme(MouseEvent::new("click").unwrap()),
"Light" => change_theme(MouseEvent::new("click").unwrap()),
"github" => { _ = window().open_with_url("http://github.com/thaw-ui/thaw"); },
"discord" => { _ = window().open_with_url("https://discord.com/channels/1031524867910148188/1270735289437913108"); },
_ => navigate_signal.get()(&value, Default::default())
}
>
<MenuTrigger slot class="demo-header__menu-mobile">
<Button
appearance=ButtonAppearance::Subtle
icon=icondata::AiUnorderedListOutlined
style="font-size: 22px; padding: 0px 6px;"
attr:style="font-size: 22px; padding: 0px 6px;"
/>
</PopoverTrigger>
<div style="height: 70vh; overflow: auto;">
<Menu value=menu_value>
<MenuItem key=theme_name label=theme_name />
<MenuItem key="github" label="Github" />
{
use crate::pages::{gen_guide_menu_data, gen_menu_data};
vec![
gen_guide_menu_data().into_view(),
gen_menu_data().into_view(),
]
</MenuTrigger>
<MenuItem value=theme_name>{theme_name}</MenuItem>
<MenuItem icon=icondata::AiGithubOutlined value="github">"Github"</MenuItem>
<MenuItem icon=icondata::BiDiscordAlt value="discord">"Discord"</MenuItem>
{
use crate::pages::{gen_nav_data, NavGroupOption, NavItemOption};
gen_nav_data().into_iter().map(|data| {
let NavGroupOption { label, children } = data;
view! {
<Caption1Strong style="margin-inline-start: 10px; margin-top: 10px; display: block">
{label}
</Caption1Strong>
{
children.into_iter().map(|item| {
let NavItemOption { label, value } = item;
view! {
<MenuItem value=value>{label}</MenuItem>
}
}).collect_view()
}
}
</Menu>
</div>
</Popover>
}).collect_view()
}
</Menu>
<Space class="demo-header__right-btn" align=SpaceAlign::Center>
<SwitchVersion/>
<Button
variant=ButtonVariant::Text
on_click=move |_| {
let navigate = use_navigate();
navigate("/guide/installation", Default::default());
}
icon=Memo::new(move |_| {
theme.with(|theme| {
if theme.name == "light" {
icondata::BiMoonRegular
} else {
icondata::BiSunRegular
}
})
})
on_click=change_theme
>
"Guide"
</Button>
<Button
variant=ButtonVariant::Text
on_click=move |_| {
let navigate = use_navigate();
navigate("/components/button", Default::default());
}
>
"Components"
</Button>
<Button variant=ButtonVariant::Text on_click=Callback::new(move |_| change_theme.call(()))>
{move || theme_name.get()}
</Button>
<SwitchVersion />
<Button
variant=ButtonVariant::Text
icon=icondata::BiDiscordAlt
on_click=move |_| {
_ = window().open_with_url("https://discord.com/channels/1031524867910148188/1270735289437913108");
}
/>
<Button
icon=icondata::AiGithubOutlined
round=true
style="font-size: 22px; padding: 0px 6px;"
on_click=move |_| {
_ = window().open_with_url("http://github.com/thaw-ui/thaw");
}
/>
</Space>
</Space>
@ -209,63 +239,62 @@ pub fn SiteHeader() -> impl IntoView {
}
}
#[derive(Clone, PartialEq)]
struct AutoCompleteOption {
pub label: String,
pub value: String,
}
fn gen_search_all_options() -> Vec<AutoCompleteOption> {
use crate::pages::{gen_guide_menu_data, gen_menu_data};
let mut options: Vec<_> = gen_menu_data()
crate::pages::gen_nav_data()
.into_iter()
.flat_map(|group| {
group.children.into_iter().map(|item| AutoCompleteOption {
value: format!("/components/{}", item.value),
value: item.value,
label: item.label,
})
})
.collect();
options.extend(gen_guide_menu_data().into_iter().flat_map(|group| {
group.children.into_iter().map(|item| AutoCompleteOption {
value: format!("/guide/{}", item.value),
label: item.label,
})
}));
options
.collect()
}
fn use_menu_value(change_theme: Callback<()>) -> RwSignal<String> {
use crate::pages::gen_guide_menu_data;
let guide = store_value(gen_guide_menu_data());
let navigate = use_navigate();
let loaction = use_location();
// TODO
// fn use_menu_value(change_theme: Callback<()>) -> RwSignal<String> {
// use crate::pages::gen_guide_menu_data;
// let guide = store_value(gen_guide_menu_data());
// let navigate = use_navigate();
// let loaction = use_location();
let menu_value = create_rw_signal({
let mut pathname = loaction.pathname.get_untracked();
if pathname.starts_with("/components/") {
pathname.drain(12..).collect()
} else if pathname.starts_with("/guide/") {
pathname.drain(7..).collect()
} else {
String::new()
}
});
// let menu_value = create_rw_signal({
// let mut pathname = loaction.pathname.get_untracked();
// if pathname.starts_with("/components/") {
// pathname.drain(12..).collect()
// } else if pathname.starts_with("/guide/") {
// pathname.drain(7..).collect()
// } else {
// String::new()
// }
// });
_ = menu_value.watch(move |name| {
if name == "Dark" || name == "Light" {
change_theme.call(());
return;
} else if name == "github" {
_ = window().open_with_url("http://github.com/thaw-ui/thaw");
return;
}
let pathname = loaction.pathname.get_untracked();
if guide.with_value(|menu| {
menu.iter()
.any(|group| group.children.iter().any(|item| &item.value == name))
}) {
if !pathname.eq(&format!("/guide/{name}")) {
navigate(&format!("/guide/{name}"), Default::default());
}
} else if !pathname.eq(&format!("/components/{name}")) {
navigate(&format!("/components/{name}"), Default::default());
}
});
// _ = menu_value.watch(move |name| {
// if name == "Dark" || name == "Light" {
// change_theme.call(());
// return;
// } else if name == "github" {
// _ = window().open_with_url("http://github.com/thaw-ui/thaw");
// return;
// }
// let pathname = loaction.pathname.get_untracked();
// if guide.with_value(|menu| {
// menu.iter()
// .any(|group| group.children.iter().any(|item| &item.value == name))
// }) {
// if !pathname.eq(&format!("/guide/{name}")) {
// navigate(&format!("/guide/{name}"), Default::default());
// }
// } else if !pathname.eq(&format!("/components/{name}")) {
// navigate(&format!("/components/{name}"), Default::default());
// }
// });
menu_value
}
// menu_value
// }

View file

@ -1,16 +1,13 @@
use leptos::*;
use leptos::prelude::*;
use thaw::*;
#[component]
pub fn SwitchVersion() -> impl IntoView {
let options = vec![
SelectOption::new("main", "https://thawui.vercel.app".into()),
SelectOption::new("0.3.3", "https://thaw-npgo2zdtv-thaw.vercel.app".into()),
SelectOption::new("0.3.2", "https://thaw-czldv7au5-thaw.vercel.app".into()),
SelectOption::new("0.3.1", "https://thaw-bwh2r7eok-thaw.vercel.app".into()),
SelectOption::new("0.3.0", "https://thaw-gxcwse9r5-thaw.vercel.app".into()),
SelectOption::new("0.2.6", "https://thaw-mzh1656cm-thaw.vercel.app".into()),
SelectOption::new("0.2.5", "https://thaw-8og1kv8zs-thaw.vercel.app".into()),
("main", "https://thawui.vercel.app"),
("0.3.0", "https://thaw-gxcwse9r5-thaw.vercel.app"),
("0.2.6", "https://thaw-mzh1656cm-thaw.vercel.app"),
("0.2.5", "https://thaw-8og1kv8zs-thaw.vercel.app"),
];
cfg_if::cfg_if! {
@ -26,11 +23,17 @@ pub fn SwitchVersion() -> impl IntoView {
}
});
} else {
let version = RwSignal::new(None::<String>);
// let version = RwSignal::new(None::<String>);
}
}
view! {
<Select value=version options/>
<Combobox>
{
options.into_iter().map(|option| view! {
<ComboboxOption value=option.1 text=option.0 />
}).collect_view()
}
</Combobox>
}
}

View file

@ -3,10 +3,11 @@ mod components;
mod pages;
use app::App;
use leptos::*;
use leptos::prelude::*;
fn main() {
#[cfg(feature = "tracing")]
leptos_devtools::devtools!();
let _ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(App)
}

View file

@ -1,7 +1,10 @@
use crate::components::SiteHeader;
use leptos::*;
use leptos::prelude::*;
use leptos_meta::Style;
use leptos_router::{use_location, use_navigate, Outlet};
use leptos_router::{
components::Outlet,
hooks::{use_location, use_navigate},
};
use thaw::*;
#[component]
@ -9,21 +12,15 @@ pub fn ComponentsPage() -> impl IntoView {
let navigate = use_navigate();
let loaction = use_location();
let select_name = create_rw_signal(String::new());
create_effect(move |_| {
let mut pathname = loaction.pathname.get();
let name = if pathname.starts_with("/components/") {
pathname.drain(12..).collect()
} else {
String::new()
};
select_name.set(name);
let select_name = RwSignal::new(String::new());
Effect::new(move |_| {
select_name.set(loaction.pathname.get());
});
_ = select_name.watch(move |name| {
let pathname = loaction.pathname.get_untracked();
if !pathname.eq(&format!("/components/{name}")) {
navigate(&format!("/components/{name}"), Default::default());
if &pathname != name {
navigate(name, Default::default());
}
});
view! {
@ -57,15 +54,30 @@ pub fn ComponentsPage() -> impl IntoView {
</Style>
<Layout position=LayoutPosition::Absolute>
<SiteHeader/>
<Layout has_sider=true position=LayoutPosition::Absolute style="top: 64px;">
<LayoutSider class="demo-components__sider">
<Menu value=select_name>
{gen_menu_data().into_view()}
</Menu>
</LayoutSider>
<Layout content_style="padding: 8px 12px 28px; display: flex;" class="doc-content">
<Layout has_sider=true position=LayoutPosition::Absolute attr:style="top: 64px;">
<div class="demo-components__sider">
<NavDrawer selected_value=select_name>
{
gen_nav_data().into_iter().map(|data| {
let NavGroupOption { label, children } = data;
view! {
<Caption1Strong style="margin-inline-start: 10px; margin-top: 10px; display: inline-block">
{label}
</Caption1Strong>
{
children.into_iter().map(|item| {
let NavItemOption { label, value } = item;
view! {
<NavItem value>{label}</NavItem>
}
}).collect_view()
}
}
}).collect_view()
}
</NavDrawer>
</div>
<Layout content_style="padding: 8px 12px 28px; display: flex;" attr:class=("doc-content", true)>
<Outlet/>
</Layout>
</Layout>
@ -73,271 +85,227 @@ pub fn ComponentsPage() -> impl IntoView {
}
}
pub(crate) struct MenuGroupOption {
pub(crate) struct NavGroupOption {
pub label: String,
pub children: Vec<MenuItemOption>,
pub children: Vec<NavItemOption>,
}
impl IntoView for MenuGroupOption {
fn into_view(self) -> View {
let Self { label, children } = self;
view! {
<MenuGroup label=format!(
"{label} ({})", children.len()
)>
{children.into_iter().map(|v| v.into_view()).collect_view()}
</MenuGroup>
}
}
}
pub(crate) struct MenuItemOption {
pub(crate) struct NavItemOption {
pub label: String,
pub value: String,
}
impl IntoView for MenuItemOption {
fn into_view(self) -> View {
let Self { label, value } = self;
view! { <MenuItem key=value label/> }
}
}
pub(crate) fn gen_menu_data() -> Vec<MenuGroupOption> {
pub(crate) fn gen_nav_data() -> Vec<NavGroupOption> {
vec![
MenuGroupOption {
label: "Common Components".into(),
NavGroupOption {
label: "Getting Started".into(),
children: vec![
MenuItemOption {
value: "avatar".into(),
label: "Avatar".into(),
NavItemOption {
value: "/guide/installation".into(),
label: "Installation".into(),
},
MenuItemOption {
value: "button".into(),
label: "Button".into(),
},
MenuItemOption {
value: "card".into(),
label: "Card".into(),
},
MenuItemOption {
value: "collapse".into(),
label: "Collapse".into(),
},
MenuItemOption {
value: "divider".into(),
label: "Divider".into(),
},
MenuItemOption {
value: "dropdown".into(),
label: "Dropdown".into(),
},
MenuItemOption {
value: "icon".into(),
label: "Icon".into(),
},
MenuItemOption {
value: "tag".into(),
label: "Tag".into(),
},
MenuItemOption {
value: "typography".into(),
label: "Typography".into(),
NavItemOption {
value: "/guide/server-sider-rendering".into(),
label: "Server Sider Rendering".into(),
},
],
},
MenuGroupOption {
label: "Data Input Components".into(),
// NavGroupOption {
// label: "Development".into(),
// children: vec![
// NavItemOption {
// value: "/guide/development/components".into(),
// label: "Components".into(),
// },
// ],
// },
NavGroupOption {
label: "Components".into(),
children: vec![
MenuItemOption {
value: "auto-complete".into(),
label: "Auto Complete".into(),
NavItemOption {
value: "/components/accordion".into(),
label: "Accordion".into(),
},
MenuItemOption {
value: "color-picker".into(),
label: "Color Picker".into(),
},
MenuItemOption {
value: "checkbox".into(),
label: "Checkbox".into(),
},
MenuItemOption {
value: "date-picker".into(),
label: "Date Picker".into(),
},
MenuItemOption {
value: "input".into(),
label: "Input".into(),
},
MenuItemOption {
value: "input-number".into(),
label: "Input Number".into(),
},
MenuItemOption {
value: "radio".into(),
label: "Radio".into(),
},
MenuItemOption {
value: "select".into(),
label: "Select".into(),
},
MenuItemOption {
value: "slider".into(),
label: "Slider".into(),
},
MenuItemOption {
value: "switch".into(),
label: "Switch".into(),
},
MenuItemOption {
value: "time-picker".into(),
label: "Time Picker".into(),
},
MenuItemOption {
value: "upload".into(),
label: "Upload".into(),
},
],
},
MenuGroupOption {
label: "Data Display Components".into(),
children: vec![
MenuItemOption {
value: "calendar".into(),
label: "Calendar".into(),
},
MenuItemOption {
value: "image".into(),
label: "Image".into(),
},
MenuItemOption {
value: "table".into(),
label: "Table".into(),
},
],
},
MenuGroupOption {
label: "Navigation Components".into(),
children: vec![
MenuItemOption {
value: "anchor".into(),
NavItemOption {
value: "/components/anchor".into(),
label: "Anchor".into(),
},
MenuItemOption {
value: "back-top".into(),
NavItemOption {
value: "/components/auto-complete".into(),
label: "Auto Complete".into(),
},
NavItemOption {
value: "/components/avatar".into(),
label: "Avatar".into(),
},
NavItemOption {
value: "/components/back-top".into(),
label: "Back Top".into(),
},
MenuItemOption {
value: "breadcrumb".into(),
label: "Breadcrumb".into(),
},
MenuItemOption {
value: "loading-bar".into(),
label: "Loading Bar".into(),
},
MenuItemOption {
value: "menu".into(),
label: "Menu".into(),
},
MenuItemOption {
value: "pagination".into(),
label: "Pagination".into(),
},
MenuItemOption {
value: "tabs".into(),
label: "Tabs".into(),
},
],
},
MenuGroupOption {
label: "Feedback Components".into(),
children: vec![
MenuItemOption {
value: "alert".into(),
label: "Alert".into(),
},
MenuItemOption {
value: "badge".into(),
NavItemOption {
value: "/components/badge".into(),
label: "Badge".into(),
},
MenuItemOption {
value: "drawer".into(),
NavItemOption {
value: "/components/breadcrumb".into(),
label: "Breadcrumb".into(),
},
NavItemOption {
value: "/components/button".into(),
label: "Button".into(),
},
NavItemOption {
value: "/components/calendar".into(),
label: "Calendar".into(),
},
NavItemOption {
value: "/components/card".into(),
label: "Card".into(),
},
NavItemOption {
value: "/components/checkbox".into(),
label: "Checkbox".into(),
},
NavItemOption {
value: "/components/color-picker".into(),
label: "Color Picker".into(),
},
NavItemOption {
value: "/components/combobox".into(),
label: "Combobox".into(),
},
NavItemOption {
value: "/components/config-provider".into(),
label: "Config Provider".into(),
},
NavItemOption {
value: "/components/date-picker".into(),
label: "Date Picker".into(),
},
NavItemOption {
value: "/components/dialog".into(),
label: "Dialog".into(),
},
NavItemOption {
value: "/components/divider".into(),
label: "Divider".into(),
},
NavItemOption {
value: "/components/drawer".into(),
label: "Drawer".into(),
},
MenuItemOption {
value: "message".into(),
label: "Message".into(),
},
MenuItemOption {
value: "modal".into(),
label: "Modal".into(),
},
MenuItemOption {
value: "popover".into(),
label: "Popover".into(),
},
MenuItemOption {
value: "progress".into(),
label: "Progress".into(),
},
MenuItemOption {
value: "spinner".into(),
label: "Spinner".into(),
},
MenuItemOption {
value: "skeleton".into(),
label: "Skeleton".into(),
},
],
},
MenuGroupOption {
label: "Layout Components".into(),
children: vec![
MenuItemOption {
value: "layout".into(),
label: "Layout".into(),
},
MenuItemOption {
value: "grid".into(),
NavItemOption {
value: "/components/grid".into(),
label: "Grid".into(),
},
MenuItemOption {
value: "space".into(),
NavItemOption {
value: "/components/icon".into(),
label: "Icon".into(),
},
NavItemOption {
value: "/components/image".into(),
label: "Image".into(),
},
NavItemOption {
value: "/components/input".into(),
label: "Input".into(),
},
NavItemOption {
value: "/components/layout".into(),
label: "Layout".into(),
},
NavItemOption {
value: "/components/loading-bar".into(),
label: "Loading Bar".into(),
},
NavItemOption {
value: "/components/menu".into(),
label: "Menu".into(),
},
NavItemOption {
value: "/components/message-bar".into(),
label: "Message Bar".into(),
},
NavItemOption {
value: "/components/nav".into(),
label: "Nav".into(),
},
NavItemOption {
value: "/components/pagination".into(),
label: "Pagination".into(),
},
NavItemOption {
value: "/components/popover".into(),
label: "Popover".into(),
},
NavItemOption {
value: "/components/progress-bar".into(),
label: "ProgressBar".into(),
},
NavItemOption {
value: "/components/radio".into(),
label: "Radio".into(),
},
NavItemOption {
value: "/components/scrollbar".into(),
label: "Scrollbar".into(),
},
NavItemOption {
value: "/components/skeleton".into(),
label: "Skeleton".into(),
},
NavItemOption {
value: "/components/slider".into(),
label: "Slider".into(),
},
NavItemOption {
value: "/components/space".into(),
label: "Space".into(),
},
],
},
MenuGroupOption {
label: "Utility Components".into(),
children: vec![MenuItemOption {
value: "scrollbar".into(),
label: "Scrollbar".into(),
}],
},
MenuGroupOption {
label: "Config Components".into(),
children: vec![MenuItemOption {
value: "theme".into(),
label: "Theme".into(),
}],
},
MenuGroupOption {
label: "Mobile Components".into(),
children: vec![
MenuItemOption {
value: "nav-bar".into(),
label: "Nav Bar".into(),
NavItemOption {
value: "/components/spin-button".into(),
label: "Spin Button".into(),
},
MenuItemOption {
value: "tabbar".into(),
label: "Tabbar".into(),
NavItemOption {
value: "/components/spinner".into(),
label: "Spinner".into(),
},
MenuItemOption {
value: "toast".into(),
NavItemOption {
value: "/components/switch".into(),
label: "Switch".into(),
},
NavItemOption {
value: "/components/tab-list".into(),
label: "Tab List".into(),
},
NavItemOption {
value: "/components/table".into(),
label: "Table".into(),
},
NavItemOption {
value: "/components/tag".into(),
label: "Tag".into(),
},
NavItemOption {
value: "/components/text".into(),
label: "Text".into(),
},
NavItemOption {
value: "/components/textarea".into(),
label: "Textarea".into(),
},
NavItemOption {
value: "/components/time-picker".into(),
label: "Time Picker".into(),
},
NavItemOption {
value: "/components/toast".into(),
label: "Toast".into(),
},
NavItemOption {
value: "/components/upload".into(),
label: "Upload".into(),
},
],
},
]

View file

@ -1,141 +0,0 @@
use crate::components::SiteHeader;
use leptos::*;
use leptos_meta::Style;
use leptos_router::{use_location, use_navigate, Outlet};
use thaw::*;
#[component]
pub fn GuidePage() -> impl IntoView {
let navigate = use_navigate();
let selected = create_rw_signal({
let loaction = use_location();
let mut pathname = loaction.pathname.get_untracked();
if pathname.starts_with("/guide/") {
pathname.drain(7..).collect()
} else {
String::new()
}
});
create_effect(move |value| {
let selected = selected.get();
if value.is_some() {
navigate(&format!("/guide/{selected}"), Default::default());
}
selected
});
view! {
<Style>
"
.demo-components__component {
width: 896px;
margin: 0 auto;
}
.demo-components__toc {
width: 190px;
margin: 12px 2px 12px 12px;
}
.demo-components__toc > .thaw-anchor {
position: sticky;
top: 36px;
}
.demo-md-table-box {
overflow: auto;
}
@media screen and (max-width: 1200px) {
.demo-components__toc,
.demo-guide__sider {
display: none;
}
.demo-components__component {
width: 100%;
}
}
"
</Style>
<Layout position=LayoutPosition::Absolute>
<SiteHeader/>
<Layout has_sider=true position=LayoutPosition::Absolute style="top: 64px;">
<LayoutSider class="demo-guide__sider">
<Menu value=selected>
{gen_guide_menu_data().into_view()}
</Menu>
</LayoutSider>
<Layout content_style="padding: 8px 12px 28px; display: flex;" class="doc-content">
<Outlet/>
</Layout>
</Layout>
</Layout>
}
}
pub(crate) struct MenuGroupOption {
pub label: String,
pub children: Vec<MenuItemOption>,
}
impl IntoView for MenuGroupOption {
fn into_view(self) -> View {
let Self { label, children } = self;
view! {
<MenuGroup label=label>
{children.into_iter().map(|v| v.into_view()).collect_view()}
</MenuGroup>
}
}
}
pub(crate) struct MenuItemOption {
pub label: String,
pub value: String,
}
impl IntoView for MenuItemOption {
fn into_view(self) -> View {
let Self { label, value } = self;
view! { <MenuItem key=value label/> }
}
}
pub(crate) fn gen_guide_menu_data() -> Vec<MenuGroupOption> {
vec![
MenuGroupOption {
label: "Getting Started".into(),
children: vec![
MenuItemOption {
value: "installation".into(),
label: "Installation".into(),
},
MenuItemOption {
value: "usage".into(),
label: "Usage".into(),
},
],
},
MenuGroupOption {
label: "Guides".into(),
children: vec![MenuItemOption {
value: "server-sider-rendering".into(),
label: "Server Sider Rendering".into(),
}],
},
MenuGroupOption {
label: "Development".into(),
children: vec![
MenuItemOption {
value: "development/guide".into(),
label: "Guide".into(),
},
MenuItemOption {
value: "development/components".into(),
label: "Components".into(),
},
],
},
]
}

View file

@ -1,14 +1,15 @@
use leptos::*;
use leptos_router::{use_navigate, use_query_map};
use leptos::prelude::*;
use leptos_router::hooks::{use_navigate, use_query_map};
use thaw::*;
#[component]
pub fn Home() -> impl IntoView {
let query_map = use_query_map().get_untracked();
let navigate = use_navigate();
// mobile page
if let Some(path) = query_map.get("path") {
let navigate = use_navigate();
navigate(path, Default::default());
navigate(&path, Default::default());
}
view! {
<Layout
@ -18,12 +19,16 @@ pub fn Home() -> impl IntoView {
<h1 style="font-size: 80px; line-height: 1;margin: 0 0 18px;">"Thaw UI"</h1>
<p>"An easy to use leptos component library"</p>
<Space>
<Button on_click=move |_| {
let navigate = use_navigate();
navigate("/components/button", Default::default());
}>"Read the docs"</Button>
<Button
variant=ButtonVariant::Text
appearance=ButtonAppearance::Primary
on_click=move |_| {
navigate("/components/button", Default::default());
}
>
"Read the docs"
</Button>
<Button
appearance=ButtonAppearance::Subtle
on_click=move |_| {
_ = window().open_with_url("http://github.com/thaw-ui/thaw");
}

View file

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

View file

@ -1,17 +1,15 @@
mod components;
mod guide;
mod home;
mod markdown;
mod mobile;
mod nav_bar;
mod tabbar;
mod toast;
// mod mobile;
// mod nav_bar;
// mod tabbar;
// mod toast;
pub use components::*;
pub use guide::*;
pub use home::*;
pub use markdown::*;
pub use mobile::*;
pub use nav_bar::*;
pub use tabbar::*;
pub use toast::*;
// pub use mobile::*;
// pub use nav_bar::*;
// pub use tabbar::*;
// pub use toast::*;

View file

@ -4,6 +4,7 @@ use leptos::*;
use thaw::mobile::{NavBar, NavBarRight};
use thaw::Icon;
#[component]
pub fn NavBarPage() -> impl IntoView {
view! {

View file

@ -1,16 +0,0 @@
# Development guide
### Code style
It is recommended to use the Rust style instead of the functional style in the newly added reactive code.
```rust
RwSignal::new(12) // ✅
create_rw_signal(12) // 🙅
Memo::new(|_| {}) // ✅
create_memo(|_| {}) // 🙅
Effect::new(|_| {}) // ✅
create_effect(|_| {}) // 🙅
```

View file

@ -1,7 +1,40 @@
# Installation
## Installation
Installation thaw
```shell
cargo add thaw --features=csr
```
## Usage
You just need to import thaw and use it.
```rust
// Import all
use thaw::*;
// Import on Demand
use thaw::{Button, ButtonAppearance};
```
A small example:
```rust
use leptos::prelude::*;
use thaw::*;
fn main() {
mount_to_body(App);
}
#[component]
pub fn App() -> impl IntoView {
view! {
<ConfigProvider>
<Button appearance=ButtonAppearance::Primary>
"Primary"
</Button>
</ConfigProvider>
}
}
```

View file

@ -12,7 +12,9 @@ To enable the hydrate mode, the following configurations are required:
thaw = { ..., features = ["hydrate"] }
```
Remember to add thaw to your `Cargo.toml` file in the corresponding feature, e.g.
### cargo-leptos
if you use [cargo-leptos](https://github.com/leptos-rs/cargo-leptos), Remember to add thaw to your `Cargo.toml` file in the corresponding feature, e.g.
```toml
[features]

View file

@ -1,28 +0,0 @@
# Usage
You just need to import thaw and use it.
```rust
// Import all
use thaw::*;
// Import on Demand
use thaw::{Button, ButtonVariant};
```
A small example:
```rust
use leptos::*;
use thaw::*;
fn main() {
mount_to_body(App);
}
#[component]
pub fn App() -> impl IntoView {
view! {
<Button variant=ButtonVariant::Primary>"Primary"</Button>
}
}
```

View file

@ -1,26 +0,0 @@
# Alert
```rust demo
view! {
<Space vertical=true>
<Alert variant=AlertVariant::Success title="title">
"success"
</Alert>
<Alert variant=AlertVariant::Warning title="title">
"warning"
</Alert>
<Alert variant=AlertVariant::Error title="title">
"error"
</Alert>
</Space>
}
```
### Alert Props
| Name | Type | Default | Description |
| -------- | ----------------------------------- | -------------------- | ----------------------------------------- |
| class | `OptionalProp<MaybeSignal<String>>` | `Default::default()` | Additional classes for the alert element. |
| title | `Option<MaybeSignal<String>>` | `Default::default()` | Title of the alert. |
| variant | `MaybeSignal<AlertVariant>` | | Alert variant. |
| children | `Children` | | The content of the alert. |

View file

@ -1,23 +1,30 @@
# Auto Complete
```rust demo
let value = create_rw_signal(String::new());
let options = create_memo(move |_| {
let value = RwSignal::new(String::new());
let options = Memo::<Vec<_>>::new(move |_| {
let prefix = value
.get()
.split_once('@')
.map_or(value.get(), |v| v.0.to_string());
vec!["@gmail.com", "@163.com"]
.into_iter()
.map(|suffix| AutoCompleteOption {
label: format!("{prefix}{suffix}"),
value: format!("{prefix}{suffix}"),
})
.map(|suffix| (format!("{prefix}{suffix}"), format!("{prefix}{suffix}")))
.collect()
});
view! {
<AutoComplete value options placeholder="Email"/>
<AutoComplete value placeholder="Email">
<For
each=move || options.get()
key=|option| option.0.clone()
let:option
>
<AutoCompleteOption value=option.0>
{option.1}
</AutoCompleteOption>
</For>
</AutoComplete>
}
```
@ -29,14 +36,6 @@ view! {
}
```
### Invalid
```rust demo
view! {
<AutoComplete placeholder="Email" invalid=true/>
}
```
### Prefix & Suffix
```rust demo

View file

@ -1,11 +1,54 @@
# Avatar
```rust demo
view! {
<Avatar />
}
```
### Name
```rust demo
view! {
<Avatar name="Ashley McCarthy" />
}
```
### Image
```rust demo
view! {
<Avatar src="https://s3.bmp.ovh/imgs/2021/10/723d457d627fe706.jpg" />
}
```
### Shape
```rust demo
view! {
<Avatar shape=AvatarShape::Square />
}
```
### Size
```rust demo
view! {
<Space>
<Avatar src="https://s3.bmp.ovh/imgs/2021/10/723d457d627fe706.jpg"/>
<Avatar src="https://s3.bmp.ovh/imgs/2021/10/723d457d627fe706.jpg" round=true/>
<Avatar src="https://s3.bmp.ovh/imgs/2021/10/723d457d627fe706.jpg" size=50/>
<Avatar initials="16" size=16 />
<Avatar initials="20" size=20 />
<Avatar initials="24" size=24 />
<Avatar initials="28" size=28 />
<Avatar initials="32" size=32 />
<Avatar initials="36" size=36 />
<Avatar initials="40" size=40 />
<Avatar initials="48" size=48 />
<Avatar initials="56" size=56 />
<Avatar initials="64" size=64 />
<Avatar initials="72" size=72 />
<Avatar initials="96" size=96 />
<Avatar initials="120" size=120 />
<Avatar initials="128" size=128 />
</Space>
}
```

View file

@ -1,33 +1,92 @@
# Badge
```rust demo
let value = create_rw_signal(0);
view! {
<Badge />
}
```
### Appearance
```rust demo
view! {
<Space>
<Badge value max=10>
<Avatar/>
</Badge>
<Badge variant=BadgeVariant::Success value max=10>
<Avatar/>
</Badge>
<Badge variant=BadgeVariant::Warning value max=10>
<Avatar/>
</Badge>
<Badge variant=BadgeVariant::Warning dot=true>
<Avatar/>
</Badge>
<Button on_click=move |_| value.update(|v| *v += 1)>"+1"</Button>
<Button on_click=move |_| {
value
.update(|v| {
if *v != 0 {
*v -= 1;
}
})
}>"-1"</Button>
"value:"
{move || value.get()}
<Badge appearance=BadgeAppearance::Filled>"999+"</Badge>
<Badge appearance=BadgeAppearance::Ghost>"999+"</Badge>
<Badge appearance=BadgeAppearance::Outline>"999+"</Badge>
<Badge appearance=BadgeAppearance::Tint>"999+"</Badge>
</Space>
}
```
### Sizes
```rust demo
view! {
<Space>
<Badge size=BadgeSize::Tiny/>
<Badge size=BadgeSize::ExtraSmall/>
<Badge size=BadgeSize::Small/>
<Badge size=BadgeSize::Medium/>
<Badge size=BadgeSize::Large/>
<Badge size=BadgeSize::ExtraLarge/>
</Space>
}
```
### Color
```rust demo
view! {
<Space vertical=true>
<Space>
<Badge appearance=BadgeAppearance::Filled color=BadgeColor::Brand>"999+"</Badge>
<Badge appearance=BadgeAppearance::Ghost color=BadgeColor::Brand>"999+"</Badge>
<Badge appearance=BadgeAppearance::Outline color=BadgeColor::Brand>"999+"</Badge>
<Badge appearance=BadgeAppearance::Tint color=BadgeColor::Brand>"999+"</Badge>
</Space>
<Space>
<Badge appearance=BadgeAppearance::Filled color=BadgeColor::Danger>"999+"</Badge>
<Badge appearance=BadgeAppearance::Ghost color=BadgeColor::Danger>"999+"</Badge>
<Badge appearance=BadgeAppearance::Outline color=BadgeColor::Danger>"999+"</Badge>
<Badge appearance=BadgeAppearance::Tint color=BadgeColor::Danger>"999+"</Badge>
</Space>
<Space>
<Badge appearance=BadgeAppearance::Filled color=BadgeColor::Important>"999+"</Badge>
<Badge appearance=BadgeAppearance::Ghost color=BadgeColor::Important>"999+"</Badge>
<Badge appearance=BadgeAppearance::Outline color=BadgeColor::Important>"999+"</Badge>
<Badge appearance=BadgeAppearance::Tint color=BadgeColor::Important>"999+"</Badge>
</Space>
<Space>
<Badge appearance=BadgeAppearance::Filled color=BadgeColor::Informative>"999+"</Badge>
<Badge appearance=BadgeAppearance::Ghost color=BadgeColor::Informative>"999+"</Badge>
<Badge appearance=BadgeAppearance::Outline color=BadgeColor::Informative>"999+"</Badge>
<Badge appearance=BadgeAppearance::Tint color=BadgeColor::Informative>"999+"</Badge>
</Space>
<Space>
<Badge appearance=BadgeAppearance::Filled color=BadgeColor::Severe>"999+"</Badge>
<Badge appearance=BadgeAppearance::Ghost color=BadgeColor::Severe>"999+"</Badge>
<Badge appearance=BadgeAppearance::Outline color=BadgeColor::Severe>"999+"</Badge>
<Badge appearance=BadgeAppearance::Tint color=BadgeColor::Severe>"999+"</Badge>
</Space>
<Space>
<Badge appearance=BadgeAppearance::Filled color=BadgeColor::Subtle>"999+"</Badge>
<Badge appearance=BadgeAppearance::Ghost color=BadgeColor::Subtle>"999+"</Badge>
<Badge appearance=BadgeAppearance::Outline color=BadgeColor::Subtle>"999+"</Badge>
<Badge appearance=BadgeAppearance::Tint color=BadgeColor::Subtle>"999+"</Badge>
</Space>
<Space>
<Badge appearance=BadgeAppearance::Filled color=BadgeColor::Success>"999+"</Badge>
<Badge appearance=BadgeAppearance::Ghost color=BadgeColor::Success>"999+"</Badge>
<Badge appearance=BadgeAppearance::Outline color=BadgeColor::Success>"999+"</Badge>
<Badge appearance=BadgeAppearance::Tint color=BadgeColor::Success>"999+"</Badge>
</Space>
<Space>
<Badge appearance=BadgeAppearance::Filled color=BadgeColor::Warning>"999+"</Badge>
<Badge appearance=BadgeAppearance::Ghost color=BadgeColor::Warning>"999+"</Badge>
<Badge appearance=BadgeAppearance::Outline color=BadgeColor::Warning>"999+"</Badge>
<Badge appearance=BadgeAppearance::Tint color=BadgeColor::Warning>"999+"</Badge>
</Space>
</Space>
}
```

View file

@ -3,21 +3,17 @@
```rust demo
view! {
<Breadcrumb>
<BreadcrumbItem>"Leptos"</BreadcrumbItem>
<BreadcrumbItem>"UI"</BreadcrumbItem>
<BreadcrumbItem>"Thaw"</BreadcrumbItem>
</Breadcrumb>
}
```
### Separator
```rust demo
view! {
<Breadcrumb separator=">">
<BreadcrumbItem>"Leptos"</BreadcrumbItem>
<BreadcrumbItem>"UI"</BreadcrumbItem>
<BreadcrumbItem>"Thaw"</BreadcrumbItem>
<BreadcrumbItem>
<BreadcrumbButton>"Leptos"</BreadcrumbButton>
</BreadcrumbItem>
<BreadcrumbDivider />
<BreadcrumbItem>
<BreadcrumbButton>"UI"</BreadcrumbButton>
</BreadcrumbItem>
<BreadcrumbDivider />
<BreadcrumbItem>
<BreadcrumbButton current=true>"Thaw"</BreadcrumbButton>
</BreadcrumbItem>
</Breadcrumb>
}
```

View file

@ -3,23 +3,22 @@
```rust demo
view! {
<Space>
<Button variant=ButtonVariant::Primary>"Primary"</Button>
<Button variant=ButtonVariant::Outlined>"Outlined"</Button>
<Button variant=ButtonVariant::Text>"Text"</Button>
<Button variant=ButtonVariant::Link>"Link"</Button>
<Button>"Secondary"</Button>
<Button appearance=ButtonAppearance::Primary>"Primary"</Button>
<Button appearance=ButtonAppearance::Subtle>"Subtle"</Button>
<Button appearance=ButtonAppearance::Transparent>"Transparent"</Button>
</Space>
}
```
### Color
### Shape
```rust demo
view! {
<Space>
<Button color=ButtonColor::Primary>"Primary Color"</Button>
<Button color=ButtonColor::Success>"Success Color"</Button>
<Button color=ButtonColor::Warning>"Warning Color"</Button>
<Button color=ButtonColor::Error>"Error Color"</Button>
<Button>"Rounded"</Button>
<Button shape=ButtonShape::Circular>"Circular"</Button>
<Button shape=ButtonShape::Square>"Square"</Button>
</Space>
}
```
@ -27,7 +26,7 @@ view! {
### Icon
```rust demo
let icon = create_rw_signal(Some(icondata::AiCloseOutlined));
let icon = RwSignal::new(Some(icondata::AiCloseOutlined));
let on_click = move |_| {
icon.update(|icon| {
@ -55,83 +54,82 @@ view! {
</Button>
</Space>
<Space>
<Button color=ButtonColor::Error icon=icondata::AiCloseOutlined>
<Button icon=icondata::AiCloseOutlined>
"Error Color Icon"
</Button>
<Button color=ButtonColor::Error icon=icondata::AiCloseOutlined>
"Error Color Icon"
<Button icon=icondata::AiCloseOutlined>
</Button>
<Button
color=ButtonColor::Error
icon=icondata::AiCloseOutlined
round=true
/>
<Button
color=ButtonColor::Error
icon=icondata::AiCloseOutlined
circle=true
/>
</Space>
</Space>
}
```
### Loading
```rust demo
let loading = create_rw_signal(false);
let on_click = move |_| {
loading.set(true);
set_timeout(
move || {
loading.set(false);
},
std::time::Duration::new(2, 0),
);
};
view! {
<Space>
<Button loading on_click icon=icondata::AiCloseOutlined>
"Click Me"
</Button>
<Button loading on_click>
"Click Me"
</Button>
</Space>
}
```
### Disabled
```rust demo
view! {
<Space>
<Button variant=ButtonVariant::Primary disabled=true>
"Primary"
</Button>
<Button variant=ButtonVariant::Outlined disabled=true>
"Outlined"
</Button>
<Button variant=ButtonVariant::Text disabled=true>
"Text"
</Button>
<Button variant=ButtonVariant::Link disabled=true>
"Link"
</Button>
</Space>
}
```
### Size
```rust demo
view! {
<Space>
<Button size=ButtonSize::Tiny>"Primary"</Button>
<Button size=ButtonSize::Small>"Primary"</Button>
<Button size=ButtonSize::Medium>"Primary"</Button>
<Button size=ButtonSize::Large>"Primary"</Button>
<Space vertical=true>
<Space>
<Button size=ButtonSize::Small>"Small"</Button>
<Button size=ButtonSize::Small icon=icondata::AiCloseOutlined>
"Small with calendar icon"
</Button>
<Button size=ButtonSize::Small icon=icondata::AiCloseOutlined>
</Button>
</Space>
<Space>
<Button>"Medium"</Button>
<Button icon=icondata::AiCloseOutlined>
"Medium with calendar icon"
</Button>
<Button icon=icondata::AiCloseOutlined>
</Button>
</Space>
<Space>
<Button size=ButtonSize::Large>"Large"</Button>
<Button size=ButtonSize::Large icon=icondata::AiCloseOutlined>
"Large with calendar icon"
</Button>
<Button size=ButtonSize::Large icon=icondata::AiCloseOutlined>
</Button>
</Space>
</Space>
}
```
### Disabled
```rust demo
view! {
<Space vertical=true>
<Space>
<Button disabled=true>
"Secondary"
</Button>
<Button appearance=ButtonAppearance::Primary disabled=true>
"Primary"
</Button>
<Button appearance=ButtonAppearance::Subtle disabled=true>
"Subtle"
</Button>
<Button appearance=ButtonAppearance::Transparent disabled=true>
"Transparent"
</Button>
</Space>
<Space>
<Button disabled_focusable=true>
"Secondary"
</Button>
<Button appearance=ButtonAppearance::Primary disabled_focusable=true>
"Primary"
</Button>
<Button appearance=ButtonAppearance::Subtle disabled_focusable=true>
"Subtle"
</Button>
<Button appearance=ButtonAppearance::Transparent disabled_focusable=true>
"Transparent"
</Button>
</Space>
</Space>
}
```
@ -140,9 +138,7 @@ view! {
```rust demo
view! {
<Space vertical=true>
<Button block=true>"Primary"</Button>
</Space>
<Button block=true>"Primary"</Button>
}
```
@ -152,14 +148,14 @@ view! {
view! {
<Space>
<ButtonGroup>
<Button variant=ButtonVariant::Outlined>"Outlined"</Button>
<Button variant=ButtonVariant::Outlined>"Outlined"</Button>
<Button variant=ButtonVariant::Outlined>"Outlined"</Button>
<Button>"Outlined"</Button>
<Button>"Outlined"</Button>
<Button>"Outlined"</Button>
</ButtonGroup>
<ButtonGroup vertical=true>
<Button variant=ButtonVariant::Outlined>"Outlined"</Button>
<Button variant=ButtonVariant::Outlined>"Outlined"</Button>
<Button variant=ButtonVariant::Outlined>"Outlined"</Button>
<Button>"Outlined"</Button>
<Button>"Outlined"</Button>
<Button>"Outlined"</Button>
</ButtonGroup>
</Space>
}
@ -171,7 +167,7 @@ view! {
| --- | --- | --- | --- |
| class | `OptionalProp<MaybeSignal<String>>` | `Default::default()` | Additional classes for the button element. |
| style | `Option<MaybeSignal<String>>` | `Default::default()` | Button's style. |
| variant | `MaybeSignal<ButtonVariant>` | `ButtonVariant::Primary` | Button's variant. |
| appearance | `MaybeSignal<ButtonAppearance>` | `ButtonAppearance::Primary` | Button's variant. |
| color | `MaybeSignal<ButtonColor>` | `ButtonColor::Primary` | Button's color. |
| block | `MaybeSignal<bool>` | `false` | Whether the button is displayed as block. |
| round | `MaybeSignal<bool>` | `false` | Whether the button shows rounded corners. |

View file

@ -2,10 +2,14 @@
```rust demo
use chrono::prelude::*;
let value = create_rw_signal(Some(Local::now().date_naive()));
let value = RwSignal::new(Local::now().date_naive());
let option_value = RwSignal::new(Some(Local::now().date_naive()));
view! {
<Calendar value />
<Space vertical=true>
<Calendar value />
<Calendar value=option_value />
</Space>
}
```

View file

@ -2,22 +2,28 @@
```rust demo
view! {
<Space vertical=true>
<Card title="title">"content"</Card>
<Card title="title">
<CardHeaderExtra slot>"header-extra"</CardHeaderExtra>
"content"
</Card>
<Card title="title">
<CardHeader slot>"header"</CardHeader>
"content"
</Card>
<Card title="title">
<CardHeaderExtra slot>"header-extra"</CardHeaderExtra>
"content"
<CardFooter slot>"footer"</CardFooter>
</Card>
</Space>
<Card>
<CardHeader>
<Body1>
<b>"Header"</b>" 2022-02-22"
</Body1>
<CardHeaderDescription slot>
<Caption1>"Description"</Caption1>
</CardHeaderDescription>
<CardHeaderAction slot>
<Button appearance=ButtonAppearance::Transparent>
"..."
</Button>
</CardHeaderAction>
</CardHeader>
<CardPreview>
<img src="https://s3.bmp.ovh/imgs/2021/10/2c3b013418d55659.jpg" style="width: 100%"/>
</CardPreview>
<CardFooter>
<Button>"Reply"</Button>
<Button>"Share"</Button>
</CardFooter>
</Card>
}
```

View file

@ -1,10 +1,10 @@
# Checkbox
```rust demo
let value = create_rw_signal(false);
let checked = RwSignal::new(false);
view! {
<Checkbox value>"Click"</Checkbox>
<Checkbox checked label="Click"/>
<Checkbox />
}
```
@ -14,13 +14,13 @@ view! {
```rust demo
use std::collections::HashSet;
let value = create_rw_signal(HashSet::new());
let value = RwSignal::new(HashSet::new());
view! {
<CheckboxGroup value>
<CheckboxItem label="apple" key="a"/>
<CheckboxItem label="b" key="b"/>
<CheckboxItem label="c" key="c"/>
<Checkbox label="apple" value="a"/>
<Checkbox label="b" value="b"/>
<Checkbox label="c" value="c"/>
</CheckboxGroup>
<div style="margin-top: 1rem">"value: " {move || format!("{:?}", value.get())}</div>
}

View file

@ -1,56 +0,0 @@
# Collapse
```rust demo
use std::collections::HashSet;
let value = create_rw_signal(HashSet::from(["thaw".to_string()]));
view! {
<Collapse value>
<CollapseItem title="Leptos" key="leptos">
"Build fast web applications with Rust."
</CollapseItem>
<CollapseItem title="Thaw" key="thaw">
"An easy to use leptos component library"
</CollapseItem>
</Collapse>
}
```
### Accordion
Like an accordion.
```rust demo
view! {
<Collapse accordion=true>
<CollapseItem title="Leptos" key="leptos">
"Build fast web applications with Rust."
</CollapseItem>
<CollapseItem title="Thaw" key="thaw">
"An easy to use leptos component library."
</CollapseItem>
<CollapseItem title="Naive UI" key="naive-ui">
"A Vue 3 Component Library. Fairly Complete. Theme Customizable. Uses TypeScript. Fast."
</CollapseItem>
</Collapse>
}
```
### Collapse Props
| Name | Type | Default | Description |
| --------- | ----------------------------------- | -------------------- | ------------------------------------------- |
| class | `OptionalProp<MaybeSignal<String>>` | `Default::default()` | Addtional classes for the collapse element. |
| value | `Model<HashSet<String>>` | `Default::default()` | Currently active panel. |
| accordion | `bool` | `false` | Only allow one panel open at a time. |
| children | `Children` | | Collapse's content. |
### CollapseItem Props
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| class | `OptionalProp<MaybeSignal<String>>` | `Default::default()` | Addtional classes for the collapse item element. |
| title | `MaybeSignal<String>` | | The title of the CollapseItem. |
| key | `MaybeSignal<String>` | | The indentifier of CollapseItem. |
| chilren | `Children` | | CollapseItem's content. |

View file

@ -3,7 +3,7 @@
```rust demo
use palette::Srgb;
let value = create_rw_signal(Color::from(Srgb::new(0.0, 0.0, 0.0)));
let value = RwSignal::new(Color::from(Srgb::new(0.0, 0.0, 0.0)));
view! {
<ColorPicker value/>
@ -17,9 +17,9 @@ Encoding formats, support RGB, HSV, HSL.
```rust demo
use palette::{Hsl, Hsv, Srgb};
let rgb = create_rw_signal(Color::from(Srgb::new(0.0, 0.0, 0.0)));
let hsv = create_rw_signal(Color::from(Hsv::new(0.0, 0.0, 0.0)));
let hsl = create_rw_signal(Color::from(Hsl::new(0.0, 0.0, 0.0)));
let rgb = RwSignal::new(Color::from(Srgb::new(0.0, 0.0, 0.0)));
let hsv = RwSignal::new(Color::from(Hsv::new(0.0, 0.0, 0.0)));
let hsl = RwSignal::new(Color::from(Hsl::new(0.0, 0.0, 0.0)));
view! {
<Space vertical=true>

View file

@ -0,0 +1,48 @@
# ConfigProvider
### Theme
```rust demo
let theme = RwSignal::new(Theme::light());
view! {
<ConfigProvider theme>
<Card>
<Space>
<Button on_click=move |_| theme.set(Theme::light())>"Light"</Button>
<Button on_click=move |_| theme.set(Theme::dark())>"Dark"</Button>
</Space>
</Card>
</ConfigProvider>
}
```
### Customize Theme
```rust demo
let theme = RwSignal::new(Theme::light());
let on_customize_theme = move |_| {
theme.update(|theme| {
theme.color.color_brand_background = "#f5222d".to_string();
theme.color.color_brand_background_hover = "#ff4d4f".to_string();
theme.color.color_brand_background_pressed = "#cf1322".to_string();
});
};
view! {
<ConfigProvider theme>
<Card>
<Space>
<Button appearance=ButtonAppearance::Primary on_click=move |_| theme.set(Theme::light())>"Light"</Button>
<Button appearance=ButtonAppearance::Primary on_click=on_customize_theme>"Customize Theme"</Button>
</Space>
</Card>
</ConfigProvider>
}
```
### ConfigProvider Props
| Name | Type | Default | Description |
| ----- | ------------------------- | -------------------- | ----------- |
| theme | `Option<RwSignal<Theme>>` | `Default::default()` | Theme. |

View file

@ -2,10 +2,14 @@
```rust demo
use chrono::prelude::*;
let value = create_rw_signal(Some(Local::now().date_naive()));
let value = RwSignal::new(Local::now().date_naive());
let option_value = RwSignal::new(Some(Local::now().date_naive()));
view! {
<DatePicker value/>
<Space vertical=true>
<DatePicker value/>
<DatePicker value=option_value/>
</Space>
}
```

View file

@ -1,13 +1,23 @@
# Modal
# Dialog
```rust demo
let show = create_rw_signal(false);
let open = RwSignal::new(false);
view! {
<Button on_click=move |_| show.set(true)>"Open Modal"</Button>
<Modal title="title" show>
"hello"
</Modal>
<Button on_click=move |_| open.set(true)>"Open Dialog"</Button>
<Dialog open>
<DialogSurface>
<DialogBody>
<DialogTitle>"Dialog title"</DialogTitle>
<DialogContent>
"Dialog body"
</DialogContent>
<DialogActions>
<Button appearance=ButtonAppearance::Primary>"Do Something"</Button>
</DialogActions>
</DialogBody>
</DialogSurface>
</Dialog>
}
```
@ -15,14 +25,12 @@ view! {
| Name | Type | Default | Description |
| -------------- | --------------------- | -------------------- | ------------------------------------------- |
| class | `OptionalProp<MaybeSignal<String>>` | `Default::default()` | Addtional classes for the modal element. |
| show | `Model<bool>` | | Whether to show modal. |
| title | `MaybeSignal<String>` | `Default::default()` | Modal title. |
| width | `MaybeSignal<String>` | `600px` | Modal width. |
| z_index | `MaybeSignal<i16>` | `2000` | z-index of the modal. |
| mask_closeable | `MaybeSignal<bool>` | `true` | Whether to emit hide event when click mask. |
| close_on_esc | `bool` | `true` | Whether to close modal on Esc is pressed. |
| closable | `bool` | `true` | Whether to display the close button. |
| children | `Children` | | Modal's content. |
### Modal Slots

View file

@ -2,9 +2,29 @@
```rust demo
view! {
"top"
<Divider/>
"bottom"
<Space vertical=true>
<div style="padding: 30px 0; background-color: var(--colorNeutralBackground1);">
<Divider />
</div>
<div style="padding: 30px 0; background-color: var(--colorNeutralBackground1);">
<Divider>"Text"</Divider>
</div>
</Space>
}
```
### Vertical
```rust demo
view! {
<Space vertical=true gap=SpaceGap::Large>
<div style="height: 100px; background-color: var(--colorNeutralBackground1);">
<Divider vertical=true/>
</div>
<div style="height: 100px; background-color: var(--colorNeutralBackground1);">
<Divider vertical=true>"Text"</Divider>
</div>
</Space>
}
```

View file

@ -1,54 +1,143 @@
# Drawer
```rust demo
let show = create_rw_signal(false);
let placement = create_rw_signal(DrawerPlacement::Top);
let open = RwSignal::new(false);
let position = RwSignal::new(DrawerPosition::Top);
let open = Callback::new(move |new_placement: DrawerPlacement| {
let open_f = move |new_position: DrawerPosition| {
// Note: Since `show` changes are made in real time,
// please put it in front of `show.set(true)` when changing `placement`.
placement.set(new_placement);
show.set(true);
});
position.set(new_position);
open.set(true);
};
view! {
<ButtonGroup>
<Button on_click=Callback::new(move |_| leptos::Callable::call(&open, DrawerPlacement::Top))>"Top"</Button>
<Button on_click=Callback::new(move |_| leptos::Callable::call(&open, DrawerPlacement::Right))>"Right"</Button>
<Button on_click=Callback::new(move |_| leptos::Callable::call(&open, DrawerPlacement::Bottom))>"Bottom"</Button>
<Button on_click=Callback::new(move |_| leptos::Callable::call(&open, DrawerPlacement::Left))>"Left"</Button>
<Button on_click=move |_| open_f(DrawerPosition::Top)>"Top"</Button>
<Button on_click=move |_| open_f(DrawerPosition::Right)>"Right"</Button>
<Button on_click=move |_| open_f(DrawerPosition::Bottom)>"Bottom"</Button>
<Button on_click=move |_| open_f(DrawerPosition::Left)>"Left"</Button>
</ButtonGroup>
<Drawer title="Title" show placement>
"Hello"
</Drawer>
<OverlayDrawer open position>
<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>
}
```
### Customize display area
### Inline
```rust demo
let show = create_rw_signal(false);
let open_left = RwSignal::new(false);
let open_right = RwSignal::new(false);
let open_buttom = RwSignal::new(false);
view! {
<div style="position: relative; overflow: hidden; height: 200px; background-color: #0078ff88;">
<Button on_click=move |_| show.set(true)>"Open"</Button>
<Drawer show mount=DrawerMount::None width="50%">
"Current position"
</Drawer>
<div style="display: flex; flex-direction: column; overflow: hidden; height: 400px; background-color: #0078ff88;">
<div style="display: flex; overflow: hidden; height: 400px;">
<InlineDrawer open=open_left>
<DrawerHeader>
<DrawerHeaderTitle>
<DrawerHeaderTitleAction slot>
<Button
appearance=ButtonAppearance::Subtle
on_click=move |_| open_left.set(false)
>
"x"
</Button>
</DrawerHeaderTitleAction>
"Inline Drawer"
</DrawerHeaderTitle>
</DrawerHeader>
<DrawerBody>
<p>"Drawer content"</p>
</DrawerBody>
</InlineDrawer>
<div style="flex: 1">
<Button on_click=move |_| open_left.set(true)>"Open left"</Button>
<Button on_click=move |_| open_right.set(true)>"Open right"</Button>
<Button on_click=move |_| open_buttom.set(true)>"Open buttom"</Button>
</div>
<InlineDrawer open=open_right position=DrawerPosition::Right>
<DrawerHeader>
<DrawerHeaderTitle>
<DrawerHeaderTitleAction slot>
<Button
appearance=ButtonAppearance::Subtle
on_click=move |_| open_right.set(false)
>
"x"
</Button>
</DrawerHeaderTitleAction>
"Inline Drawer"
</DrawerHeaderTitle>
</DrawerHeader>
<DrawerBody>
<p>"Drawer content"</p>
</DrawerBody>
</InlineDrawer>
</div>
<InlineDrawer open=open_buttom position=DrawerPosition::Bottom>
<DrawerHeader>
<DrawerHeaderTitle>
<DrawerHeaderTitleAction slot>
<Button
appearance=ButtonAppearance::Subtle
on_click=move |_| open_buttom.set(false)
>
"x"
</Button>
</DrawerHeaderTitleAction>
"Inline Drawer"
</DrawerHeaderTitle>
</DrawerHeader>
<DrawerBody>
<p>"Drawer content"</p>
</DrawerBody>
</InlineDrawer>
</div>
}
```
### Scroll content
### With Scroll
```rust demo
let show = create_rw_signal(false);
let open = RwSignal::new(false);
view! {
<Button on_click=move |_| show.set(true)>"Open"</Button>
<Drawer show width="160px" title="Scroll content">
r#"This being said, the world is moving in the direction opposite to Clarke's predictions. In 2001: A Space Odyssey, in the year of 2001, which has already passed, human beings have built magnificent cities in space, and established permanent colonies on the moon, and huge nuclear-powered spacecraft have sailed to Saturn. However, today, in 2018, the walk on the moon has become a distant memory.And the farthest reach of our manned space flights is just as long as the two-hour mileage of a high-speed train passing through my city. At the same time, information technology is developing at an unimaginable speed. With the entire world covered by the Internet, people have gradually lost their interest in space, as they find themselves increasingly comfortable in the space created by IT. Instead of an exploration of the real space, which is full of real difficulties, people now just prefer to experience virtual space through VR. Just like someone said, "You promised me an ocean of stars, but you actually gave me Facebook.""#
</Drawer>
<Button on_click=move |_| open.set(true)>"Open"</Button>
<OverlayDrawer open>
<DrawerHeader>
<DrawerHeaderTitle>
<DrawerHeaderTitleAction slot>
<Button
appearance=ButtonAppearance::Subtle
on_click=move |_| open.set(false)
>
"x"
</Button>
</DrawerHeaderTitleAction>
"Default Drawer"
</DrawerHeaderTitle>
</DrawerHeader>
<DrawerBody>
<p style="line-height: 40px">r#"This being said, the world is moving in the direction opposite to Clarke's predictions. In 2001: A Space Odyssey, in the year of 2001, which has already passed, human beings have built magnificent cities in space, and established permanent colonies on the moon, and huge nuclear-powered spacecraft have sailed to Saturn. However, today, in 2018, the walk on the moon has become a distant memory.And the farthest reach of our manned space flights is just as long as the two-hour mileage of a high-speed train passing through my city. At the same time, information technology is developing at an unimaginable speed. With the entire world covered by the Internet, people have gradually lost their interest in space, as they find themselves increasingly comfortable in the space created by IT. Instead of an exploration of the real space, which is full of real difficulties, people now just prefer to experience virtual space through VR. Just like someone said, "You promised me an ocean of stars, but you actually gave me Facebook.""#</p>
</DrawerBody>
</OverlayDrawer>
}
```

View file

@ -1,182 +0,0 @@
# Dropdown
```rust demo
let message = use_message();
let on_select = move |key: String| {
match key.as_str() {
"facebook" => message.create( "Facebook".into(), MessageVariant::Success, Default::default(),),
"twitter" => message.create( "Twitter".into(), MessageVariant::Warning, Default::default(),),
_ => ()
}
};
view! {
<Space>
<Dropdown on_select trigger_type=DropdownTriggerType::Hover>
<DropdownTrigger slot>
<Button>"Hover"</Button>
</DropdownTrigger>
<DropdownItem key="facebook" icon=icondata::AiFacebookOutlined label="Facebook"></DropdownItem>
<DropdownItem key="twitter" disabled=true icon=icondata::AiTwitterOutlined label="Twitter"></DropdownItem>
</Dropdown>
<Dropdown on_select>
<DropdownTrigger slot>
<Button>"Click"</Button>
</DropdownTrigger>
<DropdownItem key="facebook" icon=icondata::AiFacebookOutlined label="Facebook"></DropdownItem>
<DropdownItem key="twitter" icon=icondata::AiTwitterOutlined label="Twitter"></DropdownItem>
<DropdownItem key="no_icon" disabled=true label="Mastodon"></DropdownItem>
</Dropdown>
</Space>
}
```
### Placement
```rust demo
use leptos_meta::Style;
let on_select = move |key| println!("{}", key);
view! {
<Style>
".demo-dropdown .thaw-button { width: 100% } .demo-dropdown .thaw-dropdown-trigger { display: block }"
</Style>
<Grid x_gap=8 y_gap=8 cols=3 class="demo-dropdown">
<GridItem>
<Dropdown on_select placement=DropdownPlacement::TopStart>
<DropdownTrigger slot>
<Button>"Top Start"</Button>
</DropdownTrigger>
"Content"
</Dropdown>
</GridItem>
<GridItem>
<Dropdown on_select placement=DropdownPlacement::Top>
<DropdownTrigger slot>
<Button>"Top"</Button>
</DropdownTrigger>
"Content"
</Dropdown>
</GridItem>
<GridItem>
<Dropdown on_select placement=DropdownPlacement::TopEnd>
<DropdownTrigger slot>
<Button>"Top End"</Button>
</DropdownTrigger>
"Content"
</Dropdown>
</GridItem>
<GridItem>
<Dropdown on_select placement=DropdownPlacement::LeftStart>
<DropdownTrigger slot>
<Button>"Left Start"</Button>
</DropdownTrigger>
"Content"
</Dropdown>
</GridItem>
<GridItem offset=1>
<Dropdown on_select placement=DropdownPlacement::RightStart>
<DropdownTrigger slot>
<Button>"Right Start"</Button>
</DropdownTrigger>
"Content"
</Dropdown>
</GridItem>
<GridItem>
<Dropdown on_select placement=DropdownPlacement::Left>
<DropdownTrigger slot>
<Button>"Left"</Button>
</DropdownTrigger>
"Content"
</Dropdown>
</GridItem>
<GridItem offset=1>
<Dropdown on_select placement=DropdownPlacement::Right>
<DropdownTrigger slot>
<Button>"Right"</Button>
</DropdownTrigger>
"Content"
</Dropdown>
</GridItem>
<GridItem>
<Dropdown on_select placement=DropdownPlacement::LeftEnd>
<DropdownTrigger slot>
<Button>"Left End"</Button>
</DropdownTrigger>
"Content"
</Dropdown>
</GridItem>
<GridItem offset=1>
<Dropdown on_select placement=DropdownPlacement::RightEnd>
<DropdownTrigger slot>
<Button>"Right End"</Button>
</DropdownTrigger>
"Content"
</Dropdown>
</GridItem>
<GridItem>
<Dropdown on_select placement=DropdownPlacement::BottomStart>
<DropdownTrigger slot>
<Button>"Bottom Start"</Button>
</DropdownTrigger>
"Content"
</Dropdown>
</GridItem>
<GridItem>
<Dropdown on_select placement=DropdownPlacement::Bottom>
<DropdownTrigger slot>
<Button>"Bottom"</Button>
</DropdownTrigger>
"Content"
</Dropdown>
</GridItem>
<GridItem>
<Dropdown on_select placement=DropdownPlacement::BottomEnd>
<DropdownTrigger slot>
<Button>"Bottom End"</Button>
</DropdownTrigger>
"Content"
</Dropdown>
</GridItem>
</Grid>
}
```
### Dropdown Props
| Name | Type | Default | Description |
| ------------ | ----------------------------------- | ---------------------------- | ------------------------------------------- |
| class | `OptionalProp<MaybeSignal<String>>` | `Default::default()` | Addtional classes for the dropdown element. |
| on_select | `Callback<String>` | | Called when item is selected. |
| trigger_type | `DropdownTriggerType` | `DropdownTriggerType::Click` | Action that displays the dropdown. |
| placement | `DropdownPlacement` | `DropdownPlacement::Bottom` | Dropdown placement. |
| children | `Children` | | The content inside dropdown. |
### DropdownItem Props
| Name | Type | Default | Description |
| -------- | -------------------------------------------- | -------------------- | ------------------------------------------------ |
| class | `OptionalProp<MaybeSignal<String>>` | `Default::default()` | Addtional classes for the dropdown item element. |
| key | `MaybeSignal<String>` | `Default::default()` | The key of the dropdown item. |
| label | `MaybeSignal<String>` | `Default::default()` | The label of the dropdown item. |
| icon | `OptionalMaybeSignal<icondata_core::Icon>` | `None` | The icon of the dropdown item. |
| disabled | `MaybeSignal<bool>` | `false` | Whether the dropdown item is disabled. |
### Dropdown Slots
| Name | Default | Description |
| --------------- | ------- | ------------------------------------------------ |
| DropdownTrigger | `None` | The element or component that triggers dropdown. |
### DropdownTriger Props
| Name | Type | Default | Description |
| ------------ | ----------------------------------- | ---------------------------- | -------------------------------------------------- |
| class | `OptionalProp<MaybeSignal<String>>` | `Default::default()` | Addtional classes for the dropdown trigger element. |
| children | `Children` | | The content inside dropdown trigger. |

View file

@ -3,10 +3,21 @@
```rust demo
view! {
<Image src="https://s3.bmp.ovh/imgs/2021/10/2c3b013418d55659.jpg" width="500px"/>
<Image src="https://s3.bmp.ovh/imgs/2021/10/2c3b013418d55659.jpg" width="200px" height="200px" object_fit="cover"/>
<Image width="200px" height="200px"/>
}
```
### Shape
```rust demo
view! {
<Image src="https://s3.bmp.ovh/imgs/2021/10/2c3b013418d55659.jpg" width="200px" height="200px"/>
<Image src="https://s3.bmp.ovh/imgs/2021/10/2c3b013418d55659.jpg" width="200px" height="200px" shape=ImageShape::Circular/>
<Image src="https://s3.bmp.ovh/imgs/2021/10/2c3b013418d55659.jpg" width="200px" height="200px" shape=ImageShape::Rounded/>
}
```
### Image Props
| Name | Type | Default | Desciption |

View file

@ -1,13 +1,37 @@
# Input
```rust demo
let value = create_rw_signal(String::from("o"));
let value = RwSignal::new(String::from("o"));
view! {
<Space vertical=true>
<Input value/>
<Input value variant=InputVariant::Password placeholder="Password"/>
<TextArea value placeholder="Textarea"/>
<Input value input_type=InputType::Password placeholder="Password"/>
</Space>
}
```
## Prefix & Suffix
```rust demo
let value = RwSignal::new(String::from("o"));
view! {
<Space vertical=true>
<Input value>
<InputPrefix slot>
<Icon icon=icondata::AiUserOutlined/>
</InputPrefix>
</Input>
<Input value>
<InputSuffix slot>
<Icon icon=icondata::AiGithubOutlined/>
</InputSuffix>
</Input>
<Input value>
<InputPrefix slot>"$"</InputPrefix>
<InputSuffix slot>".00"</InputSuffix>
</Input>
</Space>
}
```
@ -15,42 +39,34 @@ view! {
### Disabled
```rust demo
let value = create_rw_signal(String::from("o"));
let value = RwSignal::new(String::from("o"));
view! {
<Space vertical=true>
<Input value disabled=true/>
<TextArea value disabled=true/>
</Space>
<Input value disabled=true/>
}
```
### Invalid
### Placeholder
```rust demo
let value = create_rw_signal(String::from("o"));
view! {
<Space vertical=true>
<Input value invalid=true/>
<TextArea value invalid=true/>
</Space>
<Input placeholder="This is a placeholder"/>
}
```
### Imperative handle
```rust demo
let value = create_rw_signal(String::from("o"));
let input_ref = create_component_ref::<InputRef>();
let value = RwSignal::new(String::from("o"));
let input_ref = ComponentRef::<InputRef>::new();
let focus = Callback::new(move |_| {
let focus = move |_| {
input_ref.get_untracked().unwrap().focus()
});
};
let blur = Callback::new(move |_| {
let blur = move |_| {
input_ref.get_untracked().unwrap().blur()
});
};
view! {
<Space vertical=true>
@ -67,56 +83,20 @@ view! {
}
```
### Input attrs
### Custom parsing
```rust demo
view! {
<Space>
<label for="demo-input-attrs">"Do you like cheese?"</label>
<Input attr:id="demo-input-attrs"/>
</Space>
}
```
let value = RwSignal::new(String::from("loren_ipsun"));
## Prefix & Suffix
```rust demo
let value = create_rw_signal(String::from("o"));
view! {
<Space vertical=true>
<Input value>
<InputPrefix slot>
<Icon icon=icondata::AiUserOutlined/>
</InputPrefix>
</Input>
<Input value>
<InputSuffix slot>"$"</InputSuffix>
</Input>
<Input value>
<InputSuffix slot>
<Icon icon=icondata::AiGithubOutlined/>
</InputSuffix>
</Input>
</Space>
}
```
### Formatter
```rust demo
let value = create_rw_signal(String::from("loren_ipsun"));
let formatter = Callback::<String, String>::new(move |v: String| {
let format = move |v: String| {
v.replace("_", " ")
});
let parser = Callback::<String, String>::new(move |v: String| {
v.replace(" ", "_")
});
};
let parser = move |v: String| {
Some(v.replace(" ", "_"))
};
view! {
<Input value parser formatter />
<Input value parser format />
<p>"Underlying value: "{ value }</p>
}
```
@ -135,8 +115,6 @@ view! {
| on_focus | `Option<Callback<ev::FocusEvent>>` | `None` | Callback triggered when the input is focussed on. |
| on_blur | `Option<Callback<ev::FocusEvent>>` | `None` | Callback triggered when the input is blurred. |
| attr: | `Vec<(&'static str, Attribute)>` | `Default::default()` | The dom attrs of the input element inside the component. |
| parser | `OptionalProp<Callback<String, String>>` | `Default::default()` | Modifies the user input before assigning it to the value |
| formatter | `OptionalProp<Callback<String, String>>` | `Default::default()` | Formats the value to be shown to the user |
### Input Slots

View file

@ -1,113 +0,0 @@
# Input Number
```rust demo
let value = create_rw_signal(0);
let value_f64 = create_rw_signal(0.0);
view! {
<Space vertical=true>
<InputNumber value step=1/>
<InputNumber value=value_f64 step=1.0/>
</Space>
}
```
### Min / Max
```rust demo
let value = create_rw_signal(0);
view! {
<InputNumber value step=1 min=-1 max=2/>
}
```
### Disabled
```rust demo
let value = create_rw_signal(0);
view! {
<InputNumber value step=1 disabled=true/>
}
```
### Invalid
```rust demo
let value = create_rw_signal(0);
view! {
<InputNumber value step=1 invalid=true/>
}
```
### Formatter
```rust demo
let value = create_rw_signal(0.0);
let value_2 = create_rw_signal(0.0);
let formatter = Callback::<f64, String>::new(move |v: f64| {
let v = v.to_string();
let dot_pos = v.chars().position(|c| c == '.').unwrap_or_else(|| v.chars().count());
let mut int: String = v.chars().take(dot_pos).collect();
let sign: String = if v.chars().take(1).collect::<String>() == String::from("-") {
int = int.chars().skip(1).collect();
String::from("-")
} else {
String::from("")
};
let dec: String = v.chars().skip(dot_pos + 1).take(2).collect();
let int = int
.as_bytes()
.rchunks(3)
.rev()
.map(std::str::from_utf8)
.collect::<Result<Vec<&str>, _>>()
.unwrap()
.join(".");
format!("{}{},{:0<2}", sign, int, dec)
});
let parser = Callback::<String, f64>::new(move |v: String| {
let comma_pos = v.chars().position(|c| c == ',').unwrap_or_else(|| v.chars().count());
let int_part = v.chars().take(comma_pos).filter(|a| a.is_digit(10)).collect::<String>();
let dec_part = v.chars().skip(comma_pos + 1).take(2).filter(|a| a.is_digit(10)).collect::<String>();
format!("{:0<1}.{:0<2}", int_part, dec_part).parse::<f64>().unwrap_or_default()
});
view! {
<InputNumber value parser formatter step=1.0 />
<p>"Underlying value: "{ value }</p>
}
```
### InputNumber Props
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| class | `OptionalProp<MaybeSignal<String>>` | `Default::default()` | Addtional classes for the input element. |
| value | `Model<T>` | `T::default()` | Set the input value. |
| placeholder | `OptionalProp<MaybeSignal<String>>` | `Default::default()` | Placeholder of input number. |
| step | `MaybeSignal<T>` | | The number which the current value is increased or decreased on key or button press. |
| min | `MaybeSignal<T>` | `T::min_value()` | The minimum number that the input value can take. |
| max | `MaybeSignal<T>` | `T::max_value()` | The maximum number that the input value can take. |
| disabled | `MaybeSignal<bool>` | `false` | Whether the input is disabled. |
| invalid | `MaybeSignal<bool>` | `false` | Whether the input is invalid. |
| attr: | `Vec<(&'static str, Attribute)>` | `Default::default()` | The dom attrs of the input element inside the component. |
| parser | `OptionalProp<Callback<String, T>>` | `Default::default()` | Modifies the user input before assigning it to the value |
| formatter | `OptionalProp<Callback<T, String>>` | `Default::default()` | Formats the value to be shown to the user |
#### T impl
`T: Add<Output = T> + Sub<Output = T> + PartialOrd + num::Bounded + Default + Clone + FromStr + ToString + 'static`
### InputNumber Ref
| Name | Type | Description |
| ----- | ----------- | ------------------------ |
| focus | `Fn(&self)` | Focus the input element. |
| blur | `Fn(&self)` | Blur the input element. |

View file

@ -3,10 +3,10 @@
```rust demo
view! {
<Layout>
<LayoutHeader style="background-color: #0078ffaa; padding: 20px;">
<LayoutHeader attr:style="background-color: #0078ffaa; padding: 20px;">
"Header"
</LayoutHeader>
<Layout style="background-color: #0078ff88; padding: 20px;">"Content"</Layout>
<Layout attr:style="background-color: #0078ff88; padding: 20px;">"Content"</Layout>
</Layout>
}
```
@ -16,14 +16,14 @@ view! {
```rust demo
view! {
<Layout has_sider=true>
<LayoutSider style="background-color: #0078ff99; padding: 20px;">
<LayoutSider attr:style="background-color: #0078ff99; padding: 20px;">
"Sider"
</LayoutSider>
<Layout>
<LayoutHeader style="background-color: #0078ffaa; padding: 20px;">
<LayoutHeader attr:style="background-color: #0078ffaa; padding: 20px;">
"Header"
</LayoutHeader>
<Layout style="background-color: #0078ff88; padding: 20px;">
<Layout attr:style="background-color: #0078ff88; padding: 20px;">
"Content"
</Layout>
</Layout>

View file

@ -1,20 +1,25 @@
# Loading Bar
<Alert variant=AlertVariant::Warning title="Prerequisite">
"If you want to use loading bar, you need to wrap the component where you call related methods inside LoadingBarProvider and use use_loading_bar to get the API."
</Alert>
<MessageBar layout=MessageBarLayout::Multiline intent=MessageBarIntent::Warning>
<MessageBarBody>
<h3 style="margin: 0">"Prerequisite"</h3>
<p>
"If you want to use loading bar, you need to wrap the component where you call related methods inside LoadingBarProvider and use use_loading_bar to get the API."
</p>
</MessageBarBody>
</MessageBar>
```rust demo
let loading_bar = use_loading_bar();
let start = Callback::new(move |_| {
let loading_bar = LoadingBarInjection::expect_use();
let start = move |_| {
loading_bar.start();
});
let finish = Callback::new(move |_| {
};
let finish = move |_| {
loading_bar.finish();
});
let error = Callback::new(move |_| {
};
let error = move |_| {
loading_bar.error();
});
};
view! {
<Space>

View file

@ -1,25 +1,150 @@
# Menu
```rust demo
let value = create_rw_signal(String::from("o"));
let toaster = ToasterInjection::expect_context();
let on_select = move |key: String| {
leptos::logging::warn!("{}", key);
toaster.dispatch_toast(view! {
<Toast>
<ToastBody>
"key"
</ToastBody>
</Toast>
}.into_any(), Default::default());
};
view! {
<Menu value default_expanded_keys=vec![String::from("area")]>
<MenuItem key="a" label="And"/>
<MenuItem key="o" label="Or"/>
<MenuItem icon=icondata::AiAreaChartOutlined key="area" label="Area Chart">
<MenuItem key="target" label="Target"/>
<MenuItem key="above" label="Above"/>
<MenuItem key="below" label="Below"/>
</MenuItem>
<MenuItem icon=icondata::AiPieChartOutlined key="pie" label="Pie Chart">
<MenuItem key="pie-target" label="Target"/>
<MenuItem key="pie-above" label="Above"/>
<MenuItem key="pie-below" label="Below"/>
</MenuItem>
<MenuItem icon=icondata::AiGithubOutlined key="github" label="Github"/>
<MenuItem icon=icondata::AiChromeOutlined key="chrome" label="Chrome"/>
</Menu>
<Space>
<Menu on_select trigger_type=MenuTriggerType::Hover>
<MenuTrigger slot>
<Button>"Hover"</Button>
</MenuTrigger>
<MenuItem value="facebook" icon=icondata::AiFacebookOutlined>"Facebook"</MenuItem>
<MenuItem value="twitter" disabled=true icon=icondata::AiTwitterOutlined>"Twitter"</MenuItem>
</Menu>
<Menu on_select>
<MenuTrigger slot>
<Button>"Click"</Button>
</MenuTrigger>
<MenuItem value="facebook" icon=icondata::AiFacebookOutlined>"Facebook"</MenuItem>
<MenuItem value="twitter" icon=icondata::AiTwitterOutlined>"Twitter"</MenuItem>
<MenuItem value="no_icon" disabled=true>"Mastodon"</MenuItem>
</Menu>
</Space>
}
```
### Placement
```rust demo
use leptos_meta::Style;
let on_select = move |value| leptos::logging::warn!("{}", value);
view! {
<Style>
".demo-menu .thaw-button { width: 100% } .demo-menu .thaw-menu-trigger { display: block }"
</Style>
<Grid x_gap=8 y_gap=8 cols=3 class="demo-menu">
<GridItem>
<Menu on_select position=MenuPosition::TopStart>
<MenuTrigger slot>
<Button>"Top Start"</Button>
</MenuTrigger>
<MenuItem value="content">"Content"</MenuItem>
</Menu>
</GridItem>
<GridItem>
<Menu on_select position=MenuPosition::Top>
<MenuTrigger slot>
<Button>"Top"</Button>
</MenuTrigger>
<MenuItem value="content">"Content"</MenuItem>
</Menu>
</GridItem>
<GridItem>
<Menu on_select position=MenuPosition::TopEnd>
<MenuTrigger slot>
<Button>"Top End"</Button>
</MenuTrigger>
<MenuItem value="content">"Content"</MenuItem>
</Menu>
</GridItem>
<GridItem>
<Menu on_select position=MenuPosition::LeftStart>
<MenuTrigger slot>
<Button>"Left Start"</Button>
</MenuTrigger>
<MenuItem value="content">"Content"</MenuItem>
</Menu>
</GridItem>
<GridItem offset=1>
<Menu on_select position=MenuPosition::RightStart>
<MenuTrigger slot>
<Button>"Right Start"</Button>
</MenuTrigger>
<MenuItem value="content">"Content"</MenuItem>
</Menu>
</GridItem>
<GridItem>
<Menu on_select position=MenuPosition::Left>
<MenuTrigger slot>
<Button>"Left"</Button>
</MenuTrigger>
<MenuItem value="content">"Content"</MenuItem>
</Menu>
</GridItem>
<GridItem offset=1>
<Menu on_select position=MenuPosition::Right>
<MenuTrigger slot>
<Button>"Right"</Button>
</MenuTrigger>
<MenuItem value="content">"Content"</MenuItem>
</Menu>
</GridItem>
<GridItem>
<Menu on_select position=MenuPosition::LeftEnd>
<MenuTrigger slot>
<Button>"Left End"</Button>
</MenuTrigger>
<MenuItem value="content">"Content"</MenuItem>
</Menu>
</GridItem>
<GridItem offset=1>
<Menu on_select position=MenuPosition::RightEnd>
<MenuTrigger slot>
<Button>"Right End"</Button>
</MenuTrigger>
<MenuItem value="content">"Content"</MenuItem>
</Menu>
</GridItem>
<GridItem>
<Menu on_select position=MenuPosition::BottomStart>
<MenuTrigger slot>
<Button>"Bottom Start"</Button>
</MenuTrigger>
<MenuItem value="content">"Content"</MenuItem>
</Menu>
</GridItem>
<GridItem>
<Menu on_select position=MenuPosition::Bottom>
<MenuTrigger slot>
<Button>"Bottom"</Button>
</MenuTrigger>
<MenuItem value="content">"Content"</MenuItem>
</Menu>
</GridItem>
<GridItem>
<Menu on_select position=MenuPosition::BottomEnd>
<MenuTrigger slot>
<Button>"Bottom End"</Button>
</MenuTrigger>
<MenuItem value="content">"Content"</MenuItem>
</Menu>
</GridItem>
</Grid>
}
```
@ -28,24 +153,30 @@ view! {
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| class | `OptionalProp<MaybeSignal<String>>` | `Default::default()` | Addtional classes for the menu element. |
| value | `Model<String>` | `Default::default()` | The selected item key of the menu. |
| default_expanded_keys | `Vec<String>` | `Default::default()` | The default expanded submenu keys. |
| children | `Children` | | Menu's content. |
### MenuGroup Props
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| class | `OptionalProp<MaybeSignal<String>>` | `Default::default()` | Addtional classes for the menu group element. |
| label | `String` | | The label of the menu group. |
| children | `Children` | | MenuGroup's content. |
| on_select | `Callback<String>` | | Called when item is selected. |
| trigger_type | `MenuTriggerType` | `MenuTriggerType::Click` | Action that displays the menu. |
| position | `MenuPosition` | `MenuPosition::Bottom` | Menu position. |
| children | `Children` | | The content inside menu. |
### MenuItem Props
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| class | `OptionalProp<MaybeSignal<String>>` | `Default::default()` | Addtional classes for the menu item element. |
| value | `MaybeSignal<String>` | `Default::default()` | The value of the menu item. |
| label | `MaybeSignal<String>` | `Default::default()` | The label of the menu item. |
| key | `MaybeSignal<String>` | `Default::default()` | The indentifier of the menu item. |
| icon | `OptionalMaybeSignal<icondata_core::Icon>` | `None` | The icon of the menu item. |
| children | `Option<Children>` | `None` | MenuItem's content. |
| disabled | `MaybeSignal<bool>` | `false` | Whether the menu item is disabled. |
### Menu Slots
| Name | Default | Description |
| ----------- | ------- | -------------------------------------------- |
| MenuTrigger | `None` | The element or component that triggers menu. |
### MenuTriger Props
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| class | `OptionalProp<MaybeSignal<String>>` | `Default::default()` | Addtional classes for the menu trigger element. |
| children | `Children` | | The content inside menu trigger. |

View file

@ -1,53 +0,0 @@
# Message
<Alert variant=AlertVariant::Warning title="Prerequisite">
"If you want to use message, you need to wrap the component where you call related methods inside MessageProvider and use use_message to get the API."
</Alert>
```rust demo
let message = use_message();
let success = move |_| {
message.create(
"Success".into(),
MessageVariant::Success,
MessageOptions {closable: true, duration: std::time::Duration::from_secs(0)},
);
};
let warning = move |_| {
message.create(
"Warning".into(),
MessageVariant::Warning,
Default::default(),
);
};
let error = move |_| {
message.create("Error".into(), MessageVariant::Error, Default::default());
};
view! {
<Space>
<Button on_click=success>"Success"</Button>
<Button on_click=warning>"Warning"</Button>
<Button on_click=error>"Error"</Button>
</Space>
}
```
### MessageProvider Props
| Name | Type | Default | Desciption |
| --------- | ------------------ | ----------------------- | ------------------------------- |
| placement | `MessagePlacement` | `MessagePlacement::Top` | Position to place the messages. |
### MessageProvider Injection Methods
| Name | Type | Description |
| ------ | ------------------------------------------------------------------------------ | ------------------------ |
| create | `fn(&self, content: String, variant: MessageVariant, options: MessageOptions)` | Use create type message. |
### MessageOptions fields
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| duration | `Duration` | `std::time::Duration::from_secs(3)` | How long the message will be displayed. 0 for permanent message |
| closable | `bool` | `false` | Can the message be manually closed. |

View file

@ -0,0 +1,67 @@
# MessageBar
```rust demo
view! {
<MessageBar>
<MessageBarBody>
<MessageBarTitle>"Descriptive title"</MessageBarTitle>
"Message providing information to the user with actionable insights."
</MessageBarBody>
<MessageBarActions>
<Button size=ButtonSize::Small>"Action"</Button>
<Button size=ButtonSize::Small>"Action"</Button>
<MessageBarContainerAction slot>
<Button size=ButtonSize::Small appearance=ButtonAppearance::Transparent>
"X"
</Button>
</MessageBarContainerAction>
</MessageBarActions>
</MessageBar>
}
```
### Intent
```rust demo
view! {
<Space vertical=true>
<MessageBar>
<MessageBarBody>
<MessageBarTitle>"Intent info"</MessageBarTitle>
"Message providing information to the user with actionable insights."
</MessageBarBody>
</MessageBar>
<MessageBar intent=MessageBarIntent::Warning>
<MessageBarBody>
<MessageBarTitle>"Intent warning"</MessageBarTitle>
"Message providing information to the user with actionable insights."
</MessageBarBody>
</MessageBar>
<MessageBar intent=MessageBarIntent::Error>
<MessageBarBody>
<MessageBarTitle>"Intent error"</MessageBarTitle>
"Message providing information to the user with actionable insights."
</MessageBarBody>
</MessageBar>
<MessageBar intent=MessageBarIntent::Success>
<MessageBarBody>
<MessageBarTitle>"Intent success"</MessageBarTitle>
"Message providing information to the user with actionable insights."
</MessageBarBody>
</MessageBar>
</Space>
}
```
### Manual Layout
```rust demo
view! {
<MessageBar layout=MessageBarLayout::Multiline>
<MessageBarBody>
<h3 style="margin: 0">"Descriptive title"</h3>
<p>"Message providing information to the user with actionable insights."</p>
</MessageBarBody>
</MessageBar>
}
```

View file

@ -0,0 +1,48 @@
# Nav
```rust demo
view! {
<NavDrawer>
<NavCategory value="area">
<NavCategoryItem slot icon=icondata::AiAreaChartOutlined>
"Area Chart"
</NavCategoryItem>
<NavSubItem value="target">
"Target"
</NavSubItem>
<NavSubItem value="above">
"Above"
</NavSubItem>
<NavSubItem value="below">
"Below"
</NavSubItem>
</NavCategory>
<NavCategory value="pie">
<NavCategoryItem slot icon=icondata::AiPieChartOutlined>
"Pie Chart"
</NavCategoryItem>
<NavSubItem value="pie-target">
"Pie Target"
</NavSubItem>
<NavSubItem value="pin-above">
"Pin Above"
</NavSubItem>
<NavSubItem value="pin-below">
"Pin Below"
</NavSubItem>
</NavCategory>
<NavItem
icon=icondata::AiGithubOutlined
value="github"
href="https://github.com/thaw-ui/thaw"
attr:target="_blank"
>
"Github"
</NavItem>
<NavItem icon=icondata::AiChromeOutlined value="chrome">
"Chrome"
</NavItem>
</NavDrawer>
}
```

View file

@ -1,49 +0,0 @@
# Pagination
```rust demo
let page = create_rw_signal(1);
view! {
<Space vertical=true>
<div>"Page: " {move || page.get()}</div>
<Pagination page count=10 />
</Space>
}
```
### Size
```rust demo
view! {
<Space vertical=true>
<Pagination count=100 size=ButtonSize::Tiny />
<Pagination count=100 size=ButtonSize::Small />
<Pagination count=100 size=ButtonSize::Medium />
<Pagination count=100 size=ButtonSize::Large />
</Space>
}
```
### Pagination ranges
```rust demo
view! {
<Space vertical=true>
<Pagination count=100 sibling_count=0 />
<Pagination count=100 sibling_count=1 />
<Pagination count=100 sibling_count=2 />
<Pagination count=100 sibling_count=3 />
</Space>
}
```
### Pagination Props
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| class | `OptionalProp<MaybeSignal<String>>` | `Default::default()` | Additional classes. |
| page | `Model<usize>` | `1` | The current page starts from 1. |
| count | `MaybeSignal<usize>` | | The total numbers of pages. |
| sibling_count | `MaybeSignal<usize>` | `1` | Number of visible pages after and before the current page. |
| size | `MaybeSignal<ButtonSize>` | `ButtonSize::Medium` | Button size. |
| on_change | `Option<Callback<usize>>` | `None` | Callback fired when the page is changed. |

View file

@ -30,7 +30,7 @@ view! {
</Style>
<Grid x_gap=8 y_gap=8 cols=3 class="demo-popover">
<GridItem>
<Popover placement=PopoverPlacement::TopStart>
<Popover position=PopoverPosition::TopStart>
<PopoverTrigger slot>
<Button>"Top Start"</Button>
</PopoverTrigger>
@ -38,7 +38,7 @@ view! {
</Popover>
</GridItem>
<GridItem>
<Popover placement=PopoverPlacement::Top>
<Popover position=PopoverPosition::Top>
<PopoverTrigger slot>
<Button>"Top"</Button>
</PopoverTrigger>
@ -46,7 +46,7 @@ view! {
</Popover>
</GridItem>
<GridItem>
<Popover placement=PopoverPlacement::TopEnd>
<Popover position=PopoverPosition::TopEnd>
<PopoverTrigger slot>
<Button>"Top End"</Button>
</PopoverTrigger>
@ -54,7 +54,7 @@ view! {
</Popover>
</GridItem>
<GridItem>
<Popover placement=PopoverPlacement::LeftStart>
<Popover position=PopoverPosition::LeftStart trigger_type=PopoverTriggerType::Click>
<PopoverTrigger slot>
<Button>"Left Start"</Button>
</PopoverTrigger>
@ -62,7 +62,7 @@ view! {
</Popover>
</GridItem>
<GridItem offset=1>
<Popover placement=PopoverPlacement::RightStart>
<Popover position=PopoverPosition::RightStart>
<PopoverTrigger slot>
<Button>"Right Start"</Button>
</PopoverTrigger>
@ -70,7 +70,7 @@ view! {
</Popover>
</GridItem>
<GridItem>
<Popover placement=PopoverPlacement::Left>
<Popover position=PopoverPosition::Left trigger_type=PopoverTriggerType::Click>
<PopoverTrigger slot>
<Button>"Left"</Button>
</PopoverTrigger>
@ -78,7 +78,7 @@ view! {
</Popover>
</GridItem>
<GridItem offset=1>
<Popover placement=PopoverPlacement::Right>
<Popover position=PopoverPosition::Right>
<PopoverTrigger slot>
<Button>"Right"</Button>
</PopoverTrigger>
@ -86,7 +86,7 @@ view! {
</Popover>
</GridItem>
<GridItem>
<Popover placement=PopoverPlacement::LeftEnd>
<Popover position=PopoverPosition::LeftEnd>
<PopoverTrigger slot>
<Button>"Left End"</Button>
</PopoverTrigger>
@ -94,7 +94,7 @@ view! {
</Popover>
</GridItem>
<GridItem offset=1>
<Popover placement=PopoverPlacement::RightEnd>
<Popover position=PopoverPosition::RightEnd>
<PopoverTrigger slot>
<Button>"Right End"</Button>
</PopoverTrigger>
@ -102,7 +102,7 @@ view! {
</Popover>
</GridItem>
<GridItem>
<Popover placement=PopoverPlacement::BottomStart>
<Popover position=PopoverPosition::BottomStart>
<PopoverTrigger slot>
<Button>"Bottom Start"</Button>
</PopoverTrigger>
@ -110,7 +110,7 @@ view! {
</Popover>
</GridItem>
<GridItem>
<Popover placement=PopoverPlacement::Bottom>
<Popover position=PopoverPosition::Bottom>
<PopoverTrigger slot>
<Button>"Bottom"</Button>
</PopoverTrigger>
@ -118,7 +118,7 @@ view! {
</Popover>
</GridItem>
<GridItem>
<Popover placement=PopoverPlacement::BottomEnd>
<Popover position=PopoverPosition::BottomEnd>
<PopoverTrigger slot>
<Button>"Bottom End"</Button>
</PopoverTrigger>
@ -129,12 +129,18 @@ view! {
}
```
### Tooltip
### Appearance
```rust demo
view! {
<Space>
<Popover tooltip=true>
<Popover appearance=PopoverAppearance::Brand>
<PopoverTrigger slot>
<Button>"Hover"</Button>
</PopoverTrigger>
"Content"
</Popover>
<Popover appearance=PopoverAppearance::Inverted>
<PopoverTrigger slot>
<Button>"Hover"</Button>
</PopoverTrigger>
@ -146,24 +152,15 @@ view! {
### Popover Props
| Name | Type | Default | Description |
| -------------| ----------------------------------- | -------------------------- | --------------------------------- |
| class | `OptionalProp<MaybeSignal<String>>` | `Default::default()` | Content class of the popover. |
| placement | `PopoverPlacement` | `PopoverPlacement::Top` | Popover placement. |
| trigger_type | `PopoverTriggerType` | `PopoverTriggerType::Hover`| Action that displays the dropdown |
| tooltip | `bool` | `false` | Tooltip. |
| children | `Children` | | The content inside popover. |
| Name | Type | Default | Description |
| -------- | ----------------------------------- | ---------------------- | ----------------------------- |
| class | `OptionalProp<MaybeSignal<String>>` | `Default::default()` | Content class of the popover. |
| position | `PopoverPosition` | `PopoverPosition::Top` | Popover position. |
| tooltip | `bool` | `false` | Tooltip. |
| children | `Children` | | The content inside popover. |
### Popover Slots
| Name | Default | Description |
| -------------- | ------- | ----------------------------------------------- |
| PopoverTrigger | | The element or component that triggers popover. |
### PopoverTriger Props
| Name | Type | Default | Description |
| ------------ | ----------------------------------- | ---------------------------- | -------------------------------------------------- |
| class | `OptionalProp<MaybeSignal<String>>` | `Default::default()` | Addtional classes for the popover trigger element. |
| children | `Children` | | The content inside popover trigger. |

View file

@ -1,19 +1,17 @@
# Progress
# ProgressBar
```rust demo
let percentage = create_rw_signal(0.0f32);
let value = RwSignal::new(0.0);
view! {
<Space vertical=true>
<Progress percentage show_indicator=false/>
<Progress percentage/>
<Progress percentage indicator_placement=ProgressIndicatorPlacement::Inside/>
<Progress percentage color=ProgressColor::Success/>
<Progress percentage color=ProgressColor::Warning/>
<Progress percentage color=ProgressColor::Error/>
<ProgressBar value/>
<ProgressBar value color=ProgressBarColor::Success/>
<ProgressBar value color=ProgressBarColor::Warning/>
<ProgressBar value color=ProgressBarColor::Error/>
<Space>
<Button on_click=move |_| percentage.update(|v| *v -= 10.0)>"-10%"</Button>
<Button on_click=move |_| percentage.update(|v| *v += 10.0)>"+10%"</Button>
<Button on_click=move |_| value.update(|v| *v -= 0.1)>"-10%"</Button>
<Button on_click=move |_| value.update(|v| *v += 0.1)>"+10%"</Button>
</Space>
</Space>
}
@ -22,18 +20,18 @@ view! {
### Circle
```rust demo
let percentage = create_rw_signal(0.0f32);
let value = RwSignal::new(0.0);
view! {
<Space>
<ProgressCircle percentage/>
<ProgressCircle percentage color=ProgressColor::Success/>
<ProgressCircle percentage color=ProgressColor::Warning/>
<ProgressCircle percentage color=ProgressColor::Error/>
<ProgressCircle value/>
<ProgressCircle value color=ProgressCircleColor::Success/>
<ProgressCircle value color=ProgressCircleColor::Warning/>
<ProgressCircle value color=ProgressCircleColor::Error/>
</Space>
<Space>
<Button on_click=move |_| percentage.update(|v| *v -= 10.0)>"-10%"</Button>
<Button on_click=move |_| percentage.update(|v| *v += 10.0)>"+10%"</Button>
<Button on_click=move |_| value.update(|v| *v -= 10.0)>"-10%"</Button>
<Button on_click=move |_| value.update(|v| *v += 10.0)>"+10%"</Button>
</Space>
}
```

View file

@ -1,29 +1,25 @@
# Radio
```rust demo
let value = create_rw_signal(false);
let value = RwSignal::new(String::new());
let option_value = RwSignal::new(None);
view! {
<Radio value>"Click"</Radio>
}
```
### Group
```rust demo
let value = create_rw_signal(None);
view! {
<RadioGroup value>
<RadioItem key="a">
"Apple"
</RadioItem>
<RadioItem key="o">
"Orange"
</RadioItem>
</RadioGroup>
<Space vertical=true>
<RadioGroup value>
<Radio value="a" label="Apple"/>
<Radio value="o" label="Orange"/>
</RadioGroup>
<RadioGroup value=option_value>
<Radio value="a" label="Apple"/>
<Radio value="o" label="Orange"/>
</RadioGroup>
</Space>
<div style="margin-top: 1rem">
"value: " {move || format!("{:?}", value.get())}
"value: " {move || format!("{}", value.get())}
</div>
<div style="margin-top: 1rem">
"option_value: " {move || format!("{:?}", option_value.get())}
</div>
}
```

View file

@ -1,62 +0,0 @@
# Select
```rust demo
let value = create_rw_signal(None::<String>);
let options = vec![
SelectOption::new("RwSignal", String::from("rw_signal")),
SelectOption::new("Memo", String::from("memo")),
];
view! {
<Select value options />
}
```
# Multiple Select
```rust demo
let value = create_rw_signal(vec![
"rust".to_string(),
"javascript".to_string(),
"zig".to_string(),
"python".to_string(),
"cpp".to_string(),
]);
let options = vec![
MultiSelectOption::new("Rust", String::from("rust")).with_variant(TagVariant::Success),
MultiSelectOption::new("JavaScript", String::from("javascript")),
MultiSelectOption::new("Python", String::from("python")).with_variant(TagVariant::Warning),
MultiSelectOption::new("C++", String::from("cpp")).with_variant(TagVariant::Error),
MultiSelectOption::new("Lua", String::from("lua")),
MultiSelectOption::new("Zig", String::from("zig")),
];
view! {
<MultiSelect value options />
}
```
### Select Props
| Name | Type | Default | Description |
| ------- | ----------------------------------- | -------------------- | ----------------------------------------- |
| class | `OptionalProp<MaybeSignal<String>>` | `Default::default()` | Addtional classes for the select element. |
| value | `Model<Option<T>>` | `None` | Checked value. |
| options | `MaybeSignal<Vec<SelectOption<T>>>` | `vec![]` | Options that can be selected. |
### Multiple Select Props
| Name | Type | Default | Description |
| --------- | ----------------------------------- | -------------------- | ----------------------------------------- |
| class | `OptionalProp<MaybeSignal<String>>` | `Default::default()` | Addtional classes for the select element. |
| value | `Model<Vec<T>>` | `vec![]` | Checked values. |
| options | `MaybeSignal<Vec<SelectOption<T>>>` | `vec![]` | Options that can be selected. |
| clearable | `MaybeSignal<bool>` | `false` | Allow the options to be cleared. |
### Select Slots
| Name | Default | Description |
| ----------- | ------- | ------------- |
| SelectLabel | `None` | Select label. |

View file

@ -2,8 +2,9 @@
```rust demo
view! {
<Skeleton repeat=2 text=true/>
<Skeleton width="60%" text=true/>
<Skeleton>
<SkeletonItem/>
</Skeleton>
}
```

View file

@ -1,7 +1,7 @@
# Slider
```rust demo
let value = create_rw_signal(0.0);
let value = RwSignal::new(0.0);
view! {
<Slider value/>
@ -11,20 +11,20 @@ view! {
### Step
```rust demo
let value = create_rw_signal(0.0);
let value = RwSignal::new(0.0);
view! {
<Slider step=10.0 value/>
<Slider step=25.0 value/>
}
```
## Slider Label
```rust demo
let value = create_rw_signal(0.0);
let value = RwSignal::new(0.0);
view! {
<Slider value max=10.0 step=1.0>
<Slider value max=10.0 step=5.0>
<SliderLabel value=0.0>
"0"
</SliderLabel>

View file

@ -45,18 +45,18 @@ view! {
view! {
<Space vertical=true>
<Space align=SpaceAlign::Start>
<Button style="height: 60px">"Start"</Button>
<Button style="height: 40px">"Start"</Button>
<Button attr:style="height: 60px">"Start"</Button>
<Button attr:style="height: 40px">"Start"</Button>
<Button>"Start"</Button>
</Space>
<Space align=SpaceAlign::Center>
<Button style="height: 60px">"Center"</Button>
<Button style="height: 40px">"Center"</Button>
<Button attr:style="height: 60px">"Center"</Button>
<Button attr:style="height: 40px">"Center"</Button>
<Button>"Center"</Button>
</Space>
<Space align=SpaceAlign::End>
<Button style="height: 60px">"End"</Button>
<Button style="height: 40px">"End"</Button>
<Button attr:style="height: 60px">"End"</Button>
<Button attr:style="height: 40px">"End"</Button>
<Button>"End"</Button>
</Space>
</Space>

View file

@ -0,0 +1,77 @@
# SpinButton
```rust demo
let value = RwSignal::new(0);
let value_f64 = RwSignal::new(0.0);
view! {
<Space vertical=true>
<SpinButton value step_page=1/>
<SpinButton value=value_f64 step_page=1.2/>
</Space>
}
```
### Min / Max
```rust demo
let value = RwSignal::new(0);
view! {
<SpinButton value step_page=1 min=-1 max=2/>
}
```
### Disabled
```rust demo
let value = RwSignal::new(0);
view! {
<SpinButton value step_page=1 disabled=true/>
}
```
### Custom parsing
```rust demo
let value = RwSignal::new(0.0);
let format = move |v: f64| {
let v = v.to_string();
let dot_pos = v.chars().position(|c| c == '.').unwrap_or_else(|| v.chars().count());
let mut int: String = v.chars().take(dot_pos).collect();
let sign: String = if v.chars().take(1).collect::<String>() == String::from("-") {
int = int.chars().skip(1).collect();
String::from("-")
} else {
String::from("")
};
let dec: String = v.chars().skip(dot_pos + 1).take(2).collect();
let int = int
.as_bytes()
.rchunks(3)
.rev()
.map(std::str::from_utf8)
.collect::<Result<Vec<&str>, _>>()
.unwrap()
.join(".");
format!("{}{},{:0<2}", sign, int, dec)
};
let parser = move |v: String| {
let comma_pos = v.chars().position(|c| c == ',').unwrap_or_else(|| v.chars().count());
let int_part = v.chars().take(comma_pos).filter(|a| a.is_digit(10)).collect::<String>();
let dec_part = v.chars().skip(comma_pos + 1).take(2).filter(|a| a.is_digit(10)).collect::<String>();
format!("{:0<1}.{:0<2}", int_part, dec_part).parse::<f64>().ok()
};
view! {
<SpinButton value parser format step_page=1.0 />
<p>"Underlying value: "{ value }</p>
}
```

View file

@ -10,11 +10,15 @@ view! {
```rust demo
view! {
<Space>
<Spinner size=SpinnerSize::Tiny/>
<Spinner size=SpinnerSize::Small/>
<Spinner size=SpinnerSize::Medium/>
<Spinner size=SpinnerSize::Large/>
<Space vertical=true>
<Spinner size=SpinnerSize::ExtraTiny label="Extra Tiny Spinner"/>
<Spinner size=SpinnerSize::Tiny label="Tiny Spinner"/>
<Spinner size=SpinnerSize::ExtraSmall label="Extra Small Spinner"/>
<Spinner size=SpinnerSize::Small label="Small Spinner"/>
<Spinner size=SpinnerSize::Medium label="Medium Spinner"/>
<Spinner size=SpinnerSize::Large label="Large Spinner"/>
<Spinner size=SpinnerSize::ExtraLarge label="Extra Large Spinner"/>
<Spinner size=SpinnerSize::Huge label="Huge Spinner"/>
</Space>
}
```

View file

@ -1,26 +1,16 @@
# Switch
```rust demo
let value = create_rw_signal(false);
let message = use_message();
let on_change = move |value: bool| {
message.create(
format!("{value}"),
MessageVariant::Success,
Default::default(),
);
};
let checked = RwSignal::new(false);
view! {
<Switch value on_change/>
<Switch checked />
}
```
### Switch Props
| Name | Type | Default | Description |
| --------- | ----------------------------------- | -------------------- | ------------------------------------------- |
| class | `OptionalProp<MaybeSignal<String>>` | `Default::default()` | Addtional classes for the switch element. |
| value | `Model<bool>` | `false` | Switch's value. |
| on_change | `Option<Callback>` | `None` | Trigger when the checked state is changing. |
| Name | Type | Default | Description |
| ----- | ----------------------------------- | -------------------- | ----------------------------------------- |
| class | `OptionalProp<MaybeSignal<String>>` | `Default::default()` | Addtional classes for the switch element. |
| value | `Model<bool>` | `false` | Switch's value. |

View file

@ -1,44 +1,23 @@
# Tabs
```rust demo
let value = create_rw_signal(String::from("apple"));
let selected_value = RwSignal::new(String::new());
view! {
<Tabs value>
<Tab key="apple" label="Apple">
"apple"
<TabList selected_value>
<Tab value="apple">
"Apple"
</Tab>
<Tab key="pear" label="Pear">
"pear"
<Tab value="pear">
"Pear"
</Tab>
</Tabs>
}
```
### Custom tab label
```rust demo
use leptos_meta::Style;
let value = create_rw_signal(String::from("apple"));
view! {
<Style id="demo-tab-label">
".p-0 { padding: 0 }"
</Style>
<Tabs value>
<Tab key="apple">
<TabLabel slot class="p-0">
"🍎 Apple"
</TabLabel>
"apple"
<Tab value="item1">
"Item 1"
</Tab>
<Tab key="pear">
<TabLabel slot>
"🍐 Pear"
</TabLabel>
"pear"
<Tab value="item2">
"Item 2"
</Tab>
</Tabs>
</TabList>
}
```

View file

@ -3,25 +3,37 @@
```rust demo
view! {
<Table>
<thead>
<tr>
<th>"tag"</th>
<th>"count"</th>
<th>"date"</th>
</tr>
</thead>
<tbody>
<tr>
<td>"div"</td>
<td>"2"</td>
<td>"2023-10-08"</td>
</tr>
<tr>
<td>"span"</td>
<td>"2"</td>
<td>"2023-10-08"</td>
</tr>
</tbody>
<TableHeader>
<TableRow>
<TableHeaderCell>"Tag"</TableHeaderCell>
<TableHeaderCell>"Count"</TableHeaderCell>
<TableHeaderCell>"Date"</TableHeaderCell>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>
<TableCellLayout>
"div"
</TableCellLayout>
</TableCell>
<TableCell>
<TableCellLayout>
"2"
</TableCellLayout>
</TableCell>
<TableCell>
<TableCellLayout>
"2023-10-08"
</TableCellLayout>
</TableCell>
</TableRow>
<TableRow>
<TableCell>"span"</TableCell>
<TableCell>"2"</TableCell>
<TableCell>"2023-10-08"</TableCell>
</TableRow>
</TableBody>
</Table>
}
```

View file

@ -2,34 +2,24 @@
```rust demo
view! {
<Space>
<Tag>"default"</Tag>
<Tag variant=TagVariant::Success>"success"</Tag>
<Tag variant=TagVariant::Warning>"warning"</Tag>
<Tag variant=TagVariant::Error>"error"</Tag>
</Space>
<Tag>"default"</Tag>
}
```
### Closable
```rust demo
let message = use_message();
let success = move |_| {
message.create(
"tag close".into(),
MessageVariant::Success,
Default::default(),
);
// let message = use_message();
let success = move |_: ev::MouseEvent| {
// message.create(
// "tag close".into(),
// MessageVariant::Success,
// Default::default(),
// );
};
view! {
<Space>
<Tag closable=true on_close=success>"Default"</Tag>
<Tag closable=true on_close=success variant=TagVariant::Success>"Success"</Tag>
<Tag closable=true on_close=success variant=TagVariant::Warning>"Warning"</Tag>
<Tag closable=true on_close=success variant=TagVariant::Error>"Error"</Tag>
</Space>
<Tag closable=true on_close=success>"Default"</Tag>
}
```

View file

@ -4,7 +4,10 @@
view! {
<Space>
<Text>"text"</Text>
<Text code=true>"code"</Text>
<Text tag=TextTag::Code>"code"</Text>
<Caption1>"Caption1"</Caption1>
<Caption1Strong>"Caption1Strong"</Caption1Strong>
<Body1>"Body1"</Body1>
</Space>
}
```

View file

@ -0,0 +1,35 @@
# Input
```rust demo
let value = RwSignal::new(String::from("o"));
view! {
<Textarea value placeholder="Textarea"/>
}
```
### Disabled
```rust demo
let value = RwSignal::new(String::from("o"));
view! {
<Textarea value disabled=true/>
}
```
### Resize
```rust demo
view! {
<Space vertical=true>
<Textarea placeholder=r#"Textarea with resize set to "none""#/>
<Textarea placeholder=r#"Textarea with resize set to "vertical""# resize=TextareaResize::Vertical/>
<Textarea placeholder=r#"Textarea with resize set to "horizontal""# resize=TextareaResize::Horizontal/>
<Textarea placeholder=r#"Textarea with resize set to "both""# resize=TextareaResize::Both/>
</Space>
}
```
### Textarea Props

View file

@ -1,63 +0,0 @@
# Theme
### ThemeProvider
```rust demo
let theme = create_rw_signal(Theme::light());
view! {
<ThemeProvider theme>
<Card>
<Space>
<Button on_click=move |_| theme.set(Theme::light())>"Light"</Button>
<Button on_click=move |_| theme.set(Theme::dark())>"Dark"</Button>
</Space>
</Card>
</ThemeProvider>
}
```
### GlobalStyle
You can use GlobalStyle to sync common global style to the body element.
```rust
let theme = create_rw_signal(Theme::light());
view! {
<ThemeProvider theme>
<GlobalStyle />
"..."
</ThemeProvider>
}
```
### Customize Theme
```rust demo
let theme = create_rw_signal(Theme::light());
let on_customize_theme = move |_| {
theme.update(|theme| {
theme.common.color_primary = "#f5222d".to_string();
theme.common.color_primary_hover = "#ff4d4f".to_string();
theme.common.color_primary_active = "#cf1322".to_string();
});
};
view! {
<ThemeProvider theme>
<Card>
<Space>
<Button on_click=move |_| theme.set(Theme::light())>"Light"</Button>
<Button on_click=on_customize_theme>"Customize Theme"</Button>
</Space>
</Card>
</ThemeProvider>
}
```
### ThemeProvider Props
| Name | Type | Default | Description |
| ----- | ------------------------- | -------------------- | ----------- |
| theme | `Option<RwSignal<Theme>>` | `Default::default()` | Theme. |

View file

@ -3,10 +3,14 @@
```rust demo
use chrono::prelude::*;
let value = create_rw_signal(Some(Local::now().time()));
let value = RwSignal::new(Local::now().time());
let option_value = RwSignal::new(Local::now().time());
view! {
<TimePicker value />
<Space vertical=true>
<TimePicker value />
<TimePicker value=option_value />
</Space>
}
```

View file

@ -0,0 +1,64 @@
# Toast
```rust demo
let toaster = ToasterInjection::expect_context();
let on_click = move |_| {
toaster.dispatch_toast(view! {
<Toast>
<ToastTitle>"Email sent"</ToastTitle>
<ToastBody>
"This is a toast body"
<ToastBodySubtitle slot>
"Subtitle"
</ToastBodySubtitle>
</ToastBody>
<ToastFooter>
"Footer"
// <Link>Action</Link>
// <Link>Action</Link>
</ToastFooter>
</Toast>
}.into_any(), Default::default());
};
view! {
<Button on_click=on_click>"Make toast"</Button>
}
```
### Toast Positions
```rust demo
let toaster = ToasterInjection::expect_context();
fn dispatch_toast(toaster: ToasterInjection, position: ToastPosition) {
toaster.dispatch_toast(view! {
<Toast>
<ToastTitle>"Email sent"</ToastTitle>
<ToastBody>
"This is a toast body"
<ToastBodySubtitle slot>
"Subtitle"
</ToastBodySubtitle>
</ToastBody>
<ToastFooter>
"Footer"
// <Link>Action</Link>
// <Link>Action</Link>
</ToastFooter>
</Toast>
}.into_any(), ToastOptions::default().with_position(position));
};
view! {
<Space>
<Button on_click=move |_| dispatch_toast(toaster, ToastPosition::Bottom)>"Bottom"</Button>
<Button on_click=move |_| dispatch_toast(toaster, ToastPosition::BottomStart)>"BottomStart"</Button>
<Button on_click=move |_| dispatch_toast(toaster, ToastPosition::BottomEnd)>"BottomEnd"</Button>
<Button on_click=move |_| dispatch_toast(toaster, ToastPosition::Top)>"Top"</Button>
<Button on_click=move |_| dispatch_toast(toaster, ToastPosition::TopStart)>"Topstart"</Button>
<Button on_click=move |_| dispatch_toast(toaster, ToastPosition::TopEnd)>"TopEnd"</Button>
</Space>
}
```

View file

@ -1,13 +1,15 @@
# Upload
```rust demo
let message = use_message();
let custom_request = move |file_list: FileList| {
message.create(
format!("Number of uploaded files: {}", file_list.length()),
MessageVariant::Success,
Default::default(),
);
use send_wrapper::SendWrapper;
// let message = use_message();
let custom_request = move |file_list: SendWrapper<FileList>| {
// message.create(
// format!("Number of uploaded files: {}", file_list.length()),
// MessageVariant::Success,
// Default::default(),
// );
};
view!{
@ -22,13 +24,14 @@ view!{
### Drag to upload
```rust demo
let message = use_message();
// let message = use_message();
let custom_request = move |file_list: FileList| {
message.create(
format!("Number of uploaded files: {}", file_list.length()),
MessageVariant::Success,
Default::default(),
);
// message.create(
// format!("Number of uploaded files: {}", file_list.length()),
// MessageVariant::Success,
// Default::default(),
// );
};
view! {

View file

@ -21,15 +21,11 @@ macro_rules! file_path {
pub fn include_md(_token_stream: proc_macro::TokenStream) -> proc_macro::TokenStream {
let file_list = file_path! {
"DevelopmentComponentsMdPage" => "../docs/_guide/development/components.md",
"DevelopmentGuideMdPage" => "../docs/_guide/development/guide.md",
"InstallationMdPage" => "../docs/_guide/installation.md",
"ServerSiderRenderingMdPage" => "../docs/_guide/server_sider_rendering.md",
"UsageMdPage" => "../docs/_guide/usage.md",
"NavBarMdPage" => "../docs/_mobile/nav_bar/mod.md",
"TabbarMdPage" => "../docs/_mobile/tabbar/mod.md",
"ToastMdPage" => "../docs/_mobile/toast/mod.md",
"AlertMdPage" => "../docs/alert/mod.md",
"AnchorMdPage" => "../docs/anchor/mod.md",
"AccordionMdPage" => "../../thaw/src/accordion/docs/mod.md",
// "AlertMdPage" => "../docs/alert/mod.md",
"AnchorMdPage" => "../../thaw/src/anchor/docs/mod.md",
"AutoCompleteMdPage" => "../docs/auto_complete/mod.md",
"AvatarMdPage" => "../docs/avatar/mod.md",
"BackTopMdPage" => "../docs/back_top/mod.md",
@ -39,40 +35,41 @@ pub fn include_md(_token_stream: proc_macro::TokenStream) -> proc_macro::TokenSt
"CalendarMdPage" => "../docs/calendar/mod.md",
"CardMdPage" => "../docs/card/mod.md",
"CheckboxMdPage" => "../docs/checkbox/mod.md",
"CollapseMdPage" => "../docs/collapse/mod.md",
"ColorPickerMdPage" => "../docs/color_picker/mod.md",
"ComboboxMdPage" => "../../thaw/src/combobox/docs/mod.md",
"ConfigProviderMdPage" => "../docs/config_provider/mod.md",
"DatePickerMdPage" => "../docs/date_picker/mod.md",
"DialogMdPage" => "../docs/dialog/mod.md",
"DividerMdPage" => "../docs/divider/mod.md",
"DrawerMdPage" => "../docs/drawer/mod.md",
"GridMdPage" => "../docs/grid/mod.md",
"IconMdPage" => "../docs/icon/mod.md",
"ImageMdPage" => "../docs/image/mod.md",
"InputMdPage" => "../docs/input/mod.md",
"InputNumberMdPage" => "../docs/input_number/mod.md",
"LayoutMdPage" => "../docs/layout/mod.md",
"LoadingBarMdPage" => "../docs/loading_bar/mod.md",
"MenuMdPage" => "../docs/menu/mod.md",
"MessageMdPage" => "../docs/message/mod.md",
"ModalMdPage" => "../docs/modal/mod.md",
"PaginationMdPage" => "../docs/pagination/mod.md",
"MessageBarMdPage" => "../docs/message_bar/mod.md",
"NavMdPage" => "../docs/nav/mod.md",
"PaginationMdPage" => "../../thaw/src/pagination/docs/mod.md",
"PopoverMdPage" => "../docs/popover/mod.md",
"ProgressMdPage" => "../docs/progress/mod.md",
"ProgressBarMdPage" => "../docs/progress_bar/mod.md",
"RadioMdPage" => "../docs/radio/mod.md",
"ScrollbarMdPage" => "../docs/scrollbar/mod.md",
"SelectMdPage" => "../docs/select/mod.md",
"SkeletonMdPage" => "../docs/skeleton/mod.md",
"SliderMdPage" => "../docs/slider/mod.md",
"SpaceMdPage" => "../docs/space/mod.md",
"SpinButtonMdPage" => "../docs/spin_button/mod.md",
"SpinnerMdPage" => "../docs/spinner/mod.md",
"SwitchMdPage" => "../docs/switch/mod.md",
"TabListMdPage" => "../docs/tab_list/mod.md",
"TableMdPage" => "../docs/table/mod.md",
"TabsMdPage" => "../docs/tabs/mod.md",
"TagMdPage" => "../docs/tag/mod.md",
"ThemeMdPage" => "../docs/theme/mod.md",
"TextMdPage" => "../docs/text/mod.md",
"TextareaMdPage" => "../docs/textarea/mod.md",
"TimePickerMdPage" => "../docs/time_picker/mod.md",
"TypographyMdPage" => "../docs/typography/mod.md",
"UploadMdPage" => "../docs/upload/mod.md",
"DropdownMdPage" => "../docs/dropdown/mod.md"
"ToastMdPage" => "../docs/toast/mod.md",
"UploadMdPage" => "../docs/upload/mod.md"
};
let mut fn_list = vec![];
@ -98,7 +95,7 @@ pub fn include_md(_token_stream: proc_macro::TokenStream) -> proc_macro::TokenSt
links
);
syn::parse_str::<ItemFn>(&toc)
.unwrap_or_else(|_| panic!("Cannot be resolved as a function: \n {toc}"))
.expect(&format!("Cannot be resolved as a function: \n {toc}"))
};
let demos: Vec<ItemFn> = demos

View file

@ -18,7 +18,8 @@ pub fn to_tokens(code_block: &NodeCodeBlock, demos: &mut Vec<String>) -> TokenSt
let literal = langs
.iter()
.find(|lang| lang != &&"demo")
.and_then(|lang| highlight_to_html(&code_block.literal, lang))
.map(|lang| highlight_to_html(&code_block.literal, lang))
.flatten()
.unwrap_or_else(|| {
is_highlight = false;
code_block.literal.clone()
@ -27,8 +28,7 @@ pub fn to_tokens(code_block: &NodeCodeBlock, demos: &mut Vec<String>) -> TokenSt
quote! {
<Demo>
<#demo />
<DemoCode slot is_highlight=#is_highlight>
#literal
<DemoCode slot is_highlight=#is_highlight text=#literal>
</DemoCode>
</Demo>
}
@ -36,15 +36,15 @@ pub fn to_tokens(code_block: &NodeCodeBlock, demos: &mut Vec<String>) -> TokenSt
let mut is_highlight = true;
let literal = langs
.first()
.and_then(|lang| highlight_to_html(&code_block.literal, lang))
.map(|lang| highlight_to_html(&code_block.literal, lang))
.flatten()
.unwrap_or_else(|| {
is_highlight = false;
code_block.literal.clone()
});
quote! {
<Demo>
<DemoCode slot is_highlight=#is_highlight>
#literal
<DemoCode slot is_highlight=#is_highlight text=#literal>
</DemoCode>
</Demo>
}
@ -54,8 +54,10 @@ pub fn to_tokens(code_block: &NodeCodeBlock, demos: &mut Vec<String>) -> TokenSt
static SYNTAX_SET: OnceLock<SyntaxSet> = OnceLock::new();
fn highlight_to_html(text: &str, syntax: &str) -> Option<String> {
let syntax_set = SYNTAX_SET.get_or_init(SyntaxSet::load_defaults_newlines);
let syntax = syntax_set.find_syntax_by_token(syntax)?;
let syntax_set = SYNTAX_SET.get_or_init(|| SyntaxSet::load_defaults_newlines());
let Some(syntax) = syntax_set.find_syntax_by_token(syntax) else {
return None;
};
let mut html_generator = ClassedHTMLGenerator::new_with_class_style(
syntax,

View file

@ -1,14 +1,13 @@
mod code_block;
use comrak::{
nodes::{AstNode, LineColumn, NodeValue},
nodes::{AstNode, LineColumn, NodeLink, NodeValue},
parse_document, Arena,
};
use proc_macro2::{Ident, Span, TokenStream};
use quote::quote;
use syn::ItemMacro;
#[allow(clippy::type_complexity)]
pub fn parse_markdown(
md_text: &str,
) -> Result<(TokenStream, Vec<String>, Vec<(String, String)>), String> {
@ -19,7 +18,7 @@ pub fn parse_markdown(
let mut options = comrak::Options::default();
options.extension.table = true;
let root = parse_document(&arena, md_text, &options);
let root = parse_document(&arena, &md_text, &options);
let body = iter_nodes(md_text, root, &mut demos, &mut toc);
Ok((body, demos, toc))
}
@ -48,12 +47,10 @@ fn iter_nodes<'a>(
NodeValue::HtmlBlock(node_html_block) => {
let html =
syn::parse_str::<ItemMacro>(&format!("view! {{ {} }}", node_html_block.literal))
.unwrap_or_else(|_| {
panic!(
"Cannot be resolved as a macro: \n {}",
node_html_block.literal
)
});
.expect(&format!(
"Cannot be resolved as a macro: \n {}",
node_html_block.literal
));
quote!(
{
#html
@ -67,10 +64,10 @@ fn iter_nodes<'a>(
),
NodeValue::Heading(node_h) => {
let sourcepos = node.data.borrow().sourcepos;
let text = range_text(md_text, sourcepos.start, sourcepos.end);
let text = range_text(md_text, sourcepos.start.clone(), sourcepos.end.clone());
let level = node_h.level as usize + 1;
let text = text[level..].to_string();
let h_id = text.replace(' ', "-").to_ascii_lowercase().to_string();
let h_id = format!("{}", text.replace(' ', "-").to_ascii_lowercase());
toc.push((h_id.clone(), text));
let h = Ident::new(&format!("h{}", node_h.level), Span::call_site());
quote!(
@ -101,22 +98,22 @@ fn iter_nodes<'a>(
quote!(
<div class="demo-md-table-box">
<Table single_column=true>
<thead>
<Table attr:style="table-layout: auto">
<TableHeader>
#(#header_children)*
</thead>
<tbody>
</TableHeader>
<TableBody>
#(#children)*
</tbody>
</TableBody>
</Table>
</div>
)
}
NodeValue::TableRow(_) => {
quote!(
<tr>
<TableRow>
#(#children)*
</tr>
</TableRow>
)
}
NodeValue::TableCell => {
@ -127,15 +124,15 @@ fn iter_nodes<'a>(
};
if is_header {
quote!(
<th>
<TableHeaderCell>
#(#children)*
</th>
</TableHeaderCell>
)
} else {
quote!(
<td>
<TableCell>
#(#children)*
</td>
</TableCell>
)
}
}
@ -149,7 +146,7 @@ fn iter_nodes<'a>(
NodeValue::Code(node_code) => {
let code = node_code.literal.clone();
quote!(
<Text code=true>
<Text tag=TextTag::Code>
#code
</Text>
)
@ -159,7 +156,15 @@ fn iter_nodes<'a>(
NodeValue::Strong => quote!("Strong todo!!!"),
NodeValue::Strikethrough => quote!("Strikethrough todo!!!"),
NodeValue::Superscript => quote!("Superscript todo!!!"),
NodeValue::Link(_) => quote!("Link todo!!!"),
NodeValue::Link(node_link) => {
let NodeLink { url, title } = node_link;
quote!(
<a href=#url title=#title>
#(#children)*
</a>
)
}
NodeValue::Image(_) => quote!("Image todo!!!"),
NodeValue::FootnoteReference(_) => quote!("FootnoteReference todo!!!"),
NodeValue::MultilineBlockQuote(_) => quote!("FootnoteReference todo!!!"),
@ -191,7 +196,7 @@ fn range_text(text: &str, start: LineColumn, end: LineColumn) -> &str {
let mut current_line_num = start_line + 1;
while current_line_num < end_line {
let next_line = lines.next().unwrap_or("");
start_line_text = next_line;
start_line_text = &next_line;
current_line_num += 1;
}

View file

@ -9,30 +9,23 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
axum = { version = "0.7.4", optional = true }
console_error_panic_hook = "0.1"
console_log = "1"
cfg-if = "1"
leptos = { version = "0.6.10" }
leptos_axum = { version = "0.6.10", optional = true }
leptos_meta = { version = "0.6.10" }
leptos_router = { version = "0.6.10" }
log = "0.4"
simple_logger = "4"
tokio = { version = "1.35.1", features = ["rt-multi-thread"], optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.5.1", features = ["fs"], optional = true }
leptos = { git = "https://github.com/leptos-rs/leptos", rev = "867036559489e86c1f25cdf7133d803d88b0579b" }
leptos_axum = { git = "https://github.com/leptos-rs/leptos", rev = "867036559489e86c1f25cdf7133d803d88b0579b", optional = true }
leptos_meta = { git = "https://github.com/leptos-rs/leptos", rev = "867036559489e86c1f25cdf7133d803d88b0579b" }
leptos_router = { git = "https://github.com/leptos-rs/leptos", rev = "867036559489e86c1f25cdf7133d803d88b0579b" }
tokio = { version = "1", features = ["rt-multi-thread"], optional = true }
tower = { version = "0.4", optional = true }
tower-http = { version = "0.5", features = ["fs"], optional = true }
wasm-bindgen = "=0.2.92"
thiserror = "1.0.56"
tracing = { version = "0.1.40", optional = true }
http = "0.2.8"
thiserror = "1"
tracing = { version = "0.1", optional = true }
http = "1"
console_log = "1"
log = "0.4"
demo = { path = "../../demo", default-features = false }
[features]
hydrate = [
"leptos/hydrate",
"leptos_meta/hydrate",
"leptos_router/hydrate",
"demo/hydrate",
]
hydrate = ["leptos/hydrate", "demo/hydrate"]
ssr = [
"dep:axum",
"dep:tokio",

View file

@ -1,74 +0,0 @@
{
"name": "end2end",
"version": "1.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "end2end",
"version": "1.0.0",
"license": "ISC",
"devDependencies": {
"@playwright/test": "^1.28.0"
}
},
"node_modules/@playwright/test": {
"version": "1.28.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.28.0.tgz",
"integrity": "sha512-vrHs5DFTPwYox5SGKq/7TDn/S4q6RA1zArd7uhO6EyP9hj3XgZBBM12ktMbnDQNxh/fL1IUKsTNLxihmsU38lQ==",
"dev": true,
"dependencies": {
"@types/node": "*",
"playwright-core": "1.28.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@types/node": {
"version": "18.11.9",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz",
"integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==",
"dev": true
},
"node_modules/playwright-core": {
"version": "1.28.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.28.0.tgz",
"integrity": "sha512-nJLknd28kPBiCNTbqpu6Wmkrh63OEqJSFw9xOfL9qxfNwody7h6/L3O2dZoWQ6Oxcm0VOHjWmGiCUGkc0X3VZA==",
"dev": true,
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=14"
}
}
},
"dependencies": {
"@playwright/test": {
"version": "1.28.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.28.0.tgz",
"integrity": "sha512-vrHs5DFTPwYox5SGKq/7TDn/S4q6RA1zArd7uhO6EyP9hj3XgZBBM12ktMbnDQNxh/fL1IUKsTNLxihmsU38lQ==",
"dev": true,
"requires": {
"@types/node": "*",
"playwright-core": "1.28.0"
}
},
"@types/node": {
"version": "18.11.9",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz",
"integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==",
"dev": true
},
"playwright-core": {
"version": "1.28.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.28.0.tgz",
"integrity": "sha512-nJLknd28kPBiCNTbqpu6Wmkrh63OEqJSFw9xOfL9qxfNwody7h6/L3O2dZoWQ6Oxcm0VOHjWmGiCUGkc0X3VZA==",
"dev": true
}
}
}

View file

@ -1,13 +0,0 @@
{
"name": "end2end",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@playwright/test": "^1.28.0"
}
}

View file

@ -1,107 +0,0 @@
import type { PlaywrightTestConfig } from "@playwright/test";
import { devices } from "@playwright/test";
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
const config: PlaywrightTestConfig = {
testDir: "./tests",
/* Maximum time one test can run for. */
timeout: 30 * 1000,
expect: {
/**
* Maximum time expect() should wait for the condition to be met.
* For example in `await expect(locator).toHaveText();`
*/
timeout: 5000,
},
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
actionTimeout: 0,
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://localhost:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
},
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: {
...devices["Desktop Chrome"],
},
},
{
name: "firefox",
use: {
...devices["Desktop Firefox"],
},
},
{
name: "webkit",
use: {
...devices["Desktop Safari"],
},
},
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: {
// ...devices['Pixel 5'],
// },
// },
// {
// name: 'Mobile Safari',
// use: {
// ...devices['iPhone 12'],
// },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: {
// channel: 'msedge',
// },
// },
// {
// name: 'Google Chrome',
// use: {
// channel: 'chrome',
// },
// },
],
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
// outputDir: 'test-results/',
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// port: 3000,
// },
};
export default config;

View file

@ -1,9 +0,0 @@
import { test, expect } from "@playwright/test";
test("homepage has title and links to intro page", async ({ page }) => {
await page.goto("http://localhost:3000/");
await expect(page).toHaveTitle("Welcome to Leptos");
await expect(page.locator("h1")).toHaveText("Welcome to Leptos!");
});

View file

@ -0,0 +1,21 @@
pub use demo::App;
use leptos::prelude::*;
use leptos_meta::MetaTags;
pub fn shell(options: LeptosOptions) -> impl IntoView {
view! {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<AutoReload options=options.clone() />
<HydrationScripts options/>
<MetaTags/>
</head>
<body>
<App/>
</body>
</html>
}
}

View file

@ -1,42 +0,0 @@
use axum::{
body::Body,
extract::State,
http::{Request, Response, StatusCode, Uri},
response::{IntoResponse, Response as AxumResponse},
};
use demo::App;
use leptos::{view, LeptosOptions};
use tower::ServiceExt;
use tower_http::services::ServeDir;
pub async fn file_and_error_handler(
uri: Uri,
State(options): State<LeptosOptions>,
req: Request<Body>,
) -> AxumResponse {
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await.unwrap();
if res.status() == StatusCode::OK {
res.into_response()
} else {
let handler = leptos_axum::render_app_to_stream(options.to_owned(), move || view! {<App/>});
handler(req).await.into_response()
}
}
async fn get_static_file(uri: Uri, root: &str) -> Result<Response<Body>, (StatusCode, String)> {
let req = Request::builder()
.uri(uri.clone())
.body(Body::empty())
.unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// This path is relative to the cargo root
match ServeDir::new(root).oneshot(req).await {
Ok(res) => Ok(res.into_response()),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {err}"),
)),
}
}

View file

@ -1,14 +1,10 @@
#[cfg(feature = "ssr")]
pub mod fileserv;
pub mod app;
#[cfg(feature = "hydrate")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn hydrate() {
use demo::App;
// initializes logging using the `log` crate
_ = console_log::init_with_level(log::Level::Debug);
use crate::app::*;
let _ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::mount_to_body(App);
leptos::mount::hydrate_body(App);
}

View file

@ -1,34 +1,28 @@
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
use axum::{routing::post, Router};
use demo::App;
use leptos::*;
use axum::Router;
use leptos::prelude::*;
use leptos_axum::{generate_route_list, LeptosRoutes};
use ssr_axum::fileserv::file_and_error_handler;
use ssr_axum::app::*;
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
// Setting get_configuration(None) means we'll be using cargo-leptos's env values
// For deployment these variables are:
// <https://github.com/leptos-rs/start-axum#executing-a-server-on-a-remote-machine-without-the-toolchain>
// Alternately a file can be specified such as Some("Cargo.toml")
// The file would need to be included with the executable when moved to deployment
let conf = get_configuration(None).await.unwrap();
let conf = get_configuration(None).unwrap();
let addr = conf.leptos_options.site_addr;
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_addr;
// Generate the list of routes in your Leptos App
let routes = generate_route_list(App);
// build our application with a route
let app = Router::new()
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
.leptos_routes(&leptos_options, routes, App)
.fallback(file_and_error_handler)
.leptos_routes(&leptos_options, routes, {
let leptos_options = leptos_options.clone();
move || shell(leptos_options.clone())
})
.fallback(leptos_axum::file_and_error_handler(shell))
.with_state(leptos_options);
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
log::info!("listening on http://{}", &addr);
log!("listening on http://{}", &addr);
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
axum::serve(listener, app.into_make_service())
.await

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.3.3"
version = "0.4.0-alpha"
edition = "2021"
keywords = ["web", "leptos", "ui", "thaw", "component"]
readme = "../README.md"
@ -9,12 +9,14 @@ description = "An easy to use leptos component library"
homepage = "https://github.com/thaw-ui/thaw"
repository = "https://github.com/thaw-ui/thaw"
license = "MIT"
exclude = ["src/**/*.md"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
leptos = { version = "0.6.10" }
leptos = { workspace = true }
thaw_components = { workspace = true }
thaw_macro = { workspace = true }
thaw_utils = { workspace = true }
web-sys = { version = "0.3.69", features = [
"DomRect",
@ -23,17 +25,21 @@ web-sys = { version = "0.3.69", features = [
"DataTransfer",
"ScrollToOptions",
"ScrollBehavior",
"TreeWalker",
"NodeFilter",
] }
wasm-bindgen = "0.2.92"
icondata_core = "0.1.0"
icondata_ai = "0.0.10"
uuid = { version = "1.7.0", features = ["v4"] }
uuid = { version = "1.10.0", features = ["v4", "js"] }
cfg-if = "1.0.0"
chrono = "0.4.35"
palette = "0.7.5"
num-traits = "0.2.18"
send_wrapper = "0.6"
[features]
csr = ["leptos/csr", "thaw_components/csr", "thaw_utils/csr"]
ssr = ["leptos/ssr", "thaw_components/ssr", "thaw_utils/ssr"]
hydrate = ["leptos/hydrate", "thaw_components/hydrate", "thaw_utils/hydrate"]
nightly = ["leptos/nightly", "thaw_utils/nightly"]

View file

@ -0,0 +1,4 @@
mod use_active_descendant;
mod use_option_walker;
pub use use_active_descendant::{use_active_descendant, ActiveDescendantController};

View file

@ -0,0 +1,99 @@
use super::use_option_walker::{use_option_walker, OptionWalker};
use send_wrapper::SendWrapper;
use std::{cell::RefCell, sync::Arc};
use thaw_utils::scroll_into_view;
use web_sys::{HtmlElement, Node};
/// Applied to the element that is active descendant
const ACTIVEDESCENDANT_ATTRIBUTE: &str = "data-activedescendant";
/// Applied to the active descendant when the user is navigating with keyboard
const ACTIVEDESCENDANT_FOCUSVISIBLE_ATTRIBUTE: &str = "data-activedescendant-focusvisible";
pub fn use_active_descendant<MF>(
match_option: MF,
) -> (Arc<dyn Fn(Node) + Send + Sync>, ActiveDescendantController)
where
MF: Fn(HtmlElement) -> bool + Send + Sync + 'static,
{
let (set_listbox, option_walker) = use_option_walker(match_option);
//TODO
let set_listbox = Arc::new(move |node| {
set_listbox(&node);
});
let controller = ActiveDescendantController {
option_walker,
active: Arc::new(SendWrapper::new(Default::default())),
// last_active: Default::default(),
};
(set_listbox, controller)
}
#[derive(Clone)]
pub struct ActiveDescendantController {
option_walker: OptionWalker,
active: Arc<SendWrapper<RefCell<Option<HtmlElement>>>>,
// last_active: RefCell<Option<HtmlElement>>,
}
impl ActiveDescendantController {
fn blur_active_descendant(&self) {
let mut active = self.active.borrow_mut();
let Some(active_el) = active.as_mut() else {
return;
};
let _ = active_el.remove_attribute(ACTIVEDESCENDANT_ATTRIBUTE);
let _ = active_el.remove_attribute(ACTIVEDESCENDANT_FOCUSVISIBLE_ATTRIBUTE);
*active = None;
}
fn focus_active_descendant(&self, next_active: HtmlElement) {
self.blur_active_descendant();
scroll_into_view(&next_active);
let _ = next_active.set_attribute(ACTIVEDESCENDANT_ATTRIBUTE, "");
let _ = next_active.set_attribute(ACTIVEDESCENDANT_FOCUSVISIBLE_ATTRIBUTE, "");
*self.active.borrow_mut() = Some(next_active);
}
}
impl ActiveDescendantController {
pub fn first(&self) {
if let Some(first) = self.option_walker.first() {
self.focus_active_descendant(first);
}
}
pub fn last(&self) {
if let Some(last) = self.option_walker.last() {
self.focus_active_descendant(last);
}
}
pub fn next(&self) {
if let Some(next) = self.option_walker.next() {
self.focus_active_descendant(next);
}
}
pub fn prev(&self) {
if let Some(prev) = self.option_walker.prev() {
self.focus_active_descendant(prev);
}
}
pub fn blur(&self) {
self.blur_active_descendant();
}
pub fn active(&self) -> Option<HtmlElement> {
let active = self.active.borrow();
if let Some(active) = active.as_ref() {
Some(active.clone())
} else {
None
}
}
}

View file

@ -0,0 +1,90 @@
use leptos::{prelude::document, reactive_graph::owner::StoredValue};
use send_wrapper::SendWrapper;
use std::sync::Arc;
use wasm_bindgen::{closure::Closure, JsCast, UnwrapThrowExt};
use web_sys::{HtmlElement, Node, NodeFilter, TreeWalker};
pub fn use_option_walker<MF>(match_option: MF) -> (Box<dyn Fn(&Node) + Send + Sync>, OptionWalker)
where
MF: Fn(HtmlElement) -> bool + Send + Sync + 'static,
{
let tree_walker = StoredValue::new(
None::<(
SendWrapper<TreeWalker>,
SendWrapper<Closure<dyn Fn(Node) -> u32>>,
)>,
);
let option_walker = OptionWalker(tree_walker);
let match_option = Arc::new(match_option);
let set_listbox = move |el: &Node| {
let match_option = match_option.clone();
let cb: Closure<dyn Fn(Node) -> u32> = Closure::new(move |node: Node| {
if let Ok(html_element) = node.dyn_into() {
if match_option(html_element) {
return 1u32;
}
}
3u32
});
let mut node_filter = NodeFilter::new();
node_filter.accept_node(cb.as_ref().unchecked_ref());
let tw = document()
.create_tree_walker_with_what_to_show_and_filter(el, 0x1, Some(&node_filter))
.unwrap_throw();
tree_walker.set_value(Some((SendWrapper::new(tw), SendWrapper::new(cb))));
};
(Box::new(set_listbox), option_walker)
}
#[derive(Clone)]
pub struct OptionWalker(
StoredValue<
Option<(
SendWrapper<TreeWalker>,
SendWrapper<Closure<dyn Fn(Node) -> u32>>,
)>,
>,
);
impl OptionWalker {
pub fn first(&self) -> Option<HtmlElement> {
self.0.with_value(|tree_walker| {
let Some((tree_walker, _)) = tree_walker.as_ref() else {
return None;
};
tree_walker.set_current_node(&tree_walker.root());
tree_walker.first_child().unwrap_throw()?.dyn_into().ok()
})
}
pub fn last(&self) -> Option<HtmlElement> {
self.0.with_value(|tree_walker| {
let Some((tree_walker, _)) = tree_walker.as_ref() else {
return None;
};
tree_walker.set_current_node(&tree_walker.root());
tree_walker.last_child().unwrap_throw()?.dyn_into().ok()
})
}
pub fn next(&self) -> Option<HtmlElement> {
self.0.with_value(|tree_walker| {
let Some((tree_walker, _)) = tree_walker.as_ref() else {
return None;
};
tree_walker.next_node().unwrap_throw()?.dyn_into().ok()
})
}
pub fn prev(&self) -> Option<HtmlElement> {
self.0.with_value(|tree_walker| {
let Some((tree_walker, _)) = tree_walker.as_ref() else {
return None;
};
tree_walker.previous_node().unwrap_throw()?.dyn_into().ok()
})
}
}

3
thaw/src/_aria/mod.rs Normal file
View file

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

View file

@ -0,0 +1,73 @@
.thaw-accordion-header {
margin: 0;
background-color: var(--colorTransparentBackground);
color: var(--colorNeutralForeground1);
border-radius: var(--borderRadiusMedium);
}
.thaw-accordion-header__button {
display: flex;
align-items: center;
position: relative;
padding-left: var(--spacingHorizontalMNudge);
padding-right: var(--spacingHorizontalM);
padding-top: 0px;
padding-bottom: 0px;
width: 100%;
min-height: 44px;
text-align: unset;
line-height: var(--lineHeightBase300);
font-family: var(--fontFamilyBase);
font-size: var(--fontSizeBase300);
font-weight: var(--fontWeightRegular);
background-color: inherit;
color: inherit;
border-width: 0px;
appearance: button;
overflow: visible;
box-sizing: border-box;
cursor: pointer;
}
.thaw-accordion-header__expand-icon {
display: flex;
align-items: center;
height: 100%;
padding-right: var(--spacingHorizontalS);
font-size: var(--fontSizeBase500);
line-height: var(--lineHeightBase500);
}
.thaw-accordion-header__expand-icon > svg {
display: inline;
line-height: 0;
}
.thaw-accordion-panel {
margin: 0 var(--spacingHorizontalM);
}
.thaw-accordion-panel-enter-from,
.thaw-accordion-panel-enter-to {
opacity: 1;
}
.thaw-accordion-panel-leave-to,
.thaw-accordion-panel-enter-from {
opacity: 0;
max-height: 0;
}
.thaw-accordion-panel-leave-active {
overflow: hidden;
transition: max-height 0.15s cubic-bezier(0.4, 0, 0.2, 1) 0s,
opacity 0.15s cubic-bezier(0, 0, 0.2, 1) 0s,
padding-top 0.15s cubic-bezier(0.4, 0, 0.2, 1) 0s;
}
.thaw-accordion-panel-enter-active {
overflow: hidden;
transition: max-height 0.15s cubic-bezier(0.4, 0, 0.2, 1),
opacity 0.15s cubic-bezier(0.4, 0, 1, 1),
padding-top 0.15s cubic-bezier(0.4, 0, 0.2, 1);
}

View file

@ -0,0 +1,101 @@
use crate::AccordionInjection;
use leptos::{html, prelude::*};
use thaw_components::CSSTransition;
use thaw_utils::{class_list, mount_style, update, with, StoredMaybeSignal};
#[component]
pub fn AccordionItem(
#[prop(optional, into)] class: MaybeProp<String>,
/// Required value that identifies this item inside an Accordion component.
#[prop(into)]
value: MaybeSignal<String>,
accordion_header: AccordionHeader,
children: Children,
) -> impl IntoView {
mount_style("accordion-item", include_str!("./accordion-item.css"));
let AccordionInjection {
open_items,
multiple,
collapsible,
} = AccordionInjection::expect_context();
let panel_ref = NodeRef::<html::Div>::new();
let value: StoredMaybeSignal<_> = value.into();
let is_show_panel = Memo::new(move |_| with!(|open_items, value| open_items.contains(value)));
let on_click = move |_| {
let is_show_panel = is_show_panel.get_untracked();
update!(move |open_items| {
if is_show_panel {
if collapsible {
with!(|value| open_items.remove(value));
} else if multiple {
with!(|value| open_items.remove(value));
}
} else {
if !multiple {
open_items.clear();
}
open_items.insert(value.get_untracked());
}
});
};
view! {
<div class=class_list!["thaw-accordion-item", class]>
<div class="thaw-accordion-header">
<button
class="thaw-accordion-header__button"
aria-expanded=move || is_show_panel.get().to_string()
type="button"
on:click=on_click
>
<span
class="thaw-accordion-header__expand-icon"
aria-hidden="true"
>
<svg
fill="currentColor"
aria-hidden="true"
width="1em"
height="1em"
viewBox="0 0 20 20"
style=move || if is_show_panel.get() {
"transform: rotate(90deg)"
} else {
"transform: rotate(0deg)"
}
>
<path d="M7.65 4.15c.2-.2.5-.2.7 0l5.49 5.46c.21.22.21.57 0 .78l-5.49 5.46a.5.5 0 0 1-.7-.7L12.8 10 7.65 4.85a.5.5 0 0 1 0-.7Z" fill="currentColor"></path>
</svg>
</span>
{(accordion_header.children)()}
</button>
</div>
<CSSTransition
node_ref=panel_ref
show=is_show_panel
name="thaw-accordion-panel"
let:display
>
<div
class="thaw-accordion-panel"
node_ref=panel_ref
style=move || display.get().unwrap_or_default()
>
{children()}
</div>
</CSSTransition>
</div>
}
}
#[slot]
pub struct AccordionHeader {
children: Children,
}
#[slot]
pub struct AccordionPanel {
children: Children,
}

View file

@ -0,0 +1,66 @@
# Accordion
```rust demo
view! {
<Accordion>
<AccordionItem value="leptos">
<AccordionHeader slot>
"Leptos"
</AccordionHeader>
"Build fast web applications with Rust."
</AccordionItem>
<AccordionItem value="thaw">
<AccordionHeader slot>
"Thaw"
</AccordionHeader>
"An easy to use leptos component library"
</AccordionItem>
</Accordion>
}
```
### Collapsible
An accordion can have multiple panels collapsed at the same time.
```rust demo
view! {
<Accordion collapsible=true>
<AccordionItem value="leptos">
<AccordionHeader slot>
"Leptos"
</AccordionHeader>
"Build fast web applications with Rust."
</AccordionItem>
<AccordionItem value="thaw">
<AccordionHeader slot>
"Thaw"
</AccordionHeader>
"An easy to use leptos component library"
</AccordionItem>
</Accordion>
}
```
### Multiple
An accordion supports multiple panels expanded simultaneously. Since it's not simple to determine which panel will be opened by default, multiple will also be collapsed by default on the first render.
```rust demo
view! {
<Accordion multiple=true>
<AccordionItem value="leptos">
<AccordionHeader slot>
"Leptos"
</AccordionHeader>
"Build fast web applications with Rust."
</AccordionItem>
<AccordionItem value="thaw">
<AccordionHeader slot>
"Thaw"
</AccordionHeader>
"An easy to use leptos component library"
</AccordionItem>
</Accordion>
}
```

47
thaw/src/accordion/mod.rs Normal file
View file

@ -0,0 +1,47 @@
mod accordion_item;
pub use accordion_item::*;
use leptos::{context::Provider, prelude::*};
use std::collections::HashSet;
use thaw_utils::{class_list, Model};
#[component]
pub fn Accordion(
#[prop(optional, into)] class: MaybeProp<String>,
/// Controls the state of the panel.
#[prop(optional, into)]
open_items: Model<HashSet<String>>,
/// Indicates if Accordion support multiple Panels opened at the same time.
#[prop(optional)]
multiple: bool,
/// Indicates if Accordion support multiple Panels closed at the same time.
#[prop(optional)]
collapsible: bool,
children: Children,
) -> impl IntoView {
view! {
<Provider value=AccordionInjection {
open_items,
collapsible,
multiple
}>
<div class=class_list!["thaw-accordion", class]>
{children()}
</div>
</Provider>
}
}
#[derive(Clone)]
pub(crate) struct AccordionInjection {
pub open_items: Model<HashSet<String>>,
pub multiple: bool,
pub collapsible: bool,
}
impl AccordionInjection {
pub fn expect_context() -> AccordionInjection {
expect_context()
}
}

View file

@ -1,27 +0,0 @@
.thaw-alert {
position: relative;
padding: 14px 20px 14px 42px;
background-color: var(--thaw-background-color);
border: 1px solid var(--thaw-border-color);
border-radius: 3px;
line-height: 1.6;
}
.thaw-alert__icon {
position: absolute;
top: 12px;
left: 10px;
font-size: 24px;
color: var(--thaw-icon-color);
}
.thaw-alert__header {
font-size: 16px;
line-height: 19px;
font-weight: 500;
}
.thaw-alert__header + .thaw-alert__content {
margin-top: 8px;
font-size: 14px;
}

View file

@ -1,96 +0,0 @@
mod theme;
pub use theme::AlertTheme;
use crate::{theme::use_theme, Icon, Theme};
use leptos::*;
use thaw_components::OptionComp;
use thaw_utils::{class_list, mount_style, OptionalProp};
#[derive(Clone)]
pub enum AlertVariant {
Success,
Warning,
Error,
}
impl AlertVariant {
fn theme_icon_color(&self, theme: &Theme) -> String {
match self {
AlertVariant::Success => theme.common.color_success.clone(),
AlertVariant::Warning => theme.common.color_warning.clone(),
AlertVariant::Error => theme.common.color_error.clone(),
}
}
fn theme_background_color(&self, theme: &Theme) -> String {
match self {
AlertVariant::Success => theme.alert.success_background_color.clone(),
AlertVariant::Warning => theme.alert.warning_background_color.clone(),
AlertVariant::Error => theme.alert.error_background_color.clone(),
}
}
fn theme_border_color(&self, theme: &Theme) -> String {
match self {
AlertVariant::Success => theme.alert.success_border_color.clone(),
AlertVariant::Warning => theme.alert.warning_border_color.clone(),
AlertVariant::Error => theme.alert.error_border_color.clone(),
}
}
}
#[component]
pub fn Alert(
#[prop(optional, into)] class: OptionalProp<MaybeSignal<String>>,
#[prop(optional, into)] title: Option<MaybeSignal<String>>,
#[prop(into)] variant: MaybeSignal<AlertVariant>,
children: Children,
) -> impl IntoView {
mount_style("alert", include_str!("./alert.css"));
let theme = use_theme(Theme::light);
let css_vars = create_memo({
let variant = variant.clone();
move |_| {
let mut css_vars = String::new();
theme.with(|theme| {
let variant = variant.get();
css_vars.push_str(&format!(
"--thaw-icon-color: {};",
variant.theme_icon_color(theme)
));
css_vars.push_str(&format!(
"--thaw-background-color: {};",
variant.theme_background_color(theme)
));
css_vars.push_str(&format!(
"--thaw-border-color: {};",
variant.theme_border_color(theme)
));
});
css_vars
}
});
let icon = create_memo(move |_| match variant.get() {
AlertVariant::Success => icondata_ai::AiCheckCircleFilled,
AlertVariant::Warning => icondata_ai::AiExclamationCircleFilled,
AlertVariant::Error => icondata_ai::AiCloseCircleFilled,
});
view! {
<div
class=class_list!["thaw-alert", class.map(| c | move || c.get())]
style=move || css_vars.get()
>
<Icon icon class="thaw-alert__icon"/>
<div>
<OptionComp value=title let:title>
<div class="thaw-alert__header">{move || title.get()}</div>
</OptionComp>
<div class="thaw-alert__content">{children()}</div>
</div>
</div>
}
}

View file

@ -1,35 +0,0 @@
use crate::theme::ThemeMethod;
#[derive(Clone)]
pub struct AlertTheme {
pub success_background_color: String,
pub success_border_color: String,
pub warning_background_color: String,
pub warning_border_color: String,
pub error_background_color: String,
pub error_border_color: String,
}
impl ThemeMethod for AlertTheme {
fn light() -> Self {
Self {
success_background_color: "#edf7f2".into(),
success_border_color: "#c5e7d5".into(),
warning_background_color: "#fef7ed".into(),
warning_border_color: "#fae0b5".into(),
error_background_color: "#fbeef1".into(),
error_border_color: "#f3cbd3".into(),
}
}
fn dark() -> Self {
Self {
success_background_color: "#2a947d40".into(),
success_border_color: "#2a947d59".into(),
warning_background_color: "#f08a0040".into(),
warning_border_color: "#f08a0059".into(),
error_background_color: "#d03a5240".into(),
error_border_color: "#d03a5259".into(),
}
}
}

View file

@ -8,17 +8,6 @@
margin-top: 0.5em;
}
.thaw-anchor-background {
max-width: 0;
position: absolute;
left: 2px;
width: 100%;
background-color: var(--thaw-link-background-color);
transition: top 0.15s cubic-bezier(0.4, 0, 0.2, 1),
max-width 0.15s cubic-bezier(0.4, 0, 0.2, 1),
background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.thaw-anchor-rail {
position: absolute;
left: 0;
@ -28,7 +17,7 @@
border-radius: 2px;
overflow: hidden;
transition: background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1);
background-color: var(--thaw-rail-background-color);
background-color: var(--colorNeutralStroke2);
}
.thaw-anchor-rail__bar {
@ -41,21 +30,21 @@
}
.thaw-anchor-rail__bar.thaw-anchor-rail__bar--active {
background-color: var(--thaw-title-color-active);
background-color: var(--colorBrandBackground);
}
.thaw-anchor-link {
padding: 0 0 0 16px;
position: relative;
line-height: 1.5;
font-size: 13px;
line-height: var(--lineHeightBase200);
font-size: var(--fontSizeBase200);
min-height: 1.5em;
display: flex;
flex-direction: column;
}
.thaw-anchor-link.thaw-anchor-link--active > .thaw-anchor-link__title {
color: var(--thaw-title-color-active);
color: var(--colorNeutralForeground2Active);
}
.thaw-anchor-link__title {
@ -70,8 +59,9 @@
padding-right: 16px;
color: inherit;
transition: color 0.3s cubic-bezier(0.4, 0, 0.2, 1);
color: var(--colorNeutralForeground2);
}
.thaw-anchor-link__title:hover {
color: var(--thaw-title-color-hover);
color: var(--colorNeutralForeground2Hover);
}

View file

@ -1,16 +1,18 @@
use crate::use_anchor;
use leptos::*;
use super::AnchorInjection;
use leptos::{html, prelude::*};
use thaw_components::OptionComp;
use thaw_utils::{class_list, OptionalProp, StoredMaybeSignal};
use thaw_utils::{class_list, StoredMaybeSignal};
#[component]
pub fn AnchorLink(
#[prop(optional, into)] class: OptionalProp<MaybeSignal<String>>,
#[prop(optional, into)] class: MaybeProp<String>,
/// The content of link.
#[prop(into)] title: MaybeSignal<String>,
/// The target of link.
#[prop(into)] href: String,
#[prop(optional)] children: Option<Children>,
) -> impl IntoView {
let anchor = use_anchor();
let anchor = AnchorInjection::expect_context();
let title: StoredMaybeSignal<_> = title.into();
let title_ref = NodeRef::<html::A>::new();
@ -39,17 +41,15 @@ pub fn AnchorLink(
});
});
title_ref.on_load(move |title_el| {
let _ = watch(
move || is_active.get(),
move |is_active, _, _| {
if *is_active {
let title_rect = title_el.get_bounding_client_rect();
anchor.update_background_position(title_rect);
}
},
true,
);
Effect::new(move |_| {
let Some(title_el) = title_ref.get() else {
return;
};
if is_active.get() {
let title_rect = title_el.get_bounding_client_rect();
anchor.update_background_position(title_rect);
}
});
}
}
@ -63,14 +63,12 @@ pub fn AnchorLink(
view! {
<div class=class_list![
"thaw-anchor-link", ("thaw-anchor-link--active", move || is_active.get()), class.map(| c
| move || c.get())
]>
"thaw-anchor-link", ("thaw-anchor-link--active", move || is_active.get()), class]>
<a
href=href
class="thaw-anchor-link__title"
on:click=on_click
ref=title_ref
node_ref=title_ref
title=move || title.get()
>
{move || title.get()}

View file

@ -1,132 +1,114 @@
mod anchor_link;
mod theme;
pub use anchor_link::AnchorLink;
pub use theme::AnchorTheme;
use crate::{use_theme, Theme};
use leptos::*;
use std::cmp::Ordering;
use thaw_utils::{add_event_listener_with_bool, class_list, mount_style, throttle, OptionalProp};
use leptos::{context::Provider, html, prelude::*};
use thaw_utils::{class_list, mount_style};
use web_sys::{DomRect, Element};
#[component]
pub fn Anchor(
#[prop(optional, into)] class: OptionalProp<MaybeSignal<String>>,
#[prop(into, optional)] offset_target: Option<OffsetTarget>,
#[prop(optional, into)] class: MaybeProp<String>,
/// The element or selector used to calc offset of link elements.
/// If you are not scrolling the entire document but only a part of it, you may need to set this.
#[prop(into, optional)]
offset_target: Option<OffsetTarget>,
children: Children,
) -> impl IntoView {
mount_style("anchor", include_str!("./anchor.css"));
let theme = use_theme(Theme::light);
let css_vars = Memo::new(move |_| {
let mut css_vars = String::new();
theme.with(|theme| {
css_vars.push_str(&format!(
"--thaw-rail-background-color: {};",
theme.anchor.rail_background_color
));
css_vars.push_str(&format!(
"--thaw-title-color-hover: {};",
theme.common.color_primary_hover
));
css_vars.push_str(&format!(
"--thaw-title-color-active: {};",
theme.common.color_primary
));
css_vars.push_str(&format!(
"--thaw-link-background-color: {}1a;",
theme.common.color_primary
));
});
css_vars
});
let anchor_ref = NodeRef::new();
let background_ref = NodeRef::<html::Div>::new();
let bar_ref = NodeRef::new();
let element_ids = RwSignal::new(Vec::<String>::new());
let active_id = RwSignal::new(None::<String>);
let _ = watch(
move || active_id.get(),
move |id, _, _| {
if id.is_none() {
if let Some(background_el) = background_ref.get_untracked() {
let _ = background_el.style("max-width", "0");
}
}
},
false,
);
let on_scroll = move || {
element_ids.with(|ids| {
let offset_target_top = if let Some(offset_target) = offset_target.as_ref() {
if let Some(rect) = offset_target.get_bounding_client_rect() {
rect.top()
} else {
return;
}
} else {
0.0
};
#[cfg(any(feature = "csr", feature = "hydrate"))]
{
use leptos::ev;
use std::cmp::Ordering;
use thaw_utils::{add_event_listener_with_bool, throttle};
let mut links: Vec<LinkInfo> = vec![];
for id in ids.iter() {
if let Some(link_el) = document().get_element_by_id(id) {
let link_rect = link_el.get_bounding_client_rect();
links.push(LinkInfo {
top: link_rect.top() - offset_target_top,
id: id.clone(),
});
}
}
links.sort_by(|a, b| {
if a.top > b.top {
Ordering::Greater
} else {
Ordering::Less
}
});
struct LinkInfo {
top: f64,
id: String,
}
let mut temp_link = None::<LinkInfo>;
for link in links.into_iter() {
if link.top >= 0.0 {
if link.top <= 12.0 {
temp_link = Some(link);
break;
} else if temp_link.is_some() {
break;
let offset_target = send_wrapper::SendWrapper::new(offset_target);
let on_scroll = move || {
element_ids.with(|ids| {
let offset_target_top = if let Some(offset_target) = offset_target.as_ref() {
if let Some(rect) = offset_target.get_bounding_client_rect() {
rect.top()
} else {
temp_link = None;
return;
}
} else {
temp_link = Some(link);
0.0
};
let mut links: Vec<LinkInfo> = vec![];
for id in ids.iter() {
if let Some(link_el) = document().get_element_by_id(id) {
let link_rect = link_el.get_bounding_client_rect();
links.push(LinkInfo {
top: link_rect.top() - offset_target_top,
id: id.clone(),
});
}
}
}
active_id.set(temp_link.map(|link| link.id));
links.sort_by(|a, b| {
if a.top > b.top {
Ordering::Greater
} else {
Ordering::Less
}
});
let mut temp_link = None::<LinkInfo>;
for link in links.into_iter() {
if link.top >= 0.0 {
if link.top <= 12.0 {
temp_link = Some(link);
break;
} else if temp_link.is_some() {
break;
} else {
temp_link = None;
}
} else {
temp_link = Some(link);
}
}
active_id.set(temp_link.map(|link| link.id));
});
};
let cb = throttle(
move || {
on_scroll();
},
std::time::Duration::from_millis(200),
);
let scroll_handle = add_event_listener_with_bool(
document(),
ev::scroll,
move |_| {
cb();
},
true,
);
on_cleanup(move || {
scroll_handle.remove();
});
};
let cb = throttle(
move || {
on_scroll();
},
std::time::Duration::from_millis(200),
);
let scroll_handle = add_event_listener_with_bool(
document(),
ev::scroll,
move |_| {
cb();
},
true,
);
on_cleanup(move || {
scroll_handle.remove();
});
}
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
{
let _ = offset_target;
}
view! {
<div
class=class_list!["thaw-anchor", class.map(| c | move || c.get())]
ref=anchor_ref
style=move || css_vars.get()
class=class_list!["thaw-anchor", class]
node_ref=anchor_ref
>
<div class="thaw-anchor-rail">
<div
@ -136,13 +118,11 @@ pub fn Anchor(
move || active_id.with(|id| id.is_some()),
)
ref=bar_ref
node_ref=bar_ref
></div>
</div>
<div class="thaw-anchor-background" ref=background_ref></div>
<Provider value=AnchorInjection::new(
anchor_ref,
background_ref,
bar_ref,
element_ids,
active_id,
@ -154,7 +134,6 @@ pub fn Anchor(
#[derive(Clone)]
pub(crate) struct AnchorInjection {
anchor_ref: NodeRef<html::Div>,
background_ref: NodeRef<html::Div>,
bar_ref: NodeRef<html::Div>,
element_ids: RwSignal<Vec<String>>,
pub active_id: RwSignal<Option<String>>,
@ -163,16 +142,18 @@ pub(crate) struct AnchorInjection {
impl Copy for AnchorInjection {}
impl AnchorInjection {
pub fn expect_context() -> Self {
expect_context()
}
fn new(
anchor_ref: NodeRef<html::Div>,
background_ref: NodeRef<html::Div>,
bar_ref: NodeRef<html::Div>,
element_ids: RwSignal<Vec<String>>,
active_id: RwSignal<Option<String>>,
) -> Self {
Self {
anchor_ref,
background_ref,
bar_ref,
element_ids,
active_id,
@ -202,40 +183,24 @@ impl AnchorInjection {
pub fn update_background_position(&self, title_rect: DomRect) {
if let Some(anchor_el) = self.anchor_ref.get_untracked() {
let background_el = self.background_ref.get_untracked().unwrap();
let bar_el = self.bar_ref.get_untracked().unwrap();
let anchor_rect = anchor_el.get_bounding_client_rect();
let offset_top = title_rect.top() - anchor_rect.top();
let offset_left = title_rect.left() - anchor_rect.left();
let _ = background_el
.style("top", format!("{}px", offset_top))
.style("height", format!("{}px", title_rect.height()))
.style(
"max-width",
format!("{}px", title_rect.width() + offset_left),
);
let _ = bar_el
.style("top", format!("{}px", offset_top))
.style("height", format!("{}px", title_rect.height()));
// let offset_left = title_rect.left() - anchor_rect.left();
bar_el.style(("top", format!("{}px", offset_top)));
bar_el.style(("height", format!("{}px", title_rect.height())));
}
}
}
pub(crate) fn use_anchor() -> AnchorInjection {
expect_context()
}
struct LinkInfo {
top: f64,
id: String,
}
pub enum OffsetTarget {
Selector(String),
Element(Element),
}
#[cfg(any(feature = "csr", feature = "hydrate"))]
impl OffsetTarget {
fn get_bounding_client_rect(&self) -> Option<DomRect> {
match self {

View file

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

View file

@ -1,13 +1,27 @@
.thaw-auto-complete__menu {
.thaw-auto-complete {
display: inline-flex;
}
.thaw-auto-complete > .thaw-input {
min-width: 250px;
}
div.thaw-auto-complete__listbox {
width: 100%;
row-gap: var(--spacingHorizontalXXS);
display: flex;
flex-direction: column;
min-width: 160px;
/* max-height: 80vh; */
max-height: 200px;
padding: 5px;
background-color: var(--thaw-background-color);
border-radius: 3px;
background-color: var(--colorNeutralBackground1);
padding: var(--spacingHorizontalXS);
outline: 1px solid var(--colorTransparentStroke);
border-radius: var(--borderRadiusMedium);
box-sizing: border-box;
box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12),
0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05);
overflow: auto;
box-shadow: var(--shadow16);
overflow-y: auto;
}
.thaw-auto-complete__menu-item {
padding: 6px 5px;
@ -19,30 +33,39 @@
background-color: var(--thaw-background-color-hover);
}
.thaw-auto-complete__menu.fade-in-scale-up-transition-leave-active {
transform-origin: inherit;
transition: opacity 0.2s cubic-bezier(0.4, 0, 1, 1),
transform 0.2s cubic-bezier(0.4, 0, 1, 1),
background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1),
box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1);
.thaw-auto-complete-option {
column-gap: var(--spacingHorizontalXS);
position: relative;
display: flex;
align-items: center;
padding: var(--spacingVerticalSNudge) var(--spacingHorizontalS);
line-height: var(--lineHeightBase300);
font-size: var(--fontSizeBase300);
font-family: var(--fontFamilyBase);
color: var(--colorNeutralForeground1);
border-radius: var(--borderRadiusMedium);
cursor: pointer;
}
.thaw-auto-complete__menu.fade-in-scale-up-transition-enter-active {
transform-origin: inherit;
transition: opacity 0.2s cubic-bezier(0, 0, 0.2, 1),
transform 0.2s cubic-bezier(0, 0, 0.2, 1),
background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1),
box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1);
.thaw-auto-complete-option[data-activedescendant-focusvisible]::after {
content: "";
position: absolute;
right: -2px;
left: -2px;
bottom: -2px;
top: -2px;
z-index: 1;
pointer-events: none;
border-radius: var(--borderRadiusMedium);
border: 2px solid var(--colorStrokeFocus2);
}
.thaw-auto-complete__menu.fade-in-scale-up-transition-enter-from,
.thaw-auto-complete__menu.fade-in-scale-up-transition-leave-to {
opacity: 0;
transform: scale(0.9);
.thaw-auto-complete-option:hover {
color: var(--colorNeutralForeground1Hover);
background-color: var(--colorNeutralBackground1Hover);
}
.thaw-auto-complete__menu.fade-in-scale-up-transition-leave-from,
.thaw-auto-complete__menu.fade-in-scale-up-transition-enter-to {
opacity: 1;
transform: scale(1);
.thaw-auto-complete-option:active {
color: var(--colorNeutralForeground1Pressed);
background-color: var(--colorNeutralBackground1Pressed);
}

View file

@ -0,0 +1,42 @@
use super::AutoCompleteInjection;
use crate::combobox::listbox::ListboxInjection;
use leptos::prelude::*;
use thaw_utils::class_list;
#[component]
pub fn AutoCompleteOption(
#[prop(optional, into)] class: MaybeProp<String>,
value: String,
children: Children,
) -> impl IntoView {
let auto_complete = AutoCompleteInjection::expect_context();
let listbox = ListboxInjection::expect_context();
let is_selected = Memo::new({
let value = value.clone();
let auto_complete = auto_complete.clone();
move |_| auto_complete.is_selected(&value)
});
let id = uuid::Uuid::new_v4().to_string();
{
auto_complete.insert_option(id.clone(), value.clone());
let id = id.clone();
listbox.trigger();
let auto_complete = auto_complete.clone();
on_cleanup(move || {
auto_complete.remove_option(&id);
listbox.trigger();
});
}
view! {
<div
class=class_list!["thaw-auto-complete-option", class]
role="option"
id=id
aria-selected=move || if is_selected.get() { "true" } else { "false" }
on:click=move |_| auto_complete.select_option(value.clone())
>
{children()}
</div>
}
}

View file

@ -1,17 +1,16 @@
mod theme;
mod auto_complete_option;
pub use theme::AutoCompleteTheme;
pub use auto_complete_option::AutoCompleteOption;
use crate::{use_theme, ComponentRef, Input, InputPrefix, InputRef, InputSuffix, Theme};
use leptos::*;
use thaw_components::{Binder, CSSTransition, Follower, FollowerPlacement, FollowerWidth};
use thaw_utils::{class_list, mount_style, Model, OptionalProp, StoredMaybeSignal};
#[derive(Clone, PartialEq)]
pub struct AutoCompleteOption {
pub label: String,
pub value: String,
}
use crate::{
combobox::listbox::{listbox_keyboard_event, Listbox},
ComponentRef, Input, InputPrefix, InputRef, InputSuffix,
_aria::use_active_descendant,
};
use leptos::{context::Provider, either::Either, html, prelude::*};
use std::collections::HashMap;
use thaw_components::{Binder, Follower, FollowerPlacement, FollowerWidth};
use thaw_utils::{class_list, mount_style, ArcOneCallback, BoxOneCallback, Model};
#[slot]
pub struct AutoCompletePrefix {
@ -26,152 +25,93 @@ pub struct AutoCompleteSuffix {
#[component]
pub fn AutoComplete(
#[prop(optional, into)] value: Model<String>,
#[prop(optional, into)] placeholder: OptionalProp<MaybeSignal<String>>,
#[prop(optional, into)] options: MaybeSignal<Vec<AutoCompleteOption>>,
#[prop(optional, into)] placeholder: MaybeProp<String>,
#[prop(optional, into)] clear_after_select: MaybeSignal<bool>,
#[prop(optional, into)] blur_after_select: MaybeSignal<bool>,
#[prop(optional, into)] on_select: Option<Callback<String>>,
#[prop(optional, into)] on_select: Option<BoxOneCallback<String>>,
#[prop(optional, into)] disabled: MaybeSignal<bool>,
#[prop(optional, into)] allow_free_input: bool,
#[prop(optional, into)] invalid: MaybeSignal<bool>,
#[prop(optional, into)] class: OptionalProp<MaybeSignal<String>>,
#[prop(optional, into)] class: MaybeProp<String>,
#[prop(optional)] auto_complete_prefix: Option<AutoCompletePrefix>,
#[prop(optional)] auto_complete_suffix: Option<AutoCompleteSuffix>,
#[prop(optional)] comp_ref: ComponentRef<AutoCompleteRef>,
#[prop(attrs)] attrs: Vec<(&'static str, Attribute)>,
#[prop(optional)] children: Option<Children>,
) -> impl IntoView {
mount_style("auto-complete", include_str!("./auto-complete.css"));
let theme = use_theme(Theme::light);
let menu_css_vars = create_memo(move |_| {
let mut css_vars = String::new();
theme.with(|theme| {
css_vars.push_str(&format!(
"--thaw-background-color: {};",
theme.select.menu_background_color
));
css_vars.push_str(&format!(
"--thaw-background-color-hover: {};",
theme.select.menu_background_color_hover
));
});
css_vars
});
let input_ref = ComponentRef::<InputRef>::new();
let listbox_ref = NodeRef::<html::Div>::new();
let auto_complete_ref = NodeRef::<html::Div>::new();
let open_listbox = RwSignal::new(false);
let options = StoredValue::new(HashMap::<String, String>::new());
let default_index = if allow_free_input { None } else { Some(0) };
let select_option_index = create_rw_signal::<Option<usize>>(default_index);
let menu_ref = create_node_ref::<html::Div>();
let is_show_menu = create_rw_signal(false);
let auto_complete_ref = create_node_ref::<html::Div>();
let options = StoredMaybeSignal::from(options);
let open_menu = move || {
select_option_index.set(default_index);
is_show_menu.set(true);
};
let allow_value = move |_| {
if !is_show_menu.get_untracked() {
open_menu();
if !open_listbox.get_untracked() {
open_listbox.set(true);
}
true
};
let select_value = move |option_value: String| {
let select_option = ArcOneCallback::new(move |option_value: String| {
if clear_after_select.get_untracked() {
value.set(String::new());
} else {
value.set(option_value.clone());
}
if let Some(on_select) = on_select {
on_select.call(option_value);
if let Some(on_select) = on_select.as_ref() {
on_select(option_value);
}
if allow_free_input {
select_option_index.set(None);
}
is_show_menu.set(false);
open_listbox.set(false);
if blur_after_select.get_untracked() {
if let Some(input_ref) = input_ref.get_untracked() {
input_ref.blur();
}
}
};
// we unset selection index whenever options get changed
// otherwise e.g. selection could move from one item to
// another staying on the same index
create_effect(move |_| {
options.track();
select_option_index.set(default_index);
});
let on_keydown = move |event: ev::KeyboardEvent| {
if !is_show_menu.get_untracked() {
return;
let (set_listbox, active_descendant_controller) =
use_active_descendant(move |el| el.class_list().contains("thaw-auto-complete-option"));
let on_blur = {
let active_descendant_controller = active_descendant_controller.clone();
move |_| {
active_descendant_controller.blur();
open_listbox.set(false);
}
let key = event.key();
if key == *"ArrowDown" {
select_option_index.update(|index| {
if *index == Some(options.with_untracked(|options| options.len()) - 1) {
*index = default_index
} else {
*index = Some(index.map_or(0, |index| index + 1))
}
});
} else if key == *"ArrowUp" {
select_option_index.update(|index| {
match *index {
None => *index = Some(options.with_untracked(|options| options.len()) - 1),
Some(0) => {
if allow_free_input {
*index = None
} else {
*index = Some(options.with_untracked(|options| options.len()) - 1)
};
let on_keydown = {
let select_option = select_option.clone();
move |e| {
let select_option = select_option.clone();
listbox_keyboard_event(
e,
open_listbox,
false,
&active_descendant_controller,
move |option| {
options.with_value(|options| {
if let Some(value) = options.get(&option.id()) {
select_option(value.clone());
}
}
Some(prev_index) => *index = Some(prev_index - 1),
};
});
} else if key == *"Enter" {
event.prevent_default();
let option_value = options.with_untracked(|options| {
let index = select_option_index.get_untracked();
match index {
None if allow_free_input => {
let value = value.get_untracked();
(!value.is_empty()).then_some(value)
}
Some(index) if options.len() > index => {
let option = &options[index];
Some(option.value.clone())
}
_ => None,
}
});
if let Some(option_value) = option_value {
select_value(option_value);
}
});
},
);
}
};
input_ref.on_load(move |_| {
comp_ref.load(AutoCompleteRef { input_ref });
});
comp_ref.load(AutoCompleteRef { input_ref });
view! {
<Binder target_ref=auto_complete_ref>
<div
class=class_list!["thaw-auto-complete", class.map(| c | move || c.get())]
ref=auto_complete_ref
class=class_list!["thaw-auto-complete", class]
node_ref=auto_complete_ref
on:keydown=on_keydown
>
<Input
attrs
value
placeholder
disabled
invalid
on_focus=move |_| open_menu()
on_blur=move |_| is_show_menu.set(false)
on_focus=move |_| open_listbox.set(true)
on_blur=on_blur
allow_value
comp_ref=input_ref
>
@ -197,92 +137,56 @@ pub fn AutoComplete(
</div>
<Follower
slot
show=is_show_menu
show=open_listbox
placement=FollowerPlacement::BottomStart
width=FollowerWidth::Target
>
<CSSTransition
node_ref=menu_ref
name="fade-in-scale-up-transition"
appear=is_show_menu.get_untracked()
show=is_show_menu
let:display
>
<div
class="thaw-auto-complete__menu"
style=move || {
display
.get()
.map(|d| d.to_string())
.unwrap_or_else(|| menu_css_vars.get())
<Provider value=AutoCompleteInjection{value, select_option, options}>
<Listbox open=open_listbox.read_only() set_listbox listbox_ref class="thaw-auto-complete__listbox">
{
if let Some(children) = children {
Either::Left(children())
} else {
Either::Right(())
}
}
ref=menu_ref
>
{move || {
options
.get()
.into_iter()
.enumerate()
.map(|(index, v)| {
let AutoCompleteOption { value: option_value, label } = v;
let menu_item_ref = create_node_ref::<html::Div>();
let on_click = move |_| {
select_value(option_value.clone());
};
let on_mouseenter = move |_| {
select_option_index.set(Some(index));
};
let on_mousedown = move |ev: ev::MouseEvent| {
ev.prevent_default();
};
create_effect(move |_| {
if Some(index) == select_option_index.get() {
if !is_show_menu.get() {
return;
}
if let Some(menu_item_ref) = menu_item_ref.get() {
let menu_ref = menu_ref.get().unwrap();
let menu_rect = menu_ref.get_bounding_client_rect();
let item_rect = menu_item_ref.get_bounding_client_rect();
if item_rect.y() < menu_rect.y() {
menu_item_ref.scroll_into_view_with_bool(true);
} else if item_rect.y() + item_rect.height()
> menu_rect.y() + menu_rect.height()
{
menu_item_ref.scroll_into_view_with_bool(false);
}
}
}
});
view! {
<div
class="thaw-auto-complete__menu-item"
class=(
"thaw-auto-complete__menu-item--selected",
move || Some(index) == select_option_index.get(),
)
on:click=on_click
on:mousedown=on_mousedown
on:mouseenter=on_mouseenter
ref=menu_item_ref
>
{label}
</div>
}
})
.collect_view()
}}
</div>
</CSSTransition>
</Listbox>
</Provider>
</Follower>
</Binder>
}
}
#[derive(Clone)]
pub(crate) struct AutoCompleteInjection {
value: Model<String>,
select_option: ArcOneCallback<String>,
options: StoredValue<HashMap<String, String>>,
}
impl AutoCompleteInjection {
pub fn expect_context() -> Self {
expect_context()
}
pub fn is_selected(&self, key: &String) -> bool {
self.value.with(|value| value == key)
}
pub fn select_option(&self, value: String) {
(self.select_option)(value);
}
pub fn insert_option(&self, id: String, value: String) {
self.options
.update_value(|options| options.insert(id, value));
}
pub fn remove_option(&self, id: &String) {
self.options.update_value(|options| options.remove(id));
}
}
#[derive(Clone)]
pub struct AutoCompleteRef {
input_ref: ComponentRef<InputRef>,

View file

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

Some files were not shown because too many files have changed in this diff Show more