mirror of
https://github.com/mztlive/dx-admin-template.git
synced 2025-12-22 21:59:59 +00:00
增加更多组件
This commit is contained in:
@@ -462,6 +462,245 @@
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.ui-breadcrumb {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
font-size: 0.85rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.ui-breadcrumb a {
|
||||
color: hsl(var(--foreground));
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.ui-breadcrumb a:hover {
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.ui-breadcrumb-separator {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.ui-pagination {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.ui-page-button {
|
||||
min-width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
padding: 0 0.5rem;
|
||||
border-radius: calc(var(--radius) - 2px);
|
||||
border: 1px solid hsl(var(--border));
|
||||
background-color: hsl(var(--background));
|
||||
color: hsl(var(--foreground));
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.ui-page-button:hover {
|
||||
background-color: hsl(var(--muted));
|
||||
}
|
||||
|
||||
.ui-page-button[data-active="true"] {
|
||||
border-color: hsl(var(--primary));
|
||||
background-color: hsl(var(--primary));
|
||||
color: hsl(var(--primary-foreground));
|
||||
}
|
||||
|
||||
.ui-page-button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.ui-steps {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.ui-step {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
font-size: 0.8rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.ui-step-indicator {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 999px;
|
||||
border: 2px solid hsl(var(--border));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: hsl(var(--background));
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ui-step[data-state="active"] .ui-step-indicator {
|
||||
border-color: hsl(var(--primary));
|
||||
background-color: hsl(var(--primary));
|
||||
color: hsl(var(--primary-foreground));
|
||||
}
|
||||
|
||||
.ui-step[data-state="complete"] .ui-step-indicator {
|
||||
border-color: hsl(var(--primary));
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.ui-navmenu,
|
||||
.ui-menubar {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.2rem;
|
||||
border-radius: calc(var(--radius) - 2px);
|
||||
background-color: hsl(var(--muted));
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ui-navmenu-trigger,
|
||||
.ui-menubar-trigger {
|
||||
appearance: none;
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 0.45rem 0.75rem;
|
||||
border-radius: calc(var(--radius) - 4px);
|
||||
font-size: 0.85rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ui-navmenu-trigger[data-open="true"],
|
||||
.ui-menubar-trigger[data-open="true"] {
|
||||
background-color: hsl(var(--background));
|
||||
color: hsl(var(--foreground));
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.ui-navmenu-content,
|
||||
.ui-menubar-content {
|
||||
position: absolute;
|
||||
top: calc(100% + 0.35rem);
|
||||
left: 0;
|
||||
min-width: 12rem;
|
||||
background-color: hsl(var(--popover));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: calc(var(--radius) - 2px);
|
||||
box-shadow: var(--shadow-md);
|
||||
padding: 0.5rem 0.4rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
z-index: 35;
|
||||
}
|
||||
|
||||
.ui-navmenu-item,
|
||||
.ui-menubar-item {
|
||||
padding: 0.4rem 0.6rem;
|
||||
border-radius: calc(var(--radius) - 4px);
|
||||
font-size: 0.85rem;
|
||||
color: hsl(var(--foreground));
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ui-navmenu-item:hover,
|
||||
.ui-menubar-item:hover {
|
||||
background-color: hsl(var(--muted));
|
||||
}
|
||||
|
||||
.ui-command {
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
background-color: hsl(var(--popover));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: calc(var(--radius) + 2px);
|
||||
box-shadow: var(--shadow-md);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ui-command-header {
|
||||
padding: 0.6rem 0.9rem;
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
|
||||
.ui-command-input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-size: 0.9rem;
|
||||
color: hsl(var(--foreground));
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.ui-command-list {
|
||||
max-height: 14rem;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.ui-command-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.55rem 0.9rem;
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.ui-command-item:hover,
|
||||
.ui-command-item[data-state="active"] {
|
||||
background-color: hsl(var(--muted));
|
||||
}
|
||||
|
||||
.ui-context-trigger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ui-context-menu {
|
||||
position: fixed;
|
||||
min-width: 220px;
|
||||
background-color: hsl(var(--popover));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: calc(var(--radius) - 2px);
|
||||
box-shadow: var(--shadow-md);
|
||||
padding: 0.3rem 0.2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
z-index: 40;
|
||||
}
|
||||
|
||||
.ui-context-item {
|
||||
padding: 0.45rem 0.75rem;
|
||||
border-radius: calc(var(--radius) - 4px);
|
||||
font-size: 0.85rem;
|
||||
color: hsl(var(--foreground));
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ui-context-item[data-variant="destructive"] {
|
||||
color: hsl(var(--destructive));
|
||||
}
|
||||
|
||||
.ui-context-item:hover {
|
||||
background-color: hsl(var(--muted));
|
||||
}
|
||||
|
||||
.ui-alert {
|
||||
border-radius: calc(var(--radius) + 2px);
|
||||
border: 1px solid hsl(var(--border));
|
||||
@@ -684,6 +923,129 @@
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.ui-overlay-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background-color: hsl(var(--foreground) / 0.45);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1.5rem;
|
||||
z-index: 48;
|
||||
}
|
||||
|
||||
.ui-dialog {
|
||||
background-color: hsl(var(--popover));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: calc(var(--radius) + 4px);
|
||||
padding: 1.5rem;
|
||||
width: min(480px, 92vw);
|
||||
box-shadow: var(--shadow-md);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
color: hsl(var(--popover-foreground));
|
||||
}
|
||||
|
||||
.ui-dialog-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.ui-dialog-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ui-dialog-description {
|
||||
font-size: 0.9rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.ui-dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.ui-popover,
|
||||
.ui-hovercard {
|
||||
position: absolute;
|
||||
min-width: 220px;
|
||||
border-radius: calc(var(--radius) - 2px);
|
||||
border: 1px solid hsl(var(--border));
|
||||
background-color: hsl(var(--popover));
|
||||
box-shadow: var(--shadow-md);
|
||||
padding: 0.75rem;
|
||||
z-index: 45;
|
||||
}
|
||||
|
||||
.ui-popover[data-placement="top"] {
|
||||
transform: translate(-50%, -0.75rem);
|
||||
}
|
||||
|
||||
.ui-popover[data-placement="bottom"] {
|
||||
transform: translate(-50%, 0.75rem);
|
||||
}
|
||||
|
||||
.ui-hovercard {
|
||||
transform: translate(-50%, -0.75rem);
|
||||
}
|
||||
|
||||
.ui-sheet-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background-color: hsl(var(--foreground) / 0.45);
|
||||
z-index: 49;
|
||||
}
|
||||
|
||||
.ui-sheet {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: min(360px, 85vw);
|
||||
background-color: hsl(var(--popover));
|
||||
border-left: 1px solid hsl(var(--border));
|
||||
box-shadow: -16px 0 40px -28px rgb(15 23 42 / 0.65);
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.ui-sheet[data-side="left"] {
|
||||
left: 0;
|
||||
border-left: none;
|
||||
border-right: 1px solid hsl(var(--border));
|
||||
box-shadow: 16px 0 40px -28px rgb(15 23 42 / 0.65);
|
||||
}
|
||||
|
||||
.ui-sheet[data-side="right"] {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.ui-toast-container {
|
||||
position: fixed;
|
||||
right: 1.5rem;
|
||||
bottom: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
z-index: 60;
|
||||
}
|
||||
|
||||
.ui-toast {
|
||||
background-color: hsl(var(--popover));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: calc(var(--radius) + 2px);
|
||||
padding: 0.85rem 1.1rem;
|
||||
box-shadow: var(--shadow-md);
|
||||
min-width: 240px;
|
||||
color: hsl(var(--popover-foreground));
|
||||
}
|
||||
|
||||
.ui-separator {
|
||||
background-color: hsl(var(--border));
|
||||
display: block;
|
||||
|
||||
43
src/components/ui/breadcrumb.rs
Normal file
43
src/components/ui/breadcrumb.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct Crumb {
|
||||
pub label: String,
|
||||
pub href: Option<String>,
|
||||
}
|
||||
|
||||
impl Crumb {
|
||||
pub fn new(label: impl Into<String>, href: Option<impl Into<String>>) -> Self {
|
||||
Self {
|
||||
label: label.into(),
|
||||
href: href.map(|value| value.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Breadcrumb(
|
||||
#[props(into)] items: Vec<Crumb>,
|
||||
#[props(into, default = "/".to_string())] separator: String,
|
||||
) -> Element {
|
||||
rsx! {
|
||||
nav {
|
||||
role: "navigation",
|
||||
aria_label: "Breadcrumb",
|
||||
span {
|
||||
class: "ui-breadcrumb",
|
||||
for (index, item) in items.iter().enumerate() {
|
||||
if let Some(href) = &item.href {
|
||||
a { href: href.clone(), "{item.label}" }
|
||||
} else {
|
||||
span { "{item.label}" }
|
||||
}
|
||||
|
||||
if index < items.len().saturating_sub(1) {
|
||||
span { class: "ui-breadcrumb-separator", "{separator}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
110
src/components/ui/command.rs
Normal file
110
src/components/ui/command.rs
Normal file
@@ -0,0 +1,110 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct CommandItem {
|
||||
pub label: String,
|
||||
pub value: String,
|
||||
pub shortcut: Option<String>,
|
||||
pub group: Option<String>,
|
||||
}
|
||||
|
||||
impl CommandItem {
|
||||
pub fn new(label: impl Into<String>, value: impl Into<String>) -> Self {
|
||||
Self {
|
||||
label: label.into(),
|
||||
value: value.into(),
|
||||
shortcut: None,
|
||||
group: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn shortcut(mut self, shortcut: impl Into<String>) -> Self {
|
||||
self.shortcut = Some(shortcut.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn group(mut self, group: impl Into<String>) -> Self {
|
||||
self.group = Some(group.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn CommandPalette(
|
||||
#[props(into)] items: Vec<CommandItem>,
|
||||
#[props(optional)] on_select: Option<EventHandler<String>>,
|
||||
#[props(into, default = "Search commands".to_string())] placeholder: String,
|
||||
) -> Element {
|
||||
let mut query = use_signal(|| String::new());
|
||||
|
||||
let mut filtered = items.clone();
|
||||
let q = query().to_lowercase();
|
||||
if !q.is_empty() {
|
||||
filtered.retain(|item| {
|
||||
item.label.to_lowercase().contains(&q)
|
||||
|| item
|
||||
.group
|
||||
.as_ref()
|
||||
.map(|g| g.to_lowercase().contains(&q))
|
||||
.unwrap_or(false)
|
||||
});
|
||||
}
|
||||
|
||||
let handler = on_select.clone();
|
||||
|
||||
let command_nodes: Vec<_> = filtered
|
||||
.iter()
|
||||
.map(|item| {
|
||||
let value = item.value.clone();
|
||||
let shortcut = item.shortcut.clone();
|
||||
let group = item.group.clone();
|
||||
let handler_clone = handler.clone();
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
class: "ui-command-item",
|
||||
"data-state": "inactive",
|
||||
onclick: move |_| {
|
||||
if let Some(cb) = handler_clone.clone() {
|
||||
cb.call(value.clone());
|
||||
}
|
||||
},
|
||||
div {
|
||||
style: "display: flex; flex-direction: column; gap: 0.2rem;",
|
||||
span { "{item.label}" }
|
||||
if let Some(group) = group {
|
||||
span { style: "font-size: 0.7rem; color: hsl(var(--muted-foreground));", "{group}" }
|
||||
}
|
||||
}
|
||||
if let Some(shortcut) = shortcut {
|
||||
span { style: "font-size: 0.75rem; opacity: 0.6;", "{shortcut}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
class: "ui-command",
|
||||
div {
|
||||
class: "ui-command-header",
|
||||
span { style: "font-size: 0.85rem; opacity: 0.6;", "⌘K" }
|
||||
input {
|
||||
class: "ui-command-input",
|
||||
value: query(),
|
||||
placeholder: placeholder.clone(),
|
||||
oninput: move |event| query.set(event.value()),
|
||||
}
|
||||
}
|
||||
div {
|
||||
class: "ui-command-list",
|
||||
if command_nodes.is_empty() {
|
||||
span { style: "padding: 0.6rem 0.9rem; color: hsl(var(--muted-foreground));", "No results" }
|
||||
} else {
|
||||
{command_nodes.into_iter()}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
82
src/components/ui/context_menu.rs
Normal file
82
src/components/ui/context_menu.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct ContextItem {
|
||||
pub label: String,
|
||||
pub value: String,
|
||||
pub destructive: bool,
|
||||
}
|
||||
|
||||
impl ContextItem {
|
||||
pub fn new(label: impl Into<String>, value: impl Into<String>) -> Self {
|
||||
Self {
|
||||
label: label.into(),
|
||||
value: value.into(),
|
||||
destructive: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn destructive(mut self) -> Self {
|
||||
self.destructive = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn ContextMenu(
|
||||
#[props(into)] items: Vec<ContextItem>,
|
||||
#[props(optional)] on_select: Option<EventHandler<String>>,
|
||||
children: Element,
|
||||
) -> Element {
|
||||
let mut position = use_signal(|| None::<(f32, f32)>);
|
||||
let handler = on_select.clone();
|
||||
|
||||
let menu_portal = position().map(|(x, y)| {
|
||||
let nodes: Vec<_> = items
|
||||
.iter()
|
||||
.map(|item| {
|
||||
let value = item.value.clone();
|
||||
let mut pos_signal = position.clone();
|
||||
let handler_clone = handler.clone();
|
||||
|
||||
rsx! {
|
||||
button {
|
||||
class: "ui-context-item",
|
||||
"data-variant": if item.destructive { "destructive" } else { "default" },
|
||||
onclick: move |_| {
|
||||
if let Some(cb) = handler_clone.clone() {
|
||||
cb.call(value.clone());
|
||||
}
|
||||
pos_signal.set(None);
|
||||
},
|
||||
"{item.label}"
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
(x, y, nodes)
|
||||
});
|
||||
|
||||
rsx! {
|
||||
span {
|
||||
class: "ui-context-trigger",
|
||||
oncontextmenu: move |event| {
|
||||
event.prevent_default();
|
||||
let coords = event.client_coordinates();
|
||||
position.set(Some((coords.x as f32, coords.y as f32)));
|
||||
},
|
||||
{children}
|
||||
}
|
||||
if let Some((x, y, nodes)) = menu_portal {
|
||||
div {
|
||||
style: "position: fixed; inset: 0; z-index: 39;",
|
||||
onclick: move |_| position.set(None),
|
||||
}
|
||||
div {
|
||||
class: "ui-context-menu",
|
||||
style: format!("top: {y}px; left: {x}px;"),
|
||||
{nodes.into_iter()}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
60
src/components/ui/dialog.rs
Normal file
60
src/components/ui/dialog.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
use crate::components::ui::{Button, ButtonSize, ButtonVariant};
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[component]
|
||||
pub fn Dialog(
|
||||
mut open: Signal<bool>,
|
||||
#[props(into, default)] title: Option<String>,
|
||||
#[props(into, default)] description: Option<String>,
|
||||
#[props(optional)] on_close: Option<EventHandler<()>>,
|
||||
children: Element,
|
||||
) -> Element {
|
||||
if !open() {
|
||||
return rsx! { Fragment {} };
|
||||
}
|
||||
|
||||
let mut overlay_signal = open.clone();
|
||||
let overlay_handler = on_close.clone();
|
||||
let mut button_signal = open.clone();
|
||||
let button_handler = on_close.clone();
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
class: "ui-overlay-backdrop",
|
||||
onclick: move |_| {
|
||||
overlay_signal.set(false);
|
||||
if let Some(cb) = overlay_handler.clone() {
|
||||
cb.call(());
|
||||
}
|
||||
},
|
||||
div {
|
||||
class: "ui-dialog",
|
||||
onclick: move |event| event.stop_propagation(),
|
||||
if let Some(title) = title.clone() {
|
||||
div {
|
||||
class: "ui-dialog-header",
|
||||
h3 { class: "ui-dialog-title", "{title}" }
|
||||
if let Some(desc) = description.clone() {
|
||||
p { class: "ui-dialog-description", "{desc}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
{children}
|
||||
div {
|
||||
class: "ui-dialog-footer",
|
||||
Button {
|
||||
variant: ButtonVariant::Outline,
|
||||
size: ButtonSize::Sm,
|
||||
on_click: move |_| {
|
||||
button_signal.set(false);
|
||||
if let Some(cb) = button_handler.clone() {
|
||||
cb.call(());
|
||||
}
|
||||
},
|
||||
"Close"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
22
src/components/ui/hover_card.rs
Normal file
22
src/components/ui/hover_card.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[component]
|
||||
pub fn HoverCard(trigger: Element, content: Element) -> Element {
|
||||
let mut open = use_signal(|| false);
|
||||
|
||||
rsx! {
|
||||
span {
|
||||
style: "position: relative; display: inline-flex;",
|
||||
onmouseenter: move |_| open.set(true),
|
||||
onmouseleave: move |_| open.set(false),
|
||||
{trigger}
|
||||
if open() {
|
||||
div {
|
||||
class: "ui-hovercard",
|
||||
style: "left: 50%; bottom: 100%; transform: translate(-50%, -0.75rem);",
|
||||
{content}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
116
src/components/ui/menubar.rs
Normal file
116
src/components/ui/menubar.rs
Normal file
@@ -0,0 +1,116 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct MenubarItem {
|
||||
pub label: String,
|
||||
pub value: String,
|
||||
pub shortcut: Option<String>,
|
||||
pub destructive: bool,
|
||||
}
|
||||
|
||||
impl MenubarItem {
|
||||
pub fn new(label: impl Into<String>, value: impl Into<String>) -> Self {
|
||||
Self {
|
||||
label: label.into(),
|
||||
value: value.into(),
|
||||
shortcut: None,
|
||||
destructive: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn shortcut(mut self, shortcut: impl Into<String>) -> Self {
|
||||
self.shortcut = Some(shortcut.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn destructive(mut self) -> Self {
|
||||
self.destructive = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct MenubarMenu {
|
||||
pub label: String,
|
||||
pub items: Vec<MenubarItem>,
|
||||
}
|
||||
|
||||
impl MenubarMenu {
|
||||
pub fn new(label: impl Into<String>, items: Vec<MenubarItem>) -> Self {
|
||||
Self {
|
||||
label: label.into(),
|
||||
items,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Menubar(
|
||||
#[props(into)] menus: Vec<MenubarMenu>,
|
||||
#[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()}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,38 +6,62 @@ mod accordion;
|
||||
mod alert;
|
||||
mod avatar;
|
||||
mod badge;
|
||||
mod breadcrumb;
|
||||
mod button;
|
||||
mod card;
|
||||
mod checkbox;
|
||||
mod command;
|
||||
mod context_menu;
|
||||
mod dialog;
|
||||
mod dropdown_menu;
|
||||
mod hover_card;
|
||||
mod input;
|
||||
mod label;
|
||||
mod menubar;
|
||||
mod navigation_menu;
|
||||
mod pagination;
|
||||
mod popover;
|
||||
mod progress;
|
||||
mod radio_group;
|
||||
mod select;
|
||||
mod separator;
|
||||
mod sheet;
|
||||
mod slider;
|
||||
mod steps;
|
||||
mod switch;
|
||||
mod tabs;
|
||||
mod textarea;
|
||||
mod toast;
|
||||
mod tooltip;
|
||||
|
||||
pub use accordion::*;
|
||||
pub use alert::*;
|
||||
pub use avatar::*;
|
||||
pub use badge::*;
|
||||
pub use breadcrumb::*;
|
||||
pub use button::*;
|
||||
pub use card::*;
|
||||
pub use checkbox::*;
|
||||
pub use command::*;
|
||||
pub use context_menu::*;
|
||||
pub use dialog::*;
|
||||
pub use dropdown_menu::*;
|
||||
pub use hover_card::*;
|
||||
pub use input::*;
|
||||
pub use label::*;
|
||||
pub use menubar::*;
|
||||
pub use navigation_menu::*;
|
||||
pub use pagination::*;
|
||||
pub use popover::*;
|
||||
pub use progress::*;
|
||||
pub use radio_group::*;
|
||||
pub use select::*;
|
||||
pub use separator::*;
|
||||
pub use sheet::*;
|
||||
pub use slider::*;
|
||||
pub use steps::*;
|
||||
pub use switch::*;
|
||||
pub use tabs::*;
|
||||
pub use textarea::*;
|
||||
pub use toast::*;
|
||||
pub use tooltip::*;
|
||||
|
||||
75
src/components/ui/navigation_menu.rs
Normal file
75
src/components/ui/navigation_menu.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct NavigationItem {
|
||||
pub label: String,
|
||||
pub description: Option<String>,
|
||||
pub href: String,
|
||||
}
|
||||
|
||||
impl NavigationItem {
|
||||
pub fn new(
|
||||
label: impl Into<String>,
|
||||
href: impl Into<String>,
|
||||
description: Option<impl Into<String>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
label: label.into(),
|
||||
href: href.into(),
|
||||
description: description.map(|d| d.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn NavigationMenu(#[props(into)] items: Vec<NavigationItem>) -> Element {
|
||||
let active = use_signal(|| 0usize);
|
||||
|
||||
let trigger_nodes: Vec<_> = items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, item)| {
|
||||
let mut active_signal = active.clone();
|
||||
let is_active = active() == index;
|
||||
|
||||
rsx! {
|
||||
button {
|
||||
class: "ui-navmenu-trigger",
|
||||
"data-open": if is_active { "true" } else { "false" },
|
||||
onmouseenter: move |_| active_signal.set(index),
|
||||
onclick: move |_| active_signal.set(index),
|
||||
"{item.label}"
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let selected_content = items
|
||||
.get(active().min(items.len().saturating_sub(1)))
|
||||
.map(|item| {
|
||||
let description = item.description.clone();
|
||||
rsx! {
|
||||
div {
|
||||
class: "ui-navmenu-content",
|
||||
a {
|
||||
href: item.href.clone(),
|
||||
class: "ui-navmenu-item",
|
||||
"{item.label}"
|
||||
}
|
||||
if let Some(desc) = description {
|
||||
span { style: "font-size: 0.75rem; color: hsl(var(--muted-foreground));", "{desc}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
class: "ui-navmenu",
|
||||
{trigger_nodes.into_iter()}
|
||||
if let Some(content) = selected_content {
|
||||
{content}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
101
src/components/ui/pagination.rs
Normal file
101
src/components/ui/pagination.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[component]
|
||||
pub fn Pagination(
|
||||
total_pages: usize,
|
||||
current_page: usize,
|
||||
#[props(optional)] on_page_change: Option<EventHandler<usize>>,
|
||||
) -> Element {
|
||||
let mut current = use_signal(move || current_page.max(1).min(total_pages.max(1)));
|
||||
use_effect(move || {
|
||||
if current() != current_page {
|
||||
current.set(current_page.max(1).min(total_pages.max(1)));
|
||||
}
|
||||
});
|
||||
|
||||
let on_change = on_page_change.clone();
|
||||
|
||||
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
|
||||
}
|
||||
let start = active.saturating_sub(1).max(3);
|
||||
let end = (active + 1).min(total_pages - 2);
|
||||
for page in start..=end {
|
||||
buttons.push(page);
|
||||
}
|
||||
if active + 2 < total_pages - 1 {
|
||||
buttons.push(0);
|
||||
}
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
35
src/components/ui/popover.rs
Normal file
35
src/components/ui/popover.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[component]
|
||||
pub fn Popover(
|
||||
trigger: Element,
|
||||
content: Element,
|
||||
#[props(into, default = "bottom".to_string())] placement: String,
|
||||
) -> Element {
|
||||
let mut open = use_signal(|| false);
|
||||
|
||||
rsx! {
|
||||
span {
|
||||
style: "position: relative; display: inline-flex;",
|
||||
onclick: move |_| open.set(!open()),
|
||||
tabindex: 0,
|
||||
onfocusout: move |_| open.set(false),
|
||||
{trigger}
|
||||
if open() {
|
||||
div {
|
||||
class: "ui-popover",
|
||||
"data-placement": placement.clone(),
|
||||
style: match placement.as_str() {
|
||||
"top" => "left: 50%; bottom: 100%;",
|
||||
"bottom" => "left: 50%; top: 100%;",
|
||||
"left" => "right: 100%; top: 50%; transform: translate(-0.75rem, -50%);",
|
||||
"right" => "left: 100%; top: 50%; transform: translate(0.75rem, -50%);",
|
||||
_ => "left: 50%; top: 100%;"
|
||||
},
|
||||
onclick: move |event| event.stop_propagation(),
|
||||
{content}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
81
src/components/ui/sheet.rs
Normal file
81
src/components/ui/sheet.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
use crate::components::ui::{Button, ButtonSize, ButtonVariant};
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum SheetSide {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
impl SheetSide {
|
||||
fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
SheetSide::Left => "left",
|
||||
SheetSide::Right => "right",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SheetSide {
|
||||
fn default() -> Self {
|
||||
SheetSide::Right
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Sheet(
|
||||
mut open: Signal<bool>,
|
||||
#[props(default)] side: SheetSide,
|
||||
#[props(into, default)] title: Option<String>,
|
||||
#[props(into, default)] description: Option<String>,
|
||||
#[props(optional)] on_close: Option<EventHandler<()>>,
|
||||
children: Element,
|
||||
) -> Element {
|
||||
if !open() {
|
||||
return rsx! { Fragment {} };
|
||||
}
|
||||
|
||||
let mut overlay_signal = open.clone();
|
||||
let overlay_handler = on_close.clone();
|
||||
let mut button_signal = open.clone();
|
||||
let button_handler = on_close.clone();
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
class: "ui-sheet-backdrop",
|
||||
onclick: move |_| {
|
||||
overlay_signal.set(false);
|
||||
if let Some(cb) = overlay_handler.clone() {
|
||||
cb.call(());
|
||||
}
|
||||
},
|
||||
}
|
||||
div {
|
||||
class: "ui-sheet",
|
||||
"data-side": side.as_str(),
|
||||
onclick: move |event| event.stop_propagation(),
|
||||
if let Some(title) = title.clone() {
|
||||
h3 { class: "ui-dialog-title", "{title}" }
|
||||
}
|
||||
if let Some(desc) = description.clone() {
|
||||
p { class: "ui-dialog-description", "{desc}" }
|
||||
}
|
||||
{children}
|
||||
div {
|
||||
class: "ui-dialog-footer",
|
||||
Button {
|
||||
variant: ButtonVariant::Outline,
|
||||
size: ButtonSize::Sm,
|
||||
on_click: move |_| {
|
||||
button_signal.set(false);
|
||||
if let Some(cb) = button_handler.clone() {
|
||||
cb.call(());
|
||||
}
|
||||
},
|
||||
"Close"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
55
src/components/ui/steps.rs
Normal file
55
src/components/ui/steps.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct StepItem {
|
||||
pub title: String,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
impl StepItem {
|
||||
pub fn new(title: impl Into<String>, description: Option<impl Into<String>>) -> Self {
|
||||
Self {
|
||||
title: title.into(),
|
||||
description: description.map(|d| d.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Steps(#[props(into)] steps: Vec<StepItem>, #[props(default = 1)] current: usize) -> Element {
|
||||
let rendered_steps: Vec<_> = steps
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, step)| {
|
||||
let position = index + 1;
|
||||
let state = if position < current {
|
||||
"complete"
|
||||
} else if position == current {
|
||||
"active"
|
||||
} else {
|
||||
"upcoming"
|
||||
};
|
||||
let indicator_text = if position < current {
|
||||
"✓".to_string()
|
||||
} else {
|
||||
position.to_string()
|
||||
};
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
class: "ui-step",
|
||||
"data-state": state,
|
||||
span { class: "ui-step-indicator", "{indicator_text}" }
|
||||
span { "{step.title}" }
|
||||
if let Some(description) = &step.description {
|
||||
span { style: "font-size: 0.72rem; color: hsl(var(--muted-foreground));", "{description}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let step_nodes = rendered_steps;
|
||||
|
||||
rsx! { div { class: "ui-steps", {step_nodes.into_iter()} } }
|
||||
}
|
||||
42
src/components/ui/toast.rs
Normal file
42
src/components/ui/toast.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[component]
|
||||
pub fn ToastViewport(children: Element) -> Element {
|
||||
rsx! { div { class: "ui-toast-container", {children} } }
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Toast(
|
||||
#[props(default)] open: bool,
|
||||
#[props(into, default)] title: Option<String>,
|
||||
#[props(into, default)] description: Option<String>,
|
||||
#[props(optional)] on_close: Option<EventHandler<()>>,
|
||||
) -> Element {
|
||||
if !open {
|
||||
return rsx! { Fragment {} };
|
||||
}
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
class: "ui-toast",
|
||||
if let Some(title) = title {
|
||||
h4 { style: "font-weight: 600; font-size: 0.95rem;", "{title}" }
|
||||
}
|
||||
if let Some(desc) = description {
|
||||
p { style: "font-size: 0.8rem; color: hsl(var(--muted-foreground));", "{desc}" }
|
||||
}
|
||||
if on_close.is_some() {
|
||||
button {
|
||||
class: "ui-page-button",
|
||||
style: "align-self: flex-end; height: 2rem;",
|
||||
onclick: move |_| {
|
||||
if let Some(cb) = on_close.clone() {
|
||||
cb.call(());
|
||||
}
|
||||
},
|
||||
"Dismiss"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
use crate::components::{
|
||||
ui::{
|
||||
Accordion, AccordionContent, AccordionItem, AccordionTrigger, Alert, AlertVariant, Avatar,
|
||||
Badge, BadgeVariant, Button, ButtonSize, ButtonVariant, Card, CardContent, CardDescription,
|
||||
CardFooter, CardHeader, CardTitle, Checkbox, DropdownMenu, DropdownMenuItem, Input, Label,
|
||||
Progress, RadioGroup, RadioGroupItem, Select, SelectOption, Separator,
|
||||
SeparatorOrientation, Slider, Switch, Tabs, TabsContent, TabsList, TabsTrigger, Textarea,
|
||||
Tooltip,
|
||||
Badge, BadgeVariant, Breadcrumb, Button, ButtonSize, ButtonVariant, Card, CardContent,
|
||||
CardDescription, CardFooter, CardHeader, CardTitle, Checkbox, CommandItem, CommandPalette,
|
||||
ContextItem, ContextMenu, Crumb, Dialog, DropdownMenu, DropdownMenuItem, HoverCard, Input,
|
||||
Label, Menubar, MenubarItem, MenubarMenu, NavigationItem, NavigationMenu, Pagination,
|
||||
Popover, Progress, RadioGroup, RadioGroupItem, Select, SelectOption, Separator,
|
||||
SeparatorOrientation, Sheet, SheetSide, Slider, StepItem, Steps, Switch, Tabs, TabsContent,
|
||||
TabsList, TabsTrigger, Textarea, Toast, ToastViewport, Tooltip,
|
||||
},
|
||||
Echo, Hero,
|
||||
};
|
||||
@@ -31,6 +33,14 @@ fn UiShowcase() -> Element {
|
||||
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 menubar_selection = use_signal(|| "Choose a menu item".to_string());
|
||||
let pagination_current = use_signal(|| 3usize);
|
||||
let steps_current = use_signal(|| 2usize);
|
||||
let command_selection = use_signal(|| "Nothing selected yet".to_string());
|
||||
let context_selection = use_signal(|| "Right click the area to choose an action".to_string());
|
||||
let dialog_open = use_signal(|| false);
|
||||
let sheet_open = use_signal(|| false);
|
||||
let toast_open = use_signal(|| false);
|
||||
let slider_value_signal = slider_value.clone();
|
||||
let slider_value_setter = slider_value.clone();
|
||||
let contact_method_signal = contact_method.clone();
|
||||
@@ -42,6 +52,14 @@ fn UiShowcase() -> Element {
|
||||
let dark_mode_setter = dark_mode.clone();
|
||||
let theme_choice_setter = theme_choice.clone();
|
||||
let menu_selection_setter = menu_selection.clone();
|
||||
let menubar_selection_setter = menubar_selection.clone();
|
||||
let pagination_setter = pagination_current.clone();
|
||||
let steps_setter = steps_current.clone();
|
||||
let command_selection_setter = command_selection.clone();
|
||||
let context_selection_setter = context_selection.clone();
|
||||
let dialog_signal = dialog_open.clone();
|
||||
let sheet_signal = sheet_open.clone();
|
||||
let toast_signal = toast_open.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![
|
||||
@@ -55,6 +73,70 @@ fn UiShowcase() -> Element {
|
||||
DropdownMenuItem::new("Team", "team"),
|
||||
DropdownMenuItem::new("Sign out", "logout").destructive(),
|
||||
];
|
||||
let breadcrumb_items = vec![
|
||||
Crumb::new("Dashboard", Some("#")),
|
||||
Crumb::new("Settings", Some("#settings")),
|
||||
Crumb::new("Team", None::<String>),
|
||||
];
|
||||
let navigation_items = vec![
|
||||
NavigationItem::new(
|
||||
"Overview",
|
||||
"#overview",
|
||||
Some("Project snapshots and quick metrics"),
|
||||
),
|
||||
NavigationItem::new(
|
||||
"Playground",
|
||||
"#playground",
|
||||
Some("Prototype new ideas and components"),
|
||||
),
|
||||
NavigationItem::new(
|
||||
"Documentation",
|
||||
"https://dioxuslabs.com/learn",
|
||||
Some("Dive into the latest Dioxus 0.7 docs"),
|
||||
),
|
||||
];
|
||||
let menubar_menus = vec![
|
||||
MenubarMenu::new(
|
||||
"File",
|
||||
vec![
|
||||
MenubarItem::new("New Tab", "new_tab").shortcut("⌘T"),
|
||||
MenubarItem::new("Open Workspace", "open_workspace"),
|
||||
MenubarItem::new("Save", "save").shortcut("⌘S"),
|
||||
],
|
||||
),
|
||||
MenubarMenu::new(
|
||||
"Edit",
|
||||
vec![
|
||||
MenubarItem::new("Undo", "undo").shortcut("⌘Z"),
|
||||
MenubarItem::new("Redo", "redo").shortcut("⇧⌘Z"),
|
||||
MenubarItem::new("Delete", "delete").destructive(),
|
||||
],
|
||||
),
|
||||
];
|
||||
let command_items = vec![
|
||||
CommandItem::new("Create project", "create_project")
|
||||
.shortcut("⌘N")
|
||||
.group("Actions"),
|
||||
CommandItem::new("Invite teammate", "invite").group("Actions"),
|
||||
CommandItem::new("Open documentation", "docs").group("Resources"),
|
||||
CommandItem::new("Keyboard shortcuts", "shortcuts").group("Resources"),
|
||||
];
|
||||
let context_items = vec![
|
||||
ContextItem::new("Rename", "rename"),
|
||||
ContextItem::new("Duplicate", "duplicate"),
|
||||
ContextItem::new("Archive", "archive"),
|
||||
ContextItem::new("Delete", "delete").destructive(),
|
||||
];
|
||||
let steps_items = vec![
|
||||
StepItem::new("Plan", Some("Outline requirements")),
|
||||
StepItem::new("Build", Some("Implement features")),
|
||||
StepItem::new("Review", Some("QA and ship")),
|
||||
];
|
||||
let total_pages = 8usize;
|
||||
let pagination_summary =
|
||||
move || format!("Showing page {} of {total_pages}", pagination_current());
|
||||
let steps_total = steps_items.len();
|
||||
let steps_summary = move || format!("Stage {} of {steps_total}", steps_current());
|
||||
let theme_display = {
|
||||
let current = theme_choice();
|
||||
current
|
||||
@@ -202,7 +284,7 @@ fn UiShowcase() -> Element {
|
||||
|
||||
Card {
|
||||
CardHeader {
|
||||
CardTitle { "Select & menus" }
|
||||
CardTitle { "Select & dropdowns" }
|
||||
CardDescription { "Select, dropdown menu, tooltip and dynamic feedback." }
|
||||
}
|
||||
CardContent {
|
||||
@@ -245,6 +327,75 @@ fn UiShowcase() -> Element {
|
||||
}
|
||||
}
|
||||
|
||||
Card {
|
||||
CardHeader {
|
||||
CardTitle { "Navigation patterns" }
|
||||
CardDescription { "Breadcrumbs, menus, pagination, and progress steps." }
|
||||
}
|
||||
CardContent {
|
||||
div { class: "ui-stack",
|
||||
SpanHelper { "Breadcrumb" }
|
||||
Breadcrumb { items: breadcrumb_items.clone(), separator: ">".to_string() }
|
||||
}
|
||||
div { class: "ui-stack",
|
||||
SpanHelper { "Navigation menu" }
|
||||
NavigationMenu { items: navigation_items.clone() }
|
||||
}
|
||||
div { class: "ui-stack",
|
||||
SpanHelper { "Menubar" }
|
||||
Menubar {
|
||||
menus: menubar_menus.clone(),
|
||||
on_select: move |value| {
|
||||
let mut signal = menubar_selection_setter.clone();
|
||||
signal.set(format!("Menubar selected: {value}"));
|
||||
},
|
||||
}
|
||||
SpanHelper { "{menubar_selection()}" }
|
||||
}
|
||||
div { class: "ui-stack",
|
||||
SpanHelper { "Pagination" }
|
||||
Pagination {
|
||||
total_pages: total_pages,
|
||||
current_page: pagination_current(),
|
||||
on_page_change: move |page| {
|
||||
let mut signal = pagination_setter.clone();
|
||||
signal.set(page);
|
||||
},
|
||||
}
|
||||
SpanHelper { "{pagination_summary()}" }
|
||||
}
|
||||
div { class: "ui-stack",
|
||||
SpanHelper { "Steps" }
|
||||
Steps {
|
||||
steps: steps_items.clone(),
|
||||
current: steps_current(),
|
||||
}
|
||||
div { class: "ui-cluster",
|
||||
Button {
|
||||
variant: ButtonVariant::Outline,
|
||||
size: ButtonSize::Sm,
|
||||
on_click: move |_| {
|
||||
let mut signal = steps_setter.clone();
|
||||
let prev = signal().saturating_sub(1).max(1);
|
||||
signal.set(prev);
|
||||
},
|
||||
"Previous"
|
||||
}
|
||||
Button {
|
||||
size: ButtonSize::Sm,
|
||||
on_click: move |_| {
|
||||
let mut signal = steps_setter.clone();
|
||||
let next = (signal() + 1).min(steps_total);
|
||||
signal.set(next);
|
||||
},
|
||||
"Next"
|
||||
}
|
||||
}
|
||||
SpanHelper { "{steps_summary()}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Card {
|
||||
CardHeader {
|
||||
CardTitle { "Selection controls" }
|
||||
@@ -292,6 +443,41 @@ fn UiShowcase() -> Element {
|
||||
}
|
||||
}
|
||||
|
||||
Card {
|
||||
CardHeader {
|
||||
CardTitle { "Command & context" }
|
||||
CardDescription { "Command palette filtering and contextual menus." }
|
||||
}
|
||||
CardContent {
|
||||
div { class: "ui-stack",
|
||||
SpanHelper { "Command palette" }
|
||||
CommandPalette {
|
||||
items: command_items.clone(),
|
||||
on_select: move |value| {
|
||||
let mut signal = command_selection_setter.clone();
|
||||
signal.set(format!("Command selected: {value}"));
|
||||
},
|
||||
}
|
||||
SpanHelper { "{command_selection()}" }
|
||||
}
|
||||
div { class: "ui-stack",
|
||||
SpanHelper { "Context menu" }
|
||||
ContextMenu {
|
||||
items: context_items.clone(),
|
||||
on_select: move |value| {
|
||||
let mut signal = context_selection_setter.clone();
|
||||
signal.set(format!("Context action: {value}"));
|
||||
},
|
||||
div {
|
||||
style: "padding: 1.5rem; border: 1px dashed hsl(var(--border)); border-radius: var(--radius); text-align: center;",
|
||||
"Right click anywhere in this box"
|
||||
}
|
||||
}
|
||||
SpanHelper { "{context_selection()}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Card {
|
||||
CardHeader {
|
||||
CardTitle { "Tabs & panels" }
|
||||
@@ -331,6 +517,56 @@ fn UiShowcase() -> Element {
|
||||
}
|
||||
}
|
||||
|
||||
Card {
|
||||
CardHeader {
|
||||
CardTitle { "Dialogs & overlays" }
|
||||
CardDescription { "Popover, hover card, dialogs, sheet, and toast examples." }
|
||||
}
|
||||
CardContent {
|
||||
div { class: "ui-cluster",
|
||||
Button {
|
||||
variant: ButtonVariant::Secondary,
|
||||
on_click: move |_| {
|
||||
let mut signal = dialog_signal.clone();
|
||||
signal.set(true);
|
||||
},
|
||||
"Open dialog"
|
||||
}
|
||||
Button {
|
||||
variant: ButtonVariant::Outline,
|
||||
on_click: move |_| {
|
||||
let mut signal = sheet_signal.clone();
|
||||
signal.set(true);
|
||||
},
|
||||
"Open sheet"
|
||||
}
|
||||
Button {
|
||||
variant: ButtonVariant::Ghost,
|
||||
on_click: move |_| {
|
||||
let mut signal = toast_signal.clone();
|
||||
signal.set(true);
|
||||
},
|
||||
"Notify me"
|
||||
}
|
||||
}
|
||||
div { class: "ui-stack",
|
||||
SpanHelper { "Popover" }
|
||||
Popover {
|
||||
placement: "bottom".to_string(),
|
||||
trigger: rsx! { Button { variant: ButtonVariant::Outline, size: ButtonSize::Sm, "Toggle popover" } },
|
||||
content: rsx! { SpanHelper { "Choose the dialog or sheet you want to configure." } },
|
||||
}
|
||||
}
|
||||
div { class: "ui-stack",
|
||||
SpanHelper { "Hover card" }
|
||||
HoverCard {
|
||||
trigger: rsx! { Badge { variant: BadgeVariant::Secondary, "Hover me" } },
|
||||
content: rsx! { span { style: "font-size: 0.8rem; color: hsl(var(--muted-foreground));", "Preview contextual information instantly." } },
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Card {
|
||||
CardHeader {
|
||||
CardTitle { "Alerts & extras" }
|
||||
@@ -384,6 +620,42 @@ fn UiShowcase() -> Element {
|
||||
}
|
||||
}
|
||||
}
|
||||
Dialog {
|
||||
open: dialog_signal.clone(),
|
||||
title: Some("Create project".to_string()),
|
||||
description: Some("Configure the new analytics workspace.".to_string()),
|
||||
div { class: "ui-stack",
|
||||
Label { html_for: "dialog-name", "Project name" }
|
||||
Input { id: "dialog-name", placeholder: "Analytics redesign" }
|
||||
}
|
||||
}
|
||||
Sheet {
|
||||
open: sheet_signal.clone(),
|
||||
side: SheetSide::Right,
|
||||
title: Some("Activity log".to_string()),
|
||||
description: Some("Review the latest changes from your teammates.".to_string()),
|
||||
div {
|
||||
class: "ui-stack",
|
||||
SpanHelper { "Today" }
|
||||
ul {
|
||||
style: "display: flex; flex-direction: column; gap: 0.5rem; font-size: 0.85rem;",
|
||||
li { "Maria added new metrics to the dashboard." }
|
||||
li { "Evan approved the Q2 launch plan." }
|
||||
li { "Ada commented on revenue projections." }
|
||||
}
|
||||
}
|
||||
}
|
||||
ToastViewport {
|
||||
Toast {
|
||||
open: toast_open(),
|
||||
title: Some("Changes saved".to_string()),
|
||||
description: Some("We synced your workspace preferences.".to_string()),
|
||||
on_close: move |_| {
|
||||
let mut signal = toast_signal.clone();
|
||||
signal.set(false);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user