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; @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"; "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>

View file

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

View file

@ -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,10 +144,12 @@ 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">
{roots.map((root) => ( <div className="flex flex-col items-center gap-4 min-w-max">
<OrgChartNode key={root.id} node={root} depth={0} /> {roots.map((root) => (
))} <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,68 +173,96 @@ 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 bg-muted/20 border-l-4 p-3 w-full min-w-56 max-w-xs",
"border rounded-lg p-3 bg-muted/20 border-l-4", 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 shrink-0" />
<Building2Icon className="size-3.5 text-muted-foreground" /> <span className="text-sm font-semibold truncate">{node.name}</span>
<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 shrink-0">
<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}
{TYPE_LABELS[node.type] ?? node.type} </span>
{node.callsign && (
<span className="text-[10px] text-muted-foreground font-mono shrink-0">
[{node.callsign}]
</span> </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> </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> </div>
); );
} }