@principal-ai/subsystems-react 0.56.2 → 0.56.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/index.d.ts +6 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +3 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/subsystem/DecorationPanel.d.ts +37 -0
  6. package/dist/subsystem/DecorationPanel.d.ts.map +1 -0
  7. package/dist/subsystem/DecorationPanel.js +147 -0
  8. package/dist/subsystem/DecorationPanel.js.map +1 -0
  9. package/dist/subsystem/DiagnosticToggle.d.ts +7 -1
  10. package/dist/subsystem/DiagnosticToggle.d.ts.map +1 -1
  11. package/dist/subsystem/DiagnosticToggle.js +8 -7
  12. package/dist/subsystem/DiagnosticToggle.js.map +1 -1
  13. package/dist/subsystem/FileDrawer.d.ts +3 -1
  14. package/dist/subsystem/FileDrawer.d.ts.map +1 -1
  15. package/dist/subsystem/FileDrawer.js +74 -8
  16. package/dist/subsystem/FileDrawer.js.map +1 -1
  17. package/dist/subsystem/GraphSidebar.d.ts +4 -2
  18. package/dist/subsystem/GraphSidebar.d.ts.map +1 -1
  19. package/dist/subsystem/GraphSidebar.js +3 -1
  20. package/dist/subsystem/GraphSidebar.js.map +1 -1
  21. package/dist/subsystem/SubsystemComponentGraph.d.ts +10 -13
  22. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  23. package/dist/subsystem/SubsystemComponentGraph.js +112 -272
  24. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  25. package/dist/subsystem/TrailStepsNav.d.ts.map +1 -1
  26. package/dist/subsystem/TrailStepsNav.js +6 -4
  27. package/dist/subsystem/TrailStepsNav.js.map +1 -1
  28. package/dist/subsystem/TrailsPanel.d.ts.map +1 -1
  29. package/dist/subsystem/TrailsPanel.js +14 -10
  30. package/dist/subsystem/TrailsPanel.js.map +1 -1
  31. package/dist/subsystem/decoration.d.ts +112 -0
  32. package/dist/subsystem/decoration.d.ts.map +1 -0
  33. package/dist/subsystem/decoration.js +54 -0
  34. package/dist/subsystem/decoration.js.map +1 -0
  35. package/package.json +3 -3
  36. package/src/index.ts +37 -0
  37. package/src/stories/Canvases/C4/C4ToComponentDrill.stories.tsx +38 -71
  38. package/src/stories/Canvases/Subsystem/GraphTitle.stories.tsx +1 -2
  39. package/src/stories/Canvases/Subsystem/StepEmphasis.stories.tsx +528 -0
  40. package/src/stories/Components/DecorationPanel.stories.tsx +160 -0
  41. package/src/subsystem/DecorationPanel.test.tsx +128 -0
  42. package/src/subsystem/DecorationPanel.tsx +334 -0
  43. package/src/subsystem/DiagnosticToggle.tsx +20 -5
  44. package/src/subsystem/FileDrawer.test.tsx +103 -0
  45. package/src/subsystem/FileDrawer.tsx +85 -6
  46. package/src/subsystem/GraphSidebar.test.tsx +18 -0
  47. package/src/subsystem/GraphSidebar.tsx +6 -1
  48. package/src/subsystem/SubsystemComponentGraph.sidebar.test.tsx +210 -0
  49. package/src/subsystem/SubsystemComponentGraph.tsx +185 -414
  50. package/src/subsystem/TrailStepsNav.test.tsx +32 -0
  51. package/src/subsystem/TrailStepsNav.tsx +6 -4
  52. package/src/subsystem/TrailsPanel.test.tsx +20 -0
  53. package/src/subsystem/TrailsPanel.tsx +11 -1
  54. package/src/subsystem/decoration.test.ts +72 -0
  55. package/src/subsystem/decoration.ts +151 -0
  56. package/src/subsystem/stepEmphasis.test.ts +110 -0
@@ -5,8 +5,8 @@
5
5
  * children by the graph component.
6
6
  */
7
7
 
8
- import { useEffect, useState } from "react";
9
- import type { ReactNode } from "react";
8
+ import { useEffect, useRef, useState } from "react";
9
+ import type { MouseEvent as ReactMouseEvent, ReactNode } from "react";
10
10
  import { useTheme } from "@principal-ade/industry-theme";
11
11
  import { Minimize2, Maximize2, X } from "lucide-react";
12
12
 
@@ -19,6 +19,7 @@ export function FileDrawer({
19
19
  title,
20
20
  onClose,
21
21
  placement = "bottom",
22
+ onResizeEnd,
22
23
  fillHeight = false,
23
24
  suppressEscape = false,
24
25
  hidden = false,
@@ -31,6 +32,8 @@ export function FileDrawer({
31
32
  onClose: () => void;
32
33
  /** Left docking requires a row container; bottom docking requires a column. */
33
34
  placement?: "bottom" | "left";
35
+ /** Fired when the user finishes dragging the side-docked code view's width. */
36
+ onResizeEnd?: () => void;
34
37
  /** When true and open, fill the graph area instead of ~45% of its dock axis. */
35
38
  fillHeight?: boolean;
36
39
  /** Skip Escape handling (e.g. while a full-file overlay is on top). */
@@ -62,8 +65,63 @@ export function FileDrawer({
62
65
  const fullSize = open && (maximized || fillHeight);
63
66
  const sideDocked = placement === "left";
64
67
  const sizeAxis = sideDocked ? "width" : "height";
68
+ const panelRef = useRef<HTMLDivElement>(null);
69
+ const [widthPercent, setWidthPercent] = useState(45);
70
+ const [resizing, setResizing] = useState(false);
71
+ const dragStart = useRef({ x: 0, width: 0, containerWidth: 0 });
72
+ const canResize = open && sideDocked && !fullSize;
73
+ const dockSize = sideDocked ? `${widthPercent}%` : "45%";
65
74
  // Space the panel takes out of the canvas's flow (0 when it overlays fully).
66
- const reservedSize = fullSize ? 0 : open ? "45%" : 0;
75
+ const reservedSize = fullSize ? 0 : open ? dockSize : 0;
76
+ const sizeTransition = resizing ? "none" : `${sizeAxis} ${FILE_DRAWER_HEIGHT_MS}ms ease`;
77
+
78
+ const onResizeStart = (event: ReactMouseEvent) => {
79
+ if (!canResize || event.button !== 0) return;
80
+ const panel = panelRef.current;
81
+ const containerWidth = panel?.parentElement?.getBoundingClientRect().width ?? 0;
82
+ if (panel == null || containerWidth <= 0) return;
83
+ event.preventDefault();
84
+ dragStart.current = {
85
+ x: event.clientX,
86
+ width: panel.getBoundingClientRect().width,
87
+ containerWidth,
88
+ };
89
+ setResizing(true);
90
+ };
91
+
92
+ useEffect(() => {
93
+ if (!canResize) setResizing(false);
94
+ }, [canResize]);
95
+
96
+ useEffect(() => {
97
+ if (!resizing || !canResize) return;
98
+ const { x, width, containerWidth } = dragStart.current;
99
+ // Keep 240px for both code and graph when space permits; split narrow
100
+ // containers evenly rather than giving either pane a negative width.
101
+ const minWidth = Math.min(240, containerWidth / 2);
102
+ const maxWidth = containerWidth - minWidth;
103
+ const onMove = (event: MouseEvent) => {
104
+ const next = Math.min(Math.max(width + event.clientX - x, minWidth), maxWidth);
105
+ setWidthPercent(next / containerWidth * 100);
106
+ };
107
+ const onUp = () => {
108
+ setResizing(false);
109
+ onResizeEnd?.();
110
+ };
111
+ const { userSelect, cursor } = document.body.style;
112
+ document.body.style.userSelect = "none";
113
+ document.body.style.cursor = "col-resize";
114
+ document.addEventListener("mousemove", onMove);
115
+ document.addEventListener("mouseup", onUp);
116
+ window.addEventListener("blur", onUp);
117
+ return () => {
118
+ document.body.style.userSelect = userSelect;
119
+ document.body.style.cursor = cursor;
120
+ document.removeEventListener("mousemove", onMove);
121
+ document.removeEventListener("mouseup", onUp);
122
+ window.removeEventListener("blur", onUp);
123
+ };
124
+ }, [resizing, canResize, onResizeEnd]);
67
125
 
68
126
  useEffect(() => {
69
127
  if (!open || suppressEscape) return;
@@ -83,10 +141,11 @@ export function FileDrawer({
83
141
  flexShrink: 0,
84
142
  order: sideDocked ? -1 : undefined,
85
143
  [sizeAxis]: reservedSize,
86
- transition: `${sizeAxis} ${FILE_DRAWER_HEIGHT_MS}ms ease`,
144
+ transition: sizeTransition,
87
145
  }}
88
146
  />
89
147
  <div
148
+ ref={panelRef}
90
149
  style={{
91
150
  position: "absolute",
92
151
  left: 0,
@@ -96,17 +155,18 @@ export function FileDrawer({
96
155
  // Above the edge-label overlay (zIndex 5), React Flow, and overlays
97
156
  // (zIndex 6–8); 30 lifts it over the whole graph area when maximized.
98
157
  zIndex: maximized ? 30 : 6,
99
- width: sideDocked ? (open ? (fullSize ? "100%" : "45%") : 0) : undefined,
158
+ width: sideDocked ? (open ? (fullSize ? "100%" : dockSize) : 0) : undefined,
100
159
  height: sideDocked ? "100%" : open ? (fullSize ? "100%" : "45%") : 0,
101
160
  minWidth: 0,
102
161
  minHeight: 0,
162
+ boxSizing: "border-box",
103
163
  overflow: "hidden",
104
164
  display: "flex",
105
165
  flexDirection: "column",
106
166
  background: theme.colors.background,
107
167
  borderTop: open && !sideDocked ? `1px solid ${theme.colors.border}` : "none",
108
168
  borderRight: open && sideDocked ? `1px solid ${theme.colors.border}` : "none",
109
- transition: `${sizeAxis} ${FILE_DRAWER_HEIGHT_MS}ms ease`,
169
+ transition: sizeTransition,
110
170
  }}
111
171
  >
112
172
  <div
@@ -231,6 +291,25 @@ export function FileDrawer({
231
291
  <div style={{ flex: 1, minHeight: 0, overflow: "auto" }}>
232
292
  {children}
233
293
  </div>
294
+ {canResize && (
295
+ <div
296
+ role="separator"
297
+ aria-orientation="vertical"
298
+ aria-label="Resize code view"
299
+ title="Drag to resize code view"
300
+ onMouseDown={onResizeStart}
301
+ style={{
302
+ position: "absolute",
303
+ top: 0,
304
+ right: 0,
305
+ bottom: 0,
306
+ width: 5,
307
+ cursor: "col-resize",
308
+ background: theme.colors.border,
309
+ zIndex: 2,
310
+ }}
311
+ />
312
+ )}
234
313
  </div>
235
314
  </>
236
315
  );
@@ -95,6 +95,24 @@ describe('GraphSidebar chrome', () => {
95
95
  expect(queryByText('body')).toBeNull();
96
96
  });
97
97
 
98
+ test('a capped header fits its content while the body fills the remaining space', () => {
99
+ const { container } = renderSidebar({
100
+ header: createElement('span', null, 'description'),
101
+ headerMaxHeight: '50%',
102
+ sidebarBody: createElement('span', null, 'trails'),
103
+ });
104
+ const column = sidebarColumn(container);
105
+ const header = column.firstElementChild as HTMLElement;
106
+ const body = column.lastElementChild as HTMLElement;
107
+ expect(header.style.flexGrow).toBe('0');
108
+ expect(header.style.flexBasis).toBe('auto');
109
+ expect(header.style.maxHeight).toBe('50%');
110
+ expect(header.style.boxSizing).toBe('border-box');
111
+ expect(header.style.overflowY).toBe('auto');
112
+ expect(body.style.flexGrow).toBe('1');
113
+ expect(body.style.height).toBe('');
114
+ });
115
+
98
116
  test('controlled width applies to the column and reports drag changes', () => {
99
117
  let reports = 0;
100
118
  const { container, getByLabelText } = renderSidebar({
@@ -1,5 +1,5 @@
1
1
  import { useCallback, useEffect, useRef, useState } from 'react';
2
- import type { MouseEvent as ReactMouseEvent, ReactNode } from 'react';
2
+ import type { CSSProperties, MouseEvent as ReactMouseEvent, ReactNode } from 'react';
3
3
  import { useTheme } from '@principal-ade/industry-theme';
4
4
 
5
5
  /**
@@ -55,6 +55,8 @@ export interface GraphSidebarProps {
55
55
  /** Give the header the flexible share of the column, pinning the lower
56
56
  * panel at half height (e.g. a description that unpacks on demand). */
57
57
  stretchHeader?: boolean;
58
+ /** Cap the upper block while letting short content use only the height it needs. */
59
+ headerMaxHeight?: CSSProperties['maxHeight'];
58
60
  /** Controlled sidebar width (px). */
59
61
  width?: number;
60
62
  onWidthChange?: (width: number) => void;
@@ -100,6 +102,7 @@ export function GraphSidebar({
100
102
  hidden = false,
101
103
  bodyHidden = false,
102
104
  stretchHeader = false,
105
+ headerMaxHeight,
103
106
  width,
104
107
  onWidthChange,
105
108
  defaultWidth = DEFAULT_WIDTH,
@@ -207,6 +210,8 @@ export function GraphSidebar({
207
210
  style={{
208
211
  flex: stretchHeader ? 1 : '0 0 auto',
209
212
  minHeight: 0,
213
+ maxHeight: headerMaxHeight,
214
+ boxSizing: 'border-box',
210
215
  overflowY: 'auto',
211
216
  padding: 16,
212
217
  display: 'flex',
@@ -0,0 +1,210 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { createElement } from 'react';
3
+ import { renderToStaticMarkup } from 'react-dom/server';
4
+ import { Window } from 'happy-dom';
5
+ import { ThemeProvider, defaultEditorTheme } from '@principal-ade/industry-theme';
6
+ import { SubsystemComponentGraph, type SubsystemComponentGraphProps } from './SubsystemComponentGraph';
7
+
8
+ const components: SubsystemComponentGraphProps['components'] = [
9
+ { alias: 'entry', name: 'Entry', construct: 'function', file: 'src/entry.ts', purl: 'pkg:github/test/app' },
10
+ { alias: 'store', name: 'Store', construct: 'function', file: 'src/store.ts', purl: 'pkg:github/test/app' },
11
+ ];
12
+ const trails: SubsystemComponentGraphProps['trails'] = [{
13
+ id: 'request',
14
+ title: 'Request flow',
15
+ steps: [{ from: 'entry', to: 'store', mechanism: 'calls', file: 'src/entry.ts', purl: 'pkg:github/test/app', symbol: 'Entry' }],
16
+ }];
17
+ const graphifyRelations: SubsystemComponentGraphProps['graphifyRelations'] = [
18
+ { id: 'static', from: 'entry', to: 'store', relation: 'references' },
19
+ ];
20
+
21
+ function renderGraph(props: Partial<SubsystemComponentGraphProps> = {}) {
22
+ const markup = renderToStaticMarkup(
23
+ createElement(
24
+ ThemeProvider,
25
+ { theme: defaultEditorTheme },
26
+ createElement(SubsystemComponentGraph, {
27
+ components,
28
+ trails,
29
+ title: 'Request subsystem',
30
+ description: 'Always visible **description**.',
31
+ ...props,
32
+ }),
33
+ ),
34
+ );
35
+ const dom = new Window();
36
+ dom.document.body.innerHTML = markup;
37
+ return dom.document.body;
38
+ }
39
+
40
+ describe('subsystem graph sidebar', () => {
41
+ test('puts the name in a full-width header above the sidebar and graph', () => {
42
+ const body = renderGraph();
43
+ const header = body.querySelector('header');
44
+ expect(header?.querySelector('h2')?.textContent).toBe('Request subsystem');
45
+ expect(body.querySelectorAll('h2').length).toBe(1);
46
+ expect(header?.textContent).not.toContain('Always visible');
47
+ const content = header?.nextElementSibling;
48
+ expect(content?.querySelector('[aria-label="Resize sidebar"]')).not.toBeNull();
49
+ expect(content?.textContent).toContain('Always visible description.');
50
+ expect(content?.textContent).not.toContain('Request subsystem');
51
+ });
52
+
53
+ test('keeps the Files button in the header when no name is provided', () => {
54
+ const body = renderGraph({ title: undefined });
55
+ expect(body.querySelector('header h2')).toBeNull();
56
+ expect(body.querySelector('header button[aria-label^="Files"]')?.textContent).toBe('Files2');
57
+ expect(body.textContent).toContain('Always visible description.');
58
+ });
59
+
60
+ test('the header Files button starts inactive and leaves trails visible', () => {
61
+ const body = renderGraph();
62
+ expect(body.querySelector('header button[aria-label^="Files"]')?.getAttribute('aria-pressed')).toBe('false');
63
+ expect(body.textContent).toContain('Request flow');
64
+ expect(body.querySelector('[role="tree"]')).toBeNull();
65
+ });
66
+
67
+ test('groups Trails, Files, and Issues into one switch with Trails selected by default', () => {
68
+ const body = renderGraph();
69
+ const group = body.querySelector('header [role="group"][aria-label="Sidebar view"]');
70
+ expect(group?.querySelectorAll('button').length).toBe(3);
71
+ expect(group?.querySelectorAll('button[aria-pressed="true"]').length).toBe(1);
72
+ expect(group?.querySelector('button[aria-label="Trails"]')?.getAttribute('aria-pressed')).toBe('true');
73
+ expect(group?.querySelector('button[aria-label^="Issues:"]')?.textContent).toBe('Issues');
74
+ expect((group as HTMLElement).style.borderStyle).toBe('solid');
75
+ });
76
+
77
+ test('omits the Files button when no components have files', () => {
78
+ const body = renderGraph({
79
+ components: components.map((component) => ({ ...component, file: '' })),
80
+ });
81
+ expect(body.querySelector('button[aria-label^="Files"]')).toBeNull();
82
+ expect(body.textContent).toContain('Request flow');
83
+ });
84
+
85
+ for (const active of [true, false]) {
86
+ test(`puts the ${active ? 'active' : 'inactive'} issue toggle in the header, not the sidebar`, () => {
87
+ const body = renderGraph({
88
+ diagnostic: { status: 'issues', issueCount: 3 },
89
+ showIssues: active,
90
+ });
91
+ const header = body.querySelector('header');
92
+ const toggle = header?.querySelector('button[aria-label^="Issues:"]');
93
+ expect(toggle).not.toBeNull();
94
+ expect(toggle?.textContent).toBe('Issues3');
95
+ expect((toggle as HTMLElement).style.borderStyle).toBe('none');
96
+ const filesButton = header?.querySelector('button[aria-label^="Files"]') as HTMLElement;
97
+ expect(filesButton.style.borderStyle).toBe('none');
98
+ expect(filesButton.parentElement?.style.borderStyle).toBe('solid');
99
+ expect(filesButton.parentElement?.style.display).toBe('grid');
100
+ expect(filesButton.parentElement?.style.gridAutoColumns).toBe('1fr');
101
+ expect((toggle as HTMLElement).style.justifyContent).toBe('center');
102
+ expect(toggle?.getAttribute('aria-pressed')).toBe(String(active));
103
+ expect(header?.querySelectorAll('button').length).toBe(3);
104
+ expect(header?.querySelectorAll('button[aria-pressed="true"]').length).toBe(1);
105
+ expect(header?.querySelector('button[aria-label="Trails"]')?.getAttribute('aria-pressed')).toBe(String(!active));
106
+ expect(body.querySelectorAll('button[aria-label^="Issues:"]').length).toBe(1);
107
+ expect(header?.nextElementSibling?.querySelector('button[aria-label^="Issues:"]')).toBeNull();
108
+ expect(body.textContent).toContain('Always visible description.');
109
+ });
110
+ }
111
+
112
+ test('counts unique files rather than components in the same file', () => {
113
+ const body = renderGraph({
114
+ components: [...components, { ...components[0], alias: 'helper' }],
115
+ });
116
+ expect(body.querySelector('header button[aria-label^="Files"]')?.textContent).toBe('Files2');
117
+ });
118
+
119
+ test('counts the same path in separate repositories as separate files', () => {
120
+ const body = renderGraph({
121
+ components: [...components, { ...components[0], alias: 'other-entry', purl: 'pkg:github/test/other' }],
122
+ });
123
+ expect(body.querySelector('header button[aria-label^="Files"]')?.textContent).toBe('Files3');
124
+ });
125
+
126
+ test('a diagnostic alone creates a header without an empty sidebar', () => {
127
+ const body = renderGraph({
128
+ trails: undefined,
129
+ graphifyRelations,
130
+ title: undefined,
131
+ description: undefined,
132
+ diagnostic: { status: 'ok' },
133
+ });
134
+ expect(body.querySelector('header button[aria-pressed="false"]')).not.toBeNull();
135
+ expect(body.querySelector('[aria-label="Resize sidebar"]')).toBeNull();
136
+ });
137
+
138
+ test('a name alone creates a header without an empty sidebar', () => {
139
+ const body = renderGraph({ trails: undefined, graphifyRelations, description: undefined });
140
+ expect(body.querySelector('header h2')?.textContent).toBe('Request subsystem');
141
+ expect(body.querySelector('[aria-label="Resize sidebar"]')).toBeNull();
142
+ });
143
+
144
+ test('shows the description immediately without a description toggle', () => {
145
+ const body = renderGraph();
146
+ expect(body.textContent).toContain('Always visible description.');
147
+ expect(body.querySelector('strong')?.textContent).toBe('description');
148
+ expect(body.querySelector('[aria-label="Show description"]')).toBeNull();
149
+ expect(body.querySelector('[aria-label="Hide description"]')).toBeNull();
150
+ expect(body.textContent).toContain('Request flow');
151
+ expect(body.querySelector('[role="tablist"]')).toBeNull();
152
+ expect(body.querySelector('[role="tree"]')).toBeNull();
153
+ expect(body.textContent).not.toContain('src/entry.ts');
154
+ });
155
+
156
+ test('still shows trails when an old Files tab selection was saved', () => {
157
+ const previousWindow = globalThis.window;
158
+ const dom = new Window();
159
+ dom.localStorage.setItem('principal.subsystems.viewState.sidebar-test', JSON.stringify({
160
+ sidebarView: 'files',
161
+ }));
162
+ globalThis.window = dom as unknown as typeof window;
163
+ try {
164
+ const body = renderGraph({ persistKey: 'sidebar-test' });
165
+ expect(body.textContent).toContain('Request flow');
166
+ expect(body.querySelector('[role="tree"]')).toBeNull();
167
+ expect(body.querySelector('[role="tablist"]')).toBeNull();
168
+ } finally {
169
+ globalThis.window = previousWindow;
170
+ }
171
+ });
172
+
173
+ test('static graphs keep their description but do not show a file panel', () => {
174
+ const body = renderGraph({ trails: undefined, graphifyRelations });
175
+ expect(body.textContent).toContain('Always visible description.');
176
+ expect(body.querySelector('[aria-label="Resize sidebar"]')).not.toBeNull();
177
+ expect(body.querySelector('[role="tree"]')).toBeNull();
178
+ expect(body.textContent).not.toContain('src/entry.ts');
179
+ expect(body.querySelector('header')?.nextElementSibling?.textContent).not.toContain('Files');
180
+ });
181
+
182
+ test('files alone do not create an empty sidebar', () => {
183
+ const body = renderGraph({
184
+ trails: undefined,
185
+ graphifyRelations,
186
+ title: undefined,
187
+ description: undefined,
188
+ });
189
+ expect(body.querySelector('[aria-label="Resize sidebar"]')).toBeNull();
190
+ });
191
+
192
+ test('the description stays visible while diagnostics replace the trails', () => {
193
+ const body = renderGraph({ showIssues: true, issues: [] });
194
+ expect(body.textContent).toContain('Always visible description.');
195
+ expect(body.textContent).not.toContain('Request flow');
196
+ expect(body.querySelector('[aria-label="Resize sidebar"]')).not.toBeNull();
197
+ expect(body.querySelector('[aria-label="Hide description"]')).toBeNull();
198
+ expect(body.querySelector('[role="tree"]')).toBeNull();
199
+ });
200
+
201
+ test('hideSidebar still hides the title, description, and trails', () => {
202
+ const body = renderGraph({ hideSidebar: true, diagnostic: { status: 'issues', issueCount: 3 } });
203
+ expect(body.textContent).not.toContain('Request subsystem');
204
+ expect(body.textContent).not.toContain('Always visible');
205
+ expect(body.textContent).not.toContain('Request flow');
206
+ expect(body.querySelector('[aria-label="Resize sidebar"]')).toBeNull();
207
+ expect(body.querySelector('header')).toBeNull();
208
+ expect(body.querySelector('button[aria-label^="Diagnostics:"]')).toBeNull();
209
+ });
210
+ });