@principal-ai/principal-view-react 0.16.59 → 0.16.61
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 +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts +14 -0
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +24 -4
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/model.d.ts +58 -2
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js +53 -8
- package/dist/subsystem/model.js.map +1 -1
- package/dist/subsystem/nodes.d.ts.map +1 -1
- package/dist/subsystem/nodes.js +16 -7
- package/dist/subsystem/nodes.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +10 -0
- package/src/stories/Subsystem/ComponentGraph/FrameworkStereotype.stories.tsx +216 -0
- package/src/stories/Subsystem/ComponentGraph/GraphTitle.stories.tsx +71 -0
- package/src/stories/Subsystem/ComponentGraph/Spotlights.stories.tsx +38 -3
- package/src/subsystem/SubsystemComponentGraph.tsx +47 -3
- package/src/subsystem/model.test.ts +44 -0
- package/src/subsystem/model.ts +100 -8
- package/src/subsystem/nodes.tsx +24 -11
|
@@ -79,8 +79,22 @@ export interface SubsystemComponentGraphProps {
|
|
|
79
79
|
maxNodeWidth?: number;
|
|
80
80
|
/** Show edge labels (mechanism names) on the graph. @default true */
|
|
81
81
|
showEdgeLabels?: boolean;
|
|
82
|
+
/** Show the mechanism-legend button overlay on the canvas. @default true */
|
|
83
|
+
showLegend?: boolean;
|
|
82
84
|
/** Subsystem title displayed in the sidebar. */
|
|
83
85
|
title?: string;
|
|
86
|
+
/**
|
|
87
|
+
* Suppresses the sidebar entirely (title, description, file tree,
|
|
88
|
+
* throughlines) for graph-only embeds. Pair with `graphTitle` to keep the
|
|
89
|
+
* subsystem name visible as an overlay on the canvas.
|
|
90
|
+
*/
|
|
91
|
+
hideSidebar?: boolean;
|
|
92
|
+
/**
|
|
93
|
+
* Subsystem title rendered as a non-interactive overlay chip on the graph
|
|
94
|
+
* canvas (top-center). Does not trigger the sidebar — for graph-only
|
|
95
|
+
* embeds that still need to name what they show.
|
|
96
|
+
*/
|
|
97
|
+
graphTitle?: string;
|
|
84
98
|
/** Markdown description rendered in the sidebar. */
|
|
85
99
|
description?: string;
|
|
86
100
|
/** Rendered over the graph canvas only (not the title/legend sidebar). */
|
|
@@ -143,7 +157,7 @@ interface InnerProps extends SubsystemComponentGraphProps {
|
|
|
143
157
|
measured: { w: number; h: number } | null;
|
|
144
158
|
}
|
|
145
159
|
|
|
146
|
-
function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measured: _measured, maxNodeWidth, showEdgeLabels, title, description, canvasOverlay, sidebarExtra, sidebarAfterDescription, renderFileView, renderFileViewer, onFileSelect, onVerifyComponent, componentVerification }: InnerProps) {
|
|
160
|
+
function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measured: _measured, maxNodeWidth, showEdgeLabels, showLegend, title, hideSidebar, graphTitle, description, canvasOverlay, sidebarExtra, sidebarAfterDescription, renderFileView, renderFileViewer, onFileSelect, onVerifyComponent, componentVerification }: InnerProps) {
|
|
147
161
|
const { theme } = useTheme();
|
|
148
162
|
const { fitView } = useReactFlow();
|
|
149
163
|
const viewport = useViewport();
|
|
@@ -770,7 +784,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
770
784
|
return (
|
|
771
785
|
<div style={{ width: '100%', height: '100%', display: 'flex', flexDirection: 'row' }}>
|
|
772
786
|
{/* Sidebar: scrollable title/description on top, files or flows pinned to the bottom half */}
|
|
773
|
-
{(title || description || sidebarExtra || sidebarAfterDescription || treeFilePaths.length > 0 || hasThroughlines) && (
|
|
787
|
+
{!hideSidebar && (title || description || sidebarExtra || sidebarAfterDescription || treeFilePaths.length > 0 || hasThroughlines) && (
|
|
774
788
|
<div
|
|
775
789
|
style={{
|
|
776
790
|
width: 340,
|
|
@@ -1052,7 +1066,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
1052
1066
|
<Controls showZoom showFitView showInteractive />
|
|
1053
1067
|
</ReactFlow>
|
|
1054
1068
|
{/* Legend button — top-left overlay on the canvas; opens the modal. */}
|
|
1055
|
-
{usedMechanisms.size > 0 && (
|
|
1069
|
+
{showLegend !== false && usedMechanisms.size > 0 && (
|
|
1056
1070
|
<button
|
|
1057
1071
|
type="button"
|
|
1058
1072
|
onClick={() => setLegendOpen(true)}
|
|
@@ -1078,6 +1092,36 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
1078
1092
|
<MapIcon size={13} />
|
|
1079
1093
|
Legend
|
|
1080
1094
|
</button>
|
|
1095
|
+
)}
|
|
1096
|
+
{/* Graph title — non-interactive chip centered at the top of the canvas
|
|
1097
|
+
(clear of the top-left legend button and top-right declaration
|
|
1098
|
+
card). Lets graph-only embeds name the subsystem they show. */}
|
|
1099
|
+
{graphTitle && (
|
|
1100
|
+
<div
|
|
1101
|
+
style={{
|
|
1102
|
+
position: 'absolute',
|
|
1103
|
+
top: 10,
|
|
1104
|
+
left: '50%',
|
|
1105
|
+
transform: 'translateX(-50%)',
|
|
1106
|
+
zIndex: 6,
|
|
1107
|
+
maxWidth: '60%',
|
|
1108
|
+
overflow: 'hidden',
|
|
1109
|
+
textOverflow: 'ellipsis',
|
|
1110
|
+
whiteSpace: 'nowrap',
|
|
1111
|
+
padding: '6px 18px',
|
|
1112
|
+
fontSize: theme.fontSizes[3],
|
|
1113
|
+
fontWeight: 600,
|
|
1114
|
+
fontFamily: theme.fonts.heading,
|
|
1115
|
+
color: theme.colors.text,
|
|
1116
|
+
background: theme.colors.backgroundSecondary ?? theme.colors.background,
|
|
1117
|
+
border: `1px solid ${theme.colors.border}`,
|
|
1118
|
+
borderRadius: 6,
|
|
1119
|
+
boxShadow: '0 1px 4px rgba(0,0,0,0.25)',
|
|
1120
|
+
pointerEvents: 'none',
|
|
1121
|
+
}}
|
|
1122
|
+
>
|
|
1123
|
+
{graphTitle}
|
|
1124
|
+
</div>
|
|
1081
1125
|
)}
|
|
1082
1126
|
{/* Selected-component declaration — floating card over the canvas
|
|
1083
1127
|
(top-right, clear of the top-left legend button). The graph never
|
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
processGroupNodeId,
|
|
8
8
|
buildSubsystemGraph,
|
|
9
9
|
deriveNameFromSymbol,
|
|
10
|
+
constructBadgeLabel,
|
|
11
|
+
nodeMinWidthForBadges,
|
|
10
12
|
formatPurl,
|
|
11
13
|
packageColor,
|
|
12
14
|
subsystemGraphLayoutKey,
|
|
@@ -105,6 +107,48 @@ describe('subsystem graph model', () => {
|
|
|
105
107
|
expect(deriveNameFromSymbol('Foo {}', 'class')).toBe('Foo {}');
|
|
106
108
|
});
|
|
107
109
|
|
|
110
|
+
test('deriveNameFromSymbol uses JSX decoration for component stereotype', () => {
|
|
111
|
+
expect(deriveNameFromSymbol('AnalysisView', 'function', undefined, undefined, 'component')).toBe(
|
|
112
|
+
'<AnalysisView>',
|
|
113
|
+
);
|
|
114
|
+
expect(deriveNameFromSymbol('useDrawingsHost', 'function', undefined, undefined, 'hook')).toBe(
|
|
115
|
+
'useDrawingsHost()',
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('constructBadgeLabel prefers framework · stereotype over construct', () => {
|
|
120
|
+
expect(
|
|
121
|
+
constructBadgeLabel({
|
|
122
|
+
construct: 'function',
|
|
123
|
+
framework: 'react',
|
|
124
|
+
stereotype: 'component',
|
|
125
|
+
}),
|
|
126
|
+
).toBe('react · component');
|
|
127
|
+
expect(constructBadgeLabel({ construct: 'function', stereotype: 'hook' })).toBe('hook');
|
|
128
|
+
expect(constructBadgeLabel({ construct: 'function' })).toBe('function');
|
|
129
|
+
expect(constructBadgeLabel({ construct: 'type_alias' })).toBe('type alias');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('nodeMinWidthForBadges widens for long construct badges and role pairs', () => {
|
|
133
|
+
const plain = nodeMinWidthForBadges({ construct: 'function' });
|
|
134
|
+
expect(plain).toBe(150);
|
|
135
|
+
|
|
136
|
+
const stereotype = nodeMinWidthForBadges({
|
|
137
|
+
construct: 'function',
|
|
138
|
+
framework: 'react',
|
|
139
|
+
stereotype: 'component',
|
|
140
|
+
});
|
|
141
|
+
expect(stereotype).toBeGreaterThan(150);
|
|
142
|
+
|
|
143
|
+
const withRole = nodeMinWidthForBadges({
|
|
144
|
+
construct: 'function',
|
|
145
|
+
framework: 'react',
|
|
146
|
+
stereotype: 'component',
|
|
147
|
+
role: 'entry',
|
|
148
|
+
});
|
|
149
|
+
expect(withRole).toBeGreaterThan(stereotype);
|
|
150
|
+
});
|
|
151
|
+
|
|
108
152
|
test('deriveNameFromSymbol falls back to file basename for modules', () => {
|
|
109
153
|
expect(deriveNameFromSymbol(undefined, 'module', undefined, 'transcript.ts')).toBe('transcript');
|
|
110
154
|
expect(deriveNameFromSymbol(undefined, 'module', undefined, 'src/event-processors/index.ts')).toBe('index');
|
package/src/subsystem/model.ts
CHANGED
|
@@ -45,6 +45,22 @@ export type SubsystemComponentConstruct =
|
|
|
45
45
|
*/
|
|
46
46
|
export type SubsystemComponentRole = 'entry' | 'service';
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Framework that owns a stereotype vocabulary (open string).
|
|
50
|
+
* Examples: `react`, `vue`, `nestjs`, `django`, `spring`.
|
|
51
|
+
* Empty when the node is language-only / framework-agnostic.
|
|
52
|
+
*/
|
|
53
|
+
export type SubsystemFramework = string;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Framework-level pattern stamped on a language construct (open string).
|
|
57
|
+
* Examples: `component`, `hook`, `middleware`, `controller`, `guard`.
|
|
58
|
+
* Empty when no framework pattern applies. Pair with `framework` when set —
|
|
59
|
+
* a React component stays `construct: 'function'` with
|
|
60
|
+
* `framework: 'react'` + `stereotype: 'component'`.
|
|
61
|
+
*/
|
|
62
|
+
export type SubsystemStereotype = string;
|
|
63
|
+
|
|
48
64
|
// ---------------------------------------------------------------------------
|
|
49
65
|
// Declaration tokens — structured source representation
|
|
50
66
|
// ---------------------------------------------------------------------------
|
|
@@ -97,7 +113,10 @@ export interface SubsystemComponent {
|
|
|
97
113
|
* anchors to a definition; runtime occurrences (variables, activations,
|
|
98
114
|
* instances) are NOT constructs — they belong to a future execution-mode
|
|
99
115
|
* graph whose occurrence nodes reference these definitions. Ontology:
|
|
100
|
-
* construct = what it is,
|
|
116
|
+
* construct = what it is, framework + stereotype = which framework pattern
|
|
117
|
+
* it plays, role = where it sits, process = where it runs. Prefer
|
|
118
|
+
* `framework` + `stereotype` over inventing framework-specific constructs
|
|
119
|
+
* (a React component is still `construct: 'function'`).
|
|
101
120
|
*/
|
|
102
121
|
construct: SubsystemComponentConstruct;
|
|
103
122
|
/** Source location the component lives in (repo-root-relative path). */
|
|
@@ -115,6 +134,17 @@ export interface SubsystemComponent {
|
|
|
115
134
|
* inbound, `produces` outbound).
|
|
116
135
|
*/
|
|
117
136
|
role?: SubsystemComponentRole;
|
|
137
|
+
/**
|
|
138
|
+
* Framework that owns the stereotype vocabulary (e.g. `react`, `nestjs`).
|
|
139
|
+
* Orthogonal to `construct` — leave empty for language-only units.
|
|
140
|
+
*/
|
|
141
|
+
framework?: SubsystemFramework;
|
|
142
|
+
/**
|
|
143
|
+
* Framework pattern this declaration plays (e.g. `component`, `hook`).
|
|
144
|
+
* When set, the node badge prefers this label over the construct name so
|
|
145
|
+
* a React UI unit reads as "component" rather than "function".
|
|
146
|
+
*/
|
|
147
|
+
stereotype?: SubsystemStereotype;
|
|
118
148
|
/**
|
|
119
149
|
* Runtime process membership — which deployment unit this node is a
|
|
120
150
|
* member of (e.g. `trail-viewer/host`, `trail-viewer/renderer`). Nodes
|
|
@@ -225,6 +255,7 @@ export function deriveNameFromSymbol(
|
|
|
225
255
|
construct: SubsystemComponentConstruct,
|
|
226
256
|
existingName?: string,
|
|
227
257
|
file?: string,
|
|
258
|
+
stereotype?: string,
|
|
228
259
|
): string {
|
|
229
260
|
let name: string | undefined;
|
|
230
261
|
if (symbol && symbol.trim()) {
|
|
@@ -236,10 +267,13 @@ export function deriveNameFromSymbol(
|
|
|
236
267
|
}
|
|
237
268
|
if (!name) name = existingName ?? 'untitled';
|
|
238
269
|
|
|
239
|
-
//
|
|
240
|
-
// (a
|
|
241
|
-
//
|
|
242
|
-
// Everything else (store,
|
|
270
|
+
// Decorations = what the drill-down shows. Framework stereotypes can override
|
|
271
|
+
// the language decoration (a React component wears `<>` instead of `()`).
|
|
272
|
+
// Executable constructs wear `()`; brace-bodied constructs wear ` {}`.
|
|
273
|
+
// Everything else (store, module, external) renders bare.
|
|
274
|
+
if (stereotype === 'component' && !name.startsWith('<')) {
|
|
275
|
+
return `<${name}>`;
|
|
276
|
+
}
|
|
243
277
|
if ((construct === 'function' || construct === 'method') && !name.endsWith('()')) {
|
|
244
278
|
name = `${name}()`;
|
|
245
279
|
}
|
|
@@ -443,6 +477,64 @@ export const ROLE_LABEL: Record<SubsystemComponentRole, string> = {
|
|
|
443
477
|
service: 'service',
|
|
444
478
|
};
|
|
445
479
|
|
|
480
|
+
/**
|
|
481
|
+
* Primary badge text for a node: prefer framework stereotype over the
|
|
482
|
+
* language construct so a React UI unit reads as "component" / "hook"
|
|
483
|
+
* rather than "function". When both framework and stereotype are set,
|
|
484
|
+
* show `framework · stereotype` (e.g. `react · component`).
|
|
485
|
+
*/
|
|
486
|
+
export function constructBadgeLabel(component: {
|
|
487
|
+
construct: SubsystemComponentConstruct;
|
|
488
|
+
framework?: string;
|
|
489
|
+
stereotype?: string;
|
|
490
|
+
}): string {
|
|
491
|
+
const constructLabel =
|
|
492
|
+
component.construct === 'type_alias' ? 'type alias' : component.construct;
|
|
493
|
+
if (component.stereotype && component.framework) {
|
|
494
|
+
return `${component.framework} · ${component.stereotype}`;
|
|
495
|
+
}
|
|
496
|
+
if (component.stereotype) return component.stereotype;
|
|
497
|
+
return constructLabel ?? '';
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/** Default CSS floor for component nodes (padding aside). */
|
|
501
|
+
export const NODE_CSS_MIN_WIDTH = 150;
|
|
502
|
+
/** Inset of each top badge from the node edge (`left` / `right` style). */
|
|
503
|
+
export const BADGE_EDGE_INSET = 5;
|
|
504
|
+
/** Minimum gap between left construct badge and right role badge. */
|
|
505
|
+
const BADGE_PAIR_GAP = 8;
|
|
506
|
+
/** Badge box chrome: padding 5+5 + border 1+1. */
|
|
507
|
+
const BADGE_BOX_CHROME = 12;
|
|
508
|
+
/** Approx monospace uppercase width incl. letter-spacing (~0.5px). */
|
|
509
|
+
const BADGE_CHAR_WIDTH = 8;
|
|
510
|
+
|
|
511
|
+
/** Estimated rendered width of a top tab badge label. */
|
|
512
|
+
export function estimateBadgeLabelWidth(label: string): number {
|
|
513
|
+
return (label?.length ?? 0) * BADGE_CHAR_WIDTH + BADGE_BOX_CHROME;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Minimum node width so top badges stay on one line and (when both are
|
|
518
|
+
* present) don't overlap — badges are absolutely positioned, so they don't
|
|
519
|
+
* contribute to layout unless we widen the node explicitly.
|
|
520
|
+
*/
|
|
521
|
+
export function nodeMinWidthForBadges(component: {
|
|
522
|
+
construct: SubsystemComponentConstruct;
|
|
523
|
+
framework?: string;
|
|
524
|
+
stereotype?: string;
|
|
525
|
+
role?: SubsystemComponentRole;
|
|
526
|
+
}): number {
|
|
527
|
+
const left = estimateBadgeLabelWidth(constructBadgeLabel(component));
|
|
528
|
+
if (component.role == null) {
|
|
529
|
+
return Math.max(NODE_CSS_MIN_WIDTH, BADGE_EDGE_INSET + left + BADGE_EDGE_INSET);
|
|
530
|
+
}
|
|
531
|
+
const right = estimateBadgeLabelWidth(ROLE_LABEL[component.role]);
|
|
532
|
+
return Math.max(
|
|
533
|
+
NODE_CSS_MIN_WIDTH,
|
|
534
|
+
BADGE_EDGE_INSET + left + BADGE_PAIR_GAP + right + BADGE_EDGE_INSET,
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
|
|
446
538
|
/**
|
|
447
539
|
* Convert a subsystem graph document into React Flow nodes. Components that
|
|
448
540
|
* carry a `process` get a `parentId` pointing at their boundary group node
|
|
@@ -489,9 +581,9 @@ export function convertSubsystemToNodes(
|
|
|
489
581
|
.sort((a, b) => b.length - a.length)[0];
|
|
490
582
|
const cap = maxNodeWidth ?? 300;
|
|
491
583
|
const textWidth = Math.min(cap, Math.max(60, (text?.length ?? 10) * 8));
|
|
492
|
-
// Account for CSS minWidth and padding/border so ELK's
|
|
493
|
-
// the actual rendered node boundaries.
|
|
494
|
-
const cssMinWidth =
|
|
584
|
+
// Account for CSS minWidth (incl. top badges) and padding/border so ELK's
|
|
585
|
+
// port positions match the actual rendered node boundaries.
|
|
586
|
+
const cssMinWidth = nodeMinWidthForBadges(c);
|
|
495
587
|
const cssPadding = 20; // horizontal padding (left + right)
|
|
496
588
|
const cssBorder = 4; // 2px border each side
|
|
497
589
|
const rawWidth = Math.max(cssMinWidth, textWidth + cssPadding + cssBorder);
|
package/src/subsystem/nodes.tsx
CHANGED
|
@@ -21,7 +21,10 @@ import {
|
|
|
21
21
|
MECHANISM_STYLE,
|
|
22
22
|
ROLE_COLOR,
|
|
23
23
|
ROLE_LABEL,
|
|
24
|
+
constructBadgeLabel,
|
|
24
25
|
deriveNameFromSymbol,
|
|
26
|
+
BADGE_EDGE_INSET,
|
|
27
|
+
nodeMinWidthForBadges,
|
|
25
28
|
packageColor,
|
|
26
29
|
type SubsystemGraphNodeData,
|
|
27
30
|
type SubsystemGroupNodeData,
|
|
@@ -86,7 +89,10 @@ export function SubsystemComponentNode(props: NodeProps<Node<SubsystemGraphNodeD
|
|
|
86
89
|
const configuredMax = SUBSYSTEM_CALLBACKS.maxNodeWidth;
|
|
87
90
|
const maxWidth = configuredMax ?? 300;
|
|
88
91
|
// `symbol` is the source of truth; `name` is derived from it consistently.
|
|
89
|
-
const displayName = deriveNameFromSymbol(c.symbol, c.construct, c.name, c.file);
|
|
92
|
+
const displayName = deriveNameFromSymbol(c.symbol, c.construct, c.name, c.file, c.stereotype);
|
|
93
|
+
// Top badges are absolutely positioned — widen the node so they nowrap
|
|
94
|
+
// instead of wrapping, including when construct + role badges share the top.
|
|
95
|
+
const badgeMinWidth = nodeMinWidthForBadges(c);
|
|
90
96
|
// Set while a file is open in the drawer: true → spotlight, false → dim,
|
|
91
97
|
// absent (no file open) → neutral.
|
|
92
98
|
const fileMatch = data.fileMatch as boolean | undefined;
|
|
@@ -117,7 +123,7 @@ export function SubsystemComponentNode(props: NodeProps<Node<SubsystemGraphNodeD
|
|
|
117
123
|
boxSizing: 'border-box',
|
|
118
124
|
width: nodeWidth,
|
|
119
125
|
height: nodeHeight,
|
|
120
|
-
minWidth:
|
|
126
|
+
minWidth: badgeMinWidth,
|
|
121
127
|
maxWidth,
|
|
122
128
|
padding: '6px 10px',
|
|
123
129
|
borderRadius: 8,
|
|
@@ -135,20 +141,21 @@ export function SubsystemComponentNode(props: NodeProps<Node<SubsystemGraphNodeD
|
|
|
135
141
|
fontFamily: theme.fonts.body,
|
|
136
142
|
}}
|
|
137
143
|
>
|
|
138
|
-
{/* Construct badge —
|
|
139
|
-
|
|
140
|
-
clicks pass through to the node. */}
|
|
144
|
+
{/* Construct / stereotype badge — prefers framework stereotype so a
|
|
145
|
+
React UI unit reads as "react · component" instead of "function".
|
|
146
|
+
Persistent; pointer-events none so clicks pass through to the node. */}
|
|
141
147
|
<div
|
|
142
148
|
style={{
|
|
143
149
|
position: 'absolute',
|
|
144
150
|
top: -9,
|
|
145
|
-
left:
|
|
151
|
+
left: BADGE_EDGE_INSET,
|
|
146
152
|
zIndex: 1,
|
|
147
153
|
fontFamily: theme.fonts.monospace,
|
|
148
154
|
fontSize: theme.fontSizes[0] * 1.1,
|
|
149
155
|
letterSpacing: 0.5,
|
|
150
156
|
textTransform: 'uppercase',
|
|
151
157
|
lineHeight: '17px',
|
|
158
|
+
whiteSpace: 'nowrap',
|
|
152
159
|
color,
|
|
153
160
|
background: theme.colors.backgroundSecondary ?? theme.colors.background,
|
|
154
161
|
border: `1px solid ${color}`,
|
|
@@ -156,7 +163,7 @@ export function SubsystemComponentNode(props: NodeProps<Node<SubsystemGraphNodeD
|
|
|
156
163
|
padding: '0 5px',
|
|
157
164
|
}}
|
|
158
165
|
>
|
|
159
|
-
{
|
|
166
|
+
{constructBadgeLabel(c)}
|
|
160
167
|
</div>
|
|
161
168
|
|
|
162
169
|
{/* Role badge — top-right, only when the node carries a topology role.
|
|
@@ -167,13 +174,14 @@ export function SubsystemComponentNode(props: NodeProps<Node<SubsystemGraphNodeD
|
|
|
167
174
|
style={{
|
|
168
175
|
position: 'absolute',
|
|
169
176
|
top: -9,
|
|
170
|
-
right:
|
|
177
|
+
right: BADGE_EDGE_INSET,
|
|
171
178
|
zIndex: 1,
|
|
172
179
|
fontFamily: theme.fonts.monospace,
|
|
173
180
|
fontSize: theme.fontSizes[0] * 1.1,
|
|
174
181
|
letterSpacing: 0.5,
|
|
175
182
|
textTransform: 'uppercase',
|
|
176
183
|
lineHeight: '17px',
|
|
184
|
+
whiteSpace: 'nowrap',
|
|
177
185
|
color: ROLE_COLOR[c.role],
|
|
178
186
|
background: theme.colors.backgroundSecondary ?? theme.colors.background,
|
|
179
187
|
border: `1px solid ${ROLE_COLOR[c.role]}`,
|
|
@@ -242,10 +250,15 @@ export function SubsystemComponentNode(props: NodeProps<Node<SubsystemGraphNodeD
|
|
|
242
250
|
</div>
|
|
243
251
|
|
|
244
252
|
{/* Hide the identity line when the symbol is just the title without its
|
|
245
|
-
decoration (`()
|
|
246
|
-
(e.g. the dotted host on methods, or a different code
|
|
253
|
+
decoration (`()`, ` {}`, or `<>`) — only show it when it adds
|
|
254
|
+
information (e.g. the dotted host on methods, or a different code
|
|
255
|
+
identity). */}
|
|
247
256
|
{c.symbol &&
|
|
248
|
-
c.symbol !==
|
|
257
|
+
c.symbol !==
|
|
258
|
+
displayName
|
|
259
|
+
.replace(/^<(.+)>$/, '$1')
|
|
260
|
+
.replace(/ ?\{\}$/, '')
|
|
261
|
+
.replace(/\(\)$/, '') && (
|
|
249
262
|
<div
|
|
250
263
|
style={{
|
|
251
264
|
fontSize: theme.fontSizes[0] * 0.82,
|