feat(sidebar): enhance sidebar navigation and improve org chart styling
This commit is contained in:
parent
1bf7c945cf
commit
83d5b85247
4 changed files with 174 additions and 122 deletions
|
|
@ -246,3 +246,43 @@
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
---break---
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Top-down ORBAT-style tree connectors */
|
||||||
|
.org-tree-child {
|
||||||
|
position: relative;
|
||||||
|
padding-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.org-tree-child::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 50%;
|
||||||
|
height: 1rem;
|
||||||
|
border-left: 2px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.org-tree-child::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
border-top: 2px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.org-tree-child:first-child::after {
|
||||||
|
left: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.org-tree-child:last-child::after {
|
||||||
|
right: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.org-tree-child:only-child::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Bolt, Send, Settings2, Shield, Users, Warehouse, Crosshair } from "lucide-react";
|
import { Bolt, Send, Shield, Users, Warehouse, Crosshair } from "lucide-react";
|
||||||
|
|
||||||
import { NavCore } from "@/components/frontend/blocks/NavCore";
|
import { NavCore } from "@/components/frontend/blocks/NavCore";
|
||||||
import { NavSecondary } from "@/components/frontend/blocks/NavSecondary";
|
import { NavSecondary } from "@/components/frontend/blocks/NavSecondary";
|
||||||
|
|
@ -32,29 +32,6 @@ const data = {
|
||||||
url: "/roster",
|
url: "/roster",
|
||||||
icon: Users,
|
icon: Users,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: "Settings",
|
|
||||||
url: "#",
|
|
||||||
icon: Settings2,
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
title: "General",
|
|
||||||
url: "#",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Team",
|
|
||||||
url: "#",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Billing",
|
|
||||||
url: "#",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Limits",
|
|
||||||
url: "#",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
navIntelligence: [
|
navIntelligence: [
|
||||||
{
|
{
|
||||||
|
|
@ -95,10 +72,6 @@ const data = {
|
||||||
title: "Vehicles",
|
title: "Vehicles",
|
||||||
url: "/logistics/vehicles",
|
url: "/logistics/vehicles",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: "Market",
|
|
||||||
url: "#",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: "Shipments",
|
title: "Shipments",
|
||||||
url: "/logistics/shipments",
|
url: "/logistics/shipments",
|
||||||
|
|
@ -138,7 +111,7 @@ export function AppSidebar({
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<SidebarMenuButton size="lg" asChild>
|
<SidebarMenuButton size="lg" asChild>
|
||||||
<Link href="/src/components/static">
|
<Link href="/">
|
||||||
{/*<div className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
|
{/*<div className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
|
||||||
<Command className="size-4" />
|
<Command className="size-4" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ import {
|
||||||
SidebarGroup,
|
SidebarGroup,
|
||||||
SidebarGroupLabel,
|
SidebarGroupLabel,
|
||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
SidebarMenuAction,
|
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
SidebarMenuSub,
|
SidebarMenuSub,
|
||||||
|
|
@ -41,22 +40,24 @@ export function NavCore({
|
||||||
<SidebarGroup>
|
<SidebarGroup>
|
||||||
<SidebarGroupLabel>{label}</SidebarGroupLabel>
|
<SidebarGroupLabel>{label}</SidebarGroupLabel>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
{items.map((item) => (
|
{items.map((item) => {
|
||||||
<Collapsible key={item.title} asChild defaultOpen={item.isActive}>
|
const hasChildren = (item.items?.length ?? 0) > 0;
|
||||||
|
return (
|
||||||
|
<Collapsible
|
||||||
|
key={item.title}
|
||||||
|
asChild
|
||||||
|
defaultOpen={item.isActive}
|
||||||
|
className="group/collapsible"
|
||||||
|
>
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<SidebarMenuButton asChild tooltip={item.title}>
|
{hasChildren ? (
|
||||||
<Link href={item.url}>
|
|
||||||
<item.icon />
|
|
||||||
<span>{item.title}</span>
|
|
||||||
</Link>
|
|
||||||
</SidebarMenuButton>
|
|
||||||
{item.items?.length ? (
|
|
||||||
<>
|
<>
|
||||||
<CollapsibleTrigger asChild>
|
<CollapsibleTrigger asChild>
|
||||||
<SidebarMenuAction className="data-[state=open]:rotate-90">
|
<SidebarMenuButton tooltip={item.title}>
|
||||||
<ChevronRight />
|
<item.icon />
|
||||||
<span className="sr-only">Toggle</span>
|
<span>{item.title}</span>
|
||||||
</SidebarMenuAction>
|
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
||||||
|
</SidebarMenuButton>
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
<CollapsibleContent>
|
<CollapsibleContent>
|
||||||
<SidebarMenuSub>
|
<SidebarMenuSub>
|
||||||
|
|
@ -73,10 +74,18 @@ export function NavCore({
|
||||||
</SidebarMenuSub>
|
</SidebarMenuSub>
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : (
|
||||||
|
<SidebarMenuButton asChild tooltip={item.title}>
|
||||||
|
<Link href={item.url}>
|
||||||
|
<item.icon />
|
||||||
|
<span>{item.title}</span>
|
||||||
|
</Link>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
)}
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,7 @@ function collectAllMembers(node: TreeNode): RosterMember[] {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Org Chart ──────────────────────────────────────────────────────
|
// ─── Org Chart (top-down ORBAT tree) ────────────────────────────────
|
||||||
|
|
||||||
function OrgChartView({
|
function OrgChartView({
|
||||||
assignments,
|
assignments,
|
||||||
|
|
@ -144,11 +144,13 @@ function OrgChartView({
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{roots.length > 0 ? (
|
{roots.length > 0 ? (
|
||||||
<div className="flex flex-col gap-3">
|
<div className="overflow-x-auto pb-2">
|
||||||
|
<div className="flex flex-col items-center gap-4 min-w-max">
|
||||||
{roots.map((root) => (
|
{roots.map((root) => (
|
||||||
<OrgChartNode key={root.id} node={root} depth={0} />
|
<OrgChartNode key={root.id} node={root} depth={0} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-sm text-muted-foreground">No assignments configured.</p>
|
<p className="text-sm text-muted-foreground">No assignments configured.</p>
|
||||||
)}
|
)}
|
||||||
|
|
@ -171,38 +173,77 @@ function OrgChartView({
|
||||||
|
|
||||||
function OrgChartNode({ node, depth }: { node: TreeNode; depth: number }) {
|
function OrgChartNode({ node, depth }: { node: TreeNode; depth: number }) {
|
||||||
const allMembers = collectAllMembers(node);
|
const allMembers = collectAllMembers(node);
|
||||||
const hasChildren = node.children.length > 0;
|
const leader = node.leaderId
|
||||||
|
? allMembers.find((m) => m.user.id === node.leaderId)
|
||||||
|
: null;
|
||||||
|
const directMembers = node.members.filter((m) => m.user.id !== node.leaderId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<UnitBox
|
||||||
|
node={node}
|
||||||
|
depth={depth}
|
||||||
|
leader={leader}
|
||||||
|
directMembers={directMembers}
|
||||||
|
memberCount={allMembers.length}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{node.children.length > 0 && (
|
||||||
|
<>
|
||||||
|
<div className="h-4 w-px bg-border" />
|
||||||
|
<div className="flex items-start">
|
||||||
|
{node.children.map((child) => (
|
||||||
|
<div key={child.id} className="org-tree-child px-3">
|
||||||
|
<OrgChartNode node={child} depth={depth + 1} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function UnitBox({
|
||||||
|
node,
|
||||||
|
depth,
|
||||||
|
leader,
|
||||||
|
directMembers,
|
||||||
|
memberCount,
|
||||||
|
}: {
|
||||||
|
node: TreeNode;
|
||||||
|
depth: number;
|
||||||
|
leader: RosterMember | null | undefined;
|
||||||
|
directMembers: RosterMember[];
|
||||||
|
memberCount: number;
|
||||||
|
}) {
|
||||||
const depthBorders = [
|
const depthBorders = [
|
||||||
"border-l-blue-600",
|
"border-l-blue-600",
|
||||||
"border-l-amber-600",
|
"border-l-amber-600",
|
||||||
"border-l-emerald-600",
|
"border-l-emerald-600",
|
||||||
];
|
];
|
||||||
const borderClass = depthBorders[depth] ?? "border-l-zinc-600";
|
const borderClass = depthBorders[depth] ?? "border-l-zinc-600";
|
||||||
const leader = node.leaderId
|
|
||||||
? allMembers.find((m) => m.user.id === node.leaderId)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn(depth > 0 && "ml-5 border-l-2 border-border pl-4")}>
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"border rounded-lg p-3 bg-muted/20 border-l-4",
|
"border rounded-lg bg-muted/20 border-l-4 p-3 w-full min-w-56 max-w-xs",
|
||||||
borderClass,
|
borderClass,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<Building2Icon className="size-3.5 text-muted-foreground" />
|
<Building2Icon className="size-3.5 text-muted-foreground shrink-0" />
|
||||||
<span className="text-sm font-semibold">{node.name}</span>
|
<span className="text-sm font-semibold truncate">{node.name}</span>
|
||||||
<span className="text-[10px] uppercase tracking-wider text-muted-foreground bg-muted px-1.5 py-0.5 rounded font-mono">
|
<span className="text-[10px] uppercase tracking-wider text-muted-foreground bg-muted px-1.5 py-0.5 rounded font-mono shrink-0">
|
||||||
{TYPE_LABELS[node.type] ?? node.type}
|
{TYPE_LABELS[node.type] ?? node.type}
|
||||||
</span>
|
</span>
|
||||||
{node.callsign && (
|
{node.callsign && (
|
||||||
<span className="text-[10px] text-muted-foreground font-mono">
|
<span className="text-[10px] text-muted-foreground font-mono shrink-0">
|
||||||
[{node.callsign}]
|
[{node.callsign}]
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<span className="text-[10px] text-muted-foreground ml-auto">
|
<span className="text-[10px] text-muted-foreground ml-auto shrink-0">
|
||||||
{allMembers.length} member{allMembers.length !== 1 ? "s" : ""}
|
{memberCount}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -215,24 +256,13 @@ function OrgChartNode({ node, depth }: { node: TreeNode; depth: number }) {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{node.members.filter((m) => m.user.id !== node.leaderId).length > 0 && (
|
{directMembers.length > 0 && (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
{node.members
|
{directMembers.map((m) => (
|
||||||
.filter((m) => m.user.id !== node.leaderId)
|
|
||||||
.map((m) => (
|
|
||||||
<MemberCard key={m.profileId} member={m} />
|
<MemberCard key={m.profileId} member={m} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{hasChildren && (
|
|
||||||
<div className="mt-3 flex flex-col gap-3">
|
|
||||||
{node.children.map((child) => (
|
|
||||||
<OrgChartNode key={child.id} node={child} depth={depth + 1} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue