mirror of
https://github.com/mztlive/dx-admin-template.git
synced 2025-12-23 06:10:00 +00:00
sidebar组件
This commit is contained in:
@@ -1183,3 +1183,238 @@
|
||||
color: hsl(var(--muted-foreground));
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.ui-sidebar-layout {
|
||||
background-color: hsl(var(--card));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: calc(var(--radius) + 0.5rem);
|
||||
box-shadow: var(--shadow-md);
|
||||
display: grid;
|
||||
grid-template-columns: minmax(12rem, 18rem) minmax(0, 1fr);
|
||||
min-height: 20rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ui-sidebar {
|
||||
background-color: hsl(var(--card));
|
||||
border-right: 1px solid hsl(var(--border));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
padding: 1.25rem;
|
||||
transition: width 0.25s ease;
|
||||
width: 18rem;
|
||||
}
|
||||
|
||||
.ui-sidebar[data-collapsed="true"] {
|
||||
padding-left: 0.75rem;
|
||||
padding-right: 0.75rem;
|
||||
width: 4.5rem;
|
||||
}
|
||||
|
||||
.ui-sidebar-header,
|
||||
.ui-sidebar-footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.ui-sidebar-footer {
|
||||
border-top: 1px solid hsl(var(--border));
|
||||
margin-top: auto;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.ui-sidebar-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.ui-sidebar-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.ui-sidebar-group-label {
|
||||
color: hsl(var(--muted-foreground));
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.ui-sidebar-group-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.ui-sidebar-separator {
|
||||
background-color: hsl(var(--border));
|
||||
border: none;
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ui-sidebar-menu {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ui-sidebar-menu-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ui-sidebar-menu-item {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.ui-sidebar-menu-button {
|
||||
align-items: center;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
border-radius: calc(var(--radius) - 4px);
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
justify-content: flex-start;
|
||||
padding: 0.5rem 0.75rem;
|
||||
text-align: left;
|
||||
text-decoration: none;
|
||||
transition: background-color 0.2s ease, color 0.2s ease;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ui-sidebar-menu-button:hover {
|
||||
background-color: hsl(var(--muted));
|
||||
}
|
||||
|
||||
.ui-sidebar-menu-button[data-active="true"] {
|
||||
background-color: hsl(var(--accent));
|
||||
color: hsl(var(--accent-foreground));
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ui-sidebar-button-body {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ui-sidebar-icon {
|
||||
align-items: center;
|
||||
background-color: hsl(var(--accent));
|
||||
border-radius: calc(var(--radius) - 6px);
|
||||
color: hsl(var(--accent-foreground));
|
||||
display: inline-flex;
|
||||
font-size: 1.05rem;
|
||||
height: 2.1rem;
|
||||
justify-content: center;
|
||||
min-width: 2.1rem;
|
||||
width: 2.1rem;
|
||||
}
|
||||
|
||||
.ui-sidebar-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.ui-sidebar-label {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.ui-sidebar-description {
|
||||
color: hsl(var(--muted-foreground));
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.ui-sidebar-badge {
|
||||
background-color: hsl(var(--secondary));
|
||||
border-radius: 999px;
|
||||
color: hsl(var(--secondary-foreground));
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
padding: 0.15rem 0.55rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ui-sidebar-trigger {
|
||||
align-items: center;
|
||||
background-color: transparent;
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: calc(var(--radius) - 4px);
|
||||
color: hsl(var(--muted-foreground));
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
gap: 0.5rem;
|
||||
padding: 0.35rem 0.75rem;
|
||||
transition: background-color 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
|
||||
.ui-sidebar-trigger:hover {
|
||||
background-color: hsl(var(--muted));
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.ui-sidebar-trigger-icon {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.ui-sidebar-inset {
|
||||
background-color: hsl(var(--muted));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.ui-sidebar-rail {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.ui-sidebar[data-collapsed="true"] .ui-sidebar-text,
|
||||
.ui-sidebar[data-collapsed="true"] .ui-sidebar-description,
|
||||
.ui-sidebar[data-collapsed="true"] .ui-sidebar-badge,
|
||||
.ui-sidebar[data-collapsed="true"] .ui-sidebar-group-label,
|
||||
.ui-sidebar[data-collapsed="true"] .ui-sidebar-trigger-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ui-sidebar[data-collapsed="true"] .ui-sidebar-menu-button {
|
||||
justify-content: center;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.ui-sidebar[data-collapsed="true"] .ui-sidebar-icon {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.ui-sidebar-layout {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.ui-sidebar {
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
border-right: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ui-sidebar[data-collapsed="true"] {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ mod radio_group;
|
||||
mod select;
|
||||
mod separator;
|
||||
mod sheet;
|
||||
mod sidebar;
|
||||
mod slider;
|
||||
mod steps;
|
||||
mod switch;
|
||||
@@ -58,6 +59,7 @@ pub use radio_group::*;
|
||||
pub use select::*;
|
||||
pub use separator::*;
|
||||
pub use sheet::*;
|
||||
pub use sidebar::*;
|
||||
pub use slider::*;
|
||||
pub use steps::*;
|
||||
pub use switch::*;
|
||||
|
||||
325
src/components/ui/sidebar.rs
Normal file
325
src/components/ui/sidebar.rs
Normal file
@@ -0,0 +1,325 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn merge_class(base: &str, extra: Option<String>) -> String {
|
||||
if let Some(extra) = extra.filter(|extra| !extra.trim().is_empty()) {
|
||||
format!("{base} {}", extra.trim())
|
||||
} else {
|
||||
base.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn data_bool(value: bool) -> &'static str {
|
||||
if value {
|
||||
"true"
|
||||
} else {
|
||||
"false"
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Sidebar(
|
||||
#[props(default)] collapsed: bool,
|
||||
#[props(into, default)] class: Option<String>,
|
||||
children: Element,
|
||||
) -> Element {
|
||||
let classes = merge_class("ui-sidebar", class);
|
||||
rsx! {
|
||||
aside {
|
||||
class: classes,
|
||||
"data-collapsed": data_bool(collapsed),
|
||||
{children}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn SidebarLayout(#[props(into, default)] class: Option<String>, children: Element) -> Element {
|
||||
let classes = merge_class("ui-sidebar-layout", class);
|
||||
rsx! {
|
||||
div {
|
||||
class: classes,
|
||||
{children}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn SidebarInset(#[props(into, default)] class: Option<String>, children: Element) -> Element {
|
||||
let classes = merge_class("ui-sidebar-inset", class);
|
||||
rsx! {
|
||||
div {
|
||||
class: classes,
|
||||
{children}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn SidebarRail(#[props(into, default)] class: Option<String>, children: Element) -> Element {
|
||||
let classes = merge_class("ui-sidebar-rail", class);
|
||||
rsx! {
|
||||
div {
|
||||
class: classes,
|
||||
{children}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn SidebarHeader(#[props(into, default)] class: Option<String>, children: Element) -> Element {
|
||||
let classes = merge_class("ui-sidebar-header", class);
|
||||
rsx! {
|
||||
div {
|
||||
class: classes,
|
||||
{children}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn SidebarContent(#[props(into, default)] class: Option<String>, children: Element) -> Element {
|
||||
let classes = merge_class("ui-sidebar-content", class);
|
||||
rsx! {
|
||||
div {
|
||||
class: classes,
|
||||
{children}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn SidebarFooter(#[props(into, default)] class: Option<String>, children: Element) -> Element {
|
||||
let classes = merge_class("ui-sidebar-footer", class);
|
||||
rsx! {
|
||||
div {
|
||||
class: classes,
|
||||
{children}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn SidebarSeparator(#[props(into, default)] class: Option<String>) -> Element {
|
||||
let classes = merge_class("ui-sidebar-separator", class);
|
||||
rsx! {
|
||||
div {
|
||||
class: classes,
|
||||
role: "separator",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn SidebarGroup(#[props(into, default)] class: Option<String>, children: Element) -> Element {
|
||||
let classes = merge_class("ui-sidebar-group", class);
|
||||
rsx! {
|
||||
div {
|
||||
class: classes,
|
||||
{children}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn SidebarGroupLabel(
|
||||
#[props(into, default)] class: Option<String>,
|
||||
children: Element,
|
||||
) -> Element {
|
||||
let classes = merge_class("ui-sidebar-group-label", class);
|
||||
rsx! {
|
||||
span {
|
||||
class: classes,
|
||||
{children}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn SidebarGroupContent(
|
||||
#[props(into, default)] class: Option<String>,
|
||||
children: Element,
|
||||
) -> Element {
|
||||
let classes = merge_class("ui-sidebar-group-content", class);
|
||||
rsx! {
|
||||
div {
|
||||
class: classes,
|
||||
{children}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn SidebarMenu(#[props(into, default)] class: Option<String>, children: Element) -> Element {
|
||||
let classes = merge_class("ui-sidebar-menu", class);
|
||||
rsx! {
|
||||
nav {
|
||||
class: classes,
|
||||
ul {
|
||||
class: "ui-sidebar-menu-list",
|
||||
{children}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn SidebarMenuItem(
|
||||
#[props(into, default)] class: Option<String>,
|
||||
children: Element,
|
||||
) -> Element {
|
||||
let classes = merge_class("ui-sidebar-menu-item", class);
|
||||
rsx! {
|
||||
li {
|
||||
class: classes,
|
||||
{children}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn SidebarMenuButton(
|
||||
#[props(into)] label: String,
|
||||
#[props(into, default)] description: Option<String>,
|
||||
#[props(into, default)] badge: Option<String>,
|
||||
#[props(into, default)] icon: Option<String>,
|
||||
#[props(into, default)] href: Option<String>,
|
||||
#[props(default)] active: bool,
|
||||
#[props(into, default)] class: Option<String>,
|
||||
#[props(optional)] on_click: Option<EventHandler<MouseEvent>>,
|
||||
) -> Element {
|
||||
let classes = merge_class("ui-sidebar-menu-button", class);
|
||||
let description_text = description.clone();
|
||||
let badge_text = badge.clone();
|
||||
let icon_text = icon
|
||||
.clone()
|
||||
.or_else(|| label.chars().next().map(|character| character.to_string()));
|
||||
|
||||
let handler_for_link = on_click.clone();
|
||||
let handler_for_button = on_click.clone();
|
||||
|
||||
let label_clone = label.clone();
|
||||
|
||||
if let Some(href_value) = href.clone() {
|
||||
rsx! {
|
||||
a {
|
||||
class: classes,
|
||||
href: href_value,
|
||||
"data-active": data_bool(active),
|
||||
onclick: move |event| {
|
||||
if let Some(handler) = handler_for_link.clone() {
|
||||
handler.call(event);
|
||||
}
|
||||
},
|
||||
span { class: "ui-sidebar-button-body",
|
||||
span {
|
||||
class: "ui-sidebar-icon",
|
||||
if let Some(icon_value) = icon_text.clone() {
|
||||
"{icon_value}"
|
||||
}
|
||||
}
|
||||
span {
|
||||
class: "ui-sidebar-text",
|
||||
span { class: "ui-sidebar-label", "{label_clone}" }
|
||||
if let Some(description_value) = description_text.clone() {
|
||||
span {
|
||||
class: "ui-sidebar-description",
|
||||
"{description_value}"
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(badge_value) = badge_text.clone() {
|
||||
span {
|
||||
class: "ui-sidebar-badge",
|
||||
"{badge_value}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
rsx! {
|
||||
button {
|
||||
class: classes,
|
||||
"data-active": data_bool(active),
|
||||
r#type: "button",
|
||||
onclick: move |event| {
|
||||
if let Some(handler) = handler_for_button.clone() {
|
||||
handler.call(event);
|
||||
}
|
||||
},
|
||||
span { class: "ui-sidebar-button-body",
|
||||
span {
|
||||
class: "ui-sidebar-icon",
|
||||
if let Some(icon_value) = icon_text.clone() {
|
||||
"{icon_value}"
|
||||
}
|
||||
}
|
||||
span {
|
||||
class: "ui-sidebar-text",
|
||||
span { class: "ui-sidebar-label", "{label}" }
|
||||
if let Some(description_value) = description_text.clone() {
|
||||
span {
|
||||
class: "ui-sidebar-description",
|
||||
"{description_value}"
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(badge_value) = badge_text.clone() {
|
||||
span {
|
||||
class: "ui-sidebar-badge",
|
||||
"{badge_value}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn SidebarMenuBadge(
|
||||
#[props(into)] text: String,
|
||||
#[props(into, default)] class: Option<String>,
|
||||
) -> Element {
|
||||
let classes = merge_class("ui-sidebar-badge", class);
|
||||
rsx! {
|
||||
span {
|
||||
class: classes,
|
||||
"{text}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn SidebarTrigger(
|
||||
#[props(default)] collapsed: bool,
|
||||
#[props(into, default)] class: Option<String>,
|
||||
#[props(into, default)] label: Option<String>,
|
||||
#[props(optional)] on_toggle: Option<EventHandler<bool>>,
|
||||
) -> Element {
|
||||
let classes = merge_class("ui-sidebar-trigger", class);
|
||||
let label_text = label.unwrap_or_else(|| {
|
||||
if collapsed {
|
||||
"展开侧边栏".to_string()
|
||||
} else {
|
||||
"收起侧边栏".to_string()
|
||||
}
|
||||
});
|
||||
let state = !collapsed;
|
||||
let handler = on_toggle.clone();
|
||||
|
||||
rsx! {
|
||||
button {
|
||||
class: classes,
|
||||
r#type: "button",
|
||||
"aria-pressed": data_bool(!collapsed),
|
||||
onclick: move |_| {
|
||||
if let Some(handler) = handler.clone() {
|
||||
handler.call(state);
|
||||
}
|
||||
},
|
||||
span { class: "ui-sidebar-trigger-icon", if collapsed { "→" } else { "←" } }
|
||||
span { class: "ui-sidebar-trigger-label", "{label_text}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,11 @@ use crate::components::{
|
||||
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,
|
||||
SeparatorOrientation, Sheet, SheetSide, Sidebar, SidebarContent, SidebarFooter,
|
||||
SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarInset,
|
||||
SidebarLayout, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarSeparator,
|
||||
SidebarTrigger, Slider, StepItem, Steps, Switch, Tabs, TabsContent, TabsList, TabsTrigger,
|
||||
Textarea, Toast, ToastViewport, Tooltip,
|
||||
},
|
||||
Echo, Hero,
|
||||
};
|
||||
@@ -41,6 +44,8 @@ fn UiShowcase() -> Element {
|
||||
let dialog_open = use_signal(|| false);
|
||||
let sheet_open = use_signal(|| false);
|
||||
let toast_open = use_signal(|| false);
|
||||
let sidebar_collapsed = use_signal(|| false);
|
||||
let sidebar_active = use_signal(|| "analytics".to_string());
|
||||
let slider_value_signal = slider_value.clone();
|
||||
let slider_value_setter = slider_value.clone();
|
||||
let contact_method_signal = contact_method.clone();
|
||||
@@ -60,6 +65,8 @@ fn UiShowcase() -> Element {
|
||||
let dialog_signal = dialog_open.clone();
|
||||
let sheet_signal = sheet_open.clone();
|
||||
let toast_signal = toast_open.clone();
|
||||
let sidebar_collapsed_setter = sidebar_collapsed.clone();
|
||||
let sidebar_active_setter = sidebar_active.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![
|
||||
@@ -150,6 +157,34 @@ fn UiShowcase() -> Element {
|
||||
.unwrap_or_else(|| "System".to_string())
|
||||
};
|
||||
let theme_summary = format!("Active theme: {theme_display}");
|
||||
let collapsed_state = sidebar_collapsed();
|
||||
let current_sidebar_value = sidebar_active();
|
||||
let is_analytics_active = current_sidebar_value.as_str() == "analytics";
|
||||
let is_crm_active = current_sidebar_value.as_str() == "crm";
|
||||
let is_billing_active = current_sidebar_value.as_str() == "billing";
|
||||
let is_settings_active = current_sidebar_value.as_str() == "settings";
|
||||
let (sidebar_title, sidebar_body) = match current_sidebar_value.as_str() {
|
||||
"analytics" => (
|
||||
"Analytics overview".to_string(),
|
||||
"Monitor KPI trends, conversion funnels, and health metrics in real time.".to_string(),
|
||||
),
|
||||
"crm" => (
|
||||
"Customer relationship management".to_string(),
|
||||
"Surface leads, segment accounts, and coordinate follow-ups in one place.".to_string(),
|
||||
),
|
||||
"billing" => (
|
||||
"Billing & usage".to_string(),
|
||||
"Review invoices, adjust subscription tiers, and reconcile metered usage.".to_string(),
|
||||
),
|
||||
"settings" => (
|
||||
"Workspace settings".to_string(),
|
||||
"Manage authentication, API tokens, and notification preferences.".to_string(),
|
||||
),
|
||||
_ => (
|
||||
"Select a section".to_string(),
|
||||
"Pick a destination from the sidebar to preview the content area.".to_string(),
|
||||
),
|
||||
};
|
||||
|
||||
rsx! {
|
||||
section {
|
||||
@@ -396,6 +431,114 @@ fn UiShowcase() -> Element {
|
||||
}
|
||||
}
|
||||
|
||||
Card {
|
||||
CardHeader {
|
||||
CardTitle { "Structural navigation" }
|
||||
CardDescription { "Collapsible sidebar layout with grouped menus." }
|
||||
}
|
||||
CardContent {
|
||||
SidebarLayout {
|
||||
Sidebar {
|
||||
collapsed: collapsed_state,
|
||||
SidebarHeader {
|
||||
div { class: "ui-sidebar-button-body",
|
||||
span { class: "ui-sidebar-icon", "⚡" }
|
||||
span { class: "ui-sidebar-text",
|
||||
span { class: "ui-sidebar-label", "Acme HQ" }
|
||||
span { class: "ui-sidebar-description", "Operations console" }
|
||||
}
|
||||
}
|
||||
}
|
||||
SidebarContent {
|
||||
SidebarGroup {
|
||||
SidebarGroupLabel { "Workspace" }
|
||||
SidebarGroupContent {
|
||||
SidebarMenu {
|
||||
SidebarMenuItem {
|
||||
SidebarMenuButton {
|
||||
label: "Analytics",
|
||||
description: Some("Track KPIs and trends".into()),
|
||||
icon: Some("📊".into()),
|
||||
active: is_analytics_active,
|
||||
on_click: move |_| {
|
||||
let mut signal = sidebar_active_setter.clone();
|
||||
signal.set("analytics".to_string());
|
||||
},
|
||||
}
|
||||
}
|
||||
SidebarMenuItem {
|
||||
SidebarMenuButton {
|
||||
label: "CRM",
|
||||
description: Some("Manage customer pipeline".into()),
|
||||
icon: Some("👥".into()),
|
||||
active: is_crm_active,
|
||||
on_click: move |_| {
|
||||
let mut signal = sidebar_active_setter.clone();
|
||||
signal.set("crm".to_string());
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
SidebarSeparator {}
|
||||
SidebarGroup {
|
||||
SidebarGroupLabel { "Reporting" }
|
||||
SidebarGroupContent {
|
||||
SidebarMenu {
|
||||
SidebarMenuItem {
|
||||
SidebarMenuButton {
|
||||
label: "Billing",
|
||||
description: Some("Invoices, usage, balances".into()),
|
||||
icon: Some("💳".into()),
|
||||
badge: Some("8".into()),
|
||||
active: is_billing_active,
|
||||
on_click: move |_| {
|
||||
let mut signal = sidebar_active_setter.clone();
|
||||
signal.set("billing".to_string());
|
||||
},
|
||||
}
|
||||
}
|
||||
SidebarMenuItem {
|
||||
SidebarMenuButton {
|
||||
label: "Settings",
|
||||
description: Some("Themes, tokens, notifications".into()),
|
||||
icon: Some("⚙️".into()),
|
||||
active: is_settings_active,
|
||||
on_click: move |_| {
|
||||
let mut signal = sidebar_active_setter.clone();
|
||||
signal.set("settings".to_string());
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
SidebarFooter {
|
||||
SidebarTrigger {
|
||||
collapsed: collapsed_state,
|
||||
label: Some("Toggle sidebar".to_string()),
|
||||
on_toggle: move |next| {
|
||||
let mut signal = sidebar_collapsed_setter.clone();
|
||||
signal.set(next);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
SidebarInset {
|
||||
class: "ui-stack",
|
||||
h3 { style: "font-size: 1.2rem; font-weight: 600;", "{sidebar_title}" }
|
||||
p {
|
||||
style: "color: hsl(var(--muted-foreground)); max-width: 460px;",
|
||||
"{sidebar_body}"
|
||||
}
|
||||
SpanHelper { "Use the sidebar to swap the focused surface." }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Card {
|
||||
CardHeader {
|
||||
CardTitle { "Selection controls" }
|
||||
|
||||
Reference in New Issue
Block a user