Split 4 oversized component files (all >500 LOC) into sibling modules: - SDKPipelineSidebar → Icons + Parts siblings (193/264/35 LOC) - SourcesTab → SourceModals sibling (311/243 LOC) - ScopeDecisionTab → ScopeDecisionSections sibling (127/444 LOC) - ComplianceAdvisorWidget → ComplianceAdvisorParts sibling (265/131 LOC) Zero behavior changes; all logic relocated verbatim. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
194 lines
7.5 KiB
TypeScript
194 lines
7.5 KiB
TypeScript
'use client'
|
|
|
|
/**
|
|
* SDK Pipeline Sidebar
|
|
*
|
|
* Floating Action Button mit Drawer zur Visualisierung der SDK-Pipeline.
|
|
* Zeigt die 5 Pakete mit Fortschritt und ermoeglicht schnelle Navigation.
|
|
*
|
|
* Features:
|
|
* - Desktop (xl+): Fixierte Sidebar rechts
|
|
* - Mobile/Tablet: Floating Action Button mit Slide-In Drawer
|
|
*/
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import { useSDK } from '@/lib/sdk'
|
|
import { CloseIcon, PipelineIcon } from './SDKPipelineSidebarIcons'
|
|
import { SidebarContent } from './SDKPipelineSidebarParts'
|
|
|
|
// =============================================================================
|
|
// MAIN COMPONENT - RESPONSIVE
|
|
// =============================================================================
|
|
|
|
export interface SDKPipelineSidebarProps {
|
|
/** Position des FAB auf Mobile */
|
|
fabPosition?: 'bottom-right' | 'bottom-left'
|
|
}
|
|
|
|
export function SDKPipelineSidebar({ fabPosition = 'bottom-right' }: SDKPipelineSidebarProps) {
|
|
const [isMobileOpen, setIsMobileOpen] = useState(false)
|
|
const [isDesktopCollapsed, setIsDesktopCollapsed] = useState(true) // Start collapsed
|
|
const { completionPercentage } = useSDK()
|
|
|
|
// Load collapsed state from localStorage
|
|
useEffect(() => {
|
|
const stored = localStorage.getItem('sdk-pipeline-sidebar-collapsed')
|
|
if (stored !== null) {
|
|
setIsDesktopCollapsed(stored === 'true')
|
|
}
|
|
}, [])
|
|
|
|
// Save collapsed state to localStorage
|
|
const toggleDesktopSidebar = () => {
|
|
const newState = !isDesktopCollapsed
|
|
setIsDesktopCollapsed(newState)
|
|
localStorage.setItem('sdk-pipeline-sidebar-collapsed', String(newState))
|
|
}
|
|
|
|
// Close drawer on escape key
|
|
useEffect(() => {
|
|
const handleEscape = (e: KeyboardEvent) => {
|
|
if (e.key === 'Escape') {
|
|
setIsMobileOpen(false)
|
|
setIsDesktopCollapsed(true)
|
|
}
|
|
}
|
|
window.addEventListener('keydown', handleEscape)
|
|
return () => window.removeEventListener('keydown', handleEscape)
|
|
}, [])
|
|
|
|
// Prevent body scroll when drawer is open
|
|
useEffect(() => {
|
|
if (isMobileOpen) {
|
|
document.body.style.overflow = 'hidden'
|
|
} else {
|
|
document.body.style.overflow = ''
|
|
}
|
|
return () => {
|
|
document.body.style.overflow = ''
|
|
}
|
|
}, [isMobileOpen])
|
|
|
|
const fabPositionClasses = fabPosition === 'bottom-right'
|
|
? 'right-4 bottom-6'
|
|
: 'left-4 bottom-6'
|
|
|
|
const progressCircle = (
|
|
<svg className="absolute inset-0 w-full h-full -rotate-90" viewBox="0 0 56 56">
|
|
<circle cx="28" cy="28" r="26" fill="none" stroke="rgba(255,255,255,0.3)" strokeWidth="2" />
|
|
<circle
|
|
cx="28" cy="28" r="26" fill="none" stroke="white" strokeWidth="2"
|
|
strokeDasharray={`${(completionPercentage / 100) * 163.36} 163.36`}
|
|
strokeLinecap="round"
|
|
/>
|
|
</svg>
|
|
)
|
|
|
|
return (
|
|
<>
|
|
{/* Desktop: Fixed Sidebar (when expanded) */}
|
|
{!isDesktopCollapsed && (
|
|
<div className="hidden xl:block fixed right-6 top-24 w-72 z-10">
|
|
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-lg border border-slate-200 dark:border-gray-700 overflow-hidden">
|
|
{/* Header with close button */}
|
|
<div className="px-4 py-3 bg-gradient-to-r from-purple-50 to-indigo-50 dark:from-purple-900/20 dark:to-indigo-900/20 border-b border-slate-200 dark:border-gray-700">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-purple-600 dark:text-purple-400"><PipelineIcon /></span>
|
|
<div>
|
|
<span className="font-semibold text-slate-700 dark:text-slate-200 text-sm">SDK Pipeline</span>
|
|
<span className="ml-2 text-xs text-purple-600 dark:text-purple-400">{completionPercentage}%</span>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={toggleDesktopSidebar}
|
|
className="p-1.5 text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 rounded-lg hover:bg-slate-100 dark:hover:bg-gray-700 transition-colors"
|
|
aria-label="Sidebar einklappen"
|
|
title="Einklappen"
|
|
>
|
|
<CloseIcon />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{/* Content */}
|
|
<div className="p-3 max-h-[calc(100vh-200px)] overflow-y-auto">
|
|
<SidebarContent onNavigate={() => {}} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Desktop: FAB (when collapsed) */}
|
|
{isDesktopCollapsed && (
|
|
<button
|
|
onClick={toggleDesktopSidebar}
|
|
className="hidden xl:flex fixed right-6 bottom-6 z-40 w-14 h-14 bg-gradient-to-r from-purple-500 to-indigo-500 text-white rounded-full shadow-lg hover:shadow-xl transition-all items-center justify-center group"
|
|
aria-label="SDK Pipeline Navigation oeffnen"
|
|
title="Pipeline anzeigen"
|
|
>
|
|
<PipelineIcon />
|
|
{progressCircle}
|
|
</button>
|
|
)}
|
|
|
|
{/* Mobile/Tablet: FAB */}
|
|
<button
|
|
onClick={() => setIsMobileOpen(true)}
|
|
className={`xl:hidden fixed ${fabPositionClasses} z-40 w-14 h-14 bg-gradient-to-r from-purple-500 to-indigo-500 text-white rounded-full shadow-lg hover:shadow-xl transition-all flex items-center justify-center group`}
|
|
aria-label="SDK Pipeline Navigation oeffnen"
|
|
>
|
|
<PipelineIcon />
|
|
{progressCircle}
|
|
</button>
|
|
|
|
{/* Mobile/Tablet: Drawer Overlay */}
|
|
{isMobileOpen && (
|
|
<div className="xl:hidden fixed inset-0 z-50">
|
|
{/* Backdrop */}
|
|
<div
|
|
className="absolute inset-0 bg-black/50 backdrop-blur-sm transition-opacity"
|
|
onClick={() => setIsMobileOpen(false)}
|
|
/>
|
|
{/* Drawer */}
|
|
<div className="absolute right-0 top-0 bottom-0 w-80 max-w-[85vw] bg-white dark:bg-gray-900 shadow-2xl transform transition-transform animate-slide-in-right">
|
|
{/* Drawer Header */}
|
|
<div className="flex items-center justify-between px-4 py-4 border-b border-slate-200 dark:border-gray-700 bg-gradient-to-r from-purple-50 to-indigo-50 dark:from-purple-900/20 dark:to-indigo-900/20">
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-purple-600 dark:text-purple-400"><PipelineIcon /></span>
|
|
<div>
|
|
<span className="font-semibold text-slate-700 dark:text-slate-200">SDK Pipeline</span>
|
|
<span className="ml-2 text-sm text-purple-600 dark:text-purple-400">{completionPercentage}%</span>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={() => setIsMobileOpen(false)}
|
|
className="p-2 text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 rounded-lg hover:bg-slate-100 dark:hover:bg-gray-800 transition-colors"
|
|
aria-label="Schliessen"
|
|
>
|
|
<CloseIcon />
|
|
</button>
|
|
</div>
|
|
{/* Drawer Content */}
|
|
<div className="p-4 overflow-y-auto max-h-[calc(100vh-80px)]">
|
|
<SidebarContent onNavigate={() => setIsMobileOpen(false)} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* CSS for slide-in animation */}
|
|
<style jsx>{`
|
|
@keyframes slide-in-right {
|
|
from { transform: translateX(100%); }
|
|
to { transform: translateX(0); }
|
|
}
|
|
.animate-slide-in-right {
|
|
animation: slide-in-right 0.2s ease-out;
|
|
}
|
|
`}</style>
|
|
</>
|
|
)
|
|
}
|
|
|
|
export default SDKPipelineSidebar
|