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 {
|
||||
let classes = merge_class("ui-checkbox-chip-group", class);
|
||||
let current_values = values();
|
||||
let group_disabled = disabled;
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
class: classes,
|
||||
role: "group",
|
||||
"aria-disabled": group_disabled,
|
||||
"aria-disabled": disabled,
|
||||
if let Some(label_text) = label.clone() {
|
||||
span {
|
||||
class: "ui-choice-group-label",
|
||||
@@ -112,49 +111,25 @@ pub fn CheckboxChipGroup(
|
||||
}
|
||||
div {
|
||||
class: "ui-choice-group-options",
|
||||
for option in options.iter().cloned() {
|
||||
for option in options {
|
||||
{
|
||||
let option_label = option.label.clone();
|
||||
let option_value = option.value.clone();
|
||||
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();
|
||||
|
||||
let is_selected = current_values.iter().any(|item| item == &option.value);
|
||||
let disabled_state = disabled || option.disabled;
|
||||
rsx! {
|
||||
button {
|
||||
class: "ui-checkbox-chip",
|
||||
"data-state": if is_selected { "selected" } else { "idle" },
|
||||
"data-disabled": is_disabled,
|
||||
role: "checkbox",
|
||||
"aria-checked": if is_selected { "true" } else { "false" },
|
||||
"aria-disabled": if is_disabled { "true" } else { "false" },
|
||||
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) {
|
||||
CheckboxChipItem {
|
||||
option,
|
||||
is_selected,
|
||||
disabled: disabled_state,
|
||||
on_toggle: move |value: String| {
|
||||
values.with_mut(|items| {
|
||||
if let Some(index) = items.iter().position(|item| item == &value) {
|
||||
items.remove(index);
|
||||
} else {
|
||||
items.push(option_value.clone());
|
||||
items.push(value);
|
||||
}
|
||||
});
|
||||
if let Some(callback) = handler.clone() {
|
||||
callback.call(values_signal());
|
||||
}
|
||||
},
|
||||
span {
|
||||
class: "ui-chip-label",
|
||||
"{option_label}"
|
||||
}
|
||||
if let Some(description) = option_description.clone() {
|
||||
span {
|
||||
class: "ui-chip-description",
|
||||
"{description}"
|
||||
if let Some(callback) = on_values_change.as_ref() {
|
||||
callback.call(values());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
let classes = merge_class("ui-combobox", class);
|
||||
let trigger_id = id.unwrap_or_default();
|
||||
let search_placeholder = search_placeholder.unwrap_or_else(|| "Search...".to_string());
|
||||
let open = use_signal(|| false);
|
||||
let current_selection = use_signal(move || selected.clone());
|
||||
let query = use_signal(|| String::new());
|
||||
let on_select_handler = on_select.clone();
|
||||
let search_placeholder_text = search_placeholder.unwrap_or_else(|| "Search...".to_string());
|
||||
let mut open = use_signal(|| false);
|
||||
let mut current_selection = use_signal(move || selected.clone());
|
||||
let mut query = use_signal(|| String::new());
|
||||
|
||||
let current_value = current_selection();
|
||||
let display_label = current_value
|
||||
@@ -77,94 +76,102 @@ pub fn Combobox(
|
||||
div {
|
||||
class: classes,
|
||||
"data-disabled": disabled,
|
||||
onfocusout: {
|
||||
let mut open_signal = open.clone();
|
||||
move |_| open_signal.set(false)
|
||||
},
|
||||
button {
|
||||
class: "ui-combobox-trigger",
|
||||
id: trigger_id.clone(),
|
||||
"aria-haspopup": "dialog",
|
||||
"aria-expanded": if open() { "true" } else { "false" },
|
||||
onfocusout: move |_| open.set(false),
|
||||
ComboboxTrigger {
|
||||
id: trigger_id,
|
||||
disabled,
|
||||
onclick: {
|
||||
let mut open_signal = open.clone();
|
||||
move |_| {
|
||||
if !disabled {
|
||||
let next_state = !open_signal();
|
||||
open_signal.set(next_state);
|
||||
if !next_state {
|
||||
let mut query_signal = query.clone();
|
||||
query_signal.set(String::new());
|
||||
}
|
||||
open: open(),
|
||||
display_label,
|
||||
on_toggle: move |_| {
|
||||
if !disabled {
|
||||
let next_state = !open();
|
||||
open.set(next_state);
|
||||
if !next_state {
|
||||
query.set(String::new());
|
||||
}
|
||||
}
|
||||
},
|
||||
span { "{display_label}" }
|
||||
span { class: "ui-combobox-caret", if open() { "▲" } else { "▼" } }
|
||||
}
|
||||
}
|
||||
if open() {
|
||||
div {
|
||||
class: "ui-combobox-content",
|
||||
div {
|
||||
class: "ui-combobox-search",
|
||||
input {
|
||||
class: "ui-combobox-input",
|
||||
placeholder: search_placeholder.clone(),
|
||||
r#type: "text",
|
||||
autofocus: true,
|
||||
value: "{query()}",
|
||||
oninput: {
|
||||
let mut query_signal = query.clone();
|
||||
move |event| query_signal.set(event.value())
|
||||
},
|
||||
ComboboxContent {
|
||||
search_placeholder: search_placeholder_text,
|
||||
query,
|
||||
filtered_options,
|
||||
current_value,
|
||||
on_select: move |value: String| {
|
||||
current_selection.set(Some(value.clone()));
|
||||
if let Some(callback) = on_select.as_ref() {
|
||||
callback.call(value);
|
||||
}
|
||||
open.set(false);
|
||||
query.set(String::new());
|
||||
}
|
||||
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(|value| value == &option.value)
|
||||
.unwrap_or(false);
|
||||
let option_value = option.value.clone();
|
||||
let option_label = option.label.clone();
|
||||
let option_description = option.description.clone();
|
||||
rsx! {
|
||||
li {
|
||||
class: "ui-combobox-item",
|
||||
"data-state": if is_active { "active" } else { "inactive" },
|
||||
button {
|
||||
r#type: "button",
|
||||
onclick: {
|
||||
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();
|
||||
move |_| {
|
||||
current_signal.set(Some(option_value.clone()));
|
||||
if let Some(callback) = handler.clone() {
|
||||
callback.call(option_value.clone());
|
||||
}
|
||||
open_signal.set(false);
|
||||
query_signal.set(String::new());
|
||||
}
|
||||
},
|
||||
span { class: "ui-combobox-label", "{option_label}" }
|
||||
if let Some(description) = option_description {
|
||||
span { class: "ui-combobox-description", "{description}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ComboboxTrigger(
|
||||
#[props(into)] id: String,
|
||||
disabled: bool,
|
||||
open: bool,
|
||||
#[props(into)] display_label: String,
|
||||
on_toggle: EventHandler<()>,
|
||||
) -> Element {
|
||||
rsx! {
|
||||
button {
|
||||
class: "ui-combobox-trigger",
|
||||
id,
|
||||
"aria-haspopup": "dialog",
|
||||
"aria-expanded": if open { "true" } else { "false" },
|
||||
disabled,
|
||||
onclick: move |_| on_toggle.call(()),
|
||||
span { "{display_label}" }
|
||||
span { class: "ui-combobox-caret", if open { "▲" } else { "▼" } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ComboboxContent(
|
||||
#[props(into)] search_placeholder: String,
|
||||
mut query: Signal<String>,
|
||||
filtered_options: Vec<ComboboxOption>,
|
||||
current_value: Option<String>,
|
||||
on_select: EventHandler<String>,
|
||||
) -> Element {
|
||||
rsx! {
|
||||
div {
|
||||
class: "ui-combobox-content",
|
||||
div {
|
||||
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 super::button::{Button, ButtonVariant, ButtonSize};
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub enum DropdownItemVariant {
|
||||
@@ -7,10 +8,10 @@ pub enum DropdownItemVariant {
|
||||
}
|
||||
|
||||
impl DropdownItemVariant {
|
||||
fn as_str(&self) -> &'static str {
|
||||
fn to_button_variant(&self) -> ButtonVariant {
|
||||
match self {
|
||||
DropdownItemVariant::Default => "default",
|
||||
DropdownItemVariant::Destructive => "destructive",
|
||||
DropdownItemVariant::Default => ButtonVariant::Ghost,
|
||||
DropdownItemVariant::Destructive => ButtonVariant::Destructive,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,25 +57,19 @@ pub fn DropdownMenu(
|
||||
#[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();
|
||||
let mut open = use_signal(|| false);
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
class: "ui-dropdown",
|
||||
button {
|
||||
Button {
|
||||
variant: ButtonVariant::Ghost,
|
||||
size: ButtonSize::Sm,
|
||||
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}" }
|
||||
on_click: move |_| open.set(!open()),
|
||||
"{label}"
|
||||
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",
|
||||
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}" }
|
||||
}
|
||||
for item in items {
|
||||
DropdownMenuItemButton {
|
||||
item,
|
||||
on_click: move |value: String| {
|
||||
if let Some(callback) = on_select.as_ref() {
|
||||
callback.call(value);
|
||||
}
|
||||
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 super::button::{Button, ButtonVariant, ButtonSize};
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct MenubarItem {
|
||||
@@ -50,67 +51,90 @@ pub fn Menubar(
|
||||
#[props(optional)] on_select: Option<EventHandler<String>>,
|
||||
) -> Element {
|
||||
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! {
|
||||
div {
|
||||
class: "ui-menubar",
|
||||
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 super::button::{Button, ButtonVariant, ButtonSize};
|
||||
|
||||
#[component]
|
||||
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![];
|
||||
if total_pages <= 7 {
|
||||
buttons.extend(1..=total_pages);
|
||||
} else {
|
||||
let active = current();
|
||||
buttons.extend([1, 2]);
|
||||
if active > 4 {
|
||||
buttons.push(0); // ellipsis indicator
|
||||
@@ -34,68 +97,5 @@ pub fn Pagination(
|
||||
}
|
||||
buttons.extend([total_pages - 1, total_pages]);
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
buttons
|
||||
}
|
||||
|
||||
@@ -144,13 +144,12 @@ pub fn RadioChipGroup(
|
||||
) -> Element {
|
||||
let classes = merge_class("ui-radio-chip-group", class);
|
||||
let current_value = value();
|
||||
let group_disabled = disabled;
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
class: classes,
|
||||
role: "radiogroup",
|
||||
"aria-disabled": group_disabled,
|
||||
"aria-disabled": disabled,
|
||||
if let Some(label_text) = label.clone() {
|
||||
span {
|
||||
class: "ui-choice-group-label",
|
||||
@@ -159,52 +158,19 @@ pub fn RadioChipGroup(
|
||||
}
|
||||
div {
|
||||
class: "ui-choice-group-options",
|
||||
for option in options.iter().cloned() {
|
||||
for option in options {
|
||||
{
|
||||
let option_label = option.label.clone();
|
||||
let option_value = option.value.clone();
|
||||
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();
|
||||
|
||||
let is_selected = current_value.as_ref().map(|v| v == &option.value).unwrap_or(false);
|
||||
let disabled_state = disabled || option.disabled;
|
||||
rsx! {
|
||||
button {
|
||||
class: "ui-radio-chip",
|
||||
"data-state": if is_selected { "selected" } else { "idle" },
|
||||
"data-disabled": is_disabled,
|
||||
role: "radio",
|
||||
"aria-checked": if is_selected { "true" } else { "false" },
|
||||
"aria-disabled": if is_disabled { "true" } else { "false" },
|
||||
r#type: "button",
|
||||
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}"
|
||||
RadioChipItem {
|
||||
option,
|
||||
is_selected,
|
||||
disabled: disabled_state,
|
||||
on_select: move |selected_value: String| {
|
||||
value.set(Some(selected_value.clone()));
|
||||
if let Some(callback) = on_value_change.as_ref() {
|
||||
callback.call(selected_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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