@principal-ai/principal-view-react 0.16.56 → 0.16.57
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/graphify/consolidated.d.ts +16 -1
- package/dist/graphify/consolidated.d.ts.map +1 -1
- package/dist/graphify/index.d.ts +1 -1
- package/dist/graphify/index.d.ts.map +1 -1
- package/dist/graphify/index.js.map +1 -1
- package/dist/graphify/kind.d.ts +1 -1
- package/dist/graphify/kind.d.ts.map +1 -1
- package/dist/graphify/kind.js +7 -1
- package/dist/graphify/kind.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/pierre/constructColors.d.ts +24 -0
- package/dist/pierre/constructColors.d.ts.map +1 -0
- package/dist/pierre/constructColors.js +91 -0
- package/dist/pierre/constructColors.js.map +1 -0
- package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts +8 -0
- package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts.map +1 -0
- package/dist/stories/Subsystem/ComponentGraph/fixtures.js +116 -0
- package/dist/stories/Subsystem/ComponentGraph/fixtures.js.map +1 -0
- package/dist/subsystem/ComponentDeclaration.d.ts +2 -2
- package/dist/subsystem/ComponentDeclaration.d.ts.map +1 -1
- package/dist/subsystem/ComponentDeclaration.js +12 -11
- package/dist/subsystem/ComponentDeclaration.js.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +22 -1
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/formatDeclaration.d.ts.map +1 -1
- package/dist/subsystem/formatDeclaration.js +49 -10
- package/dist/subsystem/formatDeclaration.js.map +1 -1
- package/dist/subsystem/model.d.ts +55 -12
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js +54 -24
- package/dist/subsystem/model.js.map +1 -1
- package/dist/subsystem/nodes.d.ts +4 -1
- package/dist/subsystem/nodes.d.ts.map +1 -1
- package/dist/subsystem/nodes.js +70 -21
- package/dist/subsystem/nodes.js.map +1 -1
- package/dist/subsystem/tokenizeComponent.js +1 -1
- package/dist/subsystem/tokenizeComponent.js.map +1 -1
- package/package.json +1 -1
- package/src/graphify/consolidated.ts +18 -1
- package/src/graphify/index.ts +1 -0
- package/src/graphify/kind.ts +7 -1
- package/src/index.ts +3 -2
- package/src/pierre/constructColors.test.ts +52 -0
- package/src/pierre/constructColors.ts +112 -0
- package/src/stories/{BoundaryNodes.stories.tsx → Audit/BoundaryNodes.stories.tsx} +1 -1
- package/src/stories/{CanvasEdgeTypes.stories.tsx → Audit/CanvasEdgeTypes.stories.tsx} +1 -1
- package/src/stories/{CanvasNodeTypes.stories.tsx → Audit/CanvasNodeTypes.stories.tsx} +1 -1
- package/src/stories/{ColorPriority.stories.tsx → Audit/ColorPriority.stories.tsx} +1 -1
- package/src/stories/{DiamondBadges.stories.tsx → Audit/DiamondBadges.stories.tsx} +1 -1
- package/src/stories/{NodeDimensionsTesting.stories.tsx → Audit/NodeDimensionsTesting.stories.tsx} +2 -2
- package/src/stories/{NodeFieldsAudit.stories.tsx → Audit/NodeFieldsAudit.stories.tsx} +1 -1
- package/src/stories/{NodeFontSizeTesting.stories.tsx → Audit/NodeFontSizeTesting.stories.tsx} +2 -2
- package/src/stories/{NodeShapes.stories.tsx → Audit/NodeShapes.stories.tsx} +1 -1
- package/src/stories/{WideNodesTesting.stories.tsx → Audit/WideNodesTesting.stories.tsx} +2 -2
- package/src/stories/{dashboard → Components}/DashboardRenderer.stories.tsx +2 -2
- package/src/stories/{GraphRenderer.stories.tsx → Components/GraphRenderer.stories.tsx} +2 -2
- package/src/stories/{MultiCanvasRenderer.stories.tsx → Components/MultiCanvasRenderer.stories.tsx} +2 -2
- package/src/stories/{SequenceDiagram.stories.tsx → Components/SequenceDiagram.stories.tsx} +2 -2
- package/src/stories/{StateView.stories.tsx → Components/StateView.stories.tsx} +2 -2
- package/src/stories/{TraceViewer.stories.tsx → Components/TraceViewer.stories.tsx} +3 -3
- package/src/stories/{WorkflowSequenceDiagram.stories.tsx → Components/WorkflowSequenceDiagram.stories.tsx} +1 -1
- package/src/stories/{AnimationWorkshop.stories.tsx → Features/AnimationWorkshop.stories.tsx} +5 -5
- package/src/stories/{ChangeTypeVisual.stories.tsx → Features/ChangeTypeVisual.stories.tsx} +2 -2
- package/src/stories/{EventDrivenAnimations.stories.tsx → Features/EventDrivenAnimations.stories.tsx} +2 -2
- package/src/stories/{MultiConfig.stories.tsx → Features/MultiConfig.stories.tsx} +2 -2
- package/src/stories/{ElkEdgeRouting.stories.tsx → Layout/ElkEdgeRouting.stories.tsx} +2 -2
- package/src/stories/{GraphOrientation.stories.tsx → Layout/GraphOrientation.stories.tsx} +3 -3
- package/src/stories/{MultiDirectionalConnections.stories.tsx → Layout/MultiDirectionalConnections.stories.tsx} +2 -2
- package/src/stories/{NodeDefinitionComparison.stories.tsx → Nodes/NodeDefinitionComparison.stories.tsx} +2 -2
- package/src/stories/{NodeNameFallbacks.stories.tsx → Nodes/NodeNameFallbacks.stories.tsx} +2 -2
- package/src/stories/{NumericColors.stories.tsx → Nodes/NumericColors.stories.tsx} +2 -2
- package/src/stories/{EventNameDisplay.stories.tsx → OTEL/EventNameDisplay.stories.tsx} +2 -2
- package/src/stories/{EventNameFallback.stories.tsx → OTEL/EventNameFallback.stories.tsx} +2 -2
- package/src/stories/{OtelComponents.stories.tsx → OTEL/OtelComponents.stories.tsx} +3 -3
- package/src/stories/{OtelNodeTypesPrototype.stories.tsx → OTEL/OtelNodeTypesPrototype.stories.tsx} +2 -2
- package/src/stories/{RawEventCards.stories.tsx → OTEL/RawEventCards.stories.tsx} +3 -3
- package/src/stories/{SpanBadges.stories.tsx → OTEL/SpanBadges.stories.tsx} +1 -1
- package/src/stories/{ValidationEventsCanvas.stories.tsx → OTEL/ValidationEventsCanvas.stories.tsx} +3 -3
- package/src/stories/{BacklogMdCanvases.stories.tsx → RealWorld/BacklogMdCanvases.stories.tsx} +2 -2
- package/src/stories/{FileCitySequence.stories.tsx → RealWorld/FileCitySequence.stories.tsx} +4 -4
- package/src/stories/{RealCanvasFiles.stories.tsx → RealWorld/RealCanvasFiles.stories.tsx} +7 -7
- package/src/stories/{ComponentDeclarationAudit.stories.tsx → Subsystem/ComponentDeclarationAudit.stories.tsx} +19 -20
- package/src/stories/Subsystem/ComponentGraph/Appearance.stories.tsx +365 -0
- package/src/stories/Subsystem/ComponentGraph/Basics.stories.tsx +137 -0
- package/src/stories/Subsystem/ComponentGraph/Captures.stories.tsx +238 -0
- package/src/stories/Subsystem/ComponentGraph/DetailPanel.stories.tsx +315 -0
- package/src/stories/Subsystem/ComponentGraph/Scenarios.stories.tsx +959 -0
- package/src/stories/Subsystem/ComponentGraph/Spotlights.stories.tsx +646 -0
- package/src/stories/Subsystem/ComponentGraph/fixtures.ts +129 -0
- package/src/subsystem/ComponentDeclaration.tsx +13 -13
- package/src/subsystem/SubsystemComponentGraph.tsx +25 -5
- package/src/subsystem/formatDeclaration.ts +57 -11
- package/src/subsystem/model.test.ts +38 -6
- package/src/subsystem/model.ts +108 -31
- package/src/subsystem/nodes.tsx +103 -27
- package/src/subsystem/tokenizeComponent.ts +1 -1
- package/src/stories/SubsystemComponentGraph.stories.tsx +0 -1225
- /package/src/stories/{dashboard/sample-dashboards → data}/activity-feed-analytics.dashboard.json +0 -0
package/src/subsystem/model.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Subsystem component-graph data model + converters.
|
|
3
3
|
*
|
|
4
4
|
* A subsystem snapshot (see the "Subsystem artifact: facets" topic) is captured
|
|
5
|
-
* as component nodes (
|
|
5
|
+
* as component nodes (construct-tagged source units), file refs, integration edges,
|
|
6
6
|
* and entry points. This module defines the minimal document shape for the
|
|
7
7
|
* *graph* facet and converts it to React Flow nodes/edges — packages render as
|
|
8
8
|
* subgraphs, only cross-package edges leave the box, and shared seams
|
|
@@ -21,14 +21,30 @@ import { computeElkLayout } from '../utils/elkLayout';
|
|
|
21
21
|
import type { GraphifyComponentDetail } from '../graphify';
|
|
22
22
|
import type { SubsystemDeclarationRef } from './declarationRef';
|
|
23
23
|
|
|
24
|
-
export type
|
|
24
|
+
export type SubsystemComponentConstruct =
|
|
25
25
|
| 'class'
|
|
26
26
|
| 'function'
|
|
27
27
|
| 'method'
|
|
28
|
-
| '
|
|
28
|
+
| 'interface'
|
|
29
|
+
| 'type_alias'
|
|
30
|
+
| 'enum'
|
|
29
31
|
| 'module'
|
|
32
|
+
| 'store'
|
|
30
33
|
| 'external';
|
|
31
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Semantic role — where the node sits in the topology, orthogonal to
|
|
37
|
+
* `construct` (what it is). Roles have *inherited* anatomy: an entry renders
|
|
38
|
+
* as its real code shape (function dispatcher, type contract); a service has
|
|
39
|
+
* no source at all (`construct: 'external'` + purl identity). Contrast with
|
|
40
|
+
* `construct: 'store'`, which introduces its own anatomy (the state block) —
|
|
41
|
+
* that is why store is a construct and not a role. Role nodes must stay anchored to real code: an
|
|
42
|
+
* `entry` is a boundary element (route dispatcher, message contract) carrying
|
|
43
|
+
* the wire address as identity; a `service` is an external system the process
|
|
44
|
+
* calls out to (identity via purl, no `process` — it belongs to no region).
|
|
45
|
+
*/
|
|
46
|
+
export type SubsystemComponentRole = 'entry' | 'service';
|
|
47
|
+
|
|
32
48
|
// ---------------------------------------------------------------------------
|
|
33
49
|
// Declaration tokens — structured source representation
|
|
34
50
|
// ---------------------------------------------------------------------------
|
|
@@ -65,19 +81,48 @@ export type SubsystemEdgeMechanism =
|
|
|
65
81
|
| 'contains'
|
|
66
82
|
| 'feeds'
|
|
67
83
|
| 'produces'
|
|
84
|
+
| 'writes'
|
|
85
|
+
| 'reads'
|
|
86
|
+
| 'watches'
|
|
68
87
|
| 'registers-into';
|
|
69
88
|
|
|
70
|
-
/** A component node — the named unit,
|
|
89
|
+
/** A component node — the named unit, construct-tagged; `file` is its location. */
|
|
71
90
|
export interface SubsystemComponent {
|
|
72
91
|
id: string;
|
|
73
92
|
name: string;
|
|
74
|
-
|
|
93
|
+
/**
|
|
94
|
+
* The node's construct — what it IS as a declaration (class, function,
|
|
95
|
+
* method, interface, type alias, enum, store, external), driving node
|
|
96
|
+
* anatomy, color, badge, and the verification strategy. Every construct
|
|
97
|
+
* anchors to a definition; runtime occurrences (variables, activations,
|
|
98
|
+
* instances) are NOT constructs — they belong to a future execution-mode
|
|
99
|
+
* graph whose occurrence nodes reference these definitions. Ontology:
|
|
100
|
+
* construct = what it is, role = where it sits, process = where it runs.
|
|
101
|
+
*/
|
|
102
|
+
construct: SubsystemComponentConstruct;
|
|
75
103
|
/** Source location the component lives in (repo-root-relative path). */
|
|
76
104
|
file: string;
|
|
77
105
|
/** PURL identifying the repo or package this component lives in (for subgraph grouping). */
|
|
78
106
|
purl: string;
|
|
79
107
|
/** One-line purpose shown on the node. */
|
|
80
108
|
purpose?: string;
|
|
109
|
+
/**
|
|
110
|
+
* Semantic role — where the node sits in the topology (boundary element,
|
|
111
|
+
* external system), orthogonal to `construct` (what it is). Drives the
|
|
112
|
+
* glyph and the edge-pairing rules: boundary-crossing edges must terminate
|
|
113
|
+
* at an entry or a service. Retained state is NOT a role — it is
|
|
114
|
+
* `construct: 'store'` (state-block anatomy, `writes`/`reads`/`watches`
|
|
115
|
+
* inbound, `produces` outbound).
|
|
116
|
+
*/
|
|
117
|
+
role?: SubsystemComponentRole;
|
|
118
|
+
/**
|
|
119
|
+
* Runtime process membership — which deployment unit this node is a
|
|
120
|
+
* member of (e.g. `trail-viewer/host`, `trail-viewer/renderer`). Nodes
|
|
121
|
+
* sharing a `process` are drawn inside one boundary region (grouping is
|
|
122
|
+
* `process ?? purl`); nodes without one sit outside every boundary
|
|
123
|
+
* (external actors, services, libraries).
|
|
124
|
+
*/
|
|
125
|
+
process?: string;
|
|
81
126
|
/** A symbol this component exposes / is (the node's identity). */
|
|
82
127
|
symbol?: string;
|
|
83
128
|
/**
|
|
@@ -96,7 +141,7 @@ export interface SubsystemComponent {
|
|
|
96
141
|
capture?: 'edited' | 'analyzed' | 'referenced';
|
|
97
142
|
/**
|
|
98
143
|
* Graphify drill-down detail for this component, when the facet is anchored
|
|
99
|
-
* to a graphify node. Discriminated by kind (class/function/type/module/
|
|
144
|
+
* to a graphify node. Discriminated by kind (class/function/type/module/store/
|
|
100
145
|
* external); rendered in the detail panel on click.
|
|
101
146
|
*/
|
|
102
147
|
detail?: GraphifyComponentDetail;
|
|
@@ -133,10 +178,10 @@ export interface SubsystemComponentEdge {
|
|
|
133
178
|
}
|
|
134
179
|
|
|
135
180
|
/**
|
|
136
|
-
* Derive a consistent display `name` from a code `symbol` +
|
|
181
|
+
* Derive a consistent display `name` from a code `symbol` + construct.
|
|
137
182
|
*
|
|
138
|
-
* `symbol` is the source of truth (fully-qualified code identity).
|
|
139
|
-
*
|
|
183
|
+
* `symbol` is the source of truth (fully-qualified code identity). The name
|
|
184
|
+
* is the symbol itself:
|
|
140
185
|
* - class/type/module/function/script/... symbol → symbol (e.g. `SessionReader`)
|
|
141
186
|
* - method `Owner.method` → last segment (e.g. `SessionReader.normalize` → `normalize`)
|
|
142
187
|
* - module, no symbol → basename of `file` (e.g. `transcript.ts` → `transcript`) —
|
|
@@ -145,20 +190,37 @@ export interface SubsystemComponentEdge {
|
|
|
145
190
|
*/
|
|
146
191
|
export function deriveNameFromSymbol(
|
|
147
192
|
symbol: string | undefined,
|
|
148
|
-
|
|
193
|
+
construct: SubsystemComponentConstruct,
|
|
149
194
|
existingName?: string,
|
|
150
195
|
file?: string,
|
|
151
196
|
): string {
|
|
197
|
+
let name: string | undefined;
|
|
152
198
|
if (symbol && symbol.trim()) {
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
// Module nodes without a symbol are whole files → use the file basename.
|
|
156
|
-
if (kind === 'module' && file) {
|
|
199
|
+
name = symbol;
|
|
200
|
+
} else if (construct === 'module' && file) {
|
|
157
201
|
const base = file.split('/').pop() ?? '';
|
|
158
202
|
const clean = base.replace(/\.[^.]+$/, ''); // strip extension
|
|
159
|
-
if (clean)
|
|
203
|
+
if (clean) name = clean;
|
|
204
|
+
}
|
|
205
|
+
if (!name) name = existingName ?? 'untitled';
|
|
206
|
+
|
|
207
|
+
// Decoration = what the drill-down shows. Executable constructs wear `()`
|
|
208
|
+
// (a signature you can call); brace-bodied constructs wear ` {}` (a member
|
|
209
|
+
// body — fields for types/interfaces/enums, fields+methods for classes).
|
|
210
|
+
// Everything else (store, variable, module, external) renders bare.
|
|
211
|
+
if ((construct === 'function' || construct === 'method') && !name.endsWith('()')) {
|
|
212
|
+
name = `${name}()`;
|
|
160
213
|
}
|
|
161
|
-
|
|
214
|
+
if (
|
|
215
|
+
(construct === 'class' ||
|
|
216
|
+
construct === 'interface' ||
|
|
217
|
+
construct === 'type_alias' ||
|
|
218
|
+
construct === 'enum') &&
|
|
219
|
+
!name.endsWith('{}')
|
|
220
|
+
) {
|
|
221
|
+
name = `${name} {}`;
|
|
222
|
+
}
|
|
223
|
+
return name;
|
|
162
224
|
}
|
|
163
225
|
|
|
164
226
|
/**
|
|
@@ -237,6 +299,9 @@ export const MECHANISM_COLOR: Record<SubsystemEdgeMechanism, string> = {
|
|
|
237
299
|
contains: '#6c5ce7', // indigo
|
|
238
300
|
feeds: '#22c55e', // green — data-flow into a processor
|
|
239
301
|
produces: '#e07a5f', // terracotta — emits an output type
|
|
302
|
+
writes: '#2f9e44', // deep green — mutates retained state
|
|
303
|
+
reads: '#0ea5e9', // sky — pulls from retained state
|
|
304
|
+
watches: '#9ca3af', // gray — observes, owns nothing
|
|
240
305
|
'registers-into': '#ff6b35', // orange
|
|
241
306
|
};
|
|
242
307
|
|
|
@@ -256,6 +321,9 @@ export const MECHANISM_STYLE: Record<SubsystemEdgeMechanism, 'solid' | 'dashed'
|
|
|
256
321
|
contains: 'solid',
|
|
257
322
|
feeds: 'solid',
|
|
258
323
|
produces: 'solid',
|
|
324
|
+
writes: 'solid',
|
|
325
|
+
reads: 'solid',
|
|
326
|
+
watches: 'dashed',
|
|
259
327
|
'registers-into': 'dashed',
|
|
260
328
|
};
|
|
261
329
|
|
|
@@ -274,14 +342,19 @@ export function packageColor(name: string): string {
|
|
|
274
342
|
return palette[h % palette.length];
|
|
275
343
|
}
|
|
276
344
|
|
|
277
|
-
/** Color per
|
|
278
|
-
|
|
279
|
-
class
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
345
|
+
/** Color per semantic role — applied ONLY to the role badge (top-right tab on
|
|
346
|
+
* nodes that carry a role). The node border stays construct-colored. Note:
|
|
347
|
+
* `service` currently equals the old class hex; harmless while role badges
|
|
348
|
+
* are the only surface using it, but pick a distinct value if roles ever
|
|
349
|
+
* take over node borders. */
|
|
350
|
+
export const ROLE_COLOR: Record<SubsystemComponentRole, string> = {
|
|
351
|
+
entry: '#ff6b35', // orange — boundary element
|
|
352
|
+
service: '#0893d2', // blue — external system
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
export const ROLE_LABEL: Record<SubsystemComponentRole, string> = {
|
|
356
|
+
entry: 'entry',
|
|
357
|
+
service: 'service',
|
|
285
358
|
};
|
|
286
359
|
|
|
287
360
|
/**
|
|
@@ -297,12 +370,16 @@ export function convertSubsystemToNodes(
|
|
|
297
370
|
opts: { maxNodeWidth?: number } = {},
|
|
298
371
|
): SubsystemGraphNode[] {
|
|
299
372
|
const { maxNodeWidth } = opts;
|
|
300
|
-
// Group components by
|
|
373
|
+
// Group components into boundary regions by process when authored, else by
|
|
374
|
+
// package. Process is runtime membership (drawn as one boundary region);
|
|
375
|
+
// purl is code identity — nodes without a process (external actors,
|
|
376
|
+
// services, libraries) fall back to purl and sit outside every boundary.
|
|
301
377
|
const byPkg = new Map<string, SubsystemComponent[]>();
|
|
302
378
|
for (const c of doc.components) {
|
|
303
|
-
const
|
|
379
|
+
const regionKey = c.process ?? c.purl;
|
|
380
|
+
const list = byPkg.get(regionKey) ?? [];
|
|
304
381
|
list.push(c);
|
|
305
|
-
byPkg.set(
|
|
382
|
+
byPkg.set(regionKey, list);
|
|
306
383
|
}
|
|
307
384
|
|
|
308
385
|
const nodes: SubsystemGraphNode[] = [];
|
|
@@ -322,7 +399,7 @@ export function convertSubsystemToNodes(
|
|
|
322
399
|
const row = Math.floor(i / COLS);
|
|
323
400
|
// Estimate rendered node width from the longest text line so ELK reserves
|
|
324
401
|
// the right space (names can wrap, so we cap at the configurable max).
|
|
325
|
-
const text = [c.symbol, c.name, c.file
|
|
402
|
+
const text = [c.symbol, c.name, c.file?.split('/').pop() ?? '']
|
|
326
403
|
.filter((t): t is string => !!t)
|
|
327
404
|
.sort((a, b) => b.length - a.length)[0];
|
|
328
405
|
const cap = maxNodeWidth ?? 300;
|
|
@@ -387,8 +464,8 @@ export function subsystemGraphLayoutKey(
|
|
|
387
464
|
doc: Pick<SubsystemGraphDocument, 'components' | 'edges'>,
|
|
388
465
|
): string {
|
|
389
466
|
const components = doc.components
|
|
390
|
-
.map(({ id, purl, name, symbol,
|
|
391
|
-
[id, purl, name, symbol ?? '',
|
|
467
|
+
.map(({ id, purl, name, symbol, construct, file, purpose, process }) =>
|
|
468
|
+
[id, purl, name, symbol ?? '', construct, file, purpose ?? '', process ?? ''].join('\0'))
|
|
392
469
|
.sort()
|
|
393
470
|
.join('\n');
|
|
394
471
|
const edgeKey = doc.edges
|
|
@@ -437,7 +514,7 @@ export async function buildSubsystemGraph(
|
|
|
437
514
|
component: {
|
|
438
515
|
id: extId,
|
|
439
516
|
name: label,
|
|
440
|
-
|
|
517
|
+
construct: 'external',
|
|
441
518
|
purl: 'external',
|
|
442
519
|
file: '',
|
|
443
520
|
purpose: 'cross-package integration target (not a member node)',
|
package/src/subsystem/nodes.tsx
CHANGED
|
@@ -16,18 +16,26 @@ import {
|
|
|
16
16
|
} from '@xyflow/react';
|
|
17
17
|
import { useTheme } from '@principal-ade/industry-theme';
|
|
18
18
|
import {
|
|
19
|
-
KIND_COLOR,
|
|
20
19
|
MECHANISM_COLOR,
|
|
20
|
+
MECHANISM_STYLE,
|
|
21
|
+
ROLE_COLOR,
|
|
22
|
+
ROLE_LABEL,
|
|
21
23
|
deriveNameFromSymbol,
|
|
22
24
|
type SubsystemGraphNode,
|
|
23
25
|
type SubsystemGraphEdge,
|
|
24
26
|
} from './model';
|
|
27
|
+
import { constructColorsFromPierreTheme } from '../pierre/constructColors';
|
|
28
|
+
import { resolvePierreSyntaxThemeName } from '../pierre/pierreSyntaxTheme';
|
|
25
29
|
|
|
26
|
-
export const
|
|
30
|
+
export const CONSTRUCT_LABEL: Record<string, string> = {
|
|
27
31
|
class: 'class',
|
|
28
32
|
function: 'function',
|
|
29
|
-
|
|
33
|
+
method: 'method',
|
|
34
|
+
interface: 'interface',
|
|
35
|
+
type_alias: 'type alias',
|
|
36
|
+
enum: 'enum',
|
|
30
37
|
module: 'module',
|
|
38
|
+
store: 'store',
|
|
31
39
|
external: 'external',
|
|
32
40
|
};
|
|
33
41
|
|
|
@@ -48,6 +56,9 @@ function breakWords(s: string): string {
|
|
|
48
56
|
export interface SubsystemGraphCallbacks {
|
|
49
57
|
/** Click a component — open its file/entry point. */
|
|
50
58
|
onSelect?: (componentId: string) => void;
|
|
59
|
+
/** Click the filename badge — open that component's file in the drawer
|
|
60
|
+
* (same path as clicking the file link in the declaration panel). */
|
|
61
|
+
onOpenFile?: (componentId: string) => void;
|
|
51
62
|
/** Click an edge (or its label) — select the relationship. */
|
|
52
63
|
onEdgeSelect?: (edgeId: string) => void;
|
|
53
64
|
/** Hover a component (null on leave) — associates it with the file tree. */
|
|
@@ -60,15 +71,19 @@ export interface SubsystemGraphCallbacks {
|
|
|
60
71
|
export const SUBSYSTEM_CALLBACKS: SubsystemGraphCallbacks = {};
|
|
61
72
|
|
|
62
73
|
export function SubsystemComponentNode(props: NodeProps<SubsystemGraphNode>) {
|
|
63
|
-
const { theme } = useTheme();
|
|
74
|
+
const { theme, mode } = useTheme();
|
|
64
75
|
const { data, selected, width: nodeWidth, height: nodeHeight } = props;
|
|
65
76
|
const c = data.component;
|
|
66
|
-
|
|
77
|
+
// Construct owns node color, derived from the active Pierre syntax theme —
|
|
78
|
+
// the same palette the declaration panel and file drawer render with. Role
|
|
79
|
+
// shows as the hover badge, not as color — for now; a role glyph/accent may
|
|
80
|
+
// come later.
|
|
81
|
+
const color = constructColorsFromPierreTheme(resolvePierreSyntaxThemeName(mode))[c.construct];
|
|
67
82
|
const [hover, setHover] = useState(false);
|
|
68
83
|
const configuredMax = SUBSYSTEM_CALLBACKS.maxNodeWidth;
|
|
69
84
|
const maxWidth = configuredMax ?? 300;
|
|
70
85
|
// `symbol` is the source of truth; `name` is derived from it consistently.
|
|
71
|
-
const displayName = deriveNameFromSymbol(c.symbol, c.
|
|
86
|
+
const displayName = deriveNameFromSymbol(c.symbol, c.construct, c.name, c.file);
|
|
72
87
|
// Set while a file is open in the drawer: true → spotlight, false → dim,
|
|
73
88
|
// absent (no file open) → neutral.
|
|
74
89
|
const fileMatch = data.fileMatch as boolean | undefined;
|
|
@@ -117,36 +132,91 @@ export function SubsystemComponentNode(props: NodeProps<SubsystemGraphNode>) {
|
|
|
117
132
|
fontFamily: theme.fonts.body,
|
|
118
133
|
}}
|
|
119
134
|
>
|
|
120
|
-
{/*
|
|
121
|
-
|
|
135
|
+
{/* Construct badge — a small tab riding the top-right border, in the
|
|
136
|
+
construct color. Persistent (no hover needed); pointer-events none so
|
|
137
|
+
clicks pass through to the node. */}
|
|
138
|
+
<div
|
|
139
|
+
style={{
|
|
140
|
+
position: 'absolute',
|
|
141
|
+
top: -9,
|
|
142
|
+
left: 10,
|
|
143
|
+
zIndex: 1,
|
|
144
|
+
fontFamily: theme.fonts.monospace,
|
|
145
|
+
fontSize: theme.fontSizes[0] * 1.1,
|
|
146
|
+
letterSpacing: 0.5,
|
|
147
|
+
textTransform: 'uppercase',
|
|
148
|
+
lineHeight: '17px',
|
|
149
|
+
color,
|
|
150
|
+
background: theme.colors.backgroundSecondary ?? theme.colors.background,
|
|
151
|
+
border: `1px solid ${color}`,
|
|
152
|
+
borderRadius: 4,
|
|
153
|
+
padding: '0 5px',
|
|
154
|
+
}}
|
|
155
|
+
>
|
|
156
|
+
{CONSTRUCT_LABEL[c.construct] ?? c.construct}
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
{/* Role badge — top-right, only when the node carries a topology role.
|
|
160
|
+
Role color (entry orange / service blue) on the badge; the node
|
|
161
|
+
border stays construct-colored. */}
|
|
162
|
+
{c.role != null && (
|
|
122
163
|
<div
|
|
123
164
|
style={{
|
|
124
165
|
position: 'absolute',
|
|
125
|
-
top: -
|
|
126
|
-
|
|
127
|
-
zIndex:
|
|
128
|
-
display: 'flex',
|
|
129
|
-
alignItems: 'center',
|
|
130
|
-
gap: 6,
|
|
131
|
-
whiteSpace: 'nowrap',
|
|
132
|
-
fontSize: theme.fontSizes[0] * 0.8,
|
|
166
|
+
top: -9,
|
|
167
|
+
right: 10,
|
|
168
|
+
zIndex: 1,
|
|
133
169
|
fontFamily: theme.fonts.monospace,
|
|
170
|
+
fontSize: theme.fontSizes[0] * 1.1,
|
|
134
171
|
letterSpacing: 0.5,
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
172
|
+
textTransform: 'uppercase',
|
|
173
|
+
lineHeight: '17px',
|
|
174
|
+
color: ROLE_COLOR[c.role],
|
|
175
|
+
background: theme.colors.backgroundSecondary ?? theme.colors.background,
|
|
176
|
+
border: `1px solid ${ROLE_COLOR[c.role]}`,
|
|
138
177
|
borderRadius: 4,
|
|
139
|
-
padding: '
|
|
178
|
+
padding: '0 5px',
|
|
140
179
|
}}
|
|
141
180
|
>
|
|
142
|
-
|
|
143
|
-
{KIND_LABEL[c.kind] ?? c.kind}
|
|
144
|
-
</span>
|
|
145
|
-
{c.file ? ` · ${c.file.split('/').pop()}` : ''}
|
|
146
|
-
{c.capture && c.capture !== 'edited' ? ` · ${c.capture}` : ''}
|
|
181
|
+
{ROLE_LABEL[c.role]}
|
|
147
182
|
</div>
|
|
148
183
|
)}
|
|
149
184
|
|
|
185
|
+
{/* Filename badge — a strip across the bottom border, shown on hover
|
|
186
|
+
and pinned while this node's file is open in the drawer (fileMatch).
|
|
187
|
+
Mirrors the construct/role tab badges; hidden for fileless nodes. */}
|
|
188
|
+
{(hover || fileMatch === true) && c.file ? (
|
|
189
|
+
<div
|
|
190
|
+
style={{
|
|
191
|
+
position: 'absolute',
|
|
192
|
+
bottom: -13,
|
|
193
|
+
left: '50%',
|
|
194
|
+
transform: 'translateX(-50%)',
|
|
195
|
+
zIndex: 1,
|
|
196
|
+
fontFamily: theme.fonts.monospace,
|
|
197
|
+
fontSize: theme.fontSizes[0] * 1.1,
|
|
198
|
+
letterSpacing: 0.5,
|
|
199
|
+
lineHeight: '17px',
|
|
200
|
+
whiteSpace: 'nowrap',
|
|
201
|
+
// filename stays neutral — the construct color belongs to the
|
|
202
|
+
// borders and badges, not the file name
|
|
203
|
+
color: theme.colors.text ?? theme.colors.textSecondary,
|
|
204
|
+
background: theme.colors.backgroundSecondary ?? theme.colors.background,
|
|
205
|
+
border: `1px solid ${color}`,
|
|
206
|
+
borderRadius: 4,
|
|
207
|
+
padding: '0 5px',
|
|
208
|
+
cursor: 'pointer',
|
|
209
|
+
}}
|
|
210
|
+
onClick={(e) => {
|
|
211
|
+
// Open the file directly; don't also toggle node selection.
|
|
212
|
+
e.stopPropagation();
|
|
213
|
+
SUBSYSTEM_CALLBACKS.onOpenFile?.(c.id);
|
|
214
|
+
}}
|
|
215
|
+
>
|
|
216
|
+
{c.file.split('/').pop()}
|
|
217
|
+
</div>
|
|
218
|
+
) : null}
|
|
219
|
+
|
|
150
220
|
{/* Purpose is no longer shown as a hover tooltip below the node — it
|
|
151
221
|
lives in the declaration panel and the graphify detail payload. */}
|
|
152
222
|
|
|
@@ -168,7 +238,11 @@ export function SubsystemComponentNode(props: NodeProps<SubsystemGraphNode>) {
|
|
|
168
238
|
{breakWords(displayName)}
|
|
169
239
|
</div>
|
|
170
240
|
|
|
171
|
-
{
|
|
241
|
+
{/* Hide the identity line when the symbol is just the title without its
|
|
242
|
+
decoration (`()` or ` {}`) — only show it when it adds information
|
|
243
|
+
(e.g. the dotted host on methods, or a different code identity). */}
|
|
244
|
+
{c.symbol &&
|
|
245
|
+
c.symbol !== displayName.replace(/ ?\{\}$/, '').replace(/\(\)$/, '') && (
|
|
172
246
|
<div
|
|
173
247
|
style={{
|
|
174
248
|
fontSize: theme.fontSizes[0] * 0.82,
|
|
@@ -204,7 +278,9 @@ export function SubsystemEdge({
|
|
|
204
278
|
const path = data?.elkPath ?? '';
|
|
205
279
|
const mechanism = data?.mechanism ?? 'imports';
|
|
206
280
|
const color = MECHANISM_COLOR[mechanism] ?? '#888';
|
|
207
|
-
|
|
281
|
+
// Dash style comes from the mechanism table (dashed = inverted-control or
|
|
282
|
+
// observational relationships: hierarchy, registration, watches).
|
|
283
|
+
const isDashed = MECHANISM_STYLE[mechanism] === 'dashed';
|
|
208
284
|
const dimmed = data?.dimmed === true;
|
|
209
285
|
const opacity = dimmed ? 0.15 : 1;
|
|
210
286
|
|
|
@@ -50,7 +50,7 @@ export async function tokenizeComponent(
|
|
|
50
50
|
if (component.tokens) return component.tokens;
|
|
51
51
|
|
|
52
52
|
// External kind — not valid TypeScript, bypass Prettier.
|
|
53
|
-
const kind = component.detail?.kind ?? component.
|
|
53
|
+
const kind = component.detail?.kind ?? component.construct;
|
|
54
54
|
if (kind === 'external') {
|
|
55
55
|
const label = component.detail?.kind === 'external' ? component.detail.label : component.name;
|
|
56
56
|
const escaped = label.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|