@principal-ai/subsystems-react 0.37.0 → 0.37.2
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.
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/subsystem/ComponentDeclaration.d.ts +16 -1
- package/dist/subsystem/ComponentDeclaration.d.ts.map +1 -1
- package/dist/subsystem/ComponentDeclaration.js +75 -7
- package/dist/subsystem/ComponentDeclaration.js.map +1 -1
- package/dist/subsystem/ConstructsCatalog.d.ts +13 -3
- package/dist/subsystem/ConstructsCatalog.d.ts.map +1 -1
- package/dist/subsystem/ConstructsCatalog.js +273 -220
- package/dist/subsystem/ConstructsCatalog.js.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +36 -28
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/SymbolInspectionCard.d.ts.map +1 -1
- package/dist/subsystem/SymbolInspectionCard.js +1 -1
- package/dist/subsystem/SymbolInspectionCard.js.map +1 -1
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js +7 -1
- package/dist/subsystem/model.js.map +1 -1
- package/dist/utils/edgeLabel.d.ts +51 -0
- package/dist/utils/edgeLabel.d.ts.map +1 -0
- package/dist/utils/edgeLabel.js +81 -0
- package/dist/utils/edgeLabel.js.map +1 -0
- package/dist/utils/elkLayout.d.ts +6 -0
- package/dist/utils/elkLayout.d.ts.map +1 -1
- package/dist/utils/elkLayout.js +27 -6
- package/dist/utils/elkLayout.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +7 -0
- package/src/stories/Subsystem/ConstructsCatalog.stories.tsx +457 -0
- package/src/subsystem/ComponentDeclaration.tsx +130 -7
- package/src/subsystem/ConstructsCatalog.tsx +431 -341
- package/src/subsystem/SubsystemComponentGraph.tsx +74 -29
- package/src/subsystem/SymbolInspectionCard.tsx +10 -9
- package/src/subsystem/model.ts +7 -1
- package/src/utils/edgeLabel.test.ts +23 -0
- package/src/utils/edgeLabel.ts +95 -0
- package/src/utils/elkLayout.ts +41 -6
|
@@ -2,20 +2,31 @@
|
|
|
2
2
|
* ConstructsCatalog — master/detail for a constructs-only subsystem
|
|
3
3
|
* (components, no topology or walkthrough edges).
|
|
4
4
|
*
|
|
5
|
-
* Left: the model's
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Left: the model's files as a tree; clicking a file toggles its
|
|
6
|
+
* constructs. Right: signatures grouped by repo, and within a repo stacked
|
|
7
|
+
* per file under one combined file header carrying the path and a
|
|
8
|
+
* description toggle; a declaration with a known line labels just that
|
|
9
|
+
* construct's line in a file-like gutter, and `external` constructs get a
|
|
10
|
+
* header naming their kind with the body naming the construct. The model
|
|
11
|
+
* description's toggle sits in the left chrome and opens the markdown as an
|
|
12
|
+
* overlay over the construct area. File opens still use the bottom FileDrawer
|
|
13
|
+
* when the host injects a viewer.
|
|
8
14
|
*/
|
|
9
15
|
|
|
10
16
|
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
11
17
|
import type { ReactNode } from 'react';
|
|
12
18
|
import { useTheme } from '@principal-ade/industry-theme';
|
|
13
19
|
import { IndustryMarkdownSlide } from 'themed-markdown';
|
|
14
|
-
import { ChevronDown, ChevronRight, FileText, Folder } from 'lucide-react';
|
|
15
20
|
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
21
|
+
AlignLeft,
|
|
22
|
+
Box,
|
|
23
|
+
ChevronDown,
|
|
24
|
+
ChevronRight,
|
|
25
|
+
FileText,
|
|
26
|
+
Folder,
|
|
27
|
+
X,
|
|
28
|
+
} from 'lucide-react';
|
|
29
|
+
import {
|
|
19
30
|
formatPurl,
|
|
20
31
|
type SubsystemComponent,
|
|
21
32
|
} from './model';
|
|
@@ -33,6 +44,10 @@ export interface ConstructsCatalogProps {
|
|
|
33
44
|
onSelect?: (componentAlias: string) => void;
|
|
34
45
|
title?: string;
|
|
35
46
|
hideSidebar?: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Model description. Its toggle lives in the left chrome; opening it shows
|
|
49
|
+
* the markdown as an overlay over the construct area.
|
|
50
|
+
*/
|
|
36
51
|
description?: string;
|
|
37
52
|
diagnostic?: SubsystemDiagnostic;
|
|
38
53
|
sidebarExtra?: ReactNode;
|
|
@@ -140,6 +155,24 @@ function repoLogo(c: SubsystemComponent): string | undefined {
|
|
|
140
155
|
return undefined;
|
|
141
156
|
}
|
|
142
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Split a repo's declarations into consecutive runs that share a `file`, so
|
|
160
|
+
* each run can render as one connected stack. Components with no `file` are
|
|
161
|
+
* never merged (they have no shared location to group under).
|
|
162
|
+
*/
|
|
163
|
+
function splitByFileRun(items: SubsystemComponent[]): SubsystemComponent[][] {
|
|
164
|
+
const runs: SubsystemComponent[][] = [];
|
|
165
|
+
for (const c of items) {
|
|
166
|
+
const prev = runs[runs.length - 1];
|
|
167
|
+
const key = c.file || `alias:${c.alias}`;
|
|
168
|
+
const prevKey =
|
|
169
|
+
prev && prev[0] ? prev[0].file || `alias:${prev[0].alias}` : null;
|
|
170
|
+
if (prev && prevKey === key) prev.push(c);
|
|
171
|
+
else runs.push([c]);
|
|
172
|
+
}
|
|
173
|
+
return runs;
|
|
174
|
+
}
|
|
175
|
+
|
|
143
176
|
export function ConstructsCatalog({
|
|
144
177
|
components,
|
|
145
178
|
onSelect,
|
|
@@ -177,24 +210,23 @@ export function ConstructsCatalog({
|
|
|
177
210
|
.map((x) => x.c);
|
|
178
211
|
}, [components]);
|
|
179
212
|
|
|
180
|
-
const [visibleAliases, setVisibleAliases] = useState<string[]>(
|
|
181
|
-
|
|
182
|
-
);
|
|
183
|
-
const [focusedAlias, setFocusedAlias] = useState<string | null>(
|
|
184
|
-
() => orderedComponents[0]?.alias ?? null,
|
|
185
|
-
);
|
|
213
|
+
const [visibleAliases, setVisibleAliases] = useState<string[]>([]);
|
|
214
|
+
const [focusedAlias, setFocusedAlias] = useState<string | null>(null);
|
|
215
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
186
216
|
const [descriptionVisible, setDescriptionVisible] = useState(false);
|
|
187
217
|
const [descToggleHover, setDescToggleHover] = useState(false);
|
|
188
218
|
const [hoveredRow, setHoveredRow] = useState<string | null>(null);
|
|
189
|
-
const [sidebarTab, setSidebarTab] = useState<'constructs' | 'files'>('files');
|
|
190
219
|
const [collapsedFolders, setCollapsedFolders] = useState<Set<string>>(
|
|
191
220
|
new Set(),
|
|
192
221
|
);
|
|
222
|
+
// Explicit description toggles per file run. Unset falls back to the kind's
|
|
223
|
+
// default (externals open, files closed).
|
|
224
|
+
const [purposeOverrides, setPurposeOverrides] = useState<
|
|
225
|
+
Record<string, boolean>
|
|
226
|
+
>({});
|
|
193
227
|
const [drawer, setDrawer] = useState<{ file: string; startLine?: number } | null>(
|
|
194
228
|
null,
|
|
195
229
|
);
|
|
196
|
-
const focusedAliasRef = useRef<string | null>(focusedAlias);
|
|
197
|
-
focusedAliasRef.current = focusedAlias;
|
|
198
230
|
|
|
199
231
|
const visibleSet = useMemo(() => new Set(visibleAliases), [visibleAliases]);
|
|
200
232
|
const visibleComponents = useMemo(
|
|
@@ -202,15 +234,38 @@ export function ConstructsCatalog({
|
|
|
202
234
|
[orderedComponents, visibleSet],
|
|
203
235
|
);
|
|
204
236
|
|
|
237
|
+
// When nothing is toggled on, the right pane falls back to a searchable list
|
|
238
|
+
// of every construct, rendered as if all were selected.
|
|
239
|
+
const searchActive = visibleComponents.length === 0;
|
|
240
|
+
const searchResults = useMemo(() => {
|
|
241
|
+
if (!searchActive) return visibleComponents;
|
|
242
|
+
const q = searchQuery.trim().toLowerCase();
|
|
243
|
+
if (!q) return orderedComponents;
|
|
244
|
+
return orderedComponents.filter((c) => {
|
|
245
|
+
const haystack = [
|
|
246
|
+
c.name,
|
|
247
|
+
c.symbol,
|
|
248
|
+
c.alias,
|
|
249
|
+
c.file,
|
|
250
|
+
c.construct,
|
|
251
|
+
c.stereotype,
|
|
252
|
+
c.purpose,
|
|
253
|
+
]
|
|
254
|
+
.filter(Boolean)
|
|
255
|
+
.join(' ')
|
|
256
|
+
.toLowerCase();
|
|
257
|
+
return haystack.includes(q);
|
|
258
|
+
});
|
|
259
|
+
}, [searchActive, searchQuery, orderedComponents, visibleComponents]);
|
|
260
|
+
|
|
205
261
|
const fileTree = useMemo(() => buildFileTree(orderedComponents), [orderedComponents]);
|
|
206
262
|
const hasFiles = useMemo(
|
|
207
263
|
() => orderedComponents.some((c) => !!c.file),
|
|
208
264
|
[orderedComponents],
|
|
209
265
|
);
|
|
210
|
-
const effectiveTab = hasFiles ? sidebarTab : 'constructs';
|
|
211
266
|
|
|
212
|
-
// Group
|
|
213
|
-
// renders once per group instead of on every card.
|
|
267
|
+
// Group the declarations on screen by purl so the repo/app identity
|
|
268
|
+
// (logo + name) renders once per group instead of on every card.
|
|
214
269
|
const repoGroups = useMemo(() => {
|
|
215
270
|
const groups: Array<{
|
|
216
271
|
key: string;
|
|
@@ -219,7 +274,7 @@ export function ConstructsCatalog({
|
|
|
219
274
|
items: SubsystemComponent[];
|
|
220
275
|
}> = [];
|
|
221
276
|
const index = new Map<string, number>();
|
|
222
|
-
for (const c of
|
|
277
|
+
for (const c of searchResults) {
|
|
223
278
|
const key = c.purl || c.alias;
|
|
224
279
|
let at = index.get(key);
|
|
225
280
|
if (at == null) {
|
|
@@ -231,12 +286,7 @@ export function ConstructsCatalog({
|
|
|
231
286
|
groups[at]!.items.push(c);
|
|
232
287
|
}
|
|
233
288
|
return groups;
|
|
234
|
-
}, [
|
|
235
|
-
|
|
236
|
-
useEffect(() => {
|
|
237
|
-
if (!focusedAlias) return;
|
|
238
|
-
document.getElementById(`construct-${focusedAlias}`)?.scrollIntoView({ block: 'nearest' });
|
|
239
|
-
}, [focusedAlias]);
|
|
289
|
+
}, [searchResults]);
|
|
240
290
|
|
|
241
291
|
useEffect(() => {
|
|
242
292
|
const last = visibleAliases[visibleAliases.length - 1];
|
|
@@ -260,31 +310,13 @@ export function ConstructsCatalog({
|
|
|
260
310
|
?.scrollIntoView({ block: 'nearest' });
|
|
261
311
|
}, [orderedComponents, drawer]);
|
|
262
312
|
|
|
263
|
-
// Drop aliases that left the model;
|
|
313
|
+
// Drop aliases that left the model; an empty set is valid (search fallback).
|
|
264
314
|
useEffect(() => {
|
|
265
315
|
const aliases = new Set(orderedComponents.map((c) => c.alias));
|
|
266
|
-
setVisibleAliases((prev) =>
|
|
267
|
-
|
|
268
|
-
if (next.length > 0 || orderedComponents.length === 0) return next;
|
|
269
|
-
return [orderedComponents[0]!.alias];
|
|
270
|
-
});
|
|
271
|
-
setFocusedAlias((prev) => {
|
|
272
|
-
if (prev && aliases.has(prev)) return prev;
|
|
273
|
-
return orderedComponents[0]?.alias ?? null;
|
|
274
|
-
});
|
|
316
|
+
setVisibleAliases((prev) => prev.filter((alias) => aliases.has(alias)));
|
|
317
|
+
setFocusedAlias((prev) => (prev && aliases.has(prev) ? prev : null));
|
|
275
318
|
}, [orderedComponents]);
|
|
276
319
|
|
|
277
|
-
const toggle = useCallback(
|
|
278
|
-
(alias: string) => {
|
|
279
|
-
setFocusedAlias(alias);
|
|
280
|
-
setVisibleAliases((prev) =>
|
|
281
|
-
prev.includes(alias) ? prev.filter((x) => x !== alias) : [...prev, alias],
|
|
282
|
-
);
|
|
283
|
-
onSelect?.(alias);
|
|
284
|
-
},
|
|
285
|
-
[onSelect],
|
|
286
|
-
);
|
|
287
|
-
|
|
288
320
|
const show = useCallback(
|
|
289
321
|
(alias: string) => {
|
|
290
322
|
setFocusedAlias(alias);
|
|
@@ -294,13 +326,6 @@ export function ConstructsCatalog({
|
|
|
294
326
|
[onSelect],
|
|
295
327
|
);
|
|
296
328
|
|
|
297
|
-
const focusAt = useCallback(
|
|
298
|
-
(alias: string) => {
|
|
299
|
-
setFocusedAlias(alias);
|
|
300
|
-
},
|
|
301
|
-
[],
|
|
302
|
-
);
|
|
303
|
-
|
|
304
329
|
const toggleFolder = useCallback((path: string) => {
|
|
305
330
|
setCollapsedFolders((prev) => {
|
|
306
331
|
const next = new Set(prev);
|
|
@@ -310,8 +335,12 @@ export function ConstructsCatalog({
|
|
|
310
335
|
});
|
|
311
336
|
}, []);
|
|
312
337
|
|
|
338
|
+
const setPurposeOpen = useCallback((key: string, open: boolean) => {
|
|
339
|
+
setPurposeOverrides((prev) => ({ ...prev, [key]: open }));
|
|
340
|
+
}, []);
|
|
341
|
+
|
|
313
342
|
/** File-tree click: show every construct declared in the file, or hide them
|
|
314
|
-
* all when they're already shown —
|
|
343
|
+
* all when they're already shown — a bulk toggle of the file's constructs. */
|
|
315
344
|
const toggleFile = useCallback(
|
|
316
345
|
(comps: SubsystemComponent[]) => {
|
|
317
346
|
const aliases = comps.map((c) => c.alias);
|
|
@@ -480,9 +509,18 @@ export function ConstructsCatalog({
|
|
|
480
509
|
(ref: string) => {
|
|
481
510
|
const comp = resolveRelated(components, ref);
|
|
482
511
|
if (!comp) return;
|
|
512
|
+
// In the all-constructs search fallback nothing needs revealing — just
|
|
513
|
+
// surface the target. Otherwise add it to the visible set.
|
|
514
|
+
if (searchActive) {
|
|
515
|
+
setFocusedAlias(comp.alias);
|
|
516
|
+
document
|
|
517
|
+
.getElementById(`construct-signature-${comp.alias}`)
|
|
518
|
+
?.scrollIntoView({ block: 'nearest' });
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
483
521
|
show(comp.alias);
|
|
484
522
|
},
|
|
485
|
-
[components, show],
|
|
523
|
+
[components, show, searchActive],
|
|
486
524
|
);
|
|
487
525
|
|
|
488
526
|
const fileViewer = useMemo(() => {
|
|
@@ -507,65 +545,23 @@ export function ConstructsCatalog({
|
|
|
507
545
|
[],
|
|
508
546
|
);
|
|
509
547
|
|
|
510
|
-
const moveFocus = useCallback(
|
|
511
|
-
(delta: number) => {
|
|
512
|
-
if (orderedComponents.length === 0) return;
|
|
513
|
-
const idx = Math.max(
|
|
514
|
-
0,
|
|
515
|
-
orderedComponents.findIndex((c) => c.alias === focusedAliasRef.current),
|
|
516
|
-
);
|
|
517
|
-
const next =
|
|
518
|
-
orderedComponents[
|
|
519
|
-
(idx + delta + orderedComponents.length) % orderedComponents.length
|
|
520
|
-
]!;
|
|
521
|
-
focusAt(next.alias);
|
|
522
|
-
},
|
|
523
|
-
[orderedComponents, focusAt],
|
|
524
|
-
);
|
|
525
|
-
|
|
526
548
|
useEffect(() => {
|
|
527
549
|
const onKey = (e: KeyboardEvent) => {
|
|
528
|
-
|
|
529
|
-
if (
|
|
530
|
-
el &&
|
|
531
|
-
(el.tagName === 'INPUT' ||
|
|
532
|
-
el.tagName === 'TEXTAREA' ||
|
|
533
|
-
el.tagName === 'SELECT' ||
|
|
534
|
-
el.isContentEditable)
|
|
535
|
-
) {
|
|
536
|
-
return;
|
|
537
|
-
}
|
|
538
|
-
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
|
539
|
-
if (e.key === 'ArrowDown') {
|
|
540
|
-
e.preventDefault();
|
|
541
|
-
moveFocus(1);
|
|
542
|
-
} else if (e.key === 'ArrowUp') {
|
|
543
|
-
e.preventDefault();
|
|
544
|
-
moveFocus(-1);
|
|
545
|
-
} else if (e.key === 'Home') {
|
|
546
|
-
e.preventDefault();
|
|
547
|
-
if (orderedComponents[0]) focusAt(orderedComponents[0].alias);
|
|
548
|
-
} else if (e.key === 'End') {
|
|
549
|
-
e.preventDefault();
|
|
550
|
-
const last = orderedComponents[orderedComponents.length - 1];
|
|
551
|
-
if (last) focusAt(last.alias);
|
|
552
|
-
} else if (e.key === ' ' || e.key === 'Enter') {
|
|
553
|
-
if (focusedAliasRef.current) {
|
|
554
|
-
e.preventDefault();
|
|
555
|
-
toggle(focusedAliasRef.current);
|
|
556
|
-
}
|
|
557
|
-
} else if (e.key === 'Escape' && drawer) {
|
|
558
|
-
setDrawer(null);
|
|
559
|
-
}
|
|
550
|
+
if (e.key === 'Escape' && drawer) setDrawer(null);
|
|
560
551
|
};
|
|
561
552
|
window.addEventListener('keydown', onKey);
|
|
562
553
|
return () => window.removeEventListener('keydown', onKey);
|
|
563
|
-
}, [
|
|
554
|
+
}, [drawer]);
|
|
564
555
|
|
|
565
556
|
const showChrome =
|
|
566
557
|
!hideSidebar &&
|
|
567
|
-
!!(
|
|
568
|
-
|
|
558
|
+
!!(
|
|
559
|
+
title ||
|
|
560
|
+
description ||
|
|
561
|
+
diagnostic ||
|
|
562
|
+
sidebarExtra ||
|
|
563
|
+
sidebarAfterDescription
|
|
564
|
+
);
|
|
569
565
|
|
|
570
566
|
return (
|
|
571
567
|
<div
|
|
@@ -595,7 +591,6 @@ export function ConstructsCatalog({
|
|
|
595
591
|
{showChrome && (
|
|
596
592
|
<div
|
|
597
593
|
style={{
|
|
598
|
-
flex: showDesc ? '0 0 auto' : undefined,
|
|
599
594
|
padding: '16px 16px 8px',
|
|
600
595
|
display: 'flex',
|
|
601
596
|
flexDirection: 'column',
|
|
@@ -664,201 +659,10 @@ export function ConstructsCatalog({
|
|
|
664
659
|
</div>
|
|
665
660
|
</div>
|
|
666
661
|
)}
|
|
667
|
-
{showDesc && (
|
|
668
|
-
<div style={{ fontSize: theme.fontSizes[0], lineHeight: 1.5 }}>
|
|
669
|
-
<IndustryMarkdownSlide
|
|
670
|
-
content={description!}
|
|
671
|
-
slideIdPrefix="constructs-desc"
|
|
672
|
-
slideIndex={0}
|
|
673
|
-
isVisible={true}
|
|
674
|
-
theme={theme}
|
|
675
|
-
disableScroll={true}
|
|
676
|
-
disableBasePadding
|
|
677
|
-
enableKeyboardScrolling={false}
|
|
678
|
-
autoFocusOnVisible={false}
|
|
679
|
-
/>
|
|
680
|
-
</div>
|
|
681
|
-
)}
|
|
682
662
|
{sidebarAfterDescription}
|
|
683
663
|
</div>
|
|
684
664
|
)}
|
|
685
|
-
|
|
686
|
-
role="tablist"
|
|
687
|
-
aria-label="Sidebar view"
|
|
688
|
-
style={{
|
|
689
|
-
display: 'flex',
|
|
690
|
-
width: '100%',
|
|
691
|
-
flexShrink: 0,
|
|
692
|
-
borderBottom: `1px solid ${theme.colors.border}`,
|
|
693
|
-
background:
|
|
694
|
-
theme.colors.backgroundSecondary ?? theme.colors.background,
|
|
695
|
-
}}
|
|
696
|
-
>
|
|
697
|
-
{(['files', 'constructs'] as const).map((tab) => {
|
|
698
|
-
if (tab === 'files' && !hasFiles) return null;
|
|
699
|
-
const active = effectiveTab === tab;
|
|
700
|
-
return (
|
|
701
|
-
<button
|
|
702
|
-
key={tab}
|
|
703
|
-
type="button"
|
|
704
|
-
role="tab"
|
|
705
|
-
aria-selected={active}
|
|
706
|
-
onClick={() => setSidebarTab(tab)}
|
|
707
|
-
style={{
|
|
708
|
-
flex: 1,
|
|
709
|
-
minWidth: 0,
|
|
710
|
-
padding: '8px 8px',
|
|
711
|
-
border: 'none',
|
|
712
|
-
borderRadius: 0,
|
|
713
|
-
background: active
|
|
714
|
-
? theme.colors.background
|
|
715
|
-
: 'transparent',
|
|
716
|
-
color: active ? theme.colors.text : muted,
|
|
717
|
-
fontSize: theme.fontSizes[0],
|
|
718
|
-
fontFamily: theme.fonts.monospace,
|
|
719
|
-
letterSpacing: 0.4,
|
|
720
|
-
textTransform: 'uppercase',
|
|
721
|
-
cursor: 'pointer',
|
|
722
|
-
}}
|
|
723
|
-
>
|
|
724
|
-
{tab === 'constructs' ? 'Constructs' : 'Files'}
|
|
725
|
-
</button>
|
|
726
|
-
);
|
|
727
|
-
})}
|
|
728
|
-
</div>
|
|
729
|
-
{effectiveTab === 'constructs' ? (
|
|
730
|
-
<ul
|
|
731
|
-
role="listbox"
|
|
732
|
-
aria-label="Constructs"
|
|
733
|
-
aria-multiselectable="true"
|
|
734
|
-
aria-activedescendant={focusedAlias ? `construct-${focusedAlias}` : undefined}
|
|
735
|
-
style={{
|
|
736
|
-
listStyle: 'none',
|
|
737
|
-
margin: 0,
|
|
738
|
-
padding: '0 8px 12px',
|
|
739
|
-
overflowY: 'auto',
|
|
740
|
-
flex: 1,
|
|
741
|
-
minHeight: 0,
|
|
742
|
-
}}
|
|
743
|
-
>
|
|
744
|
-
{orderedComponents.map((c) => {
|
|
745
|
-
const visible = visibleSet.has(c.alias);
|
|
746
|
-
const focused = c.alias === focusedAlias;
|
|
747
|
-
const color = componentColor(c, pierreTheme);
|
|
748
|
-
const badgeColor = constructBadgeColor(c) ?? color;
|
|
749
|
-
const badgeLabel = constructBadgeLabel(c);
|
|
750
|
-
const displayName = deriveNameFromSymbol(
|
|
751
|
-
c.symbol,
|
|
752
|
-
c.construct,
|
|
753
|
-
c.name,
|
|
754
|
-
c.file,
|
|
755
|
-
c.stereotype,
|
|
756
|
-
);
|
|
757
|
-
const rowOpen =
|
|
758
|
-
drawer != null &&
|
|
759
|
-
drawer.startLine != null &&
|
|
760
|
-
!!c.file &&
|
|
761
|
-
c.file === drawer.file &&
|
|
762
|
-
c.declarationRef?.startLine === drawer.startLine;
|
|
763
|
-
const hovered = hoveredRow === c.alias;
|
|
764
|
-
return (
|
|
765
|
-
<li key={c.alias} role="presentation">
|
|
766
|
-
<button
|
|
767
|
-
type="button"
|
|
768
|
-
id={`construct-${c.alias}`}
|
|
769
|
-
role="option"
|
|
770
|
-
aria-selected={visible}
|
|
771
|
-
data-testid={`construct-row-${c.alias}`}
|
|
772
|
-
onClick={() => toggle(c.alias)}
|
|
773
|
-
onMouseEnter={() => setHoveredRow(c.alias)}
|
|
774
|
-
onMouseLeave={() =>
|
|
775
|
-
setHoveredRow((h) => (h === c.alias ? null : h))
|
|
776
|
-
}
|
|
777
|
-
style={{
|
|
778
|
-
display: 'flex',
|
|
779
|
-
flexDirection: 'column',
|
|
780
|
-
alignItems: 'stretch',
|
|
781
|
-
gap: 2,
|
|
782
|
-
width: '100%',
|
|
783
|
-
margin: '2px 0',
|
|
784
|
-
padding: '8px 10px 8px 12px',
|
|
785
|
-
textAlign: 'left',
|
|
786
|
-
border: 'none',
|
|
787
|
-
borderRadius: 6,
|
|
788
|
-
background: hovered
|
|
789
|
-
? theme.colors.border
|
|
790
|
-
: visible
|
|
791
|
-
? theme.colors.background
|
|
792
|
-
: 'transparent',
|
|
793
|
-
cursor: 'pointer',
|
|
794
|
-
transition: 'background 100ms ease, opacity 100ms ease',
|
|
795
|
-
boxShadow: [
|
|
796
|
-
visible ? `inset 0 0 0 1px ${theme.colors.border}` : null,
|
|
797
|
-
rowOpen ? `inset 2px 0 0 ${accentColor}` : null,
|
|
798
|
-
]
|
|
799
|
-
.filter(Boolean)
|
|
800
|
-
.join(', ') || undefined,
|
|
801
|
-
outline: focused && !visible ? `1px dotted ${muted}` : undefined,
|
|
802
|
-
outlineOffset: -1,
|
|
803
|
-
opacity: visible ? 1 : hovered ? 1 : 0.72,
|
|
804
|
-
}}
|
|
805
|
-
>
|
|
806
|
-
<span
|
|
807
|
-
style={{
|
|
808
|
-
display: 'flex',
|
|
809
|
-
alignItems: 'baseline',
|
|
810
|
-
gap: 8,
|
|
811
|
-
minWidth: 0,
|
|
812
|
-
}}
|
|
813
|
-
>
|
|
814
|
-
<span
|
|
815
|
-
style={{
|
|
816
|
-
flex: 1,
|
|
817
|
-
minWidth: 0,
|
|
818
|
-
overflow: 'hidden',
|
|
819
|
-
textOverflow: 'ellipsis',
|
|
820
|
-
whiteSpace: 'nowrap',
|
|
821
|
-
fontFamily: theme.fonts.monospace,
|
|
822
|
-
fontSize: theme.fontSizes[1],
|
|
823
|
-
color,
|
|
824
|
-
fontWeight: visible ? 600 : 500,
|
|
825
|
-
}}
|
|
826
|
-
>
|
|
827
|
-
{displayName}
|
|
828
|
-
</span>
|
|
829
|
-
</span>
|
|
830
|
-
{badgeLabel && (
|
|
831
|
-
<span
|
|
832
|
-
style={{
|
|
833
|
-
display: 'flex',
|
|
834
|
-
alignItems: 'center',
|
|
835
|
-
minWidth: 0,
|
|
836
|
-
}}
|
|
837
|
-
>
|
|
838
|
-
<span
|
|
839
|
-
style={{
|
|
840
|
-
flex: 1,
|
|
841
|
-
minWidth: 0,
|
|
842
|
-
overflow: 'hidden',
|
|
843
|
-
textOverflow: 'ellipsis',
|
|
844
|
-
whiteSpace: 'nowrap',
|
|
845
|
-
fontFamily: theme.fonts.monospace,
|
|
846
|
-
fontSize: theme.fontSizes[0],
|
|
847
|
-
letterSpacing: 0.4,
|
|
848
|
-
textTransform: 'uppercase',
|
|
849
|
-
color: badgeColor,
|
|
850
|
-
}}
|
|
851
|
-
>
|
|
852
|
-
{badgeLabel}
|
|
853
|
-
</span>
|
|
854
|
-
</span>
|
|
855
|
-
)}
|
|
856
|
-
</button>
|
|
857
|
-
</li>
|
|
858
|
-
);
|
|
859
|
-
})}
|
|
860
|
-
</ul>
|
|
861
|
-
) : (
|
|
665
|
+
{hasFiles ? (
|
|
862
666
|
<ul
|
|
863
667
|
role="tree"
|
|
864
668
|
aria-label="Files"
|
|
@@ -873,6 +677,18 @@ export function ConstructsCatalog({
|
|
|
873
677
|
>
|
|
874
678
|
{renderTree(fileTree, 0)}
|
|
875
679
|
</ul>
|
|
680
|
+
) : (
|
|
681
|
+
<p
|
|
682
|
+
style={{
|
|
683
|
+
margin: 0,
|
|
684
|
+
padding: '12px 16px',
|
|
685
|
+
color: muted,
|
|
686
|
+
fontFamily: theme.fonts.body,
|
|
687
|
+
fontSize: theme.fontSizes[0],
|
|
688
|
+
}}
|
|
689
|
+
>
|
|
690
|
+
No files in this model.
|
|
691
|
+
</p>
|
|
876
692
|
)}
|
|
877
693
|
</aside>
|
|
878
694
|
<div
|
|
@@ -885,6 +701,121 @@ export function ConstructsCatalog({
|
|
|
885
701
|
flexDirection: 'column',
|
|
886
702
|
}}
|
|
887
703
|
>
|
|
704
|
+
{description && descriptionVisible && (
|
|
705
|
+
<>
|
|
706
|
+
<div
|
|
707
|
+
onClick={() => setDescriptionVisible(false)}
|
|
708
|
+
style={{
|
|
709
|
+
position: 'absolute',
|
|
710
|
+
inset: 0,
|
|
711
|
+
zIndex: 2,
|
|
712
|
+
background: 'rgba(0,0,0,0.45)',
|
|
713
|
+
}}
|
|
714
|
+
/>
|
|
715
|
+
<div
|
|
716
|
+
style={{
|
|
717
|
+
position: 'absolute',
|
|
718
|
+
top: '50%',
|
|
719
|
+
left: '50%',
|
|
720
|
+
transform: 'translate(-50%, -50%)',
|
|
721
|
+
zIndex: 3,
|
|
722
|
+
width: 360,
|
|
723
|
+
maxWidth: 'calc(100% - 16px)',
|
|
724
|
+
maxHeight: '70%',
|
|
725
|
+
overflow: 'auto',
|
|
726
|
+
padding: 16,
|
|
727
|
+
border: `1px solid ${theme.colors.border}`,
|
|
728
|
+
borderRadius: 8,
|
|
729
|
+
background: theme.colors.background,
|
|
730
|
+
boxShadow: '0 8px 24px rgba(0,0,0,0.28)',
|
|
731
|
+
fontSize: theme.fontSizes[0],
|
|
732
|
+
lineHeight: 1.5,
|
|
733
|
+
}}
|
|
734
|
+
>
|
|
735
|
+
<div
|
|
736
|
+
style={{
|
|
737
|
+
display: 'flex',
|
|
738
|
+
alignItems: 'center',
|
|
739
|
+
gap: 8,
|
|
740
|
+
marginBottom: 12,
|
|
741
|
+
}}
|
|
742
|
+
>
|
|
743
|
+
<h3
|
|
744
|
+
style={{
|
|
745
|
+
margin: 0,
|
|
746
|
+
flex: 1,
|
|
747
|
+
minWidth: 0,
|
|
748
|
+
fontSize: theme.fontSizes[1],
|
|
749
|
+
fontWeight: 600,
|
|
750
|
+
fontFamily: theme.fonts.monospace,
|
|
751
|
+
letterSpacing: 0.4,
|
|
752
|
+
textTransform: 'uppercase',
|
|
753
|
+
color: theme.colors.textSecondary ?? muted,
|
|
754
|
+
}}
|
|
755
|
+
>
|
|
756
|
+
Overview
|
|
757
|
+
</h3>
|
|
758
|
+
<button
|
|
759
|
+
type="button"
|
|
760
|
+
aria-label="Close description"
|
|
761
|
+
title="Close"
|
|
762
|
+
onClick={() => setDescriptionVisible(false)}
|
|
763
|
+
style={{
|
|
764
|
+
display: 'inline-flex',
|
|
765
|
+
alignItems: 'center',
|
|
766
|
+
justifyContent: 'center',
|
|
767
|
+
flexShrink: 0,
|
|
768
|
+
width: 22,
|
|
769
|
+
height: 22,
|
|
770
|
+
padding: 0,
|
|
771
|
+
border: 'none',
|
|
772
|
+
borderRadius: 4,
|
|
773
|
+
background: 'transparent',
|
|
774
|
+
color: muted,
|
|
775
|
+
cursor: 'pointer',
|
|
776
|
+
}}
|
|
777
|
+
>
|
|
778
|
+
<X size={14} />
|
|
779
|
+
</button>
|
|
780
|
+
</div>
|
|
781
|
+
<IndustryMarkdownSlide
|
|
782
|
+
content={description}
|
|
783
|
+
slideIdPrefix="constructs-desc-overlay"
|
|
784
|
+
slideIndex={0}
|
|
785
|
+
isVisible
|
|
786
|
+
theme={theme}
|
|
787
|
+
disableScroll
|
|
788
|
+
disableBasePadding
|
|
789
|
+
enableKeyboardScrolling={false}
|
|
790
|
+
autoFocusOnVisible={false}
|
|
791
|
+
/>
|
|
792
|
+
</div>
|
|
793
|
+
</>
|
|
794
|
+
)}
|
|
795
|
+
{searchActive && (
|
|
796
|
+
<div style={{ flexShrink: 0, padding: '12px 16px 0' }}>
|
|
797
|
+
<input
|
|
798
|
+
type="search"
|
|
799
|
+
value={searchQuery}
|
|
800
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
801
|
+
placeholder="Search constructs…"
|
|
802
|
+
aria-label="Search constructs"
|
|
803
|
+
data-testid="construct-search"
|
|
804
|
+
style={{
|
|
805
|
+
width: '100%',
|
|
806
|
+
boxSizing: 'border-box',
|
|
807
|
+
padding: '8px 10px',
|
|
808
|
+
border: `1px solid ${theme.colors.border}`,
|
|
809
|
+
borderRadius: 6,
|
|
810
|
+
background: theme.colors.background,
|
|
811
|
+
color: theme.colors.text,
|
|
812
|
+
fontFamily: theme.fonts.monospace,
|
|
813
|
+
fontSize: theme.fontSizes[1],
|
|
814
|
+
outline: 'none',
|
|
815
|
+
}}
|
|
816
|
+
/>
|
|
817
|
+
</div>
|
|
818
|
+
)}
|
|
888
819
|
<div
|
|
889
820
|
data-testid="construct-signature"
|
|
890
821
|
style={{
|
|
@@ -897,7 +828,7 @@ export function ConstructsCatalog({
|
|
|
897
828
|
gap: 12,
|
|
898
829
|
}}
|
|
899
830
|
>
|
|
900
|
-
{
|
|
831
|
+
{searchResults.length > 0 ? (
|
|
901
832
|
repoGroups.map((group) => (
|
|
902
833
|
<div
|
|
903
834
|
key={group.key}
|
|
@@ -931,53 +862,210 @@ export function ConstructsCatalog({
|
|
|
931
862
|
{group.label}
|
|
932
863
|
</span>
|
|
933
864
|
</div>
|
|
934
|
-
{group.items.map((
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
865
|
+
{splitByFileRun(group.items).map((run) => {
|
|
866
|
+
const lead = run[0]!;
|
|
867
|
+
const hasFile = !!lead.file;
|
|
868
|
+
const isExternal = lead.construct === 'external';
|
|
869
|
+
// File-backed runs and externals both get a header; externals
|
|
870
|
+
// name their kind instead of a path, so the body doesn't have
|
|
871
|
+
// to spell it out.
|
|
872
|
+
const showHeader = hasFile || isExternal;
|
|
873
|
+
const runKey = `${group.key}:${lead.file || lead.alias}`;
|
|
874
|
+
const runHasPurpose = run.some((c) => !!c.purpose?.trim());
|
|
875
|
+
const purposeOpen = purposeOverrides[runKey] ?? isExternal;
|
|
876
|
+
const runFileOpen =
|
|
877
|
+
drawer != null && hasFile && lead.file === drawer.file;
|
|
943
878
|
return (
|
|
944
879
|
<div
|
|
945
|
-
key={
|
|
946
|
-
id={`construct-signature-${c.alias}`}
|
|
880
|
+
key={runKey}
|
|
947
881
|
style={{
|
|
948
|
-
|
|
949
|
-
|
|
882
|
+
display: 'flex',
|
|
883
|
+
flexDirection: 'column',
|
|
884
|
+
border: `1px solid ${runFileOpen ? accentColor : theme.colors.border}`,
|
|
950
885
|
borderRadius: 8,
|
|
951
886
|
overflow: 'hidden',
|
|
952
887
|
flexShrink: 0,
|
|
953
888
|
}}
|
|
954
889
|
>
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
890
|
+
{showHeader && (
|
|
891
|
+
<div
|
|
892
|
+
style={{
|
|
893
|
+
display: 'flex',
|
|
894
|
+
alignItems: 'center',
|
|
895
|
+
gap: 6,
|
|
896
|
+
width: '100%',
|
|
897
|
+
boxSizing: 'border-box',
|
|
898
|
+
padding: '6px 10px',
|
|
899
|
+
borderBottom: `1px solid ${
|
|
900
|
+
runFileOpen ? accentColor : theme.colors.border
|
|
901
|
+
}`,
|
|
902
|
+
background: runFileOpen
|
|
903
|
+
? `${accentColor}14`
|
|
904
|
+
: theme.colors.backgroundSecondary ??
|
|
905
|
+
theme.colors.background,
|
|
906
|
+
color: runFileOpen
|
|
907
|
+
? accentColor
|
|
908
|
+
: theme.colors.textSecondary ?? muted,
|
|
909
|
+
fontFamily: theme.fonts.monospace,
|
|
910
|
+
fontSize: theme.fontSizes[0],
|
|
911
|
+
}}
|
|
912
|
+
>
|
|
913
|
+
{isExternal ? (
|
|
914
|
+
<span
|
|
915
|
+
style={{
|
|
916
|
+
display: 'flex',
|
|
917
|
+
alignItems: 'center',
|
|
918
|
+
gap: 6,
|
|
919
|
+
flex: 1,
|
|
920
|
+
minWidth: 0,
|
|
921
|
+
}}
|
|
922
|
+
>
|
|
923
|
+
<Box size={12} style={{ flexShrink: 0 }} />
|
|
924
|
+
<span
|
|
925
|
+
style={{
|
|
926
|
+
overflow: 'hidden',
|
|
927
|
+
textOverflow: 'ellipsis',
|
|
928
|
+
whiteSpace: 'nowrap',
|
|
929
|
+
}}
|
|
930
|
+
>
|
|
931
|
+
external
|
|
932
|
+
</span>
|
|
933
|
+
</span>
|
|
934
|
+
) : (
|
|
935
|
+
<button
|
|
936
|
+
type="button"
|
|
937
|
+
title={fileViewer ? `Open ${lead.file}` : lead.file}
|
|
938
|
+
onClick={
|
|
939
|
+
fileViewer
|
|
940
|
+
? () => onOpenFile(lead.file)
|
|
941
|
+
: undefined
|
|
942
|
+
}
|
|
943
|
+
style={{
|
|
944
|
+
display: 'flex',
|
|
945
|
+
alignItems: 'center',
|
|
946
|
+
gap: 6,
|
|
947
|
+
flex: 1,
|
|
948
|
+
minWidth: 0,
|
|
949
|
+
padding: 0,
|
|
950
|
+
border: 'none',
|
|
951
|
+
background: 'transparent',
|
|
952
|
+
color: 'inherit',
|
|
953
|
+
fontFamily: 'inherit',
|
|
954
|
+
fontSize: 'inherit',
|
|
955
|
+
textAlign: 'left',
|
|
956
|
+
cursor: fileViewer ? 'pointer' : 'default',
|
|
957
|
+
}}
|
|
958
|
+
>
|
|
959
|
+
<FileText size={12} style={{ flexShrink: 0 }} />
|
|
960
|
+
<span
|
|
961
|
+
style={{
|
|
962
|
+
overflow: 'hidden',
|
|
963
|
+
textOverflow: 'ellipsis',
|
|
964
|
+
whiteSpace: 'nowrap',
|
|
965
|
+
}}
|
|
966
|
+
>
|
|
967
|
+
{lead.file}
|
|
968
|
+
</span>
|
|
969
|
+
</button>
|
|
970
|
+
)}
|
|
971
|
+
{runHasPurpose && (
|
|
972
|
+
<button
|
|
973
|
+
type="button"
|
|
974
|
+
title={
|
|
975
|
+
purposeOpen ? 'Hide description' : 'Show description'
|
|
976
|
+
}
|
|
977
|
+
aria-label={
|
|
978
|
+
purposeOpen ? 'Hide description' : 'Show description'
|
|
979
|
+
}
|
|
980
|
+
aria-expanded={purposeOpen}
|
|
981
|
+
onClick={() => setPurposeOpen(runKey, !purposeOpen)}
|
|
982
|
+
style={{
|
|
983
|
+
display: 'inline-flex',
|
|
984
|
+
alignItems: 'center',
|
|
985
|
+
justifyContent: 'center',
|
|
986
|
+
flexShrink: 0,
|
|
987
|
+
width: 20,
|
|
988
|
+
height: 20,
|
|
989
|
+
padding: 0,
|
|
990
|
+
border: 'none',
|
|
991
|
+
borderRadius: 4,
|
|
992
|
+
background: 'transparent',
|
|
993
|
+
color: purposeOpen
|
|
994
|
+
? accentColor
|
|
995
|
+
: theme.colors.textSecondary ?? muted,
|
|
996
|
+
cursor: 'pointer',
|
|
997
|
+
}}
|
|
998
|
+
>
|
|
999
|
+
<AlignLeft size={13} />
|
|
1000
|
+
</button>
|
|
1001
|
+
)}
|
|
1002
|
+
</div>
|
|
1003
|
+
)}
|
|
1004
|
+
{run.map((c, i) => {
|
|
1005
|
+
// File-level: every declaration sharing the open file
|
|
1006
|
+
// shows the indicator. Line-level: only the declaration
|
|
1007
|
+
// actually highlighted gets the inner accent ring.
|
|
1008
|
+
const fileOpen =
|
|
1009
|
+
drawer != null && !!c.file && c.file === drawer.file;
|
|
1010
|
+
const lineOpen =
|
|
1011
|
+
fileOpen &&
|
|
1012
|
+
drawer.startLine != null &&
|
|
1013
|
+
c.declarationRef?.startLine === drawer.startLine;
|
|
1014
|
+
// A combined header owns the path, so declarations under
|
|
1015
|
+
// it drop their own badge; those with no known line just
|
|
1016
|
+
// render unnumbered. Without a header, keep the badge.
|
|
1017
|
+
const numbered =
|
|
1018
|
+
hasFile && c.declarationRef?.startLine != null;
|
|
1019
|
+
// The header names the external kind, so the body just
|
|
1020
|
+
// names the construct.
|
|
1021
|
+
const isExt = c.construct === 'external';
|
|
1022
|
+
return (
|
|
1023
|
+
<div
|
|
1024
|
+
key={c.alias}
|
|
1025
|
+
id={`construct-signature-${c.alias}`}
|
|
1026
|
+
style={{
|
|
1027
|
+
borderTop:
|
|
1028
|
+
i > 0
|
|
1029
|
+
? `1px solid ${theme.colors.border}`
|
|
1030
|
+
: undefined,
|
|
1031
|
+
boxShadow: lineOpen
|
|
1032
|
+
? `inset 0 0 0 1px ${accentColor}`
|
|
1033
|
+
: undefined,
|
|
1034
|
+
}}
|
|
1035
|
+
>
|
|
1036
|
+
<ComponentDeclaration
|
|
1037
|
+
component={c}
|
|
1038
|
+
onOpenFile={fileViewer ? onOpenFile : undefined}
|
|
1039
|
+
defaultShowFile={showHeader ? false : !!fileViewer}
|
|
1040
|
+
onRelatedSelect={onRelatedSelect}
|
|
1041
|
+
onInspectSymbol={
|
|
1042
|
+
onInspectSymbol
|
|
1043
|
+
? (symbol, ref) =>
|
|
1044
|
+
onInspectSymbol({
|
|
1045
|
+
purl: c.purl,
|
|
1046
|
+
file: c.file,
|
|
1047
|
+
symbol,
|
|
1048
|
+
ref,
|
|
1049
|
+
})
|
|
1050
|
+
: undefined
|
|
1051
|
+
}
|
|
1052
|
+
fileOpen={fileOpen}
|
|
1053
|
+
declarationOpen={lineOpen}
|
|
1054
|
+
showRepoIdentity={false}
|
|
1055
|
+
fileBadgeChrome={showHeader ? false : !!fileViewer}
|
|
1056
|
+
hideActions={showHeader}
|
|
1057
|
+
externalName={isExt}
|
|
1058
|
+
showPurpose={showHeader ? purposeOpen : undefined}
|
|
1059
|
+
lineNumbers={numbered}
|
|
1060
|
+
verification={
|
|
1061
|
+
componentVerification && focusedAlias === c.alias
|
|
1062
|
+
? componentVerification
|
|
1063
|
+
: undefined
|
|
1064
|
+
}
|
|
1065
|
+
/>
|
|
1066
|
+
</div>
|
|
1067
|
+
);
|
|
1068
|
+
})}
|
|
981
1069
|
</div>
|
|
982
1070
|
);
|
|
983
1071
|
})}
|
|
@@ -992,7 +1080,9 @@ export function ConstructsCatalog({
|
|
|
992
1080
|
fontSize: theme.fontSizes[1],
|
|
993
1081
|
}}
|
|
994
1082
|
>
|
|
995
|
-
|
|
1083
|
+
{searchQuery.trim()
|
|
1084
|
+
? `No constructs match “${searchQuery.trim()}”.`
|
|
1085
|
+
: 'No constructs in this model.'}
|
|
996
1086
|
</p>
|
|
997
1087
|
)}
|
|
998
1088
|
</div>
|