mirror of
https://github.com/mztlive/dx-admin-template.git
synced 2025-12-22 21:59:59 +00:00
优化重构
This commit is contained in:
@@ -97,13 +97,12 @@ pub fn CheckboxChipGroup(
|
|||||||
) -> Element {
|
) -> Element {
|
||||||
let classes = merge_class("ui-checkbox-chip-group", class);
|
let classes = merge_class("ui-checkbox-chip-group", class);
|
||||||
let current_values = values();
|
let current_values = values();
|
||||||
let group_disabled = disabled;
|
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
div {
|
div {
|
||||||
class: classes,
|
class: classes,
|
||||||
role: "group",
|
role: "group",
|
||||||
"aria-disabled": group_disabled,
|
"aria-disabled": disabled,
|
||||||
if let Some(label_text) = label.clone() {
|
if let Some(label_text) = label.clone() {
|
||||||
span {
|
span {
|
||||||
class: "ui-choice-group-label",
|
class: "ui-choice-group-label",
|
||||||
@@ -112,49 +111,25 @@ pub fn CheckboxChipGroup(
|
|||||||
}
|
}
|
||||||
div {
|
div {
|
||||||
class: "ui-choice-group-options",
|
class: "ui-choice-group-options",
|
||||||
for option in options.iter().cloned() {
|
for option in options {
|
||||||
{
|
{
|
||||||
let option_label = option.label.clone();
|
let is_selected = current_values.iter().any(|item| item == &option.value);
|
||||||
let option_value = option.value.clone();
|
let disabled_state = disabled || option.disabled;
|
||||||
let option_description = option.description.clone();
|
|
||||||
let is_disabled = group_disabled || option.disabled;
|
|
||||||
let is_selected = current_values.iter().any(|item| item == &option_value);
|
|
||||||
let mut values_signal = values.clone();
|
|
||||||
let handler = on_values_change.clone();
|
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
button {
|
CheckboxChipItem {
|
||||||
class: "ui-checkbox-chip",
|
option,
|
||||||
"data-state": if is_selected { "selected" } else { "idle" },
|
is_selected,
|
||||||
"data-disabled": is_disabled,
|
disabled: disabled_state,
|
||||||
role: "checkbox",
|
on_toggle: move |value: String| {
|
||||||
"aria-checked": if is_selected { "true" } else { "false" },
|
values.with_mut(|items| {
|
||||||
"aria-disabled": if is_disabled { "true" } else { "false" },
|
if let Some(index) = items.iter().position(|item| item == &value) {
|
||||||
r#type: "button",
|
|
||||||
disabled: is_disabled,
|
|
||||||
onclick: move |_| {
|
|
||||||
if is_disabled {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
values_signal.with_mut(|items| {
|
|
||||||
if let Some(index) = items.iter().position(|item| item == &option_value) {
|
|
||||||
items.remove(index);
|
items.remove(index);
|
||||||
} else {
|
} else {
|
||||||
items.push(option_value.clone());
|
items.push(value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if let Some(callback) = handler.clone() {
|
if let Some(callback) = on_values_change.as_ref() {
|
||||||
callback.call(values_signal());
|
callback.call(values());
|
||||||
}
|
|
||||||
},
|
|
||||||
span {
|
|
||||||
class: "ui-chip-label",
|
|
||||||
"{option_label}"
|
|
||||||
}
|
|
||||||
if let Some(description) = option_description.clone() {
|
|
||||||
span {
|
|
||||||
class: "ui-chip-description",
|
|
||||||
"{description}"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,3 +140,41 @@ pub fn CheckboxChipGroup(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn CheckboxChipItem(
|
||||||
|
option: CheckboxChipOption,
|
||||||
|
is_selected: bool,
|
||||||
|
disabled: bool,
|
||||||
|
on_toggle: EventHandler<String>,
|
||||||
|
) -> Element {
|
||||||
|
let value = option.value.clone();
|
||||||
|
|
||||||
|
rsx! {
|
||||||
|
button {
|
||||||
|
class: "ui-checkbox-chip",
|
||||||
|
"data-state": if is_selected { "selected" } else { "idle" },
|
||||||
|
"data-disabled": disabled,
|
||||||
|
role: "checkbox",
|
||||||
|
"aria-checked": if is_selected { "true" } else { "false" },
|
||||||
|
"aria-disabled": if disabled { "true" } else { "false" },
|
||||||
|
r#type: "button",
|
||||||
|
disabled,
|
||||||
|
onclick: move |_| {
|
||||||
|
if !disabled {
|
||||||
|
on_toggle.call(value.clone());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
span {
|
||||||
|
class: "ui-chip-label",
|
||||||
|
"{option.label}"
|
||||||
|
}
|
||||||
|
if let Some(description) = option.description {
|
||||||
|
span {
|
||||||
|
class: "ui-chip-description",
|
||||||
|
"{description}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -43,11 +43,10 @@ pub fn Combobox(
|
|||||||
) -> Element {
|
) -> Element {
|
||||||
let classes = merge_class("ui-combobox", class);
|
let classes = merge_class("ui-combobox", class);
|
||||||
let trigger_id = id.unwrap_or_default();
|
let trigger_id = id.unwrap_or_default();
|
||||||
let search_placeholder = search_placeholder.unwrap_or_else(|| "Search...".to_string());
|
let search_placeholder_text = search_placeholder.unwrap_or_else(|| "Search...".to_string());
|
||||||
let open = use_signal(|| false);
|
let mut open = use_signal(|| false);
|
||||||
let current_selection = use_signal(move || selected.clone());
|
let mut current_selection = use_signal(move || selected.clone());
|
||||||
let query = use_signal(|| String::new());
|
let mut query = use_signal(|| String::new());
|
||||||
let on_select_handler = on_select.clone();
|
|
||||||
|
|
||||||
let current_value = current_selection();
|
let current_value = current_selection();
|
||||||
let display_label = current_value
|
let display_label = current_value
|
||||||
@@ -77,94 +76,102 @@ pub fn Combobox(
|
|||||||
div {
|
div {
|
||||||
class: classes,
|
class: classes,
|
||||||
"data-disabled": disabled,
|
"data-disabled": disabled,
|
||||||
onfocusout: {
|
onfocusout: move |_| open.set(false),
|
||||||
let mut open_signal = open.clone();
|
ComboboxTrigger {
|
||||||
move |_| open_signal.set(false)
|
id: trigger_id,
|
||||||
},
|
|
||||||
button {
|
|
||||||
class: "ui-combobox-trigger",
|
|
||||||
id: trigger_id.clone(),
|
|
||||||
"aria-haspopup": "dialog",
|
|
||||||
"aria-expanded": if open() { "true" } else { "false" },
|
|
||||||
disabled,
|
disabled,
|
||||||
onclick: {
|
open: open(),
|
||||||
let mut open_signal = open.clone();
|
display_label,
|
||||||
move |_| {
|
on_toggle: move |_| {
|
||||||
if !disabled {
|
if !disabled {
|
||||||
let next_state = !open_signal();
|
let next_state = !open();
|
||||||
open_signal.set(next_state);
|
open.set(next_state);
|
||||||
if !next_state {
|
if !next_state {
|
||||||
let mut query_signal = query.clone();
|
query.set(String::new());
|
||||||
query_signal.set(String::new());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
span { "{display_label}" }
|
|
||||||
span { class: "ui-combobox-caret", if open() { "▲" } else { "▼" } }
|
|
||||||
}
|
}
|
||||||
if open() {
|
if open() {
|
||||||
div {
|
ComboboxContent {
|
||||||
class: "ui-combobox-content",
|
search_placeholder: search_placeholder_text,
|
||||||
div {
|
query,
|
||||||
class: "ui-combobox-search",
|
filtered_options,
|
||||||
input {
|
current_value,
|
||||||
class: "ui-combobox-input",
|
on_select: move |value: String| {
|
||||||
placeholder: search_placeholder.clone(),
|
current_selection.set(Some(value.clone()));
|
||||||
r#type: "text",
|
if let Some(callback) = on_select.as_ref() {
|
||||||
autofocus: true,
|
callback.call(value);
|
||||||
value: "{query()}",
|
|
||||||
oninput: {
|
|
||||||
let mut query_signal = query.clone();
|
|
||||||
move |event| query_signal.set(event.value())
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
open.set(false);
|
||||||
|
query.set(String::new());
|
||||||
}
|
}
|
||||||
if filtered_options.is_empty() {
|
}
|
||||||
div {
|
}
|
||||||
class: "ui-combobox-empty",
|
}
|
||||||
"No results found"
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
ul {
|
#[component]
|
||||||
class: "ui-combobox-list",
|
fn ComboboxTrigger(
|
||||||
for option in filtered_options {
|
#[props(into)] id: String,
|
||||||
{
|
disabled: bool,
|
||||||
let is_active = current_value
|
open: bool,
|
||||||
.as_ref()
|
#[props(into)] display_label: String,
|
||||||
.map(|value| value == &option.value)
|
on_toggle: EventHandler<()>,
|
||||||
.unwrap_or(false);
|
) -> Element {
|
||||||
let option_value = option.value.clone();
|
rsx! {
|
||||||
let option_label = option.label.clone();
|
button {
|
||||||
let option_description = option.description.clone();
|
class: "ui-combobox-trigger",
|
||||||
rsx! {
|
id,
|
||||||
li {
|
"aria-haspopup": "dialog",
|
||||||
class: "ui-combobox-item",
|
"aria-expanded": if open { "true" } else { "false" },
|
||||||
"data-state": if is_active { "active" } else { "inactive" },
|
disabled,
|
||||||
button {
|
onclick: move |_| on_toggle.call(()),
|
||||||
r#type: "button",
|
span { "{display_label}" }
|
||||||
onclick: {
|
span { class: "ui-combobox-caret", if open { "▲" } else { "▼" } }
|
||||||
let option_value = option_value.clone();
|
}
|
||||||
let handler = on_select_handler.clone();
|
}
|
||||||
let mut open_signal = open.clone();
|
}
|
||||||
let mut current_signal = current_selection.clone();
|
|
||||||
let mut query_signal = query.clone();
|
#[component]
|
||||||
move |_| {
|
fn ComboboxContent(
|
||||||
current_signal.set(Some(option_value.clone()));
|
#[props(into)] search_placeholder: String,
|
||||||
if let Some(callback) = handler.clone() {
|
mut query: Signal<String>,
|
||||||
callback.call(option_value.clone());
|
filtered_options: Vec<ComboboxOption>,
|
||||||
}
|
current_value: Option<String>,
|
||||||
open_signal.set(false);
|
on_select: EventHandler<String>,
|
||||||
query_signal.set(String::new());
|
) -> Element {
|
||||||
}
|
rsx! {
|
||||||
},
|
div {
|
||||||
span { class: "ui-combobox-label", "{option_label}" }
|
class: "ui-combobox-content",
|
||||||
if let Some(description) = option_description {
|
div {
|
||||||
span { class: "ui-combobox-description", "{description}" }
|
class: "ui-combobox-search",
|
||||||
}
|
input {
|
||||||
}
|
class: "ui-combobox-input",
|
||||||
}
|
placeholder: search_placeholder,
|
||||||
}
|
r#type: "text",
|
||||||
|
autofocus: true,
|
||||||
|
value: "{query()}",
|
||||||
|
oninput: move |event| query.set(event.value()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if filtered_options.is_empty() {
|
||||||
|
div {
|
||||||
|
class: "ui-combobox-empty",
|
||||||
|
"No results found"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ul {
|
||||||
|
class: "ui-combobox-list",
|
||||||
|
for option in filtered_options {
|
||||||
|
{
|
||||||
|
let is_active = current_value.as_ref().map(|v| v == &option.value).unwrap_or(false);
|
||||||
|
rsx! {
|
||||||
|
ComboboxItem {
|
||||||
|
option,
|
||||||
|
is_active,
|
||||||
|
on_select
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -174,3 +181,27 @@ pub fn Combobox(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn ComboboxItem(
|
||||||
|
option: ComboboxOption,
|
||||||
|
is_active: bool,
|
||||||
|
on_select: EventHandler<String>,
|
||||||
|
) -> Element {
|
||||||
|
let value = option.value.clone();
|
||||||
|
|
||||||
|
rsx! {
|
||||||
|
li {
|
||||||
|
class: "ui-combobox-item",
|
||||||
|
"data-state": if is_active { "active" } else { "inactive" },
|
||||||
|
button {
|
||||||
|
r#type: "button",
|
||||||
|
onclick: move |_| on_select.call(value.clone()),
|
||||||
|
span { class: "ui-combobox-label", "{option.label}" }
|
||||||
|
if let Some(description) = option.description {
|
||||||
|
span { class: "ui-combobox-description", "{description}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
use super::button::{Button, ButtonVariant, ButtonSize};
|
||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
pub enum DropdownItemVariant {
|
pub enum DropdownItemVariant {
|
||||||
@@ -7,10 +8,10 @@ pub enum DropdownItemVariant {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DropdownItemVariant {
|
impl DropdownItemVariant {
|
||||||
fn as_str(&self) -> &'static str {
|
fn to_button_variant(&self) -> ButtonVariant {
|
||||||
match self {
|
match self {
|
||||||
DropdownItemVariant::Default => "default",
|
DropdownItemVariant::Default => ButtonVariant::Ghost,
|
||||||
DropdownItemVariant::Destructive => "destructive",
|
DropdownItemVariant::Destructive => ButtonVariant::Destructive,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -56,25 +57,19 @@ pub fn DropdownMenu(
|
|||||||
#[props(into)] items: Vec<DropdownMenuItem>,
|
#[props(into)] items: Vec<DropdownMenuItem>,
|
||||||
#[props(optional)] on_select: Option<EventHandler<String>>,
|
#[props(optional)] on_select: Option<EventHandler<String>>,
|
||||||
) -> Element {
|
) -> Element {
|
||||||
let open = use_signal(|| false);
|
let mut open = use_signal(|| false);
|
||||||
let on_select_handler = on_select.clone();
|
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
div {
|
div {
|
||||||
class: "ui-dropdown",
|
class: "ui-dropdown",
|
||||||
button {
|
Button {
|
||||||
|
variant: ButtonVariant::Ghost,
|
||||||
|
size: ButtonSize::Sm,
|
||||||
class: "ui-dropdown-trigger",
|
class: "ui-dropdown-trigger",
|
||||||
"data-open": if open() { "true" } else { "false" },
|
on_click: move |_| open.set(!open()),
|
||||||
onclick: {
|
"{label}"
|
||||||
let mut signal = open.clone();
|
|
||||||
move |_| {
|
|
||||||
let new_state = !signal();
|
|
||||||
signal.set(new_state);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
span { "{label}" }
|
|
||||||
span {
|
span {
|
||||||
style: "font-size: 0.85rem; opacity: 0.7;",
|
style: "margin-left: 0.5rem; font-size: 0.85rem; opacity: 0.7;",
|
||||||
"⋮"
|
"⋮"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -83,33 +78,14 @@ pub fn DropdownMenu(
|
|||||||
class: "ui-dropdown-content",
|
class: "ui-dropdown-content",
|
||||||
div {
|
div {
|
||||||
class: "ui-dropdown-list",
|
class: "ui-dropdown-list",
|
||||||
for item in items.iter().cloned() {
|
for item in items {
|
||||||
{
|
DropdownMenuItemButton {
|
||||||
let value = item.value.clone();
|
item,
|
||||||
let shortcut = item.shortcut.clone();
|
on_click: move |value: String| {
|
||||||
let variant = item.variant.as_str().to_string();
|
if let Some(callback) = on_select.as_ref() {
|
||||||
let mut open_signal = open.clone();
|
callback.call(value);
|
||||||
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}" }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
open.set(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -119,3 +95,27 @@ pub fn DropdownMenu(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn DropdownMenuItemButton(
|
||||||
|
item: DropdownMenuItem,
|
||||||
|
on_click: EventHandler<String>,
|
||||||
|
) -> Element {
|
||||||
|
let value = item.value.clone();
|
||||||
|
|
||||||
|
rsx! {
|
||||||
|
Button {
|
||||||
|
variant: item.variant.to_button_variant(),
|
||||||
|
size: ButtonSize::Sm,
|
||||||
|
class: "ui-dropdown-item",
|
||||||
|
on_click: move |_| on_click.call(value.clone()),
|
||||||
|
span { "{item.label}" }
|
||||||
|
if let Some(shortcut) = item.shortcut {
|
||||||
|
span {
|
||||||
|
style: "margin-left: auto; font-size: 0.75rem; opacity: 0.6;",
|
||||||
|
"{shortcut}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
use super::button::{Button, ButtonVariant, ButtonSize};
|
||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
pub struct MenubarItem {
|
pub struct MenubarItem {
|
||||||
@@ -50,67 +51,90 @@ pub fn Menubar(
|
|||||||
#[props(optional)] on_select: Option<EventHandler<String>>,
|
#[props(optional)] on_select: Option<EventHandler<String>>,
|
||||||
) -> Element {
|
) -> Element {
|
||||||
let mut open = use_signal(|| None::<usize>);
|
let mut open = use_signal(|| None::<usize>);
|
||||||
let handler = on_select.clone();
|
|
||||||
|
|
||||||
let menu_nodes: Vec<_> = menus
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(index, menu)| {
|
|
||||||
let mut open_signal_hover = open.clone();
|
|
||||||
let mut open_signal_click = open.clone();
|
|
||||||
let is_open = open() == Some(index);
|
|
||||||
let item_nodes: Vec<_> = menu
|
|
||||||
.items
|
|
||||||
.iter()
|
|
||||||
.map(|item| {
|
|
||||||
let value = item.value.clone();
|
|
||||||
let shortcut = item.shortcut.clone();
|
|
||||||
let destructive = item.destructive;
|
|
||||||
let handler_clone = handler.clone();
|
|
||||||
let mut open_close = open.clone();
|
|
||||||
|
|
||||||
rsx! {
|
|
||||||
button {
|
|
||||||
class: "ui-menubar-item",
|
|
||||||
"data-variant": if destructive { "destructive" } else { "default" },
|
|
||||||
onclick: move |_| {
|
|
||||||
if let Some(cb) = handler_clone.clone() {
|
|
||||||
cb.call(value.clone());
|
|
||||||
}
|
|
||||||
open_close.set(None);
|
|
||||||
},
|
|
||||||
span { "{item.label}" }
|
|
||||||
if let Some(shortcut) = shortcut.clone() {
|
|
||||||
span { style: "font-size: 0.75rem; opacity: 0.6;", "{shortcut}" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
rsx! {
|
|
||||||
span {
|
|
||||||
style: "position: relative;",
|
|
||||||
button {
|
|
||||||
class: "ui-menubar-trigger",
|
|
||||||
"data-open": if is_open { "true" } else { "false" },
|
|
||||||
onmouseenter: move |_| open_signal_hover.set(Some(index)),
|
|
||||||
onclick: move |_| open_signal_click.set(Some(index)),
|
|
||||||
"{menu.label}"
|
|
||||||
}
|
|
||||||
if is_open {
|
|
||||||
div { class: "ui-menubar-content", {item_nodes.into_iter()} }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
div {
|
div {
|
||||||
class: "ui-menubar",
|
class: "ui-menubar",
|
||||||
onmouseleave: move |_| open.set(None),
|
onmouseleave: move |_| open.set(None),
|
||||||
{menu_nodes.into_iter()}
|
for (index, menu) in menus.into_iter().enumerate() {
|
||||||
|
MenubarMenuTrigger {
|
||||||
|
menu,
|
||||||
|
index,
|
||||||
|
is_open: open() == Some(index),
|
||||||
|
on_hover: move |idx| open.set(Some(idx)),
|
||||||
|
on_click: move |idx| open.set(Some(idx)),
|
||||||
|
on_item_select: move |value: String| {
|
||||||
|
if let Some(cb) = on_select.as_ref() {
|
||||||
|
cb.call(value);
|
||||||
|
}
|
||||||
|
open.set(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn MenubarMenuTrigger(
|
||||||
|
menu: MenubarMenu,
|
||||||
|
index: usize,
|
||||||
|
is_open: bool,
|
||||||
|
on_hover: EventHandler<usize>,
|
||||||
|
on_click: EventHandler<usize>,
|
||||||
|
on_item_select: EventHandler<String>,
|
||||||
|
) -> Element {
|
||||||
|
rsx! {
|
||||||
|
span {
|
||||||
|
style: "position: relative;",
|
||||||
|
onmouseenter: move |_| on_hover.call(index),
|
||||||
|
Button {
|
||||||
|
variant: if is_open { ButtonVariant::Ghost } else { ButtonVariant::Ghost },
|
||||||
|
size: ButtonSize::Sm,
|
||||||
|
class: "ui-menubar-trigger",
|
||||||
|
on_click: move |_| on_click.call(index),
|
||||||
|
"{menu.label}"
|
||||||
|
}
|
||||||
|
if is_open {
|
||||||
|
div {
|
||||||
|
class: "ui-menubar-content",
|
||||||
|
for item in menu.items {
|
||||||
|
MenubarItemButton {
|
||||||
|
item,
|
||||||
|
on_select: on_item_select
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn MenubarItemButton(
|
||||||
|
item: MenubarItem,
|
||||||
|
on_select: EventHandler<String>,
|
||||||
|
) -> Element {
|
||||||
|
let value = item.value.clone();
|
||||||
|
let variant = if item.destructive {
|
||||||
|
ButtonVariant::Destructive
|
||||||
|
} else {
|
||||||
|
ButtonVariant::Ghost
|
||||||
|
};
|
||||||
|
|
||||||
|
rsx! {
|
||||||
|
Button {
|
||||||
|
variant,
|
||||||
|
size: ButtonSize::Sm,
|
||||||
|
class: "ui-menubar-item",
|
||||||
|
on_click: move |_| on_select.call(value.clone()),
|
||||||
|
span { "{item.label}" }
|
||||||
|
if let Some(shortcut) = item.shortcut {
|
||||||
|
span {
|
||||||
|
style: "margin-left: auto; font-size: 0.75rem; opacity: 0.6;",
|
||||||
|
"{shortcut}"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
use super::button::{Button, ButtonVariant, ButtonSize};
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Pagination(
|
pub fn Pagination(
|
||||||
@@ -13,13 +14,75 @@ pub fn Pagination(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let on_change = on_page_change.clone();
|
let buttons = calculate_page_buttons(total_pages, current());
|
||||||
|
|
||||||
|
rsx! {
|
||||||
|
nav {
|
||||||
|
class: "ui-pagination",
|
||||||
|
aria_label: "Pagination",
|
||||||
|
Button {
|
||||||
|
variant: ButtonVariant::Outline,
|
||||||
|
size: ButtonSize::Sm,
|
||||||
|
disabled: current() <= 1,
|
||||||
|
on_click: move |_| {
|
||||||
|
let new_page = current().saturating_sub(1).max(1);
|
||||||
|
current.set(new_page);
|
||||||
|
if let Some(cb) = on_page_change.as_ref() {
|
||||||
|
cb.call(new_page);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Prev"
|
||||||
|
}
|
||||||
|
for page in buttons {
|
||||||
|
{
|
||||||
|
if page == 0 {
|
||||||
|
rsx! {
|
||||||
|
span {
|
||||||
|
class: "ui-pagination-ellipsis",
|
||||||
|
"…"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let is_active = current() == page;
|
||||||
|
rsx! {
|
||||||
|
Button {
|
||||||
|
variant: if is_active { ButtonVariant::Default } else { ButtonVariant::Ghost },
|
||||||
|
size: ButtonSize::Sm,
|
||||||
|
on_click: move |_| {
|
||||||
|
let new_page = page.max(1).min(total_pages.max(1));
|
||||||
|
current.set(new_page);
|
||||||
|
if let Some(cb) = on_page_change.as_ref() {
|
||||||
|
cb.call(new_page);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"{page}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
variant: ButtonVariant::Outline,
|
||||||
|
size: ButtonSize::Sm,
|
||||||
|
disabled: current() >= total_pages.max(1),
|
||||||
|
on_click: move |_| {
|
||||||
|
let new_page = (current() + 1).min(total_pages.max(1));
|
||||||
|
current.set(new_page);
|
||||||
|
if let Some(cb) = on_page_change.as_ref() {
|
||||||
|
cb.call(new_page);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Next"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calculate_page_buttons(total_pages: usize, active: usize) -> Vec<usize> {
|
||||||
let mut buttons = vec![];
|
let mut buttons = vec![];
|
||||||
if total_pages <= 7 {
|
if total_pages <= 7 {
|
||||||
buttons.extend(1..=total_pages);
|
buttons.extend(1..=total_pages);
|
||||||
} else {
|
} else {
|
||||||
let active = current();
|
|
||||||
buttons.extend([1, 2]);
|
buttons.extend([1, 2]);
|
||||||
if active > 4 {
|
if active > 4 {
|
||||||
buttons.push(0); // ellipsis indicator
|
buttons.push(0); // ellipsis indicator
|
||||||
@@ -34,68 +97,5 @@ pub fn Pagination(
|
|||||||
}
|
}
|
||||||
buttons.extend([total_pages - 1, total_pages]);
|
buttons.extend([total_pages - 1, total_pages]);
|
||||||
}
|
}
|
||||||
|
buttons
|
||||||
let page_nodes: Vec<_> = buttons
|
|
||||||
.iter()
|
|
||||||
.map(|page| {
|
|
||||||
if *page == 0 {
|
|
||||||
rsx! { span { class: "ui-page-button", style: "pointer-events: none;", "…" } }
|
|
||||||
} else {
|
|
||||||
let mut page_signal = current.clone();
|
|
||||||
let page_handler = on_change.clone();
|
|
||||||
let target = *page;
|
|
||||||
rsx! {
|
|
||||||
button {
|
|
||||||
class: "ui-page-button",
|
|
||||||
"data-active": if page_signal() == target { "true" } else { "false" },
|
|
||||||
onclick: move |_| {
|
|
||||||
let new_page = target.max(1).min(total_pages.max(1));
|
|
||||||
page_signal.set(new_page);
|
|
||||||
if let Some(cb) = page_handler.clone() {
|
|
||||||
cb.call(new_page);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"{target}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let mut prev_signal = current.clone();
|
|
||||||
let prev_handler = on_change.clone();
|
|
||||||
let mut next_signal = current.clone();
|
|
||||||
let next_handler = on_change.clone();
|
|
||||||
|
|
||||||
rsx! {
|
|
||||||
nav {
|
|
||||||
class: "ui-pagination",
|
|
||||||
aria_label: "Pagination",
|
|
||||||
button {
|
|
||||||
class: "ui-page-button",
|
|
||||||
disabled: prev_signal() <= 1,
|
|
||||||
onclick: move |_| {
|
|
||||||
let new_page = prev_signal().saturating_sub(1).max(1);
|
|
||||||
prev_signal.set(new_page);
|
|
||||||
if let Some(cb) = prev_handler.clone() {
|
|
||||||
cb.call(new_page);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Prev"
|
|
||||||
}
|
|
||||||
{page_nodes.into_iter()}
|
|
||||||
button {
|
|
||||||
class: "ui-page-button",
|
|
||||||
disabled: next_signal() >= total_pages.max(1),
|
|
||||||
onclick: move |_| {
|
|
||||||
let new_page = (next_signal() + 1).min(total_pages.max(1));
|
|
||||||
next_signal.set(new_page);
|
|
||||||
if let Some(cb) = next_handler.clone() {
|
|
||||||
cb.call(new_page);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Next"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,13 +144,12 @@ pub fn RadioChipGroup(
|
|||||||
) -> Element {
|
) -> Element {
|
||||||
let classes = merge_class("ui-radio-chip-group", class);
|
let classes = merge_class("ui-radio-chip-group", class);
|
||||||
let current_value = value();
|
let current_value = value();
|
||||||
let group_disabled = disabled;
|
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
div {
|
div {
|
||||||
class: classes,
|
class: classes,
|
||||||
role: "radiogroup",
|
role: "radiogroup",
|
||||||
"aria-disabled": group_disabled,
|
"aria-disabled": disabled,
|
||||||
if let Some(label_text) = label.clone() {
|
if let Some(label_text) = label.clone() {
|
||||||
span {
|
span {
|
||||||
class: "ui-choice-group-label",
|
class: "ui-choice-group-label",
|
||||||
@@ -159,52 +158,19 @@ pub fn RadioChipGroup(
|
|||||||
}
|
}
|
||||||
div {
|
div {
|
||||||
class: "ui-choice-group-options",
|
class: "ui-choice-group-options",
|
||||||
for option in options.iter().cloned() {
|
for option in options {
|
||||||
{
|
{
|
||||||
let option_label = option.label.clone();
|
let is_selected = current_value.as_ref().map(|v| v == &option.value).unwrap_or(false);
|
||||||
let option_value = option.value.clone();
|
let disabled_state = disabled || option.disabled;
|
||||||
let option_description = option.description.clone();
|
|
||||||
let is_disabled = group_disabled || option.disabled;
|
|
||||||
let is_selected = current_value
|
|
||||||
.as_ref()
|
|
||||||
.map(|selected| selected == &option_value)
|
|
||||||
.unwrap_or(false);
|
|
||||||
let mut value_signal = value.clone();
|
|
||||||
let handler = on_value_change.clone();
|
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
button {
|
RadioChipItem {
|
||||||
class: "ui-radio-chip",
|
option,
|
||||||
"data-state": if is_selected { "selected" } else { "idle" },
|
is_selected,
|
||||||
"data-disabled": is_disabled,
|
disabled: disabled_state,
|
||||||
role: "radio",
|
on_select: move |selected_value: String| {
|
||||||
"aria-checked": if is_selected { "true" } else { "false" },
|
value.set(Some(selected_value.clone()));
|
||||||
"aria-disabled": if is_disabled { "true" } else { "false" },
|
if let Some(callback) = on_value_change.as_ref() {
|
||||||
r#type: "button",
|
callback.call(selected_value);
|
||||||
disabled: is_disabled,
|
|
||||||
onclick: move |_| {
|
|
||||||
if is_disabled {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let already_selected = value_signal()
|
|
||||||
.as_ref()
|
|
||||||
.map(|selected| selected == &option_value)
|
|
||||||
.unwrap_or(false);
|
|
||||||
if !already_selected {
|
|
||||||
value_signal.set(Some(option_value.clone()));
|
|
||||||
if let Some(callback) = handler.clone() {
|
|
||||||
callback.call(option_value.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
span {
|
|
||||||
class: "ui-chip-label",
|
|
||||||
"{option_label}"
|
|
||||||
}
|
|
||||||
if let Some(description) = option_description.clone() {
|
|
||||||
span {
|
|
||||||
class: "ui-chip-description",
|
|
||||||
"{description}"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -215,3 +181,41 @@ pub fn RadioChipGroup(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn RadioChipItem(
|
||||||
|
option: RadioChipOption,
|
||||||
|
is_selected: bool,
|
||||||
|
disabled: bool,
|
||||||
|
on_select: EventHandler<String>,
|
||||||
|
) -> Element {
|
||||||
|
let value = option.value.clone();
|
||||||
|
|
||||||
|
rsx! {
|
||||||
|
button {
|
||||||
|
class: "ui-radio-chip",
|
||||||
|
"data-state": if is_selected { "selected" } else { "idle" },
|
||||||
|
"data-disabled": disabled,
|
||||||
|
role: "radio",
|
||||||
|
"aria-checked": if is_selected { "true" } else { "false" },
|
||||||
|
"aria-disabled": if disabled { "true" } else { "false" },
|
||||||
|
r#type: "button",
|
||||||
|
disabled,
|
||||||
|
onclick: move |_| {
|
||||||
|
if !disabled && !is_selected {
|
||||||
|
on_select.call(value.clone());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
span {
|
||||||
|
class: "ui-chip-label",
|
||||||
|
"{option.label}"
|
||||||
|
}
|
||||||
|
if let Some(description) = option.description {
|
||||||
|
span {
|
||||||
|
class: "ui-chip-description",
|
||||||
|
"{description}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user