refactor(sidebar): collapse rail/sections to single-IA, log docs
- Sidebar: kill the drifting railGroups + sections dual definition. Single source of truth (workItems / libraryItems / footerItems) rendered in both pinned and rail modes; pin/unpin is a width and label affordance, not an IA switch. Hairline divider replaces section labels. Guides moves to the footer alongside Account. Renames: Home -> Dashboard, History -> Sessions, Insights -> Analytics. - CURRENT-STATE.md: log PR #158 (session impeccable pass + tasklane keyboard flow) under "Recently shipped". - PRODUCT.md: design-context source of truth (users, brand, aesthetic); sibling to DESIGN-SYSTEM.md. - skills-lock.json: lock /impeccable + /documentation-writer skill versions so other sessions reproduce the same tooling state. - Drop stale .impeccable.md. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -2,10 +2,10 @@ import { useCallback, useEffect, useRef, useState, type PointerEvent as ReactPoi
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
import type { LucideIcon } from 'lucide-react'
|
||||
import {
|
||||
LayoutGrid, Clock, AlertTriangle, GitBranch, Code2, Wand2,
|
||||
ListChecks, Download, BarChart3,
|
||||
LayoutGrid, Clock, AlertTriangle, GitBranch,
|
||||
ListChecks, BarChart3,
|
||||
Settings, Pin, PinOff,
|
||||
History, FileText, Network, Ticket,
|
||||
FileText, Ticket, BookOpen,
|
||||
} from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useUserPreferencesStore } from '@/store/userPreferencesStore'
|
||||
@@ -31,11 +31,6 @@ interface NavEntry {
|
||||
children?: NavSubItem[]
|
||||
}
|
||||
|
||||
interface NavSection {
|
||||
title: string
|
||||
items: NavEntry[]
|
||||
}
|
||||
|
||||
/* ── Sidebar component ──────────────────────────────── */
|
||||
|
||||
export function Sidebar() {
|
||||
@@ -78,36 +73,40 @@ export function Sidebar() {
|
||||
|
||||
/* ── Navigation data ──────────────────────────────── */
|
||||
|
||||
/* ── Grouped nav: 5 top-level icons (Sentry-style) ── */
|
||||
/* Single source-of-truth IA. Same items, same order, in both rail
|
||||
* and pinned modes. Pin/unpin is a width/label affordance, not an
|
||||
* IA switch. A hairline divider separates the two groups; no labels. */
|
||||
|
||||
const railGroups: NavEntry[] = [
|
||||
const workItems: NavEntry[] = [
|
||||
{
|
||||
href: '/', icon: LayoutGrid, label: 'Home', shortLabel: 'Home',
|
||||
href: '/', icon: LayoutGrid, label: 'Dashboard', shortLabel: 'Dash',
|
||||
matchPaths: ['/'],
|
||||
},
|
||||
{
|
||||
href: '/sessions', icon: History, label: 'History', shortLabel: 'History',
|
||||
badge: stats?.active_count || undefined,
|
||||
matchPaths: ['/sessions', '/escalations', '/pilot'],
|
||||
children: [
|
||||
{ href: '/sessions', label: 'Session History', count: stats?.active_count || undefined },
|
||||
{ href: '/escalations', label: 'Escalations', count: stats?.escalation_count || undefined },
|
||||
],
|
||||
},
|
||||
{
|
||||
href: '/tickets', icon: Ticket, label: 'Tickets', shortLabel: 'Tickets',
|
||||
matchPaths: ['/tickets'],
|
||||
},
|
||||
{
|
||||
href: '/sessions', icon: Clock, label: 'Sessions', shortLabel: 'Sessions',
|
||||
badge: stats?.active_count || undefined,
|
||||
matchPaths: ['/sessions'],
|
||||
},
|
||||
{
|
||||
href: '/escalations', icon: AlertTriangle, label: 'Escalations', shortLabel: 'Escal',
|
||||
badge: stats?.escalation_count || undefined,
|
||||
matchPaths: ['/escalations'],
|
||||
},
|
||||
]
|
||||
|
||||
const libraryItems: NavEntry[] = [
|
||||
{
|
||||
href: '/trees', icon: GitBranch, label: 'Flows', shortLabel: 'Flows',
|
||||
badge: stats?.tree_counts.total || undefined,
|
||||
matchPaths: ['/trees', '/flows', '/my-trees', '/step-library', '/review-queue', '/network-diagrams'],
|
||||
matchPaths: ['/trees', '/flows', '/my-trees', '/step-library', '/network-diagrams'],
|
||||
children: [
|
||||
{ href: '/trees', label: 'Flow Library', count: stats?.tree_counts.total || undefined },
|
||||
{ href: '/trees?type=procedural', label: 'Projects', count: stats?.tree_counts.procedural || undefined },
|
||||
{ href: '/network-diagrams', label: 'Network Maps' },
|
||||
{ href: '/step-library', label: 'Solutions Library' },
|
||||
{ href: '/review-queue', label: 'Review Queue' },
|
||||
{ href: '/network-diagrams', label: 'Network Maps' },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -115,60 +114,25 @@ export function Sidebar() {
|
||||
badge: pendingDraftCount || undefined,
|
||||
matchPaths: ['/scripts', '/script-builder'],
|
||||
children: [
|
||||
{ href: '/scripts', label: 'Script Library', count: pendingDraftCount || undefined },
|
||||
{ href: '/script-builder', label: 'Script Builder' },
|
||||
],
|
||||
},
|
||||
{
|
||||
href: '/analytics', icon: BarChart3, label: 'Insights', shortLabel: 'Data',
|
||||
href: '/review-queue', icon: ListChecks, label: 'Review Queue', shortLabel: 'Review',
|
||||
matchPaths: ['/review-queue'],
|
||||
},
|
||||
{
|
||||
href: '/analytics', icon: BarChart3, label: 'Analytics', shortLabel: 'Stats',
|
||||
matchPaths: ['/analytics', '/shares'],
|
||||
children: [
|
||||
{ href: '/analytics', label: 'Analytics' },
|
||||
{ href: '/shares', label: 'Exports' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
/* Pinned mode still uses the detailed section layout */
|
||||
const sections: NavSection[] = [
|
||||
{
|
||||
title: 'RESOLVE',
|
||||
items: [
|
||||
{ href: '/', icon: LayoutGrid, label: 'Dashboard', shortLabel: 'Dash' },
|
||||
{ href: '/sessions', icon: Clock, label: 'Session History', shortLabel: 'History', badge: stats?.active_count || undefined, matchPaths: ['/sessions'] },
|
||||
{ href: '/tickets', icon: Ticket, label: 'Tickets', shortLabel: 'Tickets', matchPaths: ['/tickets'] },
|
||||
{ href: '/escalations', icon: AlertTriangle, label: 'Escalations', shortLabel: 'Escal', badge: stats?.escalation_count || undefined },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'KNOWLEDGE',
|
||||
items: [
|
||||
{
|
||||
href: '/trees', icon: GitBranch, label: 'Flow Library', shortLabel: 'Flows',
|
||||
badge: stats?.tree_counts.total || undefined,
|
||||
matchPaths: ['/trees', '/flows', '/my-trees'],
|
||||
children: [
|
||||
{ href: '/trees', label: 'Flow Library' },
|
||||
{ href: '/trees?type=procedural', label: 'Projects', count: stats?.tree_counts.procedural || undefined },
|
||||
],
|
||||
},
|
||||
{ href: '/network-diagrams', icon: Network, label: 'Network Maps', shortLabel: 'NetMap', matchPaths: ['/network-diagrams'] },
|
||||
{ href: '/scripts', icon: Code2, label: 'Scripts', shortLabel: 'Scripts' },
|
||||
{ href: '/script-builder', icon: Wand2, label: 'Script Builder', shortLabel: 'Builder' },
|
||||
{ href: '/review-queue', icon: ListChecks, label: 'Review Queue', shortLabel: 'Review' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'INSIGHTS',
|
||||
items: [
|
||||
{ href: '/analytics', icon: BarChart3, label: 'Analytics', shortLabel: 'Stats' },
|
||||
{ href: '/shares', icon: Download, label: 'Exports', shortLabel: 'Export' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const footerItems: NavEntry[] = [
|
||||
{ href: '/account', icon: Settings, label: 'Account', shortLabel: 'Acct' },
|
||||
{ href: '/guides', icon: BookOpen, label: 'Guides', shortLabel: 'Guides', matchPaths: ['/guides'] },
|
||||
{ href: '/account', icon: Settings, label: 'Account', shortLabel: 'Acct', matchPaths: ['/account'] },
|
||||
]
|
||||
|
||||
/* ── Active detection ─────────────────────────────── */
|
||||
@@ -369,9 +333,9 @@ export function Sidebar() {
|
||||
|
||||
/* ── Find active flyout group for drawer ── */
|
||||
|
||||
const allRailItems = [...workItems, ...libraryItems, ...footerItems]
|
||||
const activeFlyoutGroup = flyoutIndex && !sidebarPinned
|
||||
? railGroups.find((_, i) => `rail-${i}` === flyoutIndex) ||
|
||||
footerItems.find((_, i) => `footer-${i}` === flyoutIndex)
|
||||
? allRailItems.find(item => item.href === flyoutIndex) || null
|
||||
: null
|
||||
|
||||
/* ── Main render ──────────────────────────────────── */
|
||||
@@ -386,23 +350,20 @@ export function Sidebar() {
|
||||
>
|
||||
{/* Pinned sidebar content */}
|
||||
<div className="px-3 py-2 space-y-0.5">
|
||||
{sections.map((section, si) => (
|
||||
<div key={section.title}>
|
||||
{si > 0 && (
|
||||
<div className="font-mono text-[0.5625rem] uppercase tracking-[0.12em] text-text-muted px-3 pt-3 pb-1">
|
||||
{section.title}
|
||||
</div>
|
||||
)}
|
||||
{section.items.map((item, ii) => renderPinnedItem(item, `${si}-${ii}`))}
|
||||
</div>
|
||||
))}
|
||||
{workItems.map(item => renderPinnedItem(item, item.href))}
|
||||
<div
|
||||
className="my-3 border-t"
|
||||
style={{ borderColor: 'var(--color-border-default)' }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{libraryItems.map(item => renderPinnedItem(item, item.href))}
|
||||
</div>
|
||||
|
||||
<div className="flex-1" />
|
||||
|
||||
{/* Footer */}
|
||||
<div className="px-3 pt-2 pb-4 space-y-0.5" style={{ borderTop: '1px solid var(--color-border-default)' }}>
|
||||
{footerItems.map((item, i) => renderPinnedItem(item, `footer-${i}`))}
|
||||
{footerItems.map(item => renderPinnedItem(item, item.href))}
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleSidebarPinned}
|
||||
@@ -417,7 +378,7 @@ export function Sidebar() {
|
||||
)
|
||||
}
|
||||
|
||||
/* Icon Rail (default) — 5 grouped icons, Sentry-style */
|
||||
/* Icon rail (default, unpinned) — same items as pinned mode, narrower. */
|
||||
return (
|
||||
<div
|
||||
className="flex h-full"
|
||||
@@ -432,14 +393,20 @@ export function Sidebar() {
|
||||
>
|
||||
{/* Nav items */}
|
||||
<div className="flex flex-col items-center w-full px-1 space-y-1.5">
|
||||
{railGroups.map((item, i) => renderRailItem(item, `rail-${i}`))}
|
||||
{workItems.map(item => renderRailItem(item, item.href))}
|
||||
<div
|
||||
className="w-8 my-1 border-t self-center"
|
||||
style={{ borderColor: 'var(--color-border-default)' }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{libraryItems.map(item => renderRailItem(item, item.href))}
|
||||
</div>
|
||||
|
||||
<div className="flex-1" />
|
||||
|
||||
{/* Footer: Account + Pin */}
|
||||
{/* Footer: Guides, Account + Pin */}
|
||||
<div className="flex flex-col items-center w-full px-1 pb-5 pt-3 space-y-1.5" style={{ borderTop: '1px solid var(--color-border-default)' }}>
|
||||
{footerItems.map((item, i) => renderRailItem(item, `footer-${i}`))}
|
||||
{footerItems.map(item => renderRailItem(item, item.href))}
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleSidebarPinned}
|
||||
@@ -471,7 +438,7 @@ export function Sidebar() {
|
||||
>
|
||||
{/* Drawer header */}
|
||||
<div className="px-3 mb-3">
|
||||
<h3 className="text-[0.6875rem] font-mono uppercase tracking-[0.12em] text-[#fbbf24]">
|
||||
<h3 className="text-[0.6875rem] font-mono uppercase tracking-[0.12em] text-text-muted">
|
||||
{activeFlyoutGroup.label}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user