1
0
Fork 0

feat(sidebar): enhance sidebar navigation and improve org chart styling

This commit is contained in:
Jason Fraley 2026-08-02 03:14:30 -04:00
parent 1bf7c945cf
commit 83d5b85247
4 changed files with 174 additions and 122 deletions

View file

@ -246,3 +246,43 @@
@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;
}

View file

@ -1,7 +1,7 @@
"use client";
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 { NavSecondary } from "@/components/frontend/blocks/NavSecondary";
@ -32,29 +32,6 @@ const data = {
url: "/roster",
icon: Users,
},
{
title: "Settings",
url: "#",
icon: Settings2,
items: [
{
title: "General",
url: "#",
},
{
title: "Team",
url: "#",
},
{
title: "Billing",
url: "#",
},
{
title: "Limits",
url: "#",
},
],
},
],
navIntelligence: [
{
@ -95,10 +72,6 @@ const data = {
title: "Vehicles",
url: "/logistics/vehicles",
},
{
title: "Market",
url: "#",
},
{
title: "Shipments",
url: "/logistics/shipments",
@ -138,7 +111,7 @@ export function AppSidebar({
<SidebarMenu>
<SidebarMenuItem>
<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">
<Command className="size-4" />
</div>

View file

@ -12,7 +12,6 @@ import {
SidebarGroup,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuAction,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
@ -41,42 +40,52 @@ export function NavCore({
<SidebarGroup>
<SidebarGroupLabel>{label}</SidebarGroupLabel>
<SidebarMenu>
{items.map((item) => (
<Collapsible key={item.title} asChild defaultOpen={item.isActive}>
<SidebarMenuItem>
<SidebarMenuButton asChild tooltip={item.title}>
<Link href={item.url}>
<item.icon />
<span>{item.title}</span>
</Link>
</SidebarMenuButton>
{item.items?.length ? (
<>
<CollapsibleTrigger asChild>
<SidebarMenuAction className="data-[state=open]:rotate-90">
<ChevronRight />
<span className="sr-only">Toggle</span>
</SidebarMenuAction>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{item.items?.map((subItem) => (
<SidebarMenuSubItem key={subItem.title}>
<SidebarMenuSubButton asChild>
<Link href={subItem.url}>
{subItem.icon ? <subItem.icon /> : null}
<span>{subItem.title}</span>
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</>
) : null}
</SidebarMenuItem>
</Collapsible>
))}
{items.map((item) => {
const hasChildren = (item.items?.length ?? 0) > 0;
return (
<Collapsible
key={item.title}
asChild
defaultOpen={item.isActive}
className="group/collapsible"
>
<SidebarMenuItem>
{hasChildren ? (
<>
<CollapsibleTrigger asChild>
<SidebarMenuButton tooltip={item.title}>
<item.icon />
<span>{item.title}</span>
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{item.items?.map((subItem) => (
<SidebarMenuSubItem key={subItem.title}>
<SidebarMenuSubButton asChild>
<Link href={subItem.url}>
{subItem.icon ? <subItem.icon /> : null}
<span>{subItem.title}</span>
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</>
) : (
<SidebarMenuButton asChild tooltip={item.title}>
<Link href={item.url}>
<item.icon />
<span>{item.title}</span>
</Link>
</SidebarMenuButton>
)}
</SidebarMenuItem>
</Collapsible>
);
})}
</SidebarMenu>
</SidebarGroup>
);

View file

@ -126,7 +126,7 @@ function collectAllMembers(node: TreeNode): RosterMember[] {
return result;
}
// ─── Org Chart ──────────────────────────────────────────────────────
// ─── Org Chart (top-down ORBAT tree) ────────────────────────────────
function OrgChartView({
assignments,
@ -144,10 +144,12 @@ function OrgChartView({
return (
<div className="flex flex-col gap-4">
{roots.length > 0 ? (
<div className="flex flex-col gap-3">
{roots.map((root) => (
<OrgChartNode key={root.id} node={root} depth={0} />
))}
<div className="overflow-x-auto pb-2">
<div className="flex flex-col items-center gap-4 min-w-max">
{roots.map((root) => (
<OrgChartNode key={root.id} node={root} depth={0} />
))}
</div>
</div>
) : (
<p className="text-sm text-muted-foreground">No assignments configured.</p>
@ -171,68 +173,96 @@ function OrgChartView({
function OrgChartNode({ node, depth }: { node: TreeNode; depth: number }) {
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 = [
"border-l-blue-600",
"border-l-amber-600",
"border-l-emerald-600",
];
const borderClass = depthBorders[depth] ?? "border-l-zinc-600";
const leader = node.leaderId
? allMembers.find((m) => m.user.id === node.leaderId)
: null;
return (
<div className={cn(depth > 0 && "ml-5 border-l-2 border-border pl-4")}>
<div
className={cn(
"border rounded-lg p-3 bg-muted/20 border-l-4",
borderClass,
)}
>
<div className="flex items-center gap-2 mb-2">
<Building2Icon className="size-3.5 text-muted-foreground" />
<span className="text-sm font-semibold">{node.name}</span>
<span className="text-[10px] uppercase tracking-wider text-muted-foreground bg-muted px-1.5 py-0.5 rounded font-mono">
{TYPE_LABELS[node.type] ?? node.type}
<div
className={cn(
"border rounded-lg bg-muted/20 border-l-4 p-3 w-full min-w-56 max-w-xs",
borderClass,
)}
>
<div className="flex items-center gap-2 mb-2">
<Building2Icon className="size-3.5 text-muted-foreground shrink-0" />
<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 shrink-0">
{TYPE_LABELS[node.type] ?? node.type}
</span>
{node.callsign && (
<span className="text-[10px] text-muted-foreground font-mono shrink-0">
[{node.callsign}]
</span>
{node.callsign && (
<span className="text-[10px] text-muted-foreground font-mono">
[{node.callsign}]
</span>
)}
<span className="text-[10px] text-muted-foreground ml-auto">
{allMembers.length} member{allMembers.length !== 1 ? "s" : ""}
</span>
</div>
{leader && (
<div className="mb-2">
<span className="text-[10px] uppercase tracking-wider text-muted-foreground flex items-center gap-1 mb-1">
<CrownIcon className="size-3" /> Commanding
</span>
<MemberCard member={leader} />
</div>
)}
{node.members.filter((m) => m.user.id !== node.leaderId).length > 0 && (
<div className="flex flex-col gap-1">
{node.members
.filter((m) => m.user.id !== node.leaderId)
.map((m) => (
<MemberCard key={m.profileId} member={m} />
))}
</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>
)}
<span className="text-[10px] text-muted-foreground ml-auto shrink-0">
{memberCount}
</span>
</div>
{leader && (
<div className="mb-2">
<span className="text-[10px] uppercase tracking-wider text-muted-foreground flex items-center gap-1 mb-1">
<CrownIcon className="size-3" /> Commanding
</span>
<MemberCard member={leader} />
</div>
)}
{directMembers.length > 0 && (
<div className="flex flex-col gap-1">
{directMembers.map((m) => (
<MemberCard key={m.profileId} member={m} />
))}
</div>
)}
</div>
);
}