mirror of
https://github.com/mztlive/dx-admin-template.git
synced 2025-12-22 21:59:59 +00:00
admin基本布局
This commit is contained in:
319
assets/styling/admin.css
Normal file
319
assets/styling/admin.css
Normal file
@@ -0,0 +1,319 @@
|
||||
:root {
|
||||
--sidebar-width: 280px;
|
||||
--sidebar-collapsed-width: 72px;
|
||||
--topbar-height: 72px;
|
||||
}
|
||||
|
||||
.ui-shell {
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
padding: 0;
|
||||
background-color: hsl(var(--muted));
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.ui-sidebar-layout.admin-shell {
|
||||
display: grid;
|
||||
grid-template-columns: var(--sidebar-width) minmax(0, 1fr);
|
||||
min-height: 100vh;
|
||||
background-color: hsl(var(--background));
|
||||
}
|
||||
|
||||
.ui-sidebar-layout.admin-shell .ui-sidebar-rail {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ui-sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
background-color: hsl(var(--card));
|
||||
border-right: 1px solid hsl(var(--border));
|
||||
padding: 28px 24px;
|
||||
transition: width 0.25s ease, padding 0.25s ease;
|
||||
}
|
||||
|
||||
.ui-sidebar[data-collapsed="true"] {
|
||||
width: var(--sidebar-collapsed-width);
|
||||
padding: 28px 14px;
|
||||
}
|
||||
|
||||
.ui-sidebar-header,
|
||||
.ui-sidebar-content,
|
||||
.ui-sidebar-footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.sidebar-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.sidebar-logo {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(135deg, hsl(var(--primary)), hsl(var(--secondary)));
|
||||
color: hsl(var(--primary-foreground));
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.sidebar-name {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.sidebar-subtitle {
|
||||
font-size: 0.75rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.ui-sidebar-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.ui-sidebar-group-label {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--muted-foreground));
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.ui-sidebar-menu {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ui-sidebar-menu-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.ui-sidebar-menu-button {
|
||||
display: inline-flex;
|
||||
width: 100%;
|
||||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
border-radius: calc(var(--radius) - 2px);
|
||||
border: 1px solid transparent;
|
||||
background-color: transparent;
|
||||
color: hsl(var(--foreground));
|
||||
transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease;
|
||||
text-align: left;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.ui-sidebar-menu-button:hover {
|
||||
background-color: hsl(var(--muted));
|
||||
}
|
||||
|
||||
.ui-sidebar-menu-button[data-active="true"] {
|
||||
background-color: hsl(var(--accent));
|
||||
border-color: hsl(var(--border));
|
||||
}
|
||||
|
||||
.ui-sidebar-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 8px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: hsl(var(--muted));
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.ui-sidebar-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.ui-sidebar-label {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ui-sidebar-description {
|
||||
font-size: 0.75rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.ui-sidebar-badge {
|
||||
margin-left: auto;
|
||||
font-size: 0.75rem;
|
||||
background-color: hsl(var(--secondary));
|
||||
color: hsl(var(--secondary-foreground));
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.sidebar-profile {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: calc(var(--radius));
|
||||
background-color: hsl(var(--muted));
|
||||
}
|
||||
|
||||
.sidebar-profile-name {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.sidebar-profile-role {
|
||||
color: hsl(var(--muted-foreground));
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.admin-shell-inset {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: hsl(var(--background));
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.admin-shell-topbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
justify-content: space-between;
|
||||
padding: 24px 32px;
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
background-color: hsla(var(--background), 0.95);
|
||||
backdrop-filter: blur(12px);
|
||||
min-height: var(--topbar-height);
|
||||
}
|
||||
|
||||
.admin-shell-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.admin-shell-title {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.admin-shell-description {
|
||||
margin: 0;
|
||||
color: hsl(var(--muted-foreground));
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.admin-shell-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.admin-shell-content {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
padding: 32px;
|
||||
background-color: hsl(var(--muted));
|
||||
min-height: calc(100vh - var(--topbar-height));
|
||||
}
|
||||
|
||||
.dashboard-root {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ui-sidebar-trigger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border: 1px solid hsl(var(--border));
|
||||
background-color: hsl(var(--card));
|
||||
color: hsl(var(--foreground));
|
||||
border-radius: calc(var(--radius) - 2px);
|
||||
padding: 0.5rem 0.8rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.ui-sidebar[data-collapsed="true"] .sidebar-name,
|
||||
.ui-sidebar[data-collapsed="true"] .sidebar-subtitle,
|
||||
.ui-sidebar[data-collapsed="true"] .ui-sidebar-text,
|
||||
.ui-sidebar[data-collapsed="true"] .ui-sidebar-badge,
|
||||
.ui-sidebar[data-collapsed="true"] .sidebar-profile div,
|
||||
.ui-sidebar[data-collapsed="true"] .sidebar-profile-role,
|
||||
.ui-sidebar[data-collapsed="true"] .ui-sidebar-group-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ui-sidebar[data-collapsed="true"] .sidebar-profile {
|
||||
justify-content: center;
|
||||
padding: 12px 8px;
|
||||
}
|
||||
|
||||
.ui-sidebar[data-collapsed="true"] .ui-sidebar-menu-button {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.ui-sidebar-layout.admin-shell {
|
||||
grid-template-columns: var(--sidebar-width) minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.ui-sidebar-layout.admin-shell {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.ui-sidebar {
|
||||
position: fixed;
|
||||
inset: 0 auto 0 0;
|
||||
transform: translateX(-100%);
|
||||
width: var(--sidebar-width);
|
||||
max-width: 90vw;
|
||||
box-shadow: 0 20px 50px -24px rgba(15, 23, 42, 0.45);
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.ui-sidebar[data-collapsed="true"] {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
.ui-sidebar[data-collapsed="false"] {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.admin-shell-topbar {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.admin-shell-actions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.admin-shell-content {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.dashboard-root {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -1,42 +1,12 @@
|
||||
html,
|
||||
body {
|
||||
background-color: #0f1116;
|
||||
color: #ffffff;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
margin: 20px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#hero {
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: hsl(var(--muted));
|
||||
color: hsl(var(--foreground));
|
||||
font-family: 'Inter', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
#links {
|
||||
width: 400px;
|
||||
text-align: left;
|
||||
font-size: x-large;
|
||||
color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#links a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
margin-top: 20px;
|
||||
margin: 10px 0px;
|
||||
border: white 1px solid;
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
#links a:hover {
|
||||
background-color: #1f1f1f;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#header {
|
||||
max-width: 1200px;
|
||||
}
|
||||
15
src/main.rs
15
src/main.rs
@@ -2,7 +2,7 @@
|
||||
// need dioxus
|
||||
use dioxus::prelude::*;
|
||||
|
||||
use views::{Blog, Home, Navbar};
|
||||
use views::{Components, Home, Navbar};
|
||||
|
||||
/// Define a components module that contains all shared components for our app.
|
||||
mod components;
|
||||
@@ -11,7 +11,7 @@ mod views;
|
||||
|
||||
/// The Route enum is used to define the structure of internal routes in our app. All route enums need to derive
|
||||
/// the [`Routable`] trait, which provides the necessary methods for the router to work.
|
||||
///
|
||||
///
|
||||
/// Each variant represents a different URL pattern that can be matched by the router. If that pattern is matched,
|
||||
/// the components for that route will be rendered.
|
||||
#[derive(Debug, Clone, Routable, PartialEq)]
|
||||
@@ -24,12 +24,9 @@ enum Route {
|
||||
// the component for that route will be rendered. The component name that is rendered defaults to the variant name.
|
||||
#[route("/")]
|
||||
Home {},
|
||||
// The route attribute can include dynamic parameters that implement [`std::str::FromStr`] and [`std::fmt::Display`] with the `:` syntax.
|
||||
// In this case, id will match any integer like `/blog/123` or `/blog/-456`.
|
||||
#[route("/blog/:id")]
|
||||
// Fields of the route variant will be passed to the component as props. In this case, the blog component must accept
|
||||
// an `id` prop of type `i32`.
|
||||
Blog { id: i32 },
|
||||
#[route("/components")]
|
||||
// The components gallery reuses the full UI showcase so new primitives stay discoverable.
|
||||
Components {},
|
||||
}
|
||||
|
||||
// We can import assets in dioxus with the `asset!` macro. This macro takes a path to an asset relative to the crate root.
|
||||
@@ -38,6 +35,7 @@ const FAVICON: Asset = asset!("/assets/favicon.ico");
|
||||
// The asset macro also minifies some assets like CSS and JS to make bundled smaller
|
||||
const MAIN_CSS: Asset = asset!("/assets/styling/main.css");
|
||||
const SHADCN_CSS: Asset = asset!("/assets/styling/shadcn.css");
|
||||
const ADMIN_CSS: Asset = asset!("/assets/styling/admin.css");
|
||||
|
||||
fn main() {
|
||||
// The `launch` function is the main entry point for a dioxus app. It takes a component and renders it with the platform feature
|
||||
@@ -58,6 +56,7 @@ fn App() -> Element {
|
||||
document::Link { rel: "icon", href: FAVICON }
|
||||
document::Link { rel: "stylesheet", href: MAIN_CSS }
|
||||
document::Link { rel: "stylesheet", href: SHADCN_CSS }
|
||||
document::Link { rel: "stylesheet", href: ADMIN_CSS }
|
||||
|
||||
|
||||
// The router component renders the route enum we defined above. It will handle synchronization of the URL and render
|
||||
|
||||
@@ -15,6 +15,256 @@ use dioxus::prelude::*;
|
||||
/// The Home page component that will be rendered when the current route is `[Route::Home]`
|
||||
#[component]
|
||||
pub fn Home() -> Element {
|
||||
let kpis = [
|
||||
("Monthly Recurring Revenue", "$82.4k", "+4.1% vs last month"),
|
||||
("Active Accounts", "18,245", "+320 this week"),
|
||||
("Trial Conversion Rate", "32%", "Goal: 40%"),
|
||||
("Support CSAT", "94%", "230 responses"),
|
||||
];
|
||||
let experiments = [
|
||||
(
|
||||
"Guided onboarding checklist",
|
||||
"QA",
|
||||
"Validating instrumentation with the analytics team.",
|
||||
),
|
||||
(
|
||||
"Usage-based alerts",
|
||||
"Design",
|
||||
"Final polish on notification copy before launch.",
|
||||
),
|
||||
(
|
||||
"Unified billing dashboard",
|
||||
"Build",
|
||||
"Integration tests are running against staging.",
|
||||
),
|
||||
];
|
||||
let adoption_segments = [
|
||||
(
|
||||
"Onboarding completion",
|
||||
74.0f32,
|
||||
"Teams finished the setup checklist with no blockers.",
|
||||
),
|
||||
(
|
||||
"Automation adoption",
|
||||
58.0f32,
|
||||
"Workflows triggered in the past 7 days across customers.",
|
||||
),
|
||||
(
|
||||
"Weekly active accounts",
|
||||
83.0f32,
|
||||
"Organizations engaging with analytics at least twice.",
|
||||
),
|
||||
];
|
||||
let alert_feed = [
|
||||
(
|
||||
"Incident response playbook",
|
||||
"Workspace incidents resolved in under 4h for 92% of cases.",
|
||||
),
|
||||
(
|
||||
"Billing sync delays",
|
||||
"Elevated Stripe webhook latency observed over the weekend.",
|
||||
),
|
||||
(
|
||||
"API deprecations",
|
||||
"Notify integrators about the upcoming reporting v2 changes.",
|
||||
),
|
||||
];
|
||||
let standup_sections: [(&str, [&str; 2]); 3] = [
|
||||
(
|
||||
"Platform",
|
||||
[
|
||||
"Ship metering hotfix across clusters",
|
||||
"Pair with data engineering for realtime audits",
|
||||
],
|
||||
),
|
||||
(
|
||||
"Product",
|
||||
[
|
||||
"Enable proactive insights beta for 10 design partners",
|
||||
"Finalize empty state content review with brand",
|
||||
],
|
||||
),
|
||||
(
|
||||
"Customer Success",
|
||||
[
|
||||
"Schedule QBR with Globex expansion team",
|
||||
"Collect testimonials for the website refresh",
|
||||
],
|
||||
),
|
||||
];
|
||||
let priority_backlog = [
|
||||
(
|
||||
"Lifecycle emails",
|
||||
"Growth",
|
||||
"Net-new education journey that nurtures power users.",
|
||||
),
|
||||
(
|
||||
"Data residency",
|
||||
"Platform",
|
||||
"Coordinate EU workspace pilot timeline with legal.",
|
||||
),
|
||||
(
|
||||
"Journeys analytics",
|
||||
"Product",
|
||||
"Scope dashboards that highlight activation funnels.",
|
||||
),
|
||||
];
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
class: "dashboard-root",
|
||||
style: "display: flex; flex-direction: column; gap: 24px;",
|
||||
section {
|
||||
class: "dashboard-kpis",
|
||||
style: "display: grid; gap: 16px; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));",
|
||||
for (title, value, hint) in kpis {
|
||||
Card {
|
||||
CardHeader {
|
||||
CardTitle { "{title}" }
|
||||
CardDescription { "{hint}" }
|
||||
}
|
||||
CardContent {
|
||||
span {
|
||||
style: "font-size: 2rem; font-weight: 600;",
|
||||
"{value}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
section {
|
||||
class: "dashboard-panels",
|
||||
style: "display: grid; gap: 24px; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); align-items: start;",
|
||||
Card {
|
||||
CardHeader {
|
||||
CardTitle { "Product health" }
|
||||
CardDescription { "Experiments, adoption, and alerts surfaced from telemetry." }
|
||||
}
|
||||
CardContent {
|
||||
Tabs {
|
||||
default_value: "experiments".to_string(),
|
||||
TabsList {
|
||||
TabsTrigger { value: "experiments".to_string(), "Experiments" }
|
||||
TabsTrigger { value: "adoption".to_string(), "Adoption" }
|
||||
TabsTrigger { value: "alerts".to_string(), "Alerts" }
|
||||
}
|
||||
TabsContent { value: "experiments".to_string(),
|
||||
div {
|
||||
style: "display: flex; flex-direction: column; gap: 16px;",
|
||||
for (title, stage, note) in experiments {
|
||||
div {
|
||||
style: "display: flex; flex-direction: column; gap: 6px; background: hsl(var(--muted)); padding: 16px; border-radius: calc(var(--radius) - 2px);",
|
||||
div {
|
||||
style: "display: flex; justify-content: space-between; align-items: center; gap: 12px;",
|
||||
span { style: "font-weight: 600;", "{title}" }
|
||||
Badge { variant: BadgeVariant::Secondary, "{stage}" }
|
||||
}
|
||||
p { style: "color: hsl(var(--muted-foreground)); margin: 0;", "{note}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
TabsContent { value: "adoption".to_string(),
|
||||
div {
|
||||
style: "display: flex; flex-direction: column; gap: 16px;",
|
||||
for (label, percent, note) in adoption_segments {
|
||||
div {
|
||||
style: "display: flex; flex-direction: column; gap: 8px;",
|
||||
div {
|
||||
style: "display: flex; justify-content: space-between; align-items: baseline;",
|
||||
span { style: "font-weight: 600;", "{label}" }
|
||||
span { style: "font-size: 0.9rem; color: hsl(var(--muted-foreground));", "{percent:.0}%" }
|
||||
}
|
||||
Progress { value: percent }
|
||||
p { style: "color: hsl(var(--muted-foreground)); margin: 0;", "{note}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
TabsContent { value: "alerts".to_string(),
|
||||
div {
|
||||
style: "display: flex; flex-direction: column; gap: 12px;",
|
||||
for (title, note) in alert_feed {
|
||||
div {
|
||||
style: "display: flex; flex-direction: column; gap: 4px;",
|
||||
span { style: "font-weight: 600;", "{title}" }
|
||||
p { style: "color: hsl(var(--muted-foreground)); margin: 0;", "{note}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
CardFooter {
|
||||
Button {
|
||||
variant: ButtonVariant::Secondary,
|
||||
r#type: "button".to_string(),
|
||||
"Open product review"
|
||||
}
|
||||
}
|
||||
}
|
||||
Card {
|
||||
CardHeader {
|
||||
CardTitle { "Team stand-up" }
|
||||
CardDescription { "Snapshots ready to paste into the leadership sync." }
|
||||
}
|
||||
CardContent {
|
||||
Accordion {
|
||||
default_value: Some("Platform".to_string()),
|
||||
collapsible: true,
|
||||
for (team, updates) in standup_sections {
|
||||
AccordionItem {
|
||||
value: team.to_string(),
|
||||
AccordionTrigger { "{team}" }
|
||||
AccordionContent {
|
||||
ul {
|
||||
style: "margin: 0; padding-left: 20px; display: flex; flex-direction: column; gap: 6px;",
|
||||
for update in updates {
|
||||
li { "{update}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Card {
|
||||
CardHeader {
|
||||
CardTitle { "Escalation queue" }
|
||||
CardDescription { "Cross-functional work that needs executive unblockers." }
|
||||
}
|
||||
CardContent {
|
||||
div {
|
||||
style: "display: flex; flex-direction: column; gap: 16px;",
|
||||
for (title, owner, note) in priority_backlog {
|
||||
div {
|
||||
style: "display: flex; flex-direction: column; gap: 4px;",
|
||||
div {
|
||||
style: "display: flex; justify-content: space-between; align-items: center;",
|
||||
span { style: "font-weight: 600;", "{title}" }
|
||||
Badge { variant: BadgeVariant::Outline, "{owner}" }
|
||||
}
|
||||
p { style: "color: hsl(var(--muted-foreground)); margin: 0;", "{note}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
CardFooter {
|
||||
Button {
|
||||
variant: ButtonVariant::Ghost,
|
||||
r#type: "button".to_string(),
|
||||
"View roadmap"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Components() -> Element {
|
||||
rsx! {
|
||||
UiShowcase {}
|
||||
}
|
||||
|
||||
@@ -2,17 +2,14 @@
|
||||
//! enum will render one of these components.
|
||||
//!
|
||||
//!
|
||||
//! The [`Home`] and [`Blog`] components will be rendered when the current route is [`Route::Home`] or [`Route::Blog`] respectively.
|
||||
//! The [`Home`] and [`Components`] views back the dashboard and component gallery routes.
|
||||
//!
|
||||
//!
|
||||
//! The [`Navbar`] component will be rendered on all pages of our app since every page is under the layout. The layout defines
|
||||
//! a common wrapper around all child routes.
|
||||
|
||||
mod home;
|
||||
pub use home::Home;
|
||||
|
||||
mod blog;
|
||||
pub use blog::Blog;
|
||||
pub use home::{Components, Home};
|
||||
|
||||
mod navbar;
|
||||
pub use navbar::Navbar;
|
||||
|
||||
@@ -1,32 +1,157 @@
|
||||
use crate::Route;
|
||||
use crate::{
|
||||
components::ui::{
|
||||
Avatar, Badge, BadgeVariant, Button, ButtonVariant, Sidebar, SidebarContent, SidebarFooter,
|
||||
SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarInset,
|
||||
SidebarLayout, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarRail,
|
||||
SidebarSeparator, SidebarTrigger,
|
||||
},
|
||||
Route,
|
||||
};
|
||||
use dioxus::prelude::*;
|
||||
|
||||
const NAVBAR_CSS: Asset = asset!("/assets/styling/navbar.css");
|
||||
|
||||
/// The Navbar component that will be rendered on all pages of our app since every page is under the layout.
|
||||
///
|
||||
///
|
||||
/// This layout component wraps the UI of [Route::Home] and [Route::Blog] in a common navbar. The contents of the Home and Blog
|
||||
/// routes will be rendered under the outlet inside this component
|
||||
#[component]
|
||||
pub fn Navbar() -> Element {
|
||||
rsx! {
|
||||
document::Link { rel: "stylesheet", href: NAVBAR_CSS }
|
||||
|
||||
div {
|
||||
id: "navbar",
|
||||
Link {
|
||||
to: Route::Home {},
|
||||
"Home"
|
||||
}
|
||||
Link {
|
||||
to: Route::Blog { id: 1 },
|
||||
"Blog"
|
||||
}
|
||||
}
|
||||
|
||||
// The `Outlet` component is used to render the next component inside the layout. In this case, it will render either
|
||||
// the [`Home`] or [`Blog`] component depending on the current route.
|
||||
Outlet::<Route> {}
|
||||
fn page_meta(route: &Route) -> (&'static str, &'static str) {
|
||||
match route {
|
||||
Route::Home {} => (
|
||||
"Dashboard overview",
|
||||
"Track product health, monitor key funnels, and coordinate the team from one place.",
|
||||
),
|
||||
Route::Components {} => (
|
||||
"Component library",
|
||||
"Browse every primitive wired into this starter so new views stay consistent.",
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Navbar() -> Element {
|
||||
let collapsed = use_signal(|| false);
|
||||
let current_route: Route = use_route();
|
||||
let collapsed_state = collapsed();
|
||||
let collapsed_setter = collapsed.clone();
|
||||
|
||||
let (page_title, page_description) = page_meta(¤t_route);
|
||||
let is_dashboard = matches!(current_route, Route::Home { .. });
|
||||
let is_components = matches!(current_route, Route::Components { .. });
|
||||
|
||||
rsx! {
|
||||
section {
|
||||
class: "ui-shell shadcn",
|
||||
SidebarLayout {
|
||||
class: "admin-shell",
|
||||
SidebarRail { }
|
||||
Sidebar {
|
||||
collapsed: collapsed_state,
|
||||
SidebarHeader {
|
||||
div { class: "sidebar-brand",
|
||||
span { class: "sidebar-logo", "⚡" }
|
||||
div {
|
||||
span { class: "sidebar-name", "Dioxus Admin" }
|
||||
span { class: "sidebar-subtitle", "v0.7 toolkit" }
|
||||
}
|
||||
}
|
||||
}
|
||||
SidebarContent {
|
||||
SidebarGroup {
|
||||
SidebarGroupLabel { "Overview" }
|
||||
SidebarGroupContent {
|
||||
SidebarMenu {
|
||||
SidebarMenuItem {
|
||||
SidebarMenuButton {
|
||||
label: "Dashboard",
|
||||
description: Some("KPIs, monitors, and recent activity".to_string()),
|
||||
icon: Some("📊".to_string()),
|
||||
active: is_dashboard,
|
||||
href: Some(Route::Home {}.to_string()),
|
||||
}
|
||||
}
|
||||
SidebarMenuItem {
|
||||
SidebarMenuButton {
|
||||
label: "Components",
|
||||
description: Some("Living style guide of primitives".to_string()),
|
||||
icon: Some("🧩".to_string()),
|
||||
active: is_components,
|
||||
href: Some(Route::Components {}.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
SidebarSeparator { }
|
||||
SidebarGroup {
|
||||
SidebarGroupLabel { "Shortcuts" }
|
||||
SidebarGroupContent {
|
||||
SidebarMenu {
|
||||
SidebarMenuItem {
|
||||
SidebarMenuButton {
|
||||
label: "Team",
|
||||
description: Some("Invite and manage collaborators".to_string()),
|
||||
icon: Some("👥".to_string()),
|
||||
badge: Some("4 pending".to_string()),
|
||||
href: Some("#team".to_string()),
|
||||
}
|
||||
}
|
||||
SidebarMenuItem {
|
||||
SidebarMenuButton {
|
||||
label: "Settings",
|
||||
description: Some("Branding, auth, billing".to_string()),
|
||||
icon: Some("⚙️".to_string()),
|
||||
href: Some("#settings".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
SidebarFooter {
|
||||
div { class: "sidebar-profile",
|
||||
Avatar {
|
||||
src: Some("https://avatars.githubusercontent.com/u/3236120?v=4".to_string()),
|
||||
alt: Some("Administrator avatar".to_string()),
|
||||
fallback: Some("DX".to_string()),
|
||||
}
|
||||
div {
|
||||
span { class: "sidebar-profile-name", "Taylor Chen" }
|
||||
span { class: "sidebar-profile-role", "Product Manager" }
|
||||
}
|
||||
}
|
||||
Button {
|
||||
class: Some("mt-2 w-full".to_string()),
|
||||
variant: ButtonVariant::Secondary,
|
||||
r#type: "button".to_string(),
|
||||
"Switch account"
|
||||
}
|
||||
}
|
||||
}
|
||||
SidebarInset {
|
||||
class: "admin-shell-inset",
|
||||
header {
|
||||
class: "admin-shell-topbar",
|
||||
SidebarTrigger {
|
||||
collapsed: collapsed_state,
|
||||
on_toggle: move |next_collapsed: bool| {
|
||||
collapsed_setter.clone().set(next_collapsed);
|
||||
},
|
||||
}
|
||||
div { class: "admin-shell-meta",
|
||||
h1 { class: "admin-shell-title", "{page_title}" }
|
||||
p { class: "admin-shell-description", "{page_description}" }
|
||||
}
|
||||
div { class: "admin-shell-actions",
|
||||
Badge { variant: BadgeVariant::Secondary, "Operational" }
|
||||
Button {
|
||||
variant: ButtonVariant::Default,
|
||||
class: Some("admin-shell-report".to_string()),
|
||||
r#type: "button".to_string(),
|
||||
"New report"
|
||||
}
|
||||
}
|
||||
}
|
||||
main {
|
||||
class: "admin-shell-content",
|
||||
Outlet::<Route> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user