@principal-ai/subsystems-react 0.24.0 → 0.24.1

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 (40) hide show
  1. package/dist/index.d.ts +7 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +4 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/subsystem/ComponentDeclaration.d.ts +3 -1
  6. package/dist/subsystem/ComponentDeclaration.d.ts.map +1 -1
  7. package/dist/subsystem/ComponentDeclaration.js +2 -2
  8. package/dist/subsystem/ComponentDeclaration.js.map +1 -1
  9. package/dist/subsystem/ConstructsCatalog.d.ts +30 -0
  10. package/dist/subsystem/ConstructsCatalog.d.ts.map +1 -0
  11. package/dist/subsystem/ConstructsCatalog.js +349 -0
  12. package/dist/subsystem/ConstructsCatalog.js.map +1 -0
  13. package/dist/subsystem/DiagnosticToggle.d.ts +46 -0
  14. package/dist/subsystem/DiagnosticToggle.d.ts.map +1 -0
  15. package/dist/subsystem/DiagnosticToggle.js +95 -0
  16. package/dist/subsystem/DiagnosticToggle.js.map +1 -0
  17. package/dist/subsystem/IssueList.d.ts +102 -0
  18. package/dist/subsystem/IssueList.d.ts.map +1 -0
  19. package/dist/subsystem/IssueList.js +282 -0
  20. package/dist/subsystem/IssueList.js.map +1 -0
  21. package/dist/subsystem/SubsystemComponentGraph.d.ts +28 -0
  22. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  23. package/dist/subsystem/SubsystemComponentGraph.js +80 -57
  24. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  25. package/dist/subsystem/model.d.ts +13 -2
  26. package/dist/subsystem/model.d.ts.map +1 -1
  27. package/dist/subsystem/model.js +12 -0
  28. package/dist/subsystem/model.js.map +1 -1
  29. package/package.json +1 -1
  30. package/src/index.ts +37 -0
  31. package/src/stories/Subsystem/ComponentGraph/Constructs.stories.tsx +287 -0
  32. package/src/stories/Subsystem/ComponentGraph/Diagnostics.stories.tsx +138 -0
  33. package/src/stories/Subsystem/ComponentGraph/Issues.stories.tsx +130 -0
  34. package/src/subsystem/ComponentDeclaration.tsx +4 -1
  35. package/src/subsystem/ConstructsCatalog.tsx +644 -0
  36. package/src/subsystem/DiagnosticToggle.tsx +160 -0
  37. package/src/subsystem/IssueList.tsx +488 -0
  38. package/src/subsystem/SubsystemComponentGraph.tsx +118 -36
  39. package/src/subsystem/model.test.ts +34 -0
  40. package/src/subsystem/model.ts +18 -2
@@ -0,0 +1,644 @@
1
+ /**
2
+ * ConstructsCatalog — master/detail for a constructs-only subsystem
3
+ * (components, no topology or walkthrough edges).
4
+ *
5
+ * Left: the model's constructs as a toggle list. Right: signatures for
6
+ * every construct currently on (`ComponentDeclaration`, stacked). File
7
+ * opens still use the bottom FileDrawer when the host injects a viewer.
8
+ */
9
+
10
+ import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
11
+ import type { ReactNode } from 'react';
12
+ import { useTheme } from '@principal-ade/industry-theme';
13
+ import { IndustryMarkdownSlide } from 'themed-markdown';
14
+ import { ChevronDown, ChevronUp } from 'lucide-react';
15
+ import {
16
+ constructBadgeColor,
17
+ constructBadgeLabel,
18
+ deriveNameFromSymbol,
19
+ rightBadgeColor,
20
+ rightBadgeLabel,
21
+ type SubsystemComponent,
22
+ } from './model';
23
+ import type { SubsystemOpenFileOptions } from './declarationRef';
24
+ import { ComponentDeclaration } from './ComponentDeclaration';
25
+ import type { ComponentVerificationState } from './ComponentDeclaration';
26
+ import { SubsystemDiagnosticToggle, type SubsystemDiagnostic } from './DiagnosticToggle';
27
+ import { FileDrawer } from './FileDrawer';
28
+ import { componentColor } from '../pierre/constructColors';
29
+ import { resolvePierreSyntaxThemeName } from '../pierre/pierreSyntaxTheme';
30
+
31
+ export interface ConstructsCatalogProps {
32
+ components: SubsystemComponent[];
33
+ onSelect?: (componentId: string) => void;
34
+ title?: string;
35
+ hideSidebar?: boolean;
36
+ description?: string;
37
+ diagnostic?: SubsystemDiagnostic;
38
+ sidebarExtra?: ReactNode;
39
+ sidebarAfterDescription?: ReactNode;
40
+ renderFileViewer?: (file: string, opts?: SubsystemOpenFileOptions) => ReactNode;
41
+ renderFileView?: (component: SubsystemComponent) => ReactNode;
42
+ onFileSelect?: (file: string) => void;
43
+ onVerifyComponent?: (componentId: string) => void;
44
+ componentVerification?: ComponentVerificationState | null;
45
+ }
46
+
47
+ const FileDrawerContent = memo(function FileDrawerContent({
48
+ render,
49
+ file,
50
+ startLine,
51
+ }: {
52
+ render: (file: string, opts?: SubsystemOpenFileOptions) => ReactNode;
53
+ file: string;
54
+ startLine?: number;
55
+ }) {
56
+ return <>{render(file, startLine != null ? { startLine } : undefined)}</>;
57
+ });
58
+
59
+ function resolveRelated(
60
+ components: SubsystemComponent[],
61
+ ref: string,
62
+ ): SubsystemComponent | undefined {
63
+ const clean = ref.replace(/\(\)$/, '');
64
+ return components.find(
65
+ (c) =>
66
+ c.id === clean ||
67
+ c.name === clean ||
68
+ c.symbol === clean ||
69
+ c.symbol?.replace(/\(\)$/, '') === clean,
70
+ );
71
+ }
72
+
73
+ export function ConstructsCatalog({
74
+ components,
75
+ onSelect,
76
+ title,
77
+ hideSidebar,
78
+ description,
79
+ diagnostic,
80
+ sidebarExtra,
81
+ sidebarAfterDescription,
82
+ renderFileViewer,
83
+ renderFileView,
84
+ onFileSelect,
85
+ onVerifyComponent,
86
+ componentVerification,
87
+ }: ConstructsCatalogProps) {
88
+ const { theme, mode } = useTheme();
89
+ const pierreTheme = resolvePierreSyntaxThemeName(mode);
90
+ const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
91
+ const [visibleIds, setVisibleIds] = useState<string[]>(() =>
92
+ components[0] ? [components[0].id] : [],
93
+ );
94
+ const [focusedId, setFocusedId] = useState<string | null>(
95
+ () => components[0]?.id ?? null,
96
+ );
97
+ const [descriptionVisible, setDescriptionVisible] = useState(false);
98
+ const [descToggleHover, setDescToggleHover] = useState(false);
99
+ const [drawer, setDrawer] = useState<{ file: string; startLine?: number } | null>(
100
+ null,
101
+ );
102
+ const focusedRef = useRef<string | null>(focusedId);
103
+ focusedRef.current = focusedId;
104
+
105
+ const visibleSet = useMemo(() => new Set(visibleIds), [visibleIds]);
106
+ const visibleComponents = useMemo(
107
+ () => components.filter((c) => visibleSet.has(c.id)),
108
+ [components, visibleSet],
109
+ );
110
+
111
+ useEffect(() => {
112
+ if (!focusedId) return;
113
+ document.getElementById(`construct-${focusedId}`)?.scrollIntoView({ block: 'nearest' });
114
+ }, [focusedId]);
115
+
116
+ useEffect(() => {
117
+ const last = visibleIds[visibleIds.length - 1];
118
+ if (!last) return;
119
+ document
120
+ .getElementById(`construct-signature-${last}`)
121
+ ?.scrollIntoView({ block: 'nearest' });
122
+ }, [visibleIds]);
123
+
124
+ // Drop ids that left the model; if nothing remains, show the first construct.
125
+ useEffect(() => {
126
+ const ids = new Set(components.map((c) => c.id));
127
+ setVisibleIds((prev) => {
128
+ const next = prev.filter((id) => ids.has(id));
129
+ if (next.length > 0 || components.length === 0) return next;
130
+ return [components[0]!.id];
131
+ });
132
+ setFocusedId((prev) => {
133
+ if (prev && ids.has(prev)) return prev;
134
+ return components[0]?.id ?? null;
135
+ });
136
+ }, [components]);
137
+
138
+ const toggle = useCallback(
139
+ (id: string) => {
140
+ setFocusedId(id);
141
+ setVisibleIds((prev) =>
142
+ prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id],
143
+ );
144
+ onSelect?.(id);
145
+ },
146
+ [onSelect],
147
+ );
148
+
149
+ const show = useCallback(
150
+ (id: string) => {
151
+ setFocusedId(id);
152
+ setVisibleIds((prev) => (prev.includes(id) ? prev : [...prev, id]));
153
+ onSelect?.(id);
154
+ },
155
+ [onSelect],
156
+ );
157
+
158
+ const focusAt = useCallback(
159
+ (id: string) => {
160
+ setFocusedId(id);
161
+ },
162
+ [],
163
+ );
164
+
165
+ const onOpenFile = useCallback(
166
+ (file: string, opts?: SubsystemOpenFileOptions) => {
167
+ const startLine = opts?.startLine;
168
+ setDrawer((prev) => {
169
+ if (prev?.file === file && prev.startLine === startLine) return null;
170
+ return { file, startLine };
171
+ });
172
+ onFileSelect?.(file);
173
+ },
174
+ [onFileSelect],
175
+ );
176
+
177
+ const openComponentSource = useCallback(
178
+ (c: SubsystemComponent) => {
179
+ if (!c.file) return;
180
+ const startLine = c.declarationRef?.startLine;
181
+ onOpenFile(
182
+ c.file,
183
+ startLine != null ? { startLine } : undefined,
184
+ );
185
+ },
186
+ [onOpenFile],
187
+ );
188
+
189
+ const onRelatedSelect = useCallback(
190
+ (ref: string) => {
191
+ const comp = resolveRelated(components, ref);
192
+ if (!comp) return;
193
+ show(comp.id);
194
+ },
195
+ [components, show],
196
+ );
197
+
198
+ const fileViewer = useMemo(() => {
199
+ if (renderFileViewer) return renderFileViewer;
200
+ if (renderFileView) {
201
+ const byFile = new Map(
202
+ components.filter((c) => c.file).map((c) => [c.file, c] as const),
203
+ );
204
+ return (file: string, _opts?: SubsystemOpenFileOptions) => {
205
+ const comp = byFile.get(file);
206
+ return comp ? renderFileView(comp) : null;
207
+ };
208
+ }
209
+ return undefined;
210
+ }, [renderFileViewer, renderFileView, components]);
211
+
212
+ const fileViewerRef = useRef(fileViewer);
213
+ fileViewerRef.current = fileViewer;
214
+ const renderDrawerContent = useCallback(
215
+ (file: string, opts?: SubsystemOpenFileOptions) =>
216
+ fileViewerRef.current?.(file, opts) ?? null,
217
+ [],
218
+ );
219
+
220
+ const moveFocus = useCallback(
221
+ (delta: number) => {
222
+ if (components.length === 0) return;
223
+ const idx = Math.max(
224
+ 0,
225
+ components.findIndex((c) => c.id === focusedRef.current),
226
+ );
227
+ const next = components[(idx + delta + components.length) % components.length]!;
228
+ focusAt(next.id);
229
+ },
230
+ [components, focusAt],
231
+ );
232
+
233
+ useEffect(() => {
234
+ const onKey = (e: KeyboardEvent) => {
235
+ const el = e.target as HTMLElement | null;
236
+ if (
237
+ el &&
238
+ (el.tagName === 'INPUT' ||
239
+ el.tagName === 'TEXTAREA' ||
240
+ el.tagName === 'SELECT' ||
241
+ el.isContentEditable)
242
+ ) {
243
+ return;
244
+ }
245
+ if (e.metaKey || e.ctrlKey || e.altKey) return;
246
+ if (e.key === 'ArrowDown') {
247
+ e.preventDefault();
248
+ moveFocus(1);
249
+ } else if (e.key === 'ArrowUp') {
250
+ e.preventDefault();
251
+ moveFocus(-1);
252
+ } else if (e.key === 'Home') {
253
+ e.preventDefault();
254
+ if (components[0]) focusAt(components[0].id);
255
+ } else if (e.key === 'End') {
256
+ e.preventDefault();
257
+ const last = components[components.length - 1];
258
+ if (last) focusAt(last.id);
259
+ } else if (e.key === ' ' || e.key === 'Enter') {
260
+ if (focusedRef.current) {
261
+ e.preventDefault();
262
+ toggle(focusedRef.current);
263
+ }
264
+ } else if (e.key === 'Escape' && drawer) {
265
+ setDrawer(null);
266
+ }
267
+ };
268
+ window.addEventListener('keydown', onKey);
269
+ return () => window.removeEventListener('keydown', onKey);
270
+ }, [components, drawer, moveFocus, focusAt, toggle]);
271
+
272
+ const showChrome =
273
+ !hideSidebar &&
274
+ !!(title || description || diagnostic || sidebarExtra || sidebarAfterDescription);
275
+ const showDesc = !!description && descriptionVisible;
276
+
277
+ return (
278
+ <div
279
+ data-testid="constructs-catalog"
280
+ style={{
281
+ width: '100%',
282
+ height: '100%',
283
+ minHeight: 0,
284
+ display: 'flex',
285
+ flexDirection: 'row',
286
+ background: theme.colors.background,
287
+ }}
288
+ >
289
+ <aside
290
+ style={{
291
+ width: 280,
292
+ minWidth: 200,
293
+ maxWidth: 360,
294
+ flexShrink: 0,
295
+ borderRight: `1px solid ${theme.colors.border}`,
296
+ background: theme.colors.backgroundSecondary ?? theme.colors.background,
297
+ display: 'flex',
298
+ flexDirection: 'column',
299
+ overflow: 'hidden',
300
+ }}
301
+ >
302
+ {showChrome && (
303
+ <div
304
+ style={{
305
+ flex: showDesc ? '0 0 auto' : undefined,
306
+ padding: '16px 16px 8px',
307
+ display: 'flex',
308
+ flexDirection: 'column',
309
+ gap: 12,
310
+ }}
311
+ >
312
+ {sidebarExtra}
313
+ {(title || description || diagnostic) && (
314
+ <div style={{ display: 'flex', alignItems: 'flex-start', gap: 8 }}>
315
+ {title && (
316
+ <h2
317
+ style={{
318
+ margin: 0,
319
+ flex: 1,
320
+ minWidth: 0,
321
+ fontSize: theme.fontSizes[2],
322
+ fontWeight: 600,
323
+ color: theme.colors.text,
324
+ fontFamily: theme.fonts.monospace,
325
+ }}
326
+ >
327
+ {title}
328
+ </h2>
329
+ )}
330
+ <div
331
+ style={{
332
+ display: 'flex',
333
+ alignItems: 'center',
334
+ gap: 4,
335
+ flexShrink: 0,
336
+ marginLeft: title ? undefined : 'auto',
337
+ }}
338
+ >
339
+ {diagnostic && <SubsystemDiagnosticToggle {...diagnostic} />}
340
+ {description && (
341
+ <button
342
+ type="button"
343
+ aria-expanded={descriptionVisible}
344
+ aria-label={
345
+ descriptionVisible ? 'Hide description' : 'Show description'
346
+ }
347
+ title={
348
+ descriptionVisible ? 'Hide description' : 'Show description'
349
+ }
350
+ onMouseEnter={() => setDescToggleHover(true)}
351
+ onMouseLeave={() => setDescToggleHover(false)}
352
+ onClick={() => setDescriptionVisible((v) => !v)}
353
+ style={{
354
+ display: 'inline-flex',
355
+ alignItems: 'center',
356
+ justifyContent: 'center',
357
+ flexShrink: 0,
358
+ width: 22,
359
+ height: 22,
360
+ padding: 0,
361
+ border: 'none',
362
+ borderRadius: 4,
363
+ background: descToggleHover ? theme.colors.border : 'transparent',
364
+ color: descToggleHover ? theme.colors.text : muted,
365
+ cursor: 'pointer',
366
+ }}
367
+ >
368
+ {descriptionVisible ? (
369
+ <ChevronUp size={14} />
370
+ ) : (
371
+ <ChevronDown size={14} />
372
+ )}
373
+ </button>
374
+ )}
375
+ </div>
376
+ </div>
377
+ )}
378
+ {showDesc && (
379
+ <div style={{ fontSize: theme.fontSizes[0], lineHeight: 1.5 }}>
380
+ <IndustryMarkdownSlide
381
+ content={description!}
382
+ slideIdPrefix="constructs-desc"
383
+ slideIndex={0}
384
+ isVisible={true}
385
+ theme={theme}
386
+ disableScroll={true}
387
+ disableBasePadding
388
+ fontSizeScale={0.9}
389
+ enableKeyboardScrolling={false}
390
+ autoFocusOnVisible={false}
391
+ />
392
+ </div>
393
+ )}
394
+ {sidebarAfterDescription}
395
+ </div>
396
+ )}
397
+ <div
398
+ style={{
399
+ padding: '10px 16px 6px',
400
+ fontSize: theme.fontSizes[0],
401
+ fontFamily: theme.fonts.monospace,
402
+ letterSpacing: 0.4,
403
+ textTransform: 'uppercase',
404
+ color: muted,
405
+ flexShrink: 0,
406
+ }}
407
+ >
408
+ Constructs
409
+ </div>
410
+ <ul
411
+ role="listbox"
412
+ aria-label="Constructs"
413
+ aria-multiselectable="true"
414
+ aria-activedescendant={focusedId ? `construct-${focusedId}` : undefined}
415
+ style={{
416
+ listStyle: 'none',
417
+ margin: 0,
418
+ padding: '0 8px 12px',
419
+ overflowY: 'auto',
420
+ flex: 1,
421
+ minHeight: 0,
422
+ }}
423
+ >
424
+ {components.map((c) => {
425
+ const visible = visibleSet.has(c.id);
426
+ const focused = c.id === focusedId;
427
+ const color = componentColor(c, pierreTheme);
428
+ const badgeColor = constructBadgeColor(c) ?? color;
429
+ const displayName = deriveNameFromSymbol(
430
+ c.symbol,
431
+ c.construct,
432
+ c.name,
433
+ c.file,
434
+ c.stereotype,
435
+ );
436
+ const role = rightBadgeLabel(c);
437
+ const roleColor = rightBadgeColor(c);
438
+ return (
439
+ <li key={c.id} role="presentation">
440
+ <button
441
+ type="button"
442
+ id={`construct-${c.id}`}
443
+ role="option"
444
+ aria-selected={visible}
445
+ data-testid={`construct-row-${c.id}`}
446
+ onClick={() => toggle(c.id)}
447
+ style={{
448
+ display: 'flex',
449
+ flexDirection: 'column',
450
+ alignItems: 'stretch',
451
+ gap: 2,
452
+ width: '100%',
453
+ margin: '2px 0',
454
+ padding: '8px 10px 8px 12px',
455
+ textAlign: 'left',
456
+ border: 'none',
457
+ borderRadius: 6,
458
+ background: visible
459
+ ? theme.colors.background
460
+ : 'transparent',
461
+ cursor: 'pointer',
462
+ boxShadow: visible
463
+ ? `inset 0 0 0 1px ${theme.colors.border}`
464
+ : undefined,
465
+ outline: focused && !visible ? `1px dotted ${muted}` : undefined,
466
+ outlineOffset: -1,
467
+ opacity: visible ? 1 : 0.72,
468
+ }}
469
+ >
470
+ <span
471
+ style={{
472
+ display: 'flex',
473
+ alignItems: 'baseline',
474
+ gap: 8,
475
+ minWidth: 0,
476
+ }}
477
+ >
478
+ <span
479
+ style={{
480
+ flex: 1,
481
+ minWidth: 0,
482
+ overflow: 'hidden',
483
+ textOverflow: 'ellipsis',
484
+ whiteSpace: 'nowrap',
485
+ fontFamily: theme.fonts.monospace,
486
+ fontSize: theme.fontSizes[1],
487
+ color,
488
+ fontWeight: visible ? 600 : 500,
489
+ }}
490
+ >
491
+ {displayName}
492
+ </span>
493
+ <span
494
+ style={{
495
+ flexShrink: 0,
496
+ fontFamily: theme.fonts.monospace,
497
+ fontSize: theme.fontSizes[0] * 0.95,
498
+ letterSpacing: 0.4,
499
+ textTransform: 'uppercase',
500
+ color: badgeColor,
501
+ }}
502
+ >
503
+ {constructBadgeLabel(c)}
504
+ </span>
505
+ </span>
506
+ {(c.file || role) && (
507
+ <span
508
+ style={{
509
+ display: 'flex',
510
+ alignItems: 'center',
511
+ gap: 8,
512
+ minWidth: 0,
513
+ }}
514
+ >
515
+ {c.file ? (
516
+ <span
517
+ title={fileViewer ? `Open ${c.file}` : c.file}
518
+ onClick={
519
+ fileViewer
520
+ ? (e) => {
521
+ e.stopPropagation();
522
+ openComponentSource(c);
523
+ }
524
+ : undefined
525
+ }
526
+ style={{
527
+ flex: 1,
528
+ minWidth: 0,
529
+ overflow: 'hidden',
530
+ textOverflow: 'ellipsis',
531
+ whiteSpace: 'nowrap',
532
+ fontFamily: theme.fonts.body,
533
+ fontSize: theme.fontSizes[0],
534
+ color: muted,
535
+ cursor: fileViewer ? 'pointer' : undefined,
536
+ textDecoration: fileViewer ? 'underline' : undefined,
537
+ textDecorationStyle: fileViewer ? 'dotted' : undefined,
538
+ textUnderlineOffset: 2,
539
+ }}
540
+ >
541
+ {c.file}
542
+ </span>
543
+ ) : (
544
+ <span style={{ flex: 1 }} />
545
+ )}
546
+ {role && roleColor && (
547
+ <span
548
+ style={{
549
+ flexShrink: 0,
550
+ fontFamily: theme.fonts.monospace,
551
+ fontSize: theme.fontSizes[0] * 0.9,
552
+ letterSpacing: 0.4,
553
+ textTransform: 'uppercase',
554
+ color: roleColor,
555
+ }}
556
+ >
557
+ {role}
558
+ </span>
559
+ )}
560
+ </span>
561
+ )}
562
+ </button>
563
+ </li>
564
+ );
565
+ })}
566
+ </ul>
567
+ </aside>
568
+ <div
569
+ style={{
570
+ flex: 1,
571
+ minWidth: 0,
572
+ minHeight: 0,
573
+ display: 'flex',
574
+ flexDirection: 'column',
575
+ }}
576
+ >
577
+ <div
578
+ data-testid="construct-signature"
579
+ style={{
580
+ flex: 1,
581
+ minHeight: 0,
582
+ overflow: 'auto',
583
+ padding: 16,
584
+ display: 'flex',
585
+ flexDirection: 'column',
586
+ gap: 12,
587
+ }}
588
+ >
589
+ {visibleComponents.length > 0 ? (
590
+ visibleComponents.map((c) => (
591
+ <div
592
+ key={c.id}
593
+ id={`construct-signature-${c.id}`}
594
+ style={{
595
+ border: `1px solid ${theme.colors.border}`,
596
+ borderRadius: 8,
597
+ overflow: 'hidden',
598
+ flexShrink: 0,
599
+ }}
600
+ >
601
+ <ComponentDeclaration
602
+ component={c}
603
+ onOpenFile={fileViewer ? onOpenFile : undefined}
604
+ defaultShowFile={!!fileViewer}
605
+ onRelatedSelect={onRelatedSelect}
606
+ onVerify={onVerifyComponent}
607
+ verification={
608
+ componentVerification && focusedId === c.id
609
+ ? componentVerification
610
+ : undefined
611
+ }
612
+ />
613
+ </div>
614
+ ))
615
+ ) : (
616
+ <p
617
+ style={{
618
+ margin: 0,
619
+ color: muted,
620
+ fontFamily: theme.fonts.body,
621
+ fontSize: theme.fontSizes[1],
622
+ }}
623
+ >
624
+ Toggle constructs on the left to show their signatures.
625
+ </p>
626
+ )}
627
+ </div>
628
+ <FileDrawer
629
+ title={drawer?.file ?? null}
630
+ onClose={() => setDrawer(null)}
631
+ fillHeight={drawer != null && drawer.startLine == null}
632
+ >
633
+ {drawer ? (
634
+ <FileDrawerContent
635
+ render={renderDrawerContent}
636
+ file={drawer.file}
637
+ startLine={drawer.startLine}
638
+ />
639
+ ) : null}
640
+ </FileDrawer>
641
+ </div>
642
+ </div>
643
+ );
644
+ }