增加组件

This commit is contained in:
tommy
2025-11-03 11:38:04 +08:00
parent d440517d49
commit 77cd16a792
9 changed files with 912 additions and 29 deletions

View File

@@ -462,6 +462,228 @@
gap: 1rem;
}
.ui-alert {
border-radius: calc(var(--radius) + 2px);
border: 1px solid hsl(var(--border));
background-color: hsl(var(--muted) / 0.6);
padding: 1.1rem 1.25rem;
display: flex;
flex-direction: column;
gap: 0.3rem;
box-shadow: var(--shadow-sm);
}
.ui-alert[data-variant="destructive"] {
border-color: hsl(var(--destructive));
background-color: hsl(var(--destructive) / 0.1);
color: hsl(var(--destructive));
}
.ui-alert-title {
font-weight: 600;
font-size: 0.95rem;
}
.ui-alert-description {
font-size: 0.85rem;
color: hsl(var(--muted-foreground));
}
.ui-select,
.ui-dropdown {
position: relative;
display: inline-flex;
flex-direction: column;
gap: 0.6rem;
width: 100%;
}
.ui-select[data-disabled="true"] {
opacity: 0.6;
pointer-events: none;
}
.ui-select-trigger,
.ui-dropdown-trigger {
display: inline-flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
height: 2.5rem;
padding: 0 0.85rem;
border-radius: calc(var(--radius) - 2px);
border: 1px solid hsl(var(--border));
background-color: hsl(var(--background));
color: hsl(var(--foreground));
transition: border-color 0.2s ease, box-shadow 0.2s ease;
cursor: pointer;
}
.ui-select-trigger:focus-visible,
.ui-dropdown-trigger:focus-visible {
outline: none;
border-color: hsl(var(--ring));
box-shadow: 0 0 0 1px hsl(var(--ring));
}
.ui-select-trigger[data-open="true"],
.ui-dropdown-trigger[data-open="true"] {
border-color: hsl(var(--ring));
}
.ui-select-content,
.ui-dropdown-content {
position: absolute;
top: calc(100% + 0.35rem);
left: 0;
right: 0;
z-index: 30;
display: flex;
flex-direction: column;
background-color: hsl(var(--popover));
border: 1px solid hsl(var(--border));
border-radius: calc(var(--radius) - 2px);
box-shadow: var(--shadow-md);
overflow: hidden;
max-height: 14rem;
}
.ui-select-list,
.ui-dropdown-list {
display: flex;
flex-direction: column;
overflow-y: auto;
}
.ui-select-item,
.ui-dropdown-item {
width: 100%;
border: none;
background: transparent;
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.65rem 0.9rem;
font-size: 0.875rem;
color: hsl(var(--foreground));
transition: background-color 0.2s ease, color 0.2s ease;
}
.ui-select-item:hover,
.ui-dropdown-item:hover,
.ui-select-item[data-state="active"],
.ui-dropdown-item[data-state="active"] {
background-color: hsl(var(--muted));
}
.ui-dropdown-item[data-variant="destructive"] {
color: hsl(var(--destructive));
}
.ui-tooltip-wrapper {
position: relative;
display: inline-flex;
}
.ui-tooltip-bubble {
position: absolute;
left: 50%;
transform: translate(-50%, -8px);
bottom: 100%;
background-color: hsl(var(--foreground));
color: hsl(var(--background));
font-size: 0.72rem;
padding: 0.3rem 0.55rem;
border-radius: calc(var(--radius) - 4px);
white-space: nowrap;
box-shadow: var(--shadow-sm);
opacity: 0;
pointer-events: none;
transition: opacity 0.1s ease, transform 0.1s ease;
}
.ui-tooltip-bubble[data-state="visible"] {
opacity: 1;
transform: translate(-50%, -12px);
}
.ui-accordion {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.ui-accordion-item {
border: 1px solid hsl(var(--border));
border-radius: calc(var(--radius) - 2px);
overflow: hidden;
background-color: hsl(var(--card));
}
.ui-accordion-trigger {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
padding: 0.75rem 1rem;
background: transparent;
border: none;
cursor: pointer;
font-weight: 500;
font-size: 0.9rem;
color: hsl(var(--foreground));
}
.ui-accordion-trigger:focus-visible {
outline: none;
box-shadow: 0 0 0 1px hsl(var(--ring));
}
.ui-accordion-content {
padding: 0 1rem 0.9rem;
color: hsl(var(--muted-foreground));
font-size: 0.85rem;
display: none;
}
.ui-accordion-content[data-state="open"] {
display: block;
}
.ui-avatar {
width: 3rem;
height: 3rem;
border-radius: 999px;
position: relative;
overflow: hidden;
display: inline-flex;
align-items: center;
justify-content: center;
background-color: hsl(var(--muted));
color: hsl(var(--muted-foreground));
font-weight: 600;
text-transform: uppercase;
border: 1px solid hsl(var(--border));
}
.ui-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.ui-avatar-fallback {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 1rem;
letter-spacing: 0.03em;
}
.ui-separator {
background-color: hsl(var(--border));
display: block;

View File

@@ -0,0 +1,132 @@
use dioxus::prelude::*;
#[derive(Clone)]
struct AccordionContext {
open_value: Signal<Option<String>>,
collapsible: bool,
}
impl AccordionContext {
fn is_open(&self, value: &str) -> bool {
matches!((self.open_value)(), Some(current) if current == value)
}
fn toggle(&self, value: String) {
let mut state = self.open_value.clone();
if self.is_open(&value) {
if self.collapsible {
state.set(None);
}
} else {
state.set(Some(value));
}
}
}
#[derive(Clone)]
struct AccordionItemContext {
value: String,
root: AccordionContext,
}
#[component]
pub fn Accordion(
#[props(default)] collapsible: bool,
#[props(into, default)] default_value: Option<String>,
#[props(into, default)] class: Option<String>,
children: Element,
) -> Element {
let initial_value = default_value.clone();
let state = use_signal(move || initial_value.clone());
let context = AccordionContext {
open_value: state,
collapsible,
};
use_context_provider(|| context.clone());
let class_name = format!(
"{}{}",
"ui-accordion",
class
.filter(|c| !c.trim().is_empty())
.map(|c| format!(" {c}"))
.unwrap_or_default()
);
rsx! {
div {
class: class_name,
{children}
}
}
}
#[component]
pub fn AccordionItem(
#[props(into)] value: String,
#[props(into, default)] class: Option<String>,
children: Element,
) -> Element {
let root = use_context::<AccordionContext>();
let item_context = AccordionItemContext {
value: value.clone(),
root: root.clone(),
};
use_context_provider(|| item_context);
let class_name = format!(
"{}{}",
"ui-accordion-item",
class
.filter(|c| !c.trim().is_empty())
.map(|c| format!(" {c}"))
.unwrap_or_default()
);
rsx! {
div {
class: class_name,
"data-state": if root.is_open(&value) { "open" } else { "closed" },
{children}
}
}
}
#[component]
pub fn AccordionTrigger(children: Element) -> Element {
let item = use_context::<AccordionItemContext>();
let is_open = item.root.is_open(&item.value);
let value = item.value.clone();
let root = item.root.clone();
rsx! {
button {
class: "ui-accordion-trigger",
"data-state": if is_open { "open" } else { "closed" },
onclick: move |_| root.toggle(value.clone()),
{children}
span {
style: "font-size: 0.8rem; opacity: 0.6;",
if is_open { "" } else { "+" }
}
}
}
}
#[component]
pub fn AccordionContent(children: Element) -> Element {
let item = use_context::<AccordionItemContext>();
let is_open = item.root.is_open(&item.value);
rsx! {
div {
class: "ui-accordion-content",
"data-state": if is_open { "open" } else { "closed" },
if is_open {
{children}
}
}
}
}

View File

@@ -0,0 +1,44 @@
use dioxus::prelude::*;
/// Visual variants for alerts.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AlertVariant {
Default,
Destructive,
}
impl AlertVariant {
fn as_str(&self) -> &'static str {
match self {
AlertVariant::Default => "default",
AlertVariant::Destructive => "destructive",
}
}
}
impl Default for AlertVariant {
fn default() -> Self {
AlertVariant::Default
}
}
#[component]
pub fn Alert(
#[props(default)] variant: AlertVariant,
#[props(into, default)] title: Option<String>,
children: Element,
) -> Element {
rsx! {
div {
class: "ui-alert",
"data-variant": variant.as_str(),
if let Some(title) = title {
h4 { class: "ui-alert-title", "{title}" }
}
div {
class: "ui-alert-description",
{children}
}
}
}
}

View File

@@ -0,0 +1,51 @@
use dioxus::prelude::*;
#[component]
pub fn Avatar(
#[props(into, default)] src: Option<String>,
#[props(into, default)] alt: Option<String>,
#[props(into, default)] fallback: Option<String>,
#[props(into, default)] class: Option<String>,
) -> Element {
let initial_missing = src.is_none();
let mut show_fallback = use_signal(move || initial_missing);
let class_name = format!(
"{}{}",
"ui-avatar",
class
.filter(|c| !c.trim().is_empty())
.map(|c| format!(" {c}"))
.unwrap_or_default()
);
let fallback_text = fallback
.clone()
.or_else(|| {
alt.clone().map(|text| {
text.split_whitespace()
.filter_map(|part| part.chars().next())
.take(2)
.collect::<String>()
})
})
.unwrap_or_else(|| "??".to_string())
.to_uppercase();
rsx! {
div {
class: class_name,
if let Some(src) = src {
img {
src: src,
alt: alt.clone().unwrap_or_default(),
onerror: move |_| show_fallback.set(true),
onload: move |_| show_fallback.set(false),
}
}
if show_fallback() {
div { class: "ui-avatar-fallback", "{fallback_text}" }
}
}
}
}

View File

@@ -0,0 +1,121 @@
use dioxus::prelude::*;
#[derive(Clone, PartialEq)]
pub enum DropdownItemVariant {
Default,
Destructive,
}
impl DropdownItemVariant {
fn as_str(&self) -> &'static str {
match self {
DropdownItemVariant::Default => "default",
DropdownItemVariant::Destructive => "destructive",
}
}
}
impl Default for DropdownItemVariant {
fn default() -> Self {
DropdownItemVariant::Default
}
}
#[derive(Clone, PartialEq)]
pub struct DropdownMenuItem {
pub label: String,
pub value: String,
pub shortcut: Option<String>,
pub variant: DropdownItemVariant,
}
impl DropdownMenuItem {
pub fn new(label: impl Into<String>, value: impl Into<String>) -> Self {
Self {
label: label.into(),
value: value.into(),
shortcut: None,
variant: DropdownItemVariant::Default,
}
}
pub fn with_shortcut(mut self, shortcut: impl Into<String>) -> Self {
self.shortcut = Some(shortcut.into());
self
}
pub fn destructive(mut self) -> Self {
self.variant = DropdownItemVariant::Destructive;
self
}
}
#[component]
pub fn DropdownMenu(
#[props(into)] label: String,
#[props(into)] items: Vec<DropdownMenuItem>,
#[props(optional)] on_select: Option<EventHandler<String>>,
) -> Element {
let open = use_signal(|| false);
let on_select_handler = on_select.clone();
rsx! {
div {
class: "ui-dropdown",
button {
class: "ui-dropdown-trigger",
"data-open": if open() { "true" } else { "false" },
onclick: {
let mut signal = open.clone();
move |_| {
let new_state = !signal();
signal.set(new_state);
}
},
span { "{label}" }
span {
style: "font-size: 0.85rem; opacity: 0.7;",
""
}
}
if open() {
div {
class: "ui-dropdown-content",
div {
class: "ui-dropdown-list",
for item in items.iter().cloned() {
{
let value = item.value.clone();
let shortcut = item.shortcut.clone();
let variant = item.variant.as_str().to_string();
let mut open_signal = open.clone();
let handler = on_select_handler.clone();
rsx! {
button {
class: "ui-dropdown-item",
"data-variant": variant,
onclick: {
let value = value.clone();
let handler = handler.clone();
move |_| {
if let Some(callback) = handler.clone() {
callback.call(value.clone());
}
open_signal.set(false);
}
},
span { "{item.label}" }
if let Some(shortcut) = shortcut.clone() {
span { style: "font-size: 0.75rem; opacity: 0.6;", "{shortcut}" }
}
}
}
}
}
}
}
}
}
}
}

View File

@@ -2,30 +2,42 @@
//! Each component mirrors the styling and API conventions of the upstream React components while
//! remaining idiomatic to Rust and Dioxus.
mod accordion;
mod alert;
mod avatar;
mod badge;
mod button;
mod card;
mod checkbox;
mod dropdown_menu;
mod input;
mod label;
mod progress;
mod radio_group;
mod select;
mod separator;
mod slider;
mod switch;
mod tabs;
mod textarea;
mod tooltip;
pub use accordion::*;
pub use alert::*;
pub use avatar::*;
pub use badge::*;
pub use button::*;
pub use card::*;
pub use checkbox::*;
pub use dropdown_menu::*;
pub use input::*;
pub use label::*;
pub use progress::*;
pub use radio_group::*;
pub use select::*;
pub use separator::*;
pub use slider::*;
pub use switch::*;
pub use tabs::*;
pub use textarea::*;
pub use tooltip::*;

120
src/components/ui/select.rs Normal file
View File

@@ -0,0 +1,120 @@
use dioxus::prelude::*;
#[derive(Clone, PartialEq)]
pub struct SelectOption {
pub label: String,
pub value: String,
}
impl SelectOption {
pub fn new(label: impl Into<String>, value: impl Into<String>) -> Self {
Self {
label: label.into(),
value: value.into(),
}
}
}
#[component]
pub fn Select(
#[props(into, default)] id: Option<String>,
#[props(into)] placeholder: String,
#[props(into)] options: Vec<SelectOption>,
#[props(into, default)] selected: Option<String>,
#[props(default)] disabled: bool,
#[props(optional)] on_change: Option<EventHandler<String>>,
) -> Element {
let open = use_signal(|| false);
let current = use_signal(move || selected.clone());
let on_change_handler = on_change.clone();
let trigger_id = id.unwrap_or_default();
let selected_value = current();
let display_text = selected_value
.as_ref()
.and_then(|value| {
options
.iter()
.find(|option| option.value == *value)
.map(|option| option.label.clone())
})
.unwrap_or_else(|| placeholder.clone());
rsx! {
div {
class: "ui-select",
"data-disabled": disabled,
onfocusout: {
let mut signal = open.clone();
move |_| signal.set(false)
},
button {
class: "ui-select-trigger",
"data-open": if open() { "true" } else { "false" },
disabled,
id: trigger_id.clone(),
"aria-haspopup": "listbox",
"aria-expanded": if open() { "true" } else { "false" },
onclick: {
let mut open_signal = open.clone();
move |_| {
if !disabled {
let new_state = !open_signal();
open_signal.set(new_state);
}
}
},
span { "{display_text}" }
span {
style: "font-size: 0.8rem; opacity: 0.7;",
if open() { "" } else { "" }
}
}
if open() {
div {
class: "ui-select-content",
div {
class: "ui-select-list",
for option in options.iter().cloned() {
{
let is_active = selected_value
.as_ref()
.map(|value| value == &option.value)
.unwrap_or(false);
let value = option.value.clone();
let handler = on_change_handler.clone();
let mut open_signal = open.clone();
let mut current_signal = current.clone();
rsx! {
button {
class: "ui-select-item",
"data-state": if is_active { "active" } else { "inactive" },
onclick: {
let value = value.clone();
let handler = handler.clone();
move |_| {
current_signal.set(Some(value.clone()));
if let Some(callback) = handler.clone() {
callback.call(value.clone());
}
open_signal.set(false);
}
},
span { "{option.label}" }
if is_active {
span {
style: "font-size: 0.75rem; opacity: 0.7;",
""
}
}
}
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,29 @@
use dioxus::prelude::*;
#[component]
pub fn Tooltip(
#[props(into)] label: String,
#[props(default = 0)]
#[allow(unused)]
delay_ms: u64,
children: Element,
) -> Element {
let mut visible = use_signal(|| false);
rsx! {
span {
class: "ui-tooltip-wrapper",
tabindex: 0,
onmouseenter: move |_| visible.set(true),
onmouseleave: move |_| visible.set(false),
onfocusin: move |_| visible.set(true),
onfocusout: move |_| visible.set(false),
{children}
span {
class: "ui-tooltip-bubble",
"data-state": if visible() { "visible" } else { "hidden" },
"{label}",
}
}
}
}

View File

@@ -1,9 +1,11 @@
use crate::components::{
ui::{
Accordion, AccordionContent, AccordionItem, AccordionTrigger, Alert, AlertVariant, Avatar,
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,
CardFooter, CardHeader, CardTitle, Checkbox, DropdownMenu, DropdownMenuItem, Input, Label,
Progress, RadioGroup, RadioGroupItem, Select, SelectOption, Separator,
SeparatorOrientation, Slider, Switch, Tabs, TabsContent, TabsList, TabsTrigger, Textarea,
Tooltip,
},
Echo, Hero,
};
@@ -21,16 +23,51 @@ pub fn Home() -> Element {
#[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 accepted_terms = use_signal(|| false);
let email_notifications = use_signal(|| true);
let slider_value = use_signal(|| 42.0f32);
let contact_method = use_signal(|| "email".to_string());
let newsletter_opt_in = use_signal(|| true);
let dark_mode = use_signal(|| false);
let theme_choice = use_signal(|| Some("system".to_string()));
let menu_selection = use_signal(|| "Select a menu action".to_string());
let slider_value_signal = slider_value.clone();
let slider_value_setter = slider_value.clone();
let contact_method_signal = contact_method.clone();
let theme_choice_signal = theme_choice.clone();
let accepted_terms_setter = accepted_terms.clone();
let email_notifications_setter = email_notifications.clone();
let contact_method_setter = contact_method.clone();
let newsletter_opt_in_setter = newsletter_opt_in.clone();
let dark_mode_setter = dark_mode.clone();
let theme_choice_setter = theme_choice.clone();
let menu_selection_setter = menu_selection.clone();
let intensity_text = move || format!("Accent intensity: {:.0}%", slider_value_signal());
let contact_text = move || format!("Preferred contact: {}", contact_method_signal());
let select_options = vec![
SelectOption::new("System", "system"),
SelectOption::new("Light", "light"),
SelectOption::new("Dark", "dark"),
];
let menu_items = vec![
DropdownMenuItem::new("Profile", "profile").with_shortcut("⌘P"),
DropdownMenuItem::new("Billing", "billing").with_shortcut("⌘B"),
DropdownMenuItem::new("Team", "team"),
DropdownMenuItem::new("Sign out", "logout").destructive(),
];
let theme_display = {
let current = theme_choice();
current
.as_ref()
.and_then(|value| {
select_options
.iter()
.find(|option| option.value == *value)
.map(|option| option.label.clone())
})
.unwrap_or_else(|| "System".to_string())
};
let theme_summary = format!("Active theme: {theme_display}");
rsx! {
section {
@@ -74,29 +111,46 @@ fn UiShowcase() -> Element {
min: 0.0,
max: 100.0,
step: 1.0,
on_value_change: move |val| slider_value.set(val),
on_value_change: {
let mut signal = slider_value_setter.clone();
move |val| signal.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),
}
div { class: "ui-stack",
Label { html_for: "theme-select", "Theme preference" }
Select {
id: Some("theme-select".to_string()),
placeholder: "Select a theme",
options: select_options.clone(),
selected: theme_choice_signal(),
on_change: move |value| {
let mut signal = theme_choice_setter.clone();
signal.set(Some(value));
},
}
SpanHelper { "{theme_summary}" }
}
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_setter.clone().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_setter.clone().set(state),
}
}
}
}
CardFooter {
div { class: "ui-cluster",
@@ -146,6 +200,51 @@ fn UiShowcase() -> Element {
}
}
Card {
CardHeader {
CardTitle { "Select & menus" }
CardDescription { "Select, dropdown menu, tooltip and dynamic feedback." }
}
CardContent {
div { class: "ui-stack",
Label { html_for: "quick-theme", "Quick theme" }
Select {
id: Some("quick-theme".to_string()),
placeholder: "Choose theme",
options: select_options.clone(),
selected: theme_choice_signal(),
on_change: move |value| {
let mut signal = theme_choice_setter.clone();
signal.set(Some(value));
},
}
}
div { class: "ui-stack",
SpanHelper { "Dropdown menu" }
DropdownMenu {
label: "Open menu",
items: menu_items.clone(),
on_select: move |value| {
let mut signal = menu_selection_setter.clone();
signal.set(format!("Selected action: {value}"));
},
}
SpanHelper { "{menu_selection()}" }
}
div { class: "ui-stack",
SpanHelper { "Tooltip" }
Tooltip {
label: "Invite collaborators",
Button {
variant: ButtonVariant::Ghost,
size: ButtonSize::Sm,
"Hover me"
}
}
}
}
}
Card {
CardHeader {
CardTitle { "Selection controls" }
@@ -157,7 +256,7 @@ fn UiShowcase() -> Element {
Checkbox {
id: Some("newsletter-opt".to_string()),
checked: newsletter_opt_in(),
on_checked_change: move |state| newsletter_opt_in.set(state),
on_checked_change: move |state| newsletter_opt_in_setter.clone().set(state),
}
Label { html_for: "newsletter-opt", "Subscribe to newsletter" }
}
@@ -166,13 +265,13 @@ fn UiShowcase() -> Element {
Switch {
id: Some("dark-mode".to_string()),
checked: dark_mode(),
on_checked_change: move |state| dark_mode.set(state),
on_checked_change: move |state| dark_mode_setter.clone().set(state),
}
}
Separator { style: "margin: 0.75rem 0;" }
RadioGroup {
default_value: contact_method(),
on_value_change: move |value| contact_method.set(value),
on_value_change: move |value| contact_method_setter.clone().set(value),
div { class: "ui-stack",
div { class: "ui-cluster",
RadioGroupItem { id: Some("contact-email".to_string()), value: "email" }
@@ -231,6 +330,59 @@ fn UiShowcase() -> Element {
}
}
}
Card {
CardHeader {
CardTitle { "Alerts & extras" }
CardDescription { "Feedback surfaces, accordions, and avatar fallbacks." }
}
CardContent {
div { class: "ui-stack",
Alert {
title: Some("Heads up!".to_string()),
"We just shipped async server functions to production."
}
Alert {
variant: AlertVariant::Destructive,
title: Some("Deployment failed".to_string()),
"Check the build logs and retry once the issue is resolved."
}
}
Separator { style: "margin: 1rem 0;" }
Accordion {
collapsible: true,
default_value: Some("item-1".to_string()),
AccordionItem {
value: "item-1".to_string(),
AccordionTrigger { "What is shadcn/ui?" }
AccordionContent {
"A collection of unstyled, accessible primitives built on top of Radix, ready for your design system."
}
}
AccordionItem {
value: "item-2".to_string(),
AccordionTrigger { "Does this work with Dioxus?" }
AccordionContent {
"Yes! These components mirror the shadcn/ui ergonomics using Dioxus 0.7 signals."
}
}
}
Separator { style: "margin: 1rem 0;" }
div { class: "ui-cluster",
Tooltip {
label: "Ada Lovelace",
Avatar {
alt: Some("Ada Lovelace".to_string()),
fallback: Some("AL".to_string()),
}
}
Avatar {
alt: Some("Grace Hopper".to_string()),
fallback: Some("GH".to_string()),
}
}
}
}
}
}
}