Skip to content

Commit ceba66d

Browse files
committed
fix(calm-hub-ui): improve sidebar positioning, style, and deeplink support
1 parent 133ccec commit ceba66d

File tree

10 files changed

+148
-144
lines changed

10 files changed

+148
-144
lines changed

calm-hub-ui/src/hub/Hub.tsx

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,43 @@
1-
import { useMemo, useState } from 'react';
1+
import { useCallback, useMemo, useState } from 'react';
22
import { IoChevronForwardOutline } from 'react-icons/io5';
33
import { TreeNavigation } from './components/tree-navigation/TreeNavigation.js';
44
import { Data, Adr } from '../model/calm.js';
55
import { Navbar } from '../components/navbar/Navbar.js';
66
import { AdrRenderer } from './components/adr-renderer/AdrRenderer.js';
77
import { DocumentDetailSection } from './components/document-detail-section/DocumentDetailSection.js';
88
import { DiagramSection } from './components/diagram-section/DiagramSection.js';
9+
import { Sidebar } from '../visualizer/components/sidebar/Sidebar.js';
10+
import type { SelectedItem } from '../visualizer/contracts/contracts.js';
911
import './Hub.css';
1012

1113
export default function Hub() {
1214
const [data, setData] = useState<Data | undefined>();
1315
const [adrData, setAdrData] = useState<Adr | undefined>();
1416
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
17+
const [selectedItem, setSelectedItem] = useState<SelectedItem>(null);
1518

1619
function handleDataLoad(data: Data) {
1720
setData(data);
1821
setAdrData(undefined);
22+
setSelectedItem(null);
1923
}
2024

2125
function handleAdrLoad(adr: Adr) {
2226
setAdrData(adr);
2327
setData(undefined);
28+
setSelectedItem(null);
2429
}
2530

31+
const handleItemSelect = useCallback((item: SelectedItem) => {
32+
setSelectedItem(item);
33+
}, []);
34+
35+
const closeSidebar = useCallback(() => {
36+
setSelectedItem(null);
37+
}, []);
38+
39+
const isDiagramView = data?.calmType === 'Architectures' || data?.calmType === 'Patterns';
40+
2641
const memoizedDataLoad = useMemo(() => handleDataLoad, []);
2742
const memoizedAdrLoad = useMemo(() => handleAdrLoad, []);
2843

@@ -31,7 +46,7 @@ export default function Hub() {
3146
<Navbar />
3247
<div className="flex flex-row flex-1 overflow-hidden bg-base-300">
3348
<div className={`${isSidebarOpen ? 'w-1/4' : 'w-12'} p-4 pr-2 transition-all duration-300`}>
34-
<div className="h-full bg-base-100 rounded-2xl overflow-hidden shadow-xl flex flex-col">
49+
<div className="h-full bg-base-100 rounded-box overflow-hidden shadow-xl flex flex-col">
3550
{isSidebarOpen ? (
3651
<div className="flex-1 min-h-0 overflow-hidden">
3752
<TreeNavigation onDataLoad={memoizedDataLoad} onAdrLoad={memoizedAdrLoad} onCollapse={() => setIsSidebarOpen(false)} />
@@ -49,15 +64,18 @@ export default function Hub() {
4964
)}
5065
</div>
5166
</div>
52-
<div className="flex-1 overflow-auto">
67+
<div className="flex-1 overflow-auto min-w-0">
5368
{adrData ? (
5469
<AdrRenderer adrDetails={adrData} />
55-
) : (data?.calmType === 'Architectures' || data?.calmType === 'Patterns') ? (
56-
<DiagramSection data={data} />
70+
) : isDiagramView ? (
71+
<DiagramSection data={data} onItemSelect={handleItemSelect} hasDetailsPanel={!!selectedItem} />
5772
) : (
5873
<DocumentDetailSection data={data} />
5974
)}
6075
</div>
76+
{selectedItem && isDiagramView && (
77+
<Sidebar selectedData={selectedItem.data} closeSidebar={closeSidebar} />
78+
)}
6179
</div>
6280
</div>
6381
);

calm-hub-ui/src/hub/components/diagram-section/DiagramSection.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,20 @@ import { Data } from '../../../model/calm.js';
44
import { JsonRenderer } from '../json-renderer/JsonRenderer.js';
55
import { Drawer } from '../../../visualizer/components/drawer/Drawer.js';
66
import { SectionHeader } from '../section-header/SectionHeader.js';
7+
import type { SelectedItem } from '../../../visualizer/contracts/contracts.js';
78

89
interface DiagramSectionProps {
910
data: Data & { calmType: 'Architectures' | 'Patterns' };
11+
onItemSelect?: (item: SelectedItem) => void;
12+
hasDetailsPanel?: boolean;
1013
}
1114

1215
const iconMap = {
1316
Architectures: IoConstructOutline,
1417
Patterns: IoGridOutline,
1518
} as const;
1619

17-
export function DiagramSection({ data }: DiagramSectionProps) {
20+
export function DiagramSection({ data, onItemSelect, hasDetailsPanel }: DiagramSectionProps) {
1821
const [activeTab, setActiveTab] = useState<'diagram' | 'json'>('diagram');
1922

2023
const Icon = iconMap[data.calmType];
@@ -41,8 +44,8 @@ export function DiagramSection({ data }: DiagramSectionProps) {
4144
);
4245

4346
return (
44-
<div className="w-full h-full py-4 pl-2 pr-4">
45-
<div className="h-full bg-base-100 rounded-2xl overflow-hidden flex flex-col shadow-xl">
47+
<div className={`w-full h-full py-4 pl-2 ${hasDetailsPanel ? 'pr-2' : 'pr-4'}`}>
48+
<div className="h-full bg-base-100 rounded-box overflow-hidden flex flex-col shadow-xl">
4649
<SectionHeader
4750
icon={<Icon className="text-accent" />}
4851
namespace={data.name}
@@ -54,7 +57,7 @@ export function DiagramSection({ data }: DiagramSectionProps) {
5457
<div className="flex-1 min-h-0 overflow-hidden">
5558
{activeTab === 'diagram' ? (
5659
<div className="w-full h-full">
57-
<Drawer data={data} />
60+
<Drawer data={data} onItemSelect={onItemSelect} />
5861
</div>
5962
) : (
6063
<div className="h-full bg-base-200 overflow-auto">

calm-hub-ui/src/hub/components/document-detail-section/DocumentDetailSection.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export function DocumentDetailSection({ data }: DocumentDetailSectionProps) {
2323

2424
return (
2525
<div className="w-full h-full py-4 pl-2 pr-4">
26-
<div className="h-full bg-base-100 rounded-2xl overflow-hidden flex flex-col shadow-xl">
26+
<div className="h-full bg-base-100 rounded-box overflow-hidden flex flex-col shadow-xl">
2727
<SectionHeader
2828
icon={getIcon()}
2929
namespace={data.name}

calm-hub-ui/src/hub/components/tree-navigation/TreeNavigation.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,7 @@ function loadResource({
343343
export function TreeNavigation({ onDataLoad, onAdrLoad, onCollapse }: TreeNavigationProps) {
344344
const navigate = useNavigate();
345345
const params = useParams<HubParams>();
346-
346+
347347
const [namespaces, setNamespaces] = useState<string[]>([]);
348348
const [selectedNamespace, setSelectedNamespace] = useState<string>(EMPTY_STR_VALUE);
349349
const [selectedType, setSelectedType] = useState<string>(EMPTY_STR_VALUE);

calm-hub-ui/src/index.css

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@
1010

1111
#root {
1212
height: 100vh;
13+
14+
--radius-box: 0.5rem; /* or whatever value you prefer */
1315
}
1416

1517
body {
1618
margin: 0;
1719
font-family:
18-
-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell',
19-
'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
20+
-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans',
21+
'Helvetica Neue', sans-serif;
2022
-webkit-font-smoothing: antialiased;
2123
-moz-osx-font-smoothing: grayscale;
2224
}

calm-hub-ui/src/visualizer/components/drawer/Drawer.test.tsx

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,11 @@
11
import { describe, it, expect, vi, beforeEach } from 'vitest';
2-
import { render, screen, fireEvent, act } from '@testing-library/react';
2+
import { render, screen } from '@testing-library/react';
33
import { Drawer } from './Drawer.js';
44
import { Data } from '../../../model/calm.js';
5-
import type { SidebarProps, ReactFlowVisualizerProps } from '../../contracts/contracts.js';
5+
import type { ReactFlowVisualizerProps } from '../../contracts/contracts.js';
66
import { DropzoneOptions } from 'react-dropzone';
77

88
// Mock dependencies
9-
vi.mock('../sidebar/Sidebar.js', () => ({
10-
Sidebar: ({ selectedData, closeSidebar }: SidebarProps) => (
11-
<div data-testid="sidebar">
12-
<button onClick={closeSidebar}>Close</button>
13-
<div>{String(selectedData?.name ?? '')}</div>
14-
</div>
15-
),
16-
}));
179
vi.mock('../reactflow/ReactFlowVisualizer.js', () => ({
1810
ReactFlowVisualizer: ({ calmData }: ReactFlowVisualizerProps) => (
1911
<div data-testid="reactflow-visualizer">
@@ -98,12 +90,8 @@ describe('Drawer', () => {
9890
expect(screen.getByTestId('relationship-count')).toHaveTextContent('2');
9991
});
10092

101-
it('shows sidebar when selectedNode is set', () => {
93+
it('does not show sidebar initially', () => {
10294
render(<Drawer data={calmData as unknown as Data} />);
103-
const checkbox = screen.getByRole('checkbox', { name: /drawer-toggle/i });
104-
act(() => {
105-
fireEvent.click(checkbox);
106-
});
107-
expect(checkbox).toBeInTheDocument();
95+
expect(screen.queryByLabelText('close-sidebar')).not.toBeInTheDocument();
10896
});
10997
});

calm-hub-ui/src/visualizer/components/drawer/Drawer.tsx

Lines changed: 67 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@ import { CalmArchitectureSchema, CalmNodeSchema, CalmRelationshipSchema } from '
33
import { useDropzone } from 'react-dropzone';
44
import { ReactFlowVisualizer } from '../reactflow/ReactFlowVisualizer.js';
55
import { PatternVisualizer } from '../reactflow/PatternVisualizer.js';
6-
import { Sidebar } from '../sidebar/Sidebar.js';
76
import { MetadataPanel } from '../reactflow/MetadataPanel.js';
87
import { toSidebarNodeData, toSidebarEdgeData } from '../reactflow/utils/patternClickHandlers.js';
9-
import type { DrawerProps, SelectedItem, Flow, Control } from '../../contracts/contracts.js';
8+
import type { DrawerProps, Flow, Control } from '../../contracts/contracts.js';
109

1110
/**
1211
* Detect whether JSON data is a CALM pattern (JSON Schema) or an architecture instance.
@@ -26,11 +25,10 @@ function extractId(item: CalmNodeSchema | CalmRelationshipSchema): string {
2625
return item?.['unique-id'] || '';
2726
}
2827

29-
export function Drawer({ data }: DrawerProps) {
28+
export function Drawer({ data, onItemSelect }: DrawerProps) {
3029
const [calmInstance, setCALMInstance] = useState<CalmArchitectureSchema | undefined>(undefined);
3130
const [patternInstance, setPatternInstance] = useState<Record<string, unknown> | undefined>(undefined);
3231
const [fileInstance, setFileInstance] = useState<Record<string, unknown> | undefined>(undefined);
33-
const [selectedItem, setSelectedItem] = useState<SelectedItem>(null);
3432
// Default to collapsed as per user request
3533
const [isMetadataCollapsed, setIsMetadataCollapsed] = useState(true);
3634
// Height of the metadata panel when expanded (in pixels)
@@ -114,25 +112,25 @@ export function Drawer({ data }: DrawerProps) {
114112
const hasContent = !!(calmInstance || patternInstance);
115113

116114
const closeSidebar = useCallback(() => {
117-
setSelectedItem(null);
118-
}, []);
115+
onItemSelect?.(null);
116+
}, [onItemSelect]);
119117

120118
// Pattern-specific click handlers
121119
const handlePatternNodeClick = useCallback((nodeData: Record<string, unknown>) => {
122-
setSelectedItem({ data: toSidebarNodeData(nodeData) });
123-
}, []);
120+
onItemSelect?.({ data: toSidebarNodeData(nodeData) });
121+
}, [onItemSelect]);
124122

125123
const handlePatternEdgeClick = useCallback((edgeData: Record<string, unknown>) => {
126-
setSelectedItem({ data: toSidebarEdgeData(edgeData) });
127-
}, []);
124+
onItemSelect?.({ data: toSidebarEdgeData(edgeData) });
125+
}, [onItemSelect]);
128126

129127
const handleNodeClick = useCallback((nodeData: CalmNodeSchema) => {
130-
setSelectedItem({ data: toSidebarNodeData(nodeData as Record<string, unknown>) });
131-
}, []);
128+
onItemSelect?.({ data: toSidebarNodeData(nodeData as Record<string, unknown>) });
129+
}, [onItemSelect]);
132130

133131
const handleEdgeClick = useCallback((edgeData: CalmRelationshipSchema) => {
134-
setSelectedItem({ data: toSidebarEdgeData(edgeData as Record<string, unknown>) });
135-
}, []);
132+
onItemSelect?.({ data: toSidebarEdgeData(edgeData as Record<string, unknown>) });
133+
}, [onItemSelect]);
136134

137135
// Handle transition click from flows panel - highlight the relationship
138136
const handleTransitionClick = useCallback(
@@ -157,77 +155,65 @@ export function Drawer({ data }: DrawerProps) {
157155
);
158156

159157
return (
160-
<div {...getRootProps()} className="flex-1 flex overflow-hidden h-full">
158+
<div {...getRootProps()} className="flex-1 flex flex-col overflow-hidden h-full">
161159
{!hasContent && <input {...getInputProps()} />}
162-
<div className={`drawer drawer-end ${selectedItem ? 'drawer-open' : ''} w-full h-full`}>
163-
<input
164-
type="checkbox"
165-
aria-label="drawer-toggle"
166-
className="drawer-toggle"
167-
checked={!!selectedItem}
168-
onChange={closeSidebar}
169-
/>
170-
<div className="drawer-content h-full flex flex-col">
171-
{hasContent ? (
172-
<>
173-
<div
174-
style={{
175-
flex: 1,
176-
minHeight: 0,
177-
...(hasMetadata && !isMetadataCollapsed ? { height: `calc(100% - ${metadataPanelHeight}px)` } : {}),
178-
}}
179-
>
180-
{patternInstance ? (
181-
<PatternVisualizer
182-
patternData={patternInstance}
183-
onNodeClick={handlePatternNodeClick}
184-
onEdgeClick={handlePatternEdgeClick}
185-
onBackgroundClick={closeSidebar}
186-
/>
187-
) : calmInstance ? (
188-
<ReactFlowVisualizer
189-
calmData={calmInstance}
190-
onNodeClick={handleNodeClick}
191-
onEdgeClick={handleEdgeClick}
192-
onBackgroundClick={closeSidebar}
193-
/>
194-
) : null}
195-
</div>
196-
{hasMetadata && !patternInstance && (
197-
<div
198-
style={{
199-
height: isMetadataCollapsed ? '48px' : `${metadataPanelHeight}px`,
200-
flexShrink: 0,
201-
}}
202-
>
203-
<MetadataPanel
204-
flows={flows}
205-
controls={controls}
206-
onTransitionClick={handleTransitionClick}
207-
onNodeClick={handleControlNodeClick}
208-
isCollapsed={isMetadataCollapsed}
209-
onToggleCollapse={() => setIsMetadataCollapsed(!isMetadataCollapsed)}
210-
height={metadataPanelHeight}
211-
onHeightChange={setMetadataPanelHeight}
212-
/>
213-
</div>
214-
)}
215-
</>
216-
) : (
217-
<div className="flex justify-center items-center h-full w-full">
218-
{isDragActive ? (
219-
<p>Drop your file here ...</p>
220-
) : (
221-
<p>
222-
{'Drag and drop your file here or '}
223-
<span className="border-b border-dotted border-black pb-1">Browse</span>
224-
</p>
225-
)}
160+
{hasContent ? (
161+
<>
162+
<div
163+
style={{
164+
flex: 1,
165+
minHeight: 0,
166+
...(hasMetadata && !isMetadataCollapsed ? { height: `calc(100% - ${metadataPanelHeight}px)` } : {}),
167+
}}
168+
>
169+
{patternInstance ? (
170+
<PatternVisualizer
171+
patternData={patternInstance}
172+
onNodeClick={handlePatternNodeClick}
173+
onEdgeClick={handlePatternEdgeClick}
174+
onBackgroundClick={closeSidebar}
175+
/>
176+
) : calmInstance ? (
177+
<ReactFlowVisualizer
178+
calmData={calmInstance}
179+
onNodeClick={handleNodeClick}
180+
onEdgeClick={handleEdgeClick}
181+
onBackgroundClick={closeSidebar}
182+
/>
183+
) : null}
184+
</div>
185+
{hasMetadata && !patternInstance && (
186+
<div
187+
style={{
188+
height: isMetadataCollapsed ? '48px' : `${metadataPanelHeight}px`,
189+
flexShrink: 0,
190+
}}
191+
>
192+
<MetadataPanel
193+
flows={flows}
194+
controls={controls}
195+
onTransitionClick={handleTransitionClick}
196+
onNodeClick={handleControlNodeClick}
197+
isCollapsed={isMetadataCollapsed}
198+
onToggleCollapse={() => setIsMetadataCollapsed(!isMetadataCollapsed)}
199+
height={metadataPanelHeight}
200+
onHeightChange={setMetadataPanelHeight}
201+
/>
226202
</div>
227203
)}
204+
</>
205+
) : (
206+
<div className="flex justify-center items-center h-full w-full">
207+
{isDragActive ? (
208+
<p>Drop your file here ...</p>
209+
) : (
210+
<p>
211+
{'Drag and drop your file here or '}
212+
<span className="border-b border-dotted border-black pb-1">Browse</span>
213+
</p>
214+
)}
228215
</div>
229-
{selectedItem && <Sidebar selectedData={selectedItem.data} closeSidebar={closeSidebar} />}
230-
</div>
216+
)}
231217
</div>
232218
);
233219
}

0 commit comments

Comments
 (0)