mirror of
https://github.com/mztlive/dx-admin-template.git
synced 2026-05-17 21:20:37 +00:00
一些shadcn的基础组件
This commit is contained in:
61
src/components/echo.rs
Normal file
61
src/components/echo.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
const ECHO_CSS: Asset = asset!("/assets/styling/echo.css");
|
||||
|
||||
/// Echo component that demonstrates fullstack server functions.
|
||||
#[component]
|
||||
pub fn Echo() -> Element {
|
||||
// use_signal is a hook. Hooks in dioxus must be run in a consistent order every time the component is rendered.
|
||||
// That means they can't be run inside other hooks, async blocks, if statements, or loops.
|
||||
//
|
||||
// use_signal is a hook that creates a state for the component. It takes a closure that returns the initial value of the state.
|
||||
// The state is automatically tracked and will rerun any other hooks or components that read it whenever it changes.
|
||||
let mut response = use_signal(|| String::new());
|
||||
|
||||
rsx! {
|
||||
document::Link { rel: "stylesheet", href: ECHO_CSS }
|
||||
|
||||
div {
|
||||
id: "echo",
|
||||
h4 { "ServerFn Echo" }
|
||||
input {
|
||||
placeholder: "Type here to echo...",
|
||||
// `oninput` is an event handler that will run when the input changes. It can return either nothing or a future
|
||||
// that will be run when the event runs.
|
||||
oninput: move |event| async move {
|
||||
// When we call the echo_server function from the client, it will fire a request to the server and return
|
||||
// the response. It handles serialization and deserialization of the request and response for us.
|
||||
let data = echo_server(event.value()).await.unwrap();
|
||||
|
||||
// After we have the data from the server, we can set the state of the signal to the new value.
|
||||
// Since we read the `response` signal later in this component, the component will rerun.
|
||||
response.set(data);
|
||||
},
|
||||
}
|
||||
|
||||
// Signals can be called like a function to clone the current value of the signal
|
||||
if !response().is_empty() {
|
||||
p {
|
||||
"Server echoed: "
|
||||
// Since we read the signal inside this component, the component "subscribes" to the signal. Whenever
|
||||
// the signal changes, the component will rerun.
|
||||
i { "{response}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Server functions let us define public APIs on the server that can be called like a normal async function from the client.
|
||||
// Each server function needs to be annotated with the `#[post]`/`#[get]` attributes, accept and return serializable types, and return
|
||||
// a `Result` with the error type [`ServerFnError`].
|
||||
//
|
||||
// When the server function is called from the client, it will just serialize the arguments, call the API, and deserialize the
|
||||
// response.
|
||||
#[post("/api/echo")]
|
||||
async fn echo_server(input: String) -> Result<String> {
|
||||
// The body of server function like this comment are only included on the server. If you have any server-only logic like
|
||||
// database queries, you can put it here. Any imports for the server function should either be imported inside the function
|
||||
// or imported under a `#[cfg(feature = "server")]` block.
|
||||
Ok(input)
|
||||
}
|
||||
25
src/components/hero.rs
Normal file
25
src/components/hero.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
const HEADER_SVG: Asset = asset!("/assets/header.svg");
|
||||
|
||||
#[component]
|
||||
pub fn Hero() -> Element {
|
||||
rsx! {
|
||||
// We can create elements inside the rsx macro with the element name followed by a block of attributes and children.
|
||||
div {
|
||||
// Attributes should be defined in the element before any children
|
||||
id: "hero",
|
||||
// After all attributes are defined, we can define child elements and components
|
||||
img { src: HEADER_SVG, id: "header" }
|
||||
div { id: "links",
|
||||
// The RSX macro also supports text nodes surrounded by quotes
|
||||
a { href: "https://dioxuslabs.com/learn/0.6/", "📚 Learn Dioxus" }
|
||||
a { href: "https://dioxuslabs.com/awesome", "🚀 Awesome Dioxus" }
|
||||
a { href: "https://github.com/dioxus-community/", "📡 Community Libraries" }
|
||||
a { href: "https://github.com/DioxusLabs/sdk", "⚙️ Dioxus Development Kit" }
|
||||
a { href: "https://marketplace.visualstudio.com/items?itemName=DioxusLabs.dioxus", "💫 VSCode Extension" }
|
||||
a { href: "https://discord.gg/XgGxMSkvUM", "👋 Community Discord" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
src/components/mod.rs
Normal file
11
src/components/mod.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
//! The components module contains all shared components for our app. Components are the building blocks of dioxus apps.
|
||||
//! They can be used to defined common UI elements like buttons, forms, and modals. In this template, we define a Hero
|
||||
//! component and an Echo component for fullstack apps to be used in our app.
|
||||
|
||||
mod hero;
|
||||
pub use hero::Hero;
|
||||
|
||||
mod echo;
|
||||
pub use echo::Echo;
|
||||
|
||||
pub mod ui;
|
||||
47
src/components/ui/badge.rs
Normal file
47
src/components/ui/badge.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum BadgeVariant {
|
||||
Default,
|
||||
Secondary,
|
||||
Outline,
|
||||
Destructive,
|
||||
}
|
||||
|
||||
impl BadgeVariant {
|
||||
fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
BadgeVariant::Default => "default",
|
||||
BadgeVariant::Secondary => "secondary",
|
||||
BadgeVariant::Outline => "outline",
|
||||
BadgeVariant::Destructive => "destructive",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for BadgeVariant {
|
||||
fn default() -> Self {
|
||||
BadgeVariant::Default
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Badge(
|
||||
#[props(default)] variant: BadgeVariant,
|
||||
#[props(into, default)] class: Option<String>,
|
||||
children: Element,
|
||||
) -> Element {
|
||||
let mut classes = String::from("ui-badge");
|
||||
if let Some(extra) = class.filter(|extra| !extra.trim().is_empty()) {
|
||||
classes.push(' ');
|
||||
classes.push_str(extra.trim());
|
||||
}
|
||||
|
||||
rsx! {
|
||||
span {
|
||||
class: classes,
|
||||
"data-variant": variant.as_str(),
|
||||
{children}
|
||||
}
|
||||
}
|
||||
}
|
||||
95
src/components/ui/button.rs
Normal file
95
src/components/ui/button.rs
Normal file
@@ -0,0 +1,95 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
/// Visual style variants that match shadcn button presets.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum ButtonVariant {
|
||||
Default,
|
||||
Secondary,
|
||||
Destructive,
|
||||
Outline,
|
||||
Ghost,
|
||||
Link,
|
||||
}
|
||||
|
||||
impl ButtonVariant {
|
||||
fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
ButtonVariant::Default => "default",
|
||||
ButtonVariant::Secondary => "secondary",
|
||||
ButtonVariant::Destructive => "destructive",
|
||||
ButtonVariant::Outline => "outline",
|
||||
ButtonVariant::Ghost => "ghost",
|
||||
ButtonVariant::Link => "link",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ButtonVariant {
|
||||
fn default() -> Self {
|
||||
ButtonVariant::Default
|
||||
}
|
||||
}
|
||||
|
||||
/// Sizing presets lifted from the shadcn button component.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum ButtonSize {
|
||||
Default,
|
||||
Sm,
|
||||
Lg,
|
||||
Icon,
|
||||
}
|
||||
|
||||
impl ButtonSize {
|
||||
fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
ButtonSize::Default => "default",
|
||||
ButtonSize::Sm => "sm",
|
||||
ButtonSize::Lg => "lg",
|
||||
ButtonSize::Icon => "icon",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ButtonSize {
|
||||
fn default() -> Self {
|
||||
ButtonSize::Default
|
||||
}
|
||||
}
|
||||
|
||||
/// A faithful port of `Button` from shadcn/ui. Styling is provided by `shadcn.css`.
|
||||
#[component]
|
||||
pub fn Button(
|
||||
#[props(default)] variant: ButtonVariant,
|
||||
#[props(default)] size: ButtonSize,
|
||||
#[props(into, default)] class: Option<String>,
|
||||
#[props(default)] disabled: bool,
|
||||
#[props(default = "button".to_string())]
|
||||
#[props(into)]
|
||||
r#type: String,
|
||||
#[props(optional)] on_click: Option<EventHandler<MouseEvent>>,
|
||||
children: Element,
|
||||
) -> Element {
|
||||
let mut classes = String::from("ui-button");
|
||||
if let Some(extra) = class.filter(|extra| !extra.trim().is_empty()) {
|
||||
classes.push(' ');
|
||||
classes.push_str(extra.trim());
|
||||
}
|
||||
|
||||
let click_handler = on_click.clone();
|
||||
|
||||
rsx! {
|
||||
button {
|
||||
class: classes,
|
||||
disabled,
|
||||
r#type: r#type,
|
||||
"data-variant": variant.as_str(),
|
||||
"data-size": size.as_str(),
|
||||
onclick: move |event| {
|
||||
if let Some(handler) = click_handler.clone() {
|
||||
handler.call(event);
|
||||
}
|
||||
},
|
||||
{children}
|
||||
}
|
||||
}
|
||||
}
|
||||
78
src/components/ui/card.rs
Normal file
78
src/components/ui/card.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn merge_class(base: &str, extra: Option<String>) -> String {
|
||||
if let Some(extra) = extra.filter(|extra| !extra.trim().is_empty()) {
|
||||
format!("{base} {}", extra.trim())
|
||||
} else {
|
||||
base.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Card(#[props(into, default)] class: Option<String>, children: Element) -> Element {
|
||||
let classes = merge_class("ui-card", class);
|
||||
rsx! {
|
||||
div {
|
||||
class: classes,
|
||||
{children}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn CardHeader(#[props(into, default)] class: Option<String>, children: Element) -> Element {
|
||||
let classes = merge_class("ui-card-header", class);
|
||||
rsx! {
|
||||
div {
|
||||
class: classes,
|
||||
{children}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn CardTitle(#[props(into, default)] class: Option<String>, children: Element) -> Element {
|
||||
let classes = merge_class("ui-card-title", class);
|
||||
rsx! {
|
||||
h3 {
|
||||
class: classes,
|
||||
{children}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn CardDescription(
|
||||
#[props(into, default)] class: Option<String>,
|
||||
children: Element,
|
||||
) -> Element {
|
||||
let classes = merge_class("ui-card-description", class);
|
||||
rsx! {
|
||||
p {
|
||||
class: classes,
|
||||
{children}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn CardContent(#[props(into, default)] class: Option<String>, children: Element) -> Element {
|
||||
let classes = merge_class("ui-card-content", class);
|
||||
rsx! {
|
||||
div {
|
||||
class: classes,
|
||||
{children}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn CardFooter(#[props(into, default)] class: Option<String>, children: Element) -> Element {
|
||||
let classes = merge_class("ui-card-footer", class);
|
||||
rsx! {
|
||||
div {
|
||||
class: classes,
|
||||
{children}
|
||||
}
|
||||
}
|
||||
}
|
||||
58
src/components/ui/checkbox.rs
Normal file
58
src/components/ui/checkbox.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn merge_class(base: &str, extra: Option<String>) -> String {
|
||||
if let Some(extra) = extra.filter(|extra| !extra.trim().is_empty()) {
|
||||
format!("{base} {}", extra.trim())
|
||||
} else {
|
||||
base.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Checkbox(
|
||||
#[props(default)] checked: bool,
|
||||
#[props(default)] disabled: bool,
|
||||
#[props(default)] required: bool,
|
||||
#[props(into, default)] id: Option<String>,
|
||||
#[props(into, default)] name: Option<String>,
|
||||
#[props(into, default)] value: Option<String>,
|
||||
#[props(into, default)] class: Option<String>,
|
||||
#[props(optional)] on_checked_change: Option<EventHandler<bool>>,
|
||||
#[props(optional)] on_input: Option<EventHandler<FormEvent>>,
|
||||
#[props(optional)] on_change: Option<EventHandler<FormEvent>>,
|
||||
) -> Element {
|
||||
let classes = merge_class("ui-checkbox", class);
|
||||
let checked_handler = on_checked_change.clone();
|
||||
let input_handler = on_input.clone();
|
||||
let change_handler = on_change.clone();
|
||||
let id_attr = id.unwrap_or_default();
|
||||
let name_attr = name.unwrap_or_default();
|
||||
let value_attr = value.unwrap_or_else(|| "on".to_string());
|
||||
|
||||
rsx! {
|
||||
input {
|
||||
class: classes,
|
||||
r#type: "checkbox",
|
||||
role: "checkbox",
|
||||
checked,
|
||||
disabled,
|
||||
required,
|
||||
id: id_attr,
|
||||
name: name_attr,
|
||||
value: value_attr,
|
||||
oninput: move |event| {
|
||||
if let Some(handler) = checked_handler.clone() {
|
||||
handler.call(event.checked());
|
||||
}
|
||||
if let Some(handler) = input_handler.clone() {
|
||||
handler.call(event);
|
||||
}
|
||||
},
|
||||
onchange: move |event| {
|
||||
if let Some(handler) = change_handler.clone() {
|
||||
handler.call(event);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
62
src/components/ui/input.rs
Normal file
62
src/components/ui/input.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn merge_class(base: &str, extra: Option<String>) -> String {
|
||||
if let Some(extra) = extra.filter(|extra| !extra.trim().is_empty()) {
|
||||
format!("{base} {}", extra.trim())
|
||||
} else {
|
||||
base.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Input(
|
||||
#[props(into, default)] class: Option<String>,
|
||||
#[props(into, default)] value: Option<String>,
|
||||
#[props(into, default)] default_value: Option<String>,
|
||||
#[props(into, default)] placeholder: Option<String>,
|
||||
#[props(into, default)] name: Option<String>,
|
||||
#[props(into, default)] id: Option<String>,
|
||||
#[props(into, default)] autocomplete: Option<String>,
|
||||
#[props(into, default)] r#type: Option<String>,
|
||||
#[props(default)] disabled: bool,
|
||||
#[props(default)] readonly: bool,
|
||||
#[props(default)] required: bool,
|
||||
#[props(optional)] on_input: Option<EventHandler<FormEvent>>,
|
||||
#[props(optional)] on_change: Option<EventHandler<FormEvent>>,
|
||||
) -> Element {
|
||||
let classes = merge_class("ui-input", class);
|
||||
let input_handler = on_input.clone();
|
||||
let change_handler = on_change.clone();
|
||||
|
||||
// Clone optional attributes so they can be moved into rsx
|
||||
let resolved_value = value.or(default_value).unwrap_or_default();
|
||||
let placeholder_attr = placeholder.unwrap_or_default();
|
||||
let name_attr = name.unwrap_or_default();
|
||||
let id_attr = id.unwrap_or_default();
|
||||
let autocomplete_attr = autocomplete.unwrap_or_default();
|
||||
|
||||
rsx! {
|
||||
input {
|
||||
class: classes,
|
||||
r#type: r#type.unwrap_or_else(|| "text".to_string()),
|
||||
disabled,
|
||||
readonly,
|
||||
required,
|
||||
id: id_attr,
|
||||
name: name_attr,
|
||||
value: resolved_value,
|
||||
placeholder: placeholder_attr,
|
||||
autocomplete: autocomplete_attr,
|
||||
oninput: move |event| {
|
||||
if let Some(handler) = input_handler.clone() {
|
||||
handler.call(event);
|
||||
}
|
||||
},
|
||||
onchange: move |event| {
|
||||
if let Some(handler) = change_handler.clone() {
|
||||
handler.call(event);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
30
src/components/ui/label.rs
Normal file
30
src/components/ui/label.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn merge_class(base: &str, extra: Option<String>) -> String {
|
||||
if let Some(extra) = extra.filter(|extra| !extra.trim().is_empty()) {
|
||||
format!("{base} {}", extra.trim())
|
||||
} else {
|
||||
base.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Label(
|
||||
#[props(into, default)] class: Option<String>,
|
||||
#[props(into, default)] html_for: Option<String>,
|
||||
#[props(default)] disabled: bool,
|
||||
children: Element,
|
||||
) -> Element {
|
||||
let classes = merge_class("ui-label", class);
|
||||
|
||||
let html_for_attr = html_for.unwrap_or_default();
|
||||
|
||||
rsx! {
|
||||
label {
|
||||
class: classes,
|
||||
"data-disabled": disabled,
|
||||
r#for: html_for_attr,
|
||||
{children}
|
||||
}
|
||||
}
|
||||
}
|
||||
31
src/components/ui/mod.rs
Normal file
31
src/components/ui/mod.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
//! Shadcn-inspired reusable primitives implemented with Dioxus 0.7 signals and the shared `shadcn.css`.
|
||||
//! Each component mirrors the styling and API conventions of the upstream React components while
|
||||
//! remaining idiomatic to Rust and Dioxus.
|
||||
|
||||
mod badge;
|
||||
mod button;
|
||||
mod card;
|
||||
mod checkbox;
|
||||
mod input;
|
||||
mod label;
|
||||
mod progress;
|
||||
mod radio_group;
|
||||
mod separator;
|
||||
mod slider;
|
||||
mod switch;
|
||||
mod tabs;
|
||||
mod textarea;
|
||||
|
||||
pub use badge::*;
|
||||
pub use button::*;
|
||||
pub use card::*;
|
||||
pub use checkbox::*;
|
||||
pub use input::*;
|
||||
pub use label::*;
|
||||
pub use progress::*;
|
||||
pub use radio_group::*;
|
||||
pub use separator::*;
|
||||
pub use slider::*;
|
||||
pub use switch::*;
|
||||
pub use tabs::*;
|
||||
pub use textarea::*;
|
||||
37
src/components/ui/progress.rs
Normal file
37
src/components/ui/progress.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn merge_class(base: &str, extra: Option<String>) -> String {
|
||||
if let Some(extra) = extra.filter(|extra| !extra.trim().is_empty()) {
|
||||
format!("{base} {}", extra.trim())
|
||||
} else {
|
||||
base.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Progress(
|
||||
#[props(default = 0.0f32)] value: f32,
|
||||
#[props(default = 100.0f32)] max: f32,
|
||||
#[props(into, default)] class: Option<String>,
|
||||
) -> Element {
|
||||
let classes = merge_class("ui-progress", class);
|
||||
let percent = if max <= 0.0f32 {
|
||||
0.0
|
||||
} else {
|
||||
(value / max).clamp(0.0, 1.0) * 100.0
|
||||
};
|
||||
let indicator_style = format!("width: {percent:.2}%;");
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
class: classes,
|
||||
role: "progressbar",
|
||||
"aria-valuemin": 0,
|
||||
"aria-valuemax": max,
|
||||
"aria-valuenow": value,
|
||||
span {
|
||||
style: indicator_style,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
105
src/components/ui/radio_group.rs
Normal file
105
src/components/ui/radio_group.rs
Normal file
@@ -0,0 +1,105 @@
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn merge_class(base: &str, extra: Option<String>) -> String {
|
||||
if let Some(extra) = extra.filter(|extra| !extra.trim().is_empty()) {
|
||||
format!("{base} {}", extra.trim())
|
||||
} else {
|
||||
base.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
static RADIO_GROUP_IDS: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
fn next_radio_group_name() -> String {
|
||||
let id = RADIO_GROUP_IDS.fetch_add(1, Ordering::Relaxed);
|
||||
format!("radio-group-{id}")
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct RadioGroupContext {
|
||||
name: Signal<String>,
|
||||
value: Signal<Option<String>>,
|
||||
disabled: bool,
|
||||
on_change: Option<EventHandler<String>>,
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn RadioGroup(
|
||||
#[props(into, default)] class: Option<String>,
|
||||
#[props(into, default)] name: Option<String>,
|
||||
#[props(into, default)] default_value: Option<String>,
|
||||
#[props(default)] disabled: bool,
|
||||
#[props(optional)] on_value_change: Option<EventHandler<String>>,
|
||||
children: Element,
|
||||
) -> Element {
|
||||
let provided_name = name.clone();
|
||||
let group_name = use_signal(move || {
|
||||
provided_name
|
||||
.clone()
|
||||
.unwrap_or_else(|| next_radio_group_name())
|
||||
});
|
||||
let initial_value = default_value.clone();
|
||||
let selected = use_signal(move || initial_value.clone());
|
||||
|
||||
let context = RadioGroupContext {
|
||||
name: group_name.clone(),
|
||||
value: selected.clone(),
|
||||
disabled,
|
||||
on_change: on_value_change.clone(),
|
||||
};
|
||||
|
||||
use_context_provider(|| context);
|
||||
|
||||
let classes = merge_class("ui-radio-group", class);
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
class: classes,
|
||||
role: "radiogroup",
|
||||
"aria-disabled": disabled,
|
||||
{children}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn RadioGroupItem(
|
||||
#[props(into)] value: String,
|
||||
#[props(default)] disabled: bool,
|
||||
#[props(into, default)] id: Option<String>,
|
||||
#[props(into, default)] class: Option<String>,
|
||||
) -> Element {
|
||||
let context = use_context::<RadioGroupContext>();
|
||||
let classes = merge_class("ui-radio", class);
|
||||
let id_attr = id.unwrap_or_default();
|
||||
let is_disabled = disabled || context.disabled;
|
||||
let mut group_value_signal = context.value.clone();
|
||||
let current_value = group_value_signal();
|
||||
let is_selected = current_value.as_ref() == Some(&value);
|
||||
let group_name_signal = context.name.clone();
|
||||
let group_name = group_name_signal();
|
||||
let value_attr = value.clone();
|
||||
let value_for_handler = value.clone();
|
||||
let on_change = context.on_change.clone();
|
||||
|
||||
rsx! {
|
||||
input {
|
||||
class: classes,
|
||||
r#type: "radio",
|
||||
role: "radio",
|
||||
name: "{group_name}",
|
||||
value: "{value_attr}",
|
||||
checked: is_selected,
|
||||
disabled: is_disabled,
|
||||
id: format_args!("{}", id_attr),
|
||||
onchange: move |_| {
|
||||
group_value_signal.set(Some(value_for_handler.clone()));
|
||||
if let Some(handler) = on_change.clone() {
|
||||
handler.call(value_for_handler.clone());
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
50
src/components/ui/separator.rs
Normal file
50
src/components/ui/separator.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum SeparatorOrientation {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
}
|
||||
|
||||
impl SeparatorOrientation {
|
||||
fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
SeparatorOrientation::Horizontal => "horizontal",
|
||||
SeparatorOrientation::Vertical => "vertical",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SeparatorOrientation {
|
||||
fn default() -> Self {
|
||||
SeparatorOrientation::Horizontal
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_class(base: &str, extra: Option<String>) -> String {
|
||||
if let Some(extra) = extra.filter(|extra| !extra.trim().is_empty()) {
|
||||
format!("{base} {}", extra.trim())
|
||||
} else {
|
||||
base.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Separator(
|
||||
#[props(default)] orientation: SeparatorOrientation,
|
||||
#[props(into, default)] class: Option<String>,
|
||||
#[props(into, default)] style: Option<String>,
|
||||
) -> Element {
|
||||
let classes = merge_class("ui-separator", class);
|
||||
let style_attr = style.unwrap_or_default();
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
class: classes,
|
||||
role: "separator",
|
||||
"data-orientation": orientation.as_str(),
|
||||
"aria-orientation": orientation.as_str(),
|
||||
style: style_attr,
|
||||
}
|
||||
}
|
||||
}
|
||||
63
src/components/ui/slider.rs
Normal file
63
src/components/ui/slider.rs
Normal file
@@ -0,0 +1,63 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn merge_class(base: &str, extra: Option<String>) -> String {
|
||||
if let Some(extra) = extra.filter(|extra| !extra.trim().is_empty()) {
|
||||
format!("{base} {}", extra.trim())
|
||||
} else {
|
||||
base.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Slider(
|
||||
#[props(default = 0.0f32)] value: f32,
|
||||
#[props(default = 0.0f32)] min: f32,
|
||||
#[props(default = 100.0f32)] max: f32,
|
||||
#[props(default = 1.0f32)] step: f32,
|
||||
#[props(default)] disabled: bool,
|
||||
#[props(into, default)] class: Option<String>,
|
||||
#[props(optional)] on_value_change: Option<EventHandler<f32>>,
|
||||
#[props(optional)] on_input: Option<EventHandler<FormEvent>>,
|
||||
#[props(optional)] on_change: Option<EventHandler<FormEvent>>,
|
||||
) -> Element {
|
||||
let classes = merge_class("ui-slider", class);
|
||||
let percent = if (max - min).abs() <= f32::EPSILON {
|
||||
0.0
|
||||
} else {
|
||||
((value - min) / (max - min)).clamp(0.0, 1.0) * 100.0
|
||||
};
|
||||
let style = format!("--fill: {percent:.2}%;");
|
||||
let value_change = on_value_change.clone();
|
||||
let input_handler = on_input.clone();
|
||||
let change_handler = on_change.clone();
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
class: classes,
|
||||
input {
|
||||
r#type: "range",
|
||||
min: min,
|
||||
max: max,
|
||||
step: step,
|
||||
value: value,
|
||||
disabled,
|
||||
style: style,
|
||||
oninput: move |event| {
|
||||
if let Some(handler) = input_handler.clone() {
|
||||
handler.call(event.clone());
|
||||
}
|
||||
if let Some(handler) = value_change.clone() {
|
||||
if let Ok(parsed) = event.value().parse::<f32>() {
|
||||
handler.call(parsed);
|
||||
}
|
||||
}
|
||||
},
|
||||
onchange: move |event| {
|
||||
if let Some(handler) = change_handler.clone() {
|
||||
handler.call(event);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
54
src/components/ui/switch.rs
Normal file
54
src/components/ui/switch.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn merge_class(base: &str, extra: Option<String>) -> String {
|
||||
if let Some(extra) = extra.filter(|extra| !extra.trim().is_empty()) {
|
||||
format!("{base} {}", extra.trim())
|
||||
} else {
|
||||
base.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Switch(
|
||||
#[props(default)] checked: bool,
|
||||
#[props(default)] disabled: bool,
|
||||
#[props(into, default)] id: Option<String>,
|
||||
#[props(into, default)] name: Option<String>,
|
||||
#[props(into, default)] class: Option<String>,
|
||||
#[props(optional)] on_checked_change: Option<EventHandler<bool>>,
|
||||
#[props(optional)] on_input: Option<EventHandler<FormEvent>>,
|
||||
#[props(optional)] on_change: Option<EventHandler<FormEvent>>,
|
||||
) -> Element {
|
||||
let classes = merge_class("ui-switch", class);
|
||||
let checked_handler = on_checked_change.clone();
|
||||
let input_handler = on_input.clone();
|
||||
let change_handler = on_change.clone();
|
||||
let id_attr = id.unwrap_or_default();
|
||||
let name_attr = name.unwrap_or_default();
|
||||
|
||||
rsx! {
|
||||
input {
|
||||
class: classes,
|
||||
r#type: "checkbox",
|
||||
role: "switch",
|
||||
checked,
|
||||
disabled,
|
||||
"aria-checked": checked,
|
||||
id: id_attr,
|
||||
name: name_attr,
|
||||
oninput: move |event| {
|
||||
if let Some(handler) = checked_handler.clone() {
|
||||
handler.call(event.checked());
|
||||
}
|
||||
if let Some(handler) = input_handler.clone() {
|
||||
handler.call(event);
|
||||
}
|
||||
},
|
||||
onchange: move |event| {
|
||||
if let Some(handler) = change_handler.clone() {
|
||||
handler.call(event);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
137
src/components/ui/tabs.rs
Normal file
137
src/components/ui/tabs.rs
Normal file
@@ -0,0 +1,137 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum TabsOrientation {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
}
|
||||
|
||||
impl TabsOrientation {
|
||||
fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
TabsOrientation::Horizontal => "horizontal",
|
||||
TabsOrientation::Vertical => "vertical",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TabsOrientation {
|
||||
fn default() -> Self {
|
||||
TabsOrientation::Horizontal
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TabsContext {
|
||||
value: Signal<String>,
|
||||
on_change: Option<EventHandler<String>>,
|
||||
}
|
||||
|
||||
fn merge_class(base: &str, extra: Option<String>) -> String {
|
||||
if let Some(extra) = extra.filter(|extra| !extra.trim().is_empty()) {
|
||||
format!("{base} {}", extra.trim())
|
||||
} else {
|
||||
base.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Tabs(
|
||||
#[props(into)] default_value: String,
|
||||
#[props(default)] orientation: TabsOrientation,
|
||||
#[props(into, default)] class: Option<String>,
|
||||
#[props(optional)] on_value_change: Option<EventHandler<String>>,
|
||||
children: Element,
|
||||
) -> Element {
|
||||
let initial_value = default_value.clone();
|
||||
let selected = use_signal(move || initial_value.clone());
|
||||
|
||||
let context = TabsContext {
|
||||
value: selected.clone(),
|
||||
on_change: on_value_change.clone(),
|
||||
};
|
||||
|
||||
use_context_provider(|| context);
|
||||
|
||||
let classes = merge_class("ui-tabs", class);
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
class: classes,
|
||||
"data-orientation": orientation.as_str(),
|
||||
{children}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn TabsList(#[props(into, default)] class: Option<String>, children: Element) -> Element {
|
||||
let classes = merge_class("ui-tabs-nav", class);
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
class: classes,
|
||||
role: "tablist",
|
||||
{children}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn TabsTrigger(
|
||||
#[props(into)] value: String,
|
||||
#[props(into, default)] class: Option<String>,
|
||||
#[props(default)] disabled: bool,
|
||||
children: Element,
|
||||
) -> Element {
|
||||
let context = use_context::<TabsContext>();
|
||||
let classes = merge_class("ui-tabs-trigger", class);
|
||||
let mut selected_signal = context.value.clone();
|
||||
let is_active = selected_signal() == value;
|
||||
let trigger_value = value.clone();
|
||||
let trigger_attr_value = trigger_value.clone();
|
||||
let on_change = context.on_change.clone();
|
||||
|
||||
rsx! {
|
||||
button {
|
||||
class: classes,
|
||||
role: "tab",
|
||||
"data-state": if is_active { "active" } else { "inactive" },
|
||||
"aria-selected": is_active,
|
||||
"aria-controls": format!("tab-panel-{}", trigger_value),
|
||||
value: trigger_attr_value,
|
||||
disabled,
|
||||
onclick: move |_event| {
|
||||
selected_signal.set(trigger_value.clone());
|
||||
if let Some(handler) = on_change.clone() {
|
||||
handler.call(trigger_value.clone());
|
||||
}
|
||||
},
|
||||
{children}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn TabsContent(
|
||||
#[props(into)] value: String,
|
||||
#[props(into, default)] class: Option<String>,
|
||||
children: Element,
|
||||
) -> Element {
|
||||
let context = use_context::<TabsContext>();
|
||||
let classes = merge_class("ui-tabs-content", class);
|
||||
let selected_signal = context.value.clone();
|
||||
let is_active = selected_signal() == value;
|
||||
let panel_id = format!("tab-panel-{value}");
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
class: classes,
|
||||
role: "tabpanel",
|
||||
id: panel_id,
|
||||
hidden: !is_active,
|
||||
{children}
|
||||
}
|
||||
}
|
||||
}
|
||||
57
src/components/ui/textarea.rs
Normal file
57
src/components/ui/textarea.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn merge_class(base: &str, extra: Option<String>) -> String {
|
||||
if let Some(extra) = extra.filter(|extra| !extra.trim().is_empty()) {
|
||||
format!("{base} {}", extra.trim())
|
||||
} else {
|
||||
base.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Textarea(
|
||||
#[props(into, default)] class: Option<String>,
|
||||
#[props(into, default)] value: Option<String>,
|
||||
#[props(into, default)] placeholder: Option<String>,
|
||||
#[props(into, default)] name: Option<String>,
|
||||
#[props(into, default)] id: Option<String>,
|
||||
#[props(into, default)] rows: Option<u16>,
|
||||
#[props(default)] disabled: bool,
|
||||
#[props(default)] readonly: bool,
|
||||
#[props(default)] required: bool,
|
||||
#[props(optional)] on_input: Option<EventHandler<FormEvent>>,
|
||||
#[props(optional)] on_change: Option<EventHandler<FormEvent>>,
|
||||
) -> Element {
|
||||
let classes = merge_class("ui-textarea", class);
|
||||
let input_handler = on_input.clone();
|
||||
let change_handler = on_change.clone();
|
||||
let resolved_value = value.unwrap_or_default();
|
||||
let placeholder_attr = placeholder.unwrap_or_default();
|
||||
let name_attr = name.unwrap_or_default();
|
||||
let id_attr = id.unwrap_or_default();
|
||||
let rows_attr = rows.unwrap_or(5);
|
||||
|
||||
rsx! {
|
||||
textarea {
|
||||
class: classes,
|
||||
disabled,
|
||||
readonly,
|
||||
required,
|
||||
rows: rows_attr,
|
||||
id: id_attr,
|
||||
name: name_attr,
|
||||
value: resolved_value,
|
||||
placeholder: placeholder_attr,
|
||||
oninput: move |event| {
|
||||
if let Some(handler) = input_handler.clone() {
|
||||
handler.call(event);
|
||||
}
|
||||
},
|
||||
onchange: move |event| {
|
||||
if let Some(handler) = change_handler.clone() {
|
||||
handler.call(event);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
67
src/main.rs
Normal file
67
src/main.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
// The dioxus prelude contains a ton of common items used in dioxus apps. It's a good idea to import wherever you
|
||||
// need dioxus
|
||||
use dioxus::prelude::*;
|
||||
|
||||
use views::{Blog, Home, Navbar};
|
||||
|
||||
/// Define a components module that contains all shared components for our app.
|
||||
mod components;
|
||||
/// Define a views module that contains the UI for all Layouts and Routes for our app.
|
||||
mod views;
|
||||
|
||||
/// The Route enum is used to define the structure of internal routes in our app. All route enums need to derive
|
||||
/// the [`Routable`] trait, which provides the necessary methods for the router to work.
|
||||
///
|
||||
/// Each variant represents a different URL pattern that can be matched by the router. If that pattern is matched,
|
||||
/// the components for that route will be rendered.
|
||||
#[derive(Debug, Clone, Routable, PartialEq)]
|
||||
#[rustfmt::skip]
|
||||
enum Route {
|
||||
// The layout attribute defines a wrapper for all routes under the layout. Layouts are great for wrapping
|
||||
// many routes with a common UI like a navbar.
|
||||
#[layout(Navbar)]
|
||||
// The route attribute defines the URL pattern that a specific route matches. If that pattern matches the URL,
|
||||
// the component for that route will be rendered. The component name that is rendered defaults to the variant name.
|
||||
#[route("/")]
|
||||
Home {},
|
||||
// The route attribute can include dynamic parameters that implement [`std::str::FromStr`] and [`std::fmt::Display`] with the `:` syntax.
|
||||
// In this case, id will match any integer like `/blog/123` or `/blog/-456`.
|
||||
#[route("/blog/:id")]
|
||||
// Fields of the route variant will be passed to the component as props. In this case, the blog component must accept
|
||||
// an `id` prop of type `i32`.
|
||||
Blog { id: i32 },
|
||||
}
|
||||
|
||||
// We can import assets in dioxus with the `asset!` macro. This macro takes a path to an asset relative to the crate root.
|
||||
// The macro returns an `Asset` type that will display as the path to the asset in the browser or a local path in desktop bundles.
|
||||
const FAVICON: Asset = asset!("/assets/favicon.ico");
|
||||
// The asset macro also minifies some assets like CSS and JS to make bundled smaller
|
||||
const MAIN_CSS: Asset = asset!("/assets/styling/main.css");
|
||||
const SHADCN_CSS: Asset = asset!("/assets/styling/shadcn.css");
|
||||
|
||||
fn main() {
|
||||
// The `launch` function is the main entry point for a dioxus app. It takes a component and renders it with the platform feature
|
||||
// you have enabled
|
||||
dioxus::launch(App);
|
||||
}
|
||||
|
||||
/// App is the main component of our app. Components are the building blocks of dioxus apps. Each component is a function
|
||||
/// that takes some props and returns an Element. In this case, App takes no props because it is the root of our app.
|
||||
///
|
||||
/// Components should be annotated with `#[component]` to support props, better error messages, and autocomplete
|
||||
#[component]
|
||||
fn App() -> Element {
|
||||
// The `rsx!` macro lets us define HTML inside of rust. It expands to an Element with all of our HTML inside.
|
||||
rsx! {
|
||||
// In addition to element and text (which we will see later), rsx can contain other components. In this case,
|
||||
// we are using the `document::Link` component to add a link to our favicon and main CSS file into the head of our app.
|
||||
document::Link { rel: "icon", href: FAVICON }
|
||||
document::Link { rel: "stylesheet", href: MAIN_CSS }
|
||||
document::Link { rel: "stylesheet", href: SHADCN_CSS }
|
||||
|
||||
|
||||
// The router component renders the route enum we defined above. It will handle synchronization of the URL and render
|
||||
// the layouts and components for the active route.
|
||||
Router::<Route> {}
|
||||
}
|
||||
}
|
||||
39
src/views/blog.rs
Normal file
39
src/views/blog.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use crate::Route;
|
||||
use dioxus::prelude::*;
|
||||
|
||||
const BLOG_CSS: Asset = asset!("/assets/styling/blog.css");
|
||||
|
||||
/// The Blog page component that will be rendered when the current route is `[Route::Blog]`
|
||||
///
|
||||
/// The component takes a `id` prop of type `i32` from the route enum. Whenever the id changes, the component function will be
|
||||
/// re-run and the rendered HTML will be updated.
|
||||
#[component]
|
||||
pub fn Blog(id: i32) -> Element {
|
||||
rsx! {
|
||||
document::Link { rel: "stylesheet", href: BLOG_CSS }
|
||||
|
||||
div {
|
||||
id: "blog",
|
||||
|
||||
// Content
|
||||
h1 { "This is blog #{id}!" }
|
||||
p { "In blog #{id}, we show how the Dioxus router works and how URL parameters can be passed as props to our route components." }
|
||||
|
||||
// Navigation links
|
||||
// The `Link` component lets us link to other routes inside our app. It takes a `to` prop of type `Route` and
|
||||
// any number of child nodes.
|
||||
Link {
|
||||
// The `to` prop is the route that the link should navigate to. We can use the `Route` enum to link to the
|
||||
// blog page with the id of -1. Since we are using an enum instead of a string, all of the routes will be checked
|
||||
// at compile time to make sure they are valid.
|
||||
to: Route::Blog { id: id - 1 },
|
||||
"Previous"
|
||||
}
|
||||
span { " <---> " }
|
||||
Link {
|
||||
to: Route::Blog { id: id + 1 },
|
||||
"Next"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
241
src/views/home.rs
Normal file
241
src/views/home.rs
Normal file
@@ -0,0 +1,241 @@
|
||||
use crate::components::{
|
||||
ui::{
|
||||
Badge, BadgeVariant, Button, ButtonSize, ButtonVariant, Card, CardContent, CardDescription,
|
||||
CardFooter, CardHeader, CardTitle, Checkbox, Input, Label, Progress, RadioGroup,
|
||||
RadioGroupItem, Separator, SeparatorOrientation, Slider, Switch, Tabs, TabsContent,
|
||||
TabsList, TabsTrigger, Textarea,
|
||||
},
|
||||
Echo, Hero,
|
||||
};
|
||||
use dioxus::prelude::*;
|
||||
|
||||
/// The Home page component that will be rendered when the current route is `[Route::Home]`
|
||||
#[component]
|
||||
pub fn Home() -> Element {
|
||||
rsx! {
|
||||
Hero {}
|
||||
Echo {}
|
||||
UiShowcase {}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn UiShowcase() -> Element {
|
||||
let mut accepted_terms = use_signal(|| false);
|
||||
let mut email_notifications = use_signal(|| true);
|
||||
let mut slider_value = use_signal(|| 42.0f32);
|
||||
let mut contact_method = use_signal(|| "email".to_string());
|
||||
let mut newsletter_opt_in = use_signal(|| true);
|
||||
let mut dark_mode = use_signal(|| false);
|
||||
let slider_value_signal = slider_value.clone();
|
||||
let contact_method_signal = contact_method.clone();
|
||||
let intensity_text = move || format!("Accent intensity: {:.0}%", slider_value_signal());
|
||||
let contact_text = move || format!("Preferred contact: {}", contact_method_signal());
|
||||
|
||||
rsx! {
|
||||
section {
|
||||
class: "ui-shell shadcn",
|
||||
div {
|
||||
class: "ui-stack",
|
||||
h2 { style: "font-size: 1.75rem; font-weight: 600;", "Shadcn primitives for Dioxus" }
|
||||
p {
|
||||
style: "color: hsl(var(--muted-foreground)); max-width: 640px;",
|
||||
"A compact gallery of the shadcn/ui building blocks, rebuilt with Dioxus 0.7 signals."
|
||||
}
|
||||
}
|
||||
div {
|
||||
class: "ui-demo-grid",
|
||||
|
||||
Card {
|
||||
CardHeader {
|
||||
CardTitle { "Profile form" }
|
||||
CardDescription { "Inputs, sliders, helpers, and actions inside a card layout." }
|
||||
}
|
||||
CardContent {
|
||||
div { class: "ui-stack",
|
||||
Label { html_for: "profile-name", "Name" }
|
||||
Input { id: "profile-name", placeholder: "Ada Lovelace" }
|
||||
}
|
||||
div { class: "ui-stack",
|
||||
Label { html_for: "profile-about", "About" }
|
||||
Textarea {
|
||||
id: "profile-about",
|
||||
placeholder: "Tell us something fun...",
|
||||
rows: 4,
|
||||
}
|
||||
SpanHelper { "Textarea adopts shadcn spacing and typography out of the box." }
|
||||
}
|
||||
Separator { style: "margin: 1rem 0;" }
|
||||
div { class: "ui-stack",
|
||||
Label { html_for: "accent-slider", "Accent strength" }
|
||||
Slider {
|
||||
value: slider_value(),
|
||||
min: 0.0,
|
||||
max: 100.0,
|
||||
step: 1.0,
|
||||
on_value_change: move |val| slider_value.set(val),
|
||||
}
|
||||
Progress { value: slider_value(), max: 100.0 }
|
||||
SpanHelper { "{intensity_text()}" }
|
||||
}
|
||||
div { class: "ui-bleed",
|
||||
div { class: "ui-cluster",
|
||||
Checkbox {
|
||||
id: Some("accept-terms".to_string()),
|
||||
checked: accepted_terms(),
|
||||
on_checked_change: move |state| accepted_terms.set(state),
|
||||
}
|
||||
Label { html_for: "accept-terms", "Agree to terms" }
|
||||
}
|
||||
div { class: "ui-cluster",
|
||||
Label { html_for: "profile-emails", "Email notifications" }
|
||||
Switch {
|
||||
id: Some("profile-emails".to_string()),
|
||||
checked: email_notifications(),
|
||||
on_checked_change: move |state| email_notifications.set(state),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
CardFooter {
|
||||
div { class: "ui-cluster",
|
||||
Button { variant: ButtonVariant::Outline, size: ButtonSize::Sm, "Cancel" }
|
||||
Button { disabled: !accepted_terms(), "Save changes" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Card {
|
||||
CardHeader {
|
||||
CardTitle { "Buttons & badges" }
|
||||
CardDescription { "Variant + size matrix copied directly from shadcn/ui." }
|
||||
}
|
||||
CardContent {
|
||||
div { class: "ui-stack",
|
||||
SpanHelper { "Buttons – variants" }
|
||||
div { class: "ui-cluster",
|
||||
Button { "Primary" }
|
||||
Button { variant: ButtonVariant::Secondary, "Secondary" }
|
||||
Button { variant: ButtonVariant::Destructive, "Destructive" }
|
||||
Button { variant: ButtonVariant::Outline, "Outline" }
|
||||
Button { variant: ButtonVariant::Ghost, "Ghost" }
|
||||
Button { variant: ButtonVariant::Link, "Learn more" }
|
||||
}
|
||||
}
|
||||
div { class: "ui-stack",
|
||||
SpanHelper { "Buttons – sizes" }
|
||||
div { class: "ui-cluster",
|
||||
Button { size: ButtonSize::Sm, "Small" }
|
||||
Button { "Default" }
|
||||
Button { size: ButtonSize::Lg, "Large" }
|
||||
Button { size: ButtonSize::Icon, "★" }
|
||||
}
|
||||
}
|
||||
Separator { style: "margin: 1rem 0;" }
|
||||
div { class: "ui-stack",
|
||||
SpanHelper { "Badges" }
|
||||
div { class: "ui-cluster",
|
||||
Badge { "Default" }
|
||||
Badge { variant: BadgeVariant::Secondary, "Secondary" }
|
||||
Badge { variant: BadgeVariant::Destructive, "Destructive" }
|
||||
Separator { orientation: SeparatorOrientation::Vertical, style: "height: 1.5rem;" }
|
||||
Badge { variant: BadgeVariant::Outline, "Outline" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Card {
|
||||
CardHeader {
|
||||
CardTitle { "Selection controls" }
|
||||
CardDescription { "Checkboxes, switches, and radio groups stay in sync with signals." }
|
||||
}
|
||||
CardContent {
|
||||
div { class: "ui-stack",
|
||||
div { class: "ui-cluster",
|
||||
Checkbox {
|
||||
id: Some("newsletter-opt".to_string()),
|
||||
checked: newsletter_opt_in(),
|
||||
on_checked_change: move |state| newsletter_opt_in.set(state),
|
||||
}
|
||||
Label { html_for: "newsletter-opt", "Subscribe to newsletter" }
|
||||
}
|
||||
div { class: "ui-cluster",
|
||||
Label { html_for: "dark-mode", "Dark mode" }
|
||||
Switch {
|
||||
id: Some("dark-mode".to_string()),
|
||||
checked: dark_mode(),
|
||||
on_checked_change: move |state| dark_mode.set(state),
|
||||
}
|
||||
}
|
||||
Separator { style: "margin: 0.75rem 0;" }
|
||||
RadioGroup {
|
||||
default_value: contact_method(),
|
||||
on_value_change: move |value| contact_method.set(value),
|
||||
div { class: "ui-stack",
|
||||
div { class: "ui-cluster",
|
||||
RadioGroupItem { id: Some("contact-email".to_string()), value: "email" }
|
||||
Label { html_for: "contact-email", "Email" }
|
||||
}
|
||||
div { class: "ui-cluster",
|
||||
RadioGroupItem { id: Some("contact-sms".to_string()), value: "sms" }
|
||||
Label { html_for: "contact-sms", "SMS" }
|
||||
}
|
||||
div { class: "ui-cluster",
|
||||
RadioGroupItem { id: Some("contact-call".to_string()), value: "call" }
|
||||
Label { html_for: "contact-call", "Phone call" }
|
||||
}
|
||||
}
|
||||
}
|
||||
SpanHelper { "{contact_text()}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Card {
|
||||
CardHeader {
|
||||
CardTitle { "Tabs & panels" }
|
||||
CardDescription { "Tabbed navigation with content surfaces that stay in sync." }
|
||||
}
|
||||
CardContent {
|
||||
Tabs {
|
||||
default_value: "overview",
|
||||
TabsList {
|
||||
TabsTrigger { value: "overview", "Overview" }
|
||||
TabsTrigger { value: "analytics", "Analytics" }
|
||||
TabsTrigger { value: "reports", "Reports" }
|
||||
}
|
||||
TabsContent {
|
||||
value: "overview",
|
||||
div { class: "ui-stack",
|
||||
Label { html_for: "overview-search", "Search" }
|
||||
Input { id: "overview-search", placeholder: "Search docs..." }
|
||||
SpanHelper { "Triggers share the same focus ring and sizing as the original UI kit." }
|
||||
}
|
||||
}
|
||||
TabsContent {
|
||||
value: "analytics",
|
||||
div { class: "ui-stack",
|
||||
SpanHelper { "Analytics aggregates live metrics and shows their progress." }
|
||||
Progress { value: 64.0, max: 100.0 }
|
||||
}
|
||||
}
|
||||
TabsContent {
|
||||
value: "reports",
|
||||
div { class: "ui-stack",
|
||||
SpanHelper { "Generate PDF, CSV, or scheduled exports directly from here." }
|
||||
Button { variant: ButtonVariant::Secondary, "Create report" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn SpanHelper(children: Element) -> Element {
|
||||
rsx! { span { class: "ui-field-helper", {children} } }
|
||||
}
|
||||
18
src/views/mod.rs
Normal file
18
src/views/mod.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
//! The views module contains the components for all Layouts and Routes for our app. Each layout and route in our [`Route`]
|
||||
//! enum will render one of these components.
|
||||
//!
|
||||
//!
|
||||
//! The [`Home`] and [`Blog`] components will be rendered when the current route is [`Route::Home`] or [`Route::Blog`] respectively.
|
||||
//!
|
||||
//!
|
||||
//! The [`Navbar`] component will be rendered on all pages of our app since every page is under the layout. The layout defines
|
||||
//! a common wrapper around all child routes.
|
||||
|
||||
mod home;
|
||||
pub use home::Home;
|
||||
|
||||
mod blog;
|
||||
pub use blog::Blog;
|
||||
|
||||
mod navbar;
|
||||
pub use navbar::Navbar;
|
||||
32
src/views/navbar.rs
Normal file
32
src/views/navbar.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
use crate::Route;
|
||||
use dioxus::prelude::*;
|
||||
|
||||
const NAVBAR_CSS: Asset = asset!("/assets/styling/navbar.css");
|
||||
|
||||
/// The Navbar component that will be rendered on all pages of our app since every page is under the layout.
|
||||
///
|
||||
///
|
||||
/// This layout component wraps the UI of [Route::Home] and [Route::Blog] in a common navbar. The contents of the Home and Blog
|
||||
/// routes will be rendered under the outlet inside this component
|
||||
#[component]
|
||||
pub fn Navbar() -> Element {
|
||||
rsx! {
|
||||
document::Link { rel: "stylesheet", href: NAVBAR_CSS }
|
||||
|
||||
div {
|
||||
id: "navbar",
|
||||
Link {
|
||||
to: Route::Home {},
|
||||
"Home"
|
||||
}
|
||||
Link {
|
||||
to: Route::Blog { id: 1 },
|
||||
"Blog"
|
||||
}
|
||||
}
|
||||
|
||||
// The `Outlet` component is used to render the next component inside the layout. In this case, it will render either
|
||||
// the [`Home`] or [`Blog`] component depending on the current route.
|
||||
Outlet::<Route> {}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user