@principal-ai/subsystems-react 0.24.27 → 0.25.0
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/stories/Subsystem/ComponentGraph/fixtures.d.ts +1 -1
- package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts.map +1 -1
- package/dist/stories/Subsystem/ComponentGraph/fixtures.js +7 -7
- package/dist/stories/Subsystem/ComponentGraph/fixtures.js.map +1 -1
- package/dist/subsystem/ComponentDeclaration.d.ts +1 -1
- package/dist/subsystem/ComponentDeclaration.d.ts.map +1 -1
- package/dist/subsystem/ComponentDeclaration.js +1 -1
- package/dist/subsystem/ComponentDeclaration.js.map +1 -1
- package/dist/subsystem/ConstructsCatalog.d.ts +2 -2
- package/dist/subsystem/ConstructsCatalog.d.ts.map +1 -1
- package/dist/subsystem/ConstructsCatalog.js +62 -62
- package/dist/subsystem/ConstructsCatalog.js.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts +2 -2
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +27 -27
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/model.d.ts +11 -5
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js +36 -36
- package/dist/subsystem/model.js.map +1 -1
- package/dist/subsystem/nodes.d.ts +3 -3
- package/dist/subsystem/nodes.d.ts.map +1 -1
- package/dist/subsystem/nodes.js +3 -3
- package/dist/subsystem/nodes.js.map +1 -1
- package/package.json +1 -1
- package/src/stories/Subsystem/ComponentGraph/ModuleBadges.stories.tsx +1 -1
- package/src/stories/Subsystem/ComponentGraph/fixtures.ts +8 -8
- package/src/subsystem/ComponentDeclaration.tsx +2 -2
- package/src/subsystem/ConstructsCatalog.tsx +71 -75
- package/src/subsystem/SubsystemComponentGraph.tsx +28 -28
- package/src/subsystem/model.test.ts +51 -51
- package/src/subsystem/model.ts +47 -41
- package/src/subsystem/nodes.tsx +6 -6
|
@@ -11,7 +11,7 @@ import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
|
11
11
|
import type { ReactNode } from 'react';
|
|
12
12
|
import { useTheme } from '@principal-ade/industry-theme';
|
|
13
13
|
import { IndustryMarkdownSlide } from 'themed-markdown';
|
|
14
|
-
import { ChevronDown, ChevronRight,
|
|
14
|
+
import { ChevronDown, ChevronRight, FileText, Folder } from 'lucide-react';
|
|
15
15
|
import {
|
|
16
16
|
constructBadgeColor,
|
|
17
17
|
constructBadgeLabel,
|
|
@@ -29,7 +29,7 @@ import { resolvePierreSyntaxThemeName } from '../pierre/pierreSyntaxTheme';
|
|
|
29
29
|
|
|
30
30
|
export interface ConstructsCatalogProps {
|
|
31
31
|
components: SubsystemComponent[];
|
|
32
|
-
onSelect?: (
|
|
32
|
+
onSelect?: (componentAlias: string) => void;
|
|
33
33
|
title?: string;
|
|
34
34
|
hideSidebar?: boolean;
|
|
35
35
|
description?: string;
|
|
@@ -39,7 +39,7 @@ export interface ConstructsCatalogProps {
|
|
|
39
39
|
renderFileViewer?: (file: string, opts?: SubsystemOpenFileOptions) => ReactNode;
|
|
40
40
|
renderFileView?: (component: SubsystemComponent) => ReactNode;
|
|
41
41
|
onFileSelect?: (file: string) => void;
|
|
42
|
-
onVerifyComponent?: (
|
|
42
|
+
onVerifyComponent?: (componentAlias: string) => void;
|
|
43
43
|
componentVerification?: ComponentVerificationState | null;
|
|
44
44
|
}
|
|
45
45
|
|
|
@@ -111,7 +111,7 @@ function resolveRelated(
|
|
|
111
111
|
const clean = ref.replace(/\(\)$/, '');
|
|
112
112
|
return components.find(
|
|
113
113
|
(c) =>
|
|
114
|
-
c.
|
|
114
|
+
c.alias === clean ||
|
|
115
115
|
c.name === clean ||
|
|
116
116
|
c.symbol === clean ||
|
|
117
117
|
c.symbol?.replace(/\(\)$/, '') === clean,
|
|
@@ -170,11 +170,11 @@ export function ConstructsCatalog({
|
|
|
170
170
|
.map((x) => x.c);
|
|
171
171
|
}, [components]);
|
|
172
172
|
|
|
173
|
-
const [
|
|
174
|
-
orderedComponents[0] ? [orderedComponents[0].
|
|
173
|
+
const [visibleAliases, setVisibleAliases] = useState<string[]>(() =>
|
|
174
|
+
orderedComponents[0] ? [orderedComponents[0].alias] : [],
|
|
175
175
|
);
|
|
176
|
-
const [
|
|
177
|
-
() => orderedComponents[0]?.
|
|
176
|
+
const [focusedAlias, setFocusedAlias] = useState<string | null>(
|
|
177
|
+
() => orderedComponents[0]?.alias ?? null,
|
|
178
178
|
);
|
|
179
179
|
const [descriptionVisible, setDescriptionVisible] = useState(false);
|
|
180
180
|
const [descToggleHover, setDescToggleHover] = useState(false);
|
|
@@ -186,12 +186,12 @@ export function ConstructsCatalog({
|
|
|
186
186
|
const [drawer, setDrawer] = useState<{ file: string; startLine?: number } | null>(
|
|
187
187
|
null,
|
|
188
188
|
);
|
|
189
|
-
const
|
|
190
|
-
|
|
189
|
+
const focusedAliasRef = useRef<string | null>(focusedAlias);
|
|
190
|
+
focusedAliasRef.current = focusedAlias;
|
|
191
191
|
|
|
192
|
-
const visibleSet = useMemo(() => new Set(
|
|
192
|
+
const visibleSet = useMemo(() => new Set(visibleAliases), [visibleAliases]);
|
|
193
193
|
const visibleComponents = useMemo(
|
|
194
|
-
() => orderedComponents.filter((c) => visibleSet.has(c.
|
|
194
|
+
() => orderedComponents.filter((c) => visibleSet.has(c.alias)),
|
|
195
195
|
[orderedComponents, visibleSet],
|
|
196
196
|
);
|
|
197
197
|
|
|
@@ -213,7 +213,7 @@ export function ConstructsCatalog({
|
|
|
213
213
|
}> = [];
|
|
214
214
|
const index = new Map<string, number>();
|
|
215
215
|
for (const c of visibleComponents) {
|
|
216
|
-
const key = c.purl || c.
|
|
216
|
+
const key = c.purl || c.alias;
|
|
217
217
|
let at = index.get(key);
|
|
218
218
|
if (at == null) {
|
|
219
219
|
at = groups.length;
|
|
@@ -227,17 +227,17 @@ export function ConstructsCatalog({
|
|
|
227
227
|
}, [visibleComponents]);
|
|
228
228
|
|
|
229
229
|
useEffect(() => {
|
|
230
|
-
if (!
|
|
231
|
-
document.getElementById(`construct-${
|
|
232
|
-
}, [
|
|
230
|
+
if (!focusedAlias) return;
|
|
231
|
+
document.getElementById(`construct-${focusedAlias}`)?.scrollIntoView({ block: 'nearest' });
|
|
232
|
+
}, [focusedAlias]);
|
|
233
233
|
|
|
234
234
|
useEffect(() => {
|
|
235
|
-
const last =
|
|
235
|
+
const last = visibleAliases[visibleAliases.length - 1];
|
|
236
236
|
if (!last) return;
|
|
237
237
|
document
|
|
238
238
|
.getElementById(`construct-signature-${last}`)
|
|
239
239
|
?.scrollIntoView({ block: 'nearest' });
|
|
240
|
-
}, [
|
|
240
|
+
}, [visibleAliases]);
|
|
241
241
|
|
|
242
242
|
// Keep the declaration whose file is open in view, so the source and its
|
|
243
243
|
// signature stay legible together.
|
|
@@ -249,47 +249,47 @@ export function ConstructsCatalog({
|
|
|
249
249
|
) ?? orderedComponents.find((c) => c.file === drawer.file);
|
|
250
250
|
if (!match) return;
|
|
251
251
|
document
|
|
252
|
-
.getElementById(`construct-signature-${match.
|
|
252
|
+
.getElementById(`construct-signature-${match.alias}`)
|
|
253
253
|
?.scrollIntoView({ block: 'nearest' });
|
|
254
254
|
}, [orderedComponents, drawer]);
|
|
255
255
|
|
|
256
|
-
// Drop
|
|
256
|
+
// Drop aliases that left the model; if nothing remains, show the first construct.
|
|
257
257
|
useEffect(() => {
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
const next = prev.filter((
|
|
258
|
+
const aliases = new Set(orderedComponents.map((c) => c.alias));
|
|
259
|
+
setVisibleAliases((prev) => {
|
|
260
|
+
const next = prev.filter((alias) => aliases.has(alias));
|
|
261
261
|
if (next.length > 0 || orderedComponents.length === 0) return next;
|
|
262
|
-
return [orderedComponents[0]!.
|
|
262
|
+
return [orderedComponents[0]!.alias];
|
|
263
263
|
});
|
|
264
|
-
|
|
265
|
-
if (prev &&
|
|
266
|
-
return orderedComponents[0]?.
|
|
264
|
+
setFocusedAlias((prev) => {
|
|
265
|
+
if (prev && aliases.has(prev)) return prev;
|
|
266
|
+
return orderedComponents[0]?.alias ?? null;
|
|
267
267
|
});
|
|
268
268
|
}, [orderedComponents]);
|
|
269
269
|
|
|
270
270
|
const toggle = useCallback(
|
|
271
|
-
(
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
prev.includes(
|
|
271
|
+
(alias: string) => {
|
|
272
|
+
setFocusedAlias(alias);
|
|
273
|
+
setVisibleAliases((prev) =>
|
|
274
|
+
prev.includes(alias) ? prev.filter((x) => x !== alias) : [...prev, alias],
|
|
275
275
|
);
|
|
276
|
-
onSelect?.(
|
|
276
|
+
onSelect?.(alias);
|
|
277
277
|
},
|
|
278
278
|
[onSelect],
|
|
279
279
|
);
|
|
280
280
|
|
|
281
281
|
const show = useCallback(
|
|
282
|
-
(
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
onSelect?.(
|
|
282
|
+
(alias: string) => {
|
|
283
|
+
setFocusedAlias(alias);
|
|
284
|
+
setVisibleAliases((prev) => (prev.includes(alias) ? prev : [...prev, alias]));
|
|
285
|
+
onSelect?.(alias);
|
|
286
286
|
},
|
|
287
287
|
[onSelect],
|
|
288
288
|
);
|
|
289
289
|
|
|
290
290
|
const focusAt = useCallback(
|
|
291
|
-
(
|
|
292
|
-
|
|
291
|
+
(alias: string) => {
|
|
292
|
+
setFocusedAlias(alias);
|
|
293
293
|
},
|
|
294
294
|
[],
|
|
295
295
|
);
|
|
@@ -307,17 +307,17 @@ export function ConstructsCatalog({
|
|
|
307
307
|
* all when they're already shown — mirroring a construct row toggle. */
|
|
308
308
|
const toggleFile = useCallback(
|
|
309
309
|
(comps: SubsystemComponent[]) => {
|
|
310
|
-
const
|
|
311
|
-
|
|
310
|
+
const aliases = comps.map((c) => c.alias);
|
|
311
|
+
setVisibleAliases((prev) => {
|
|
312
312
|
const set = new Set(prev);
|
|
313
|
-
const allVisible =
|
|
314
|
-
|
|
315
|
-
return orderedComponents.filter((c) => set.has(c.
|
|
313
|
+
const allVisible = aliases.every((alias) => set.has(alias));
|
|
314
|
+
aliases.forEach((alias) => (allVisible ? set.delete(alias) : set.add(alias)));
|
|
315
|
+
return orderedComponents.filter((c) => set.has(c.alias)).map((c) => c.alias);
|
|
316
316
|
});
|
|
317
317
|
const first = comps[0];
|
|
318
318
|
if (first) {
|
|
319
|
-
|
|
320
|
-
onSelect?.(first.
|
|
319
|
+
setFocusedAlias(first.alias);
|
|
320
|
+
onSelect?.(first.alias);
|
|
321
321
|
}
|
|
322
322
|
},
|
|
323
323
|
[orderedComponents, onSelect],
|
|
@@ -380,8 +380,8 @@ export function ConstructsCatalog({
|
|
|
380
380
|
</li>
|
|
381
381
|
);
|
|
382
382
|
}
|
|
383
|
-
const
|
|
384
|
-
const allVisible =
|
|
383
|
+
const aliases = child.components.map((c) => c.alias);
|
|
384
|
+
const allVisible = aliases.length > 0 && aliases.every((alias) => visibleSet.has(alias));
|
|
385
385
|
const hovered = hoveredRow === child.path;
|
|
386
386
|
const color = componentColor(child.components[0]!, pierreTheme);
|
|
387
387
|
return (
|
|
@@ -438,7 +438,7 @@ export function ConstructsCatalog({
|
|
|
438
438
|
color: allVisible ? color : muted,
|
|
439
439
|
}}
|
|
440
440
|
>
|
|
441
|
-
{
|
|
441
|
+
{aliases.length}
|
|
442
442
|
</span>
|
|
443
443
|
</button>
|
|
444
444
|
</li>
|
|
@@ -473,7 +473,7 @@ export function ConstructsCatalog({
|
|
|
473
473
|
(ref: string) => {
|
|
474
474
|
const comp = resolveRelated(components, ref);
|
|
475
475
|
if (!comp) return;
|
|
476
|
-
show(comp.
|
|
476
|
+
show(comp.alias);
|
|
477
477
|
},
|
|
478
478
|
[components, show],
|
|
479
479
|
);
|
|
@@ -505,13 +505,13 @@ export function ConstructsCatalog({
|
|
|
505
505
|
if (orderedComponents.length === 0) return;
|
|
506
506
|
const idx = Math.max(
|
|
507
507
|
0,
|
|
508
|
-
orderedComponents.findIndex((c) => c.
|
|
508
|
+
orderedComponents.findIndex((c) => c.alias === focusedAliasRef.current),
|
|
509
509
|
);
|
|
510
510
|
const next =
|
|
511
511
|
orderedComponents[
|
|
512
512
|
(idx + delta + orderedComponents.length) % orderedComponents.length
|
|
513
513
|
]!;
|
|
514
|
-
focusAt(next.
|
|
514
|
+
focusAt(next.alias);
|
|
515
515
|
},
|
|
516
516
|
[orderedComponents, focusAt],
|
|
517
517
|
);
|
|
@@ -537,15 +537,15 @@ export function ConstructsCatalog({
|
|
|
537
537
|
moveFocus(-1);
|
|
538
538
|
} else if (e.key === 'Home') {
|
|
539
539
|
e.preventDefault();
|
|
540
|
-
if (orderedComponents[0]) focusAt(orderedComponents[0].
|
|
540
|
+
if (orderedComponents[0]) focusAt(orderedComponents[0].alias);
|
|
541
541
|
} else if (e.key === 'End') {
|
|
542
542
|
e.preventDefault();
|
|
543
543
|
const last = orderedComponents[orderedComponents.length - 1];
|
|
544
|
-
if (last) focusAt(last.
|
|
544
|
+
if (last) focusAt(last.alias);
|
|
545
545
|
} else if (e.key === ' ' || e.key === 'Enter') {
|
|
546
|
-
if (
|
|
546
|
+
if (focusedAliasRef.current) {
|
|
547
547
|
e.preventDefault();
|
|
548
|
-
toggle(
|
|
548
|
+
toggle(focusedAliasRef.current);
|
|
549
549
|
}
|
|
550
550
|
} else if (e.key === 'Escape' && drawer) {
|
|
551
551
|
setDrawer(null);
|
|
@@ -646,16 +646,12 @@ export function ConstructsCatalog({
|
|
|
646
646
|
padding: 0,
|
|
647
647
|
border: 'none',
|
|
648
648
|
borderRadius: 4,
|
|
649
|
-
background: descToggleHover ? theme.colors.border : 'transparent',
|
|
650
|
-
color: descToggleHover ? theme.colors.text : muted,
|
|
649
|
+
background: descriptionVisible || descToggleHover ? theme.colors.border : 'transparent',
|
|
650
|
+
color: descriptionVisible || descToggleHover ? theme.colors.text : muted,
|
|
651
651
|
cursor: 'pointer',
|
|
652
652
|
}}
|
|
653
653
|
>
|
|
654
|
-
{
|
|
655
|
-
<ChevronUp size={14} />
|
|
656
|
-
) : (
|
|
657
|
-
<ChevronDown size={14} />
|
|
658
|
-
)}
|
|
654
|
+
<FileText size={14} />
|
|
659
655
|
</button>
|
|
660
656
|
)}
|
|
661
657
|
</div>
|
|
@@ -728,7 +724,7 @@ export function ConstructsCatalog({
|
|
|
728
724
|
role="listbox"
|
|
729
725
|
aria-label="Constructs"
|
|
730
726
|
aria-multiselectable="true"
|
|
731
|
-
aria-activedescendant={
|
|
727
|
+
aria-activedescendant={focusedAlias ? `construct-${focusedAlias}` : undefined}
|
|
732
728
|
style={{
|
|
733
729
|
listStyle: 'none',
|
|
734
730
|
margin: 0,
|
|
@@ -739,8 +735,8 @@ export function ConstructsCatalog({
|
|
|
739
735
|
}}
|
|
740
736
|
>
|
|
741
737
|
{orderedComponents.map((c) => {
|
|
742
|
-
const visible = visibleSet.has(c.
|
|
743
|
-
const focused = c.
|
|
738
|
+
const visible = visibleSet.has(c.alias);
|
|
739
|
+
const focused = c.alias === focusedAlias;
|
|
744
740
|
const color = componentColor(c, pierreTheme);
|
|
745
741
|
const badgeColor = constructBadgeColor(c) ?? color;
|
|
746
742
|
const badgeLabel = constructBadgeLabel(c);
|
|
@@ -757,19 +753,19 @@ export function ConstructsCatalog({
|
|
|
757
753
|
!!c.file &&
|
|
758
754
|
c.file === drawer.file &&
|
|
759
755
|
c.declarationRef?.startLine === drawer.startLine;
|
|
760
|
-
const hovered = hoveredRow === c.
|
|
756
|
+
const hovered = hoveredRow === c.alias;
|
|
761
757
|
return (
|
|
762
|
-
<li key={c.
|
|
758
|
+
<li key={c.alias} role="presentation">
|
|
763
759
|
<button
|
|
764
760
|
type="button"
|
|
765
|
-
id={`construct-${c.
|
|
761
|
+
id={`construct-${c.alias}`}
|
|
766
762
|
role="option"
|
|
767
763
|
aria-selected={visible}
|
|
768
|
-
data-testid={`construct-row-${c.
|
|
769
|
-
onClick={() => toggle(c.
|
|
770
|
-
onMouseEnter={() => setHoveredRow(c.
|
|
764
|
+
data-testid={`construct-row-${c.alias}`}
|
|
765
|
+
onClick={() => toggle(c.alias)}
|
|
766
|
+
onMouseEnter={() => setHoveredRow(c.alias)}
|
|
771
767
|
onMouseLeave={() =>
|
|
772
|
-
setHoveredRow((h) => (h === c.
|
|
768
|
+
setHoveredRow((h) => (h === c.alias ? null : h))
|
|
773
769
|
}
|
|
774
770
|
style={{
|
|
775
771
|
display: 'flex',
|
|
@@ -938,8 +934,8 @@ export function ConstructsCatalog({
|
|
|
938
934
|
c.declarationRef?.startLine === drawer.startLine;
|
|
939
935
|
return (
|
|
940
936
|
<div
|
|
941
|
-
key={c.
|
|
942
|
-
id={`construct-signature-${c.
|
|
937
|
+
key={c.alias}
|
|
938
|
+
id={`construct-signature-${c.alias}`}
|
|
943
939
|
style={{
|
|
944
940
|
border: `1px solid ${lineOpen ? accentColor : theme.colors.border}`,
|
|
945
941
|
boxShadow: lineOpen ? `0 0 0 1px ${accentColor}` : undefined,
|
|
@@ -959,7 +955,7 @@ export function ConstructsCatalog({
|
|
|
959
955
|
showRepoIdentity={false}
|
|
960
956
|
fileBadgeChrome={!!fileViewer}
|
|
961
957
|
verification={
|
|
962
|
-
componentVerification &&
|
|
958
|
+
componentVerification && focusedAlias === c.alias
|
|
963
959
|
? componentVerification
|
|
964
960
|
: undefined
|
|
965
961
|
}
|
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
applyNodeChanges,
|
|
36
36
|
} from '@xyflow/react';
|
|
37
37
|
import { useTheme } from '@principal-ade/industry-theme';
|
|
38
|
-
import {
|
|
38
|
+
import { FileText, Pause, Play, X } from 'lucide-react';
|
|
39
39
|
import { IndustryMarkdownSlide } from 'themed-markdown';
|
|
40
40
|
import {
|
|
41
41
|
buildSubsystemGraph,
|
|
@@ -101,7 +101,7 @@ export interface SubsystemComponentGraphProps {
|
|
|
101
101
|
* (unselected ones dimmed); everything else is hidden.
|
|
102
102
|
*/
|
|
103
103
|
walkthroughs?: SubsystemWalkthrough[];
|
|
104
|
-
onSelect?: (
|
|
104
|
+
onSelect?: (componentAlias: string) => void;
|
|
105
105
|
/** Called when an edge is clicked (relationship / mechanism + refs seam). */
|
|
106
106
|
onEdgeSelect?: (edge: SubsystemComponentEdge) => void;
|
|
107
107
|
/** Upper bound for node width in px; nodes grow with content up to this,
|
|
@@ -217,7 +217,7 @@ export interface SubsystemComponentGraphProps {
|
|
|
217
217
|
*/
|
|
218
218
|
onFileSelect?: (file: string) => void;
|
|
219
219
|
/** Verify the selected component against graphify (declaration panel). */
|
|
220
|
-
onVerifyComponent?: (
|
|
220
|
+
onVerifyComponent?: (componentAlias: string) => void;
|
|
221
221
|
/** Live verification status for the selected component. */
|
|
222
222
|
componentVerification?: ComponentVerificationState | null;
|
|
223
223
|
}
|
|
@@ -335,7 +335,7 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
335
335
|
// Pending deferred fit after opening the drawer; cancelled on re-entry / unmount.
|
|
336
336
|
const pendingFocusFitRef = useRef<number | null>(null);
|
|
337
337
|
// Component the pointer is over (null on leave) → transient tree highlight.
|
|
338
|
-
const [
|
|
338
|
+
const [hoveredComponentAlias, setHoveredComponentId] = useState<string | null>(null);
|
|
339
339
|
// Walkthrough focus — selected flow (or step) is full strength; other
|
|
340
340
|
// opened-flow members stay visible but dimmed; everything else is hidden.
|
|
341
341
|
const [focusedWalkthroughId, setFocusedWalkthroughId] = useState<string | null>(null);
|
|
@@ -496,7 +496,7 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
496
496
|
// writes back declarationRef).
|
|
497
497
|
useEffect(() => {
|
|
498
498
|
if (!selected) return;
|
|
499
|
-
const fresh = components.find((c) => c.
|
|
499
|
+
const fresh = components.find((c) => c.alias === selected.alias);
|
|
500
500
|
if (fresh && fresh !== selected) setSelected(fresh);
|
|
501
501
|
}, [components, selected]);
|
|
502
502
|
|
|
@@ -646,12 +646,12 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
646
646
|
);
|
|
647
647
|
|
|
648
648
|
useEffect(() => {
|
|
649
|
-
SUBSYSTEM_CALLBACKS.onSelect = (
|
|
650
|
-
const comp = components.find((c) => c.
|
|
649
|
+
SUBSYSTEM_CALLBACKS.onSelect = (alias: string) => {
|
|
650
|
+
const comp = components.find((c) => c.alias === alias);
|
|
651
651
|
if (comp) {
|
|
652
652
|
// Clicking the already-selected node unselects it (toggle off).
|
|
653
653
|
// Selection is independent of the file drawer — nodes never open it.
|
|
654
|
-
if (selectedRef.current?.
|
|
654
|
+
if (selectedRef.current?.alias === comp.alias) {
|
|
655
655
|
setSelected(null);
|
|
656
656
|
setSelectedEdgeId(null);
|
|
657
657
|
return;
|
|
@@ -660,7 +660,7 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
660
660
|
setSelectedEdgeId(null);
|
|
661
661
|
setFocusedWalkthroughId(null);
|
|
662
662
|
setFocusedStepIndex(null);
|
|
663
|
-
onSelect?.(
|
|
663
|
+
onSelect?.(alias);
|
|
664
664
|
}
|
|
665
665
|
};
|
|
666
666
|
SUBSYSTEM_CALLBACKS.onEdgeSelect = selectEdge;
|
|
@@ -800,15 +800,15 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
800
800
|
// Boundary frames follow their members: hidden when no member is
|
|
801
801
|
// visible, dimmed when members are dimmed. Never selectable.
|
|
802
802
|
if (n.type === 'subsystem-group') {
|
|
803
|
-
const
|
|
803
|
+
const memberAliases = ((n.data as { region?: { memberAliases?: string[] } } | undefined)?.region?.memberAliases) ?? [];
|
|
804
804
|
const vis = flowElementVisibility({
|
|
805
|
-
inOpened:
|
|
806
|
-
inSelected:
|
|
805
|
+
inOpened: memberAliases.some((alias) => openedNodeIds?.has(alias) === true),
|
|
806
|
+
inSelected: memberAliases.some((alias) => brightNodeIds?.has(alias) === true),
|
|
807
807
|
anyOpened: openedNodeIds != null,
|
|
808
808
|
anySelected: brightNodeIds != null,
|
|
809
809
|
});
|
|
810
810
|
const dimmed = previewNodeIds
|
|
811
|
-
? vis.hidden || !
|
|
811
|
+
? vis.hidden || !memberAliases.some((alias) => previewNodeIds.has(alias))
|
|
812
812
|
: vis.dimmed;
|
|
813
813
|
const hidden = vis.hidden;
|
|
814
814
|
return {
|
|
@@ -823,7 +823,7 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
823
823
|
}
|
|
824
824
|
const comp = (n.data as { component?: SubsystemComponent } | undefined)?.component;
|
|
825
825
|
const fileMatch = fileMatchForNode(comp?.file, openFile, focusNodeIds?.has(n.id) === true);
|
|
826
|
-
const isSelected = selected?.
|
|
826
|
+
const isSelected = selected?.alias !== undefined && comp?.alias === selected.alias;
|
|
827
827
|
const vis = flowElementVisibility({
|
|
828
828
|
inOpened: openedNodeIds?.has(n.id) === true,
|
|
829
829
|
inSelected: brightNodeIds?.has(n.id) === true,
|
|
@@ -944,12 +944,12 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
944
944
|
if (node.type === 'subsystem-component' && comp) {
|
|
945
945
|
// Clicking the already-selected node unselects it (toggle off).
|
|
946
946
|
// Selection is independent of the file drawer — nodes never open it.
|
|
947
|
-
if (selected?.
|
|
947
|
+
if (selected?.alias === comp.alias) {
|
|
948
948
|
setSelected(null);
|
|
949
949
|
return;
|
|
950
950
|
}
|
|
951
951
|
setSelected(comp);
|
|
952
|
-
if (comp.
|
|
952
|
+
if (comp.alias) onSelect?.(comp.alias);
|
|
953
953
|
}
|
|
954
954
|
},
|
|
955
955
|
[onSelect, selected],
|
|
@@ -1384,8 +1384,8 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
1384
1384
|
// Filename-badge clicks on nodes open the drawer through the same path as
|
|
1385
1385
|
// the declaration panel's file link (toggle + tree sync, no start line).
|
|
1386
1386
|
useEffect(() => {
|
|
1387
|
-
SUBSYSTEM_CALLBACKS.onOpenFile = (
|
|
1388
|
-
const comp = components.find((c) => c.
|
|
1387
|
+
SUBSYSTEM_CALLBACKS.onOpenFile = (componentAlias: string) => {
|
|
1388
|
+
const comp = components.find((c) => c.alias === componentAlias);
|
|
1389
1389
|
if (comp?.file) onOpenDeclarationFile(comp.file);
|
|
1390
1390
|
};
|
|
1391
1391
|
return () => {
|
|
@@ -1394,23 +1394,23 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
1394
1394
|
}, [components, onOpenDeclarationFile]);
|
|
1395
1395
|
|
|
1396
1396
|
// Detail-panel links: related-name clicks select the matching component —
|
|
1397
|
-
// resolved by
|
|
1397
|
+
// resolved by alias, name, or symbol (call labels may carry a trailing `()`).
|
|
1398
1398
|
const resolveRelatedComponent = useCallback(
|
|
1399
1399
|
(ref: string) => {
|
|
1400
1400
|
const clean = ref.replace(/\(\)$/, '');
|
|
1401
1401
|
const comp = components.find(
|
|
1402
1402
|
(c) =>
|
|
1403
|
-
c.
|
|
1403
|
+
c.alias === clean ||
|
|
1404
1404
|
c.name === clean ||
|
|
1405
1405
|
c.symbol === clean ||
|
|
1406
1406
|
c.symbol?.replace(/\(\)$/, '') === clean,
|
|
1407
1407
|
);
|
|
1408
|
-
if (!comp || comp.
|
|
1408
|
+
if (!comp || comp.alias === selectedRef.current?.alias) return;
|
|
1409
1409
|
setSelected(comp);
|
|
1410
1410
|
setSelectedEdgeId(null);
|
|
1411
1411
|
setFocusedWalkthroughId(null);
|
|
1412
1412
|
setFocusedStepIndex(null);
|
|
1413
|
-
onSelect?.(comp.
|
|
1413
|
+
onSelect?.(comp.alias);
|
|
1414
1414
|
},
|
|
1415
1415
|
[components, onSelect],
|
|
1416
1416
|
);
|
|
@@ -1450,9 +1450,9 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
1450
1450
|
|
|
1451
1451
|
// The hovered node's file (null when not hovering / file-less component).
|
|
1452
1452
|
const hoveredFile = useMemo(() => {
|
|
1453
|
-
if (!
|
|
1454
|
-
return components.find((c) => c.
|
|
1455
|
-
}, [components,
|
|
1453
|
+
if (!hoveredComponentAlias) return null;
|
|
1454
|
+
return components.find((c) => c.alias === hoveredComponentAlias)?.file ?? null;
|
|
1455
|
+
}, [components, hoveredComponentAlias]);
|
|
1456
1456
|
|
|
1457
1457
|
// Drawer content renderer: prefer the path-keyed viewer; fall back to the
|
|
1458
1458
|
// legacy component-keyed one via a file → first-component lookup.
|
|
@@ -1569,13 +1569,13 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
1569
1569
|
padding: 0,
|
|
1570
1570
|
border: 'none',
|
|
1571
1571
|
borderRadius: 4,
|
|
1572
|
-
background: descToggleHover ? theme.colors.border : 'transparent',
|
|
1573
|
-
color: descToggleHover ? theme.colors.text : (theme.colors.textMuted ?? theme.colors.textSecondary),
|
|
1572
|
+
background: descriptionVisible || descToggleHover ? theme.colors.border : 'transparent',
|
|
1573
|
+
color: descriptionVisible || descToggleHover ? theme.colors.text : (theme.colors.textMuted ?? theme.colors.textSecondary),
|
|
1574
1574
|
cursor: 'pointer',
|
|
1575
1575
|
transition: 'background 120ms ease, color 120ms ease',
|
|
1576
1576
|
}}
|
|
1577
1577
|
>
|
|
1578
|
-
|
|
1578
|
+
<FileText size={14} />
|
|
1579
1579
|
</button>
|
|
1580
1580
|
)}
|
|
1581
1581
|
</div>
|