@principal-ai/principal-view-react 0.16.55 → 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.
Files changed (125) hide show
  1. package/dist/graphify/consolidated.d.ts +16 -1
  2. package/dist/graphify/consolidated.d.ts.map +1 -1
  3. package/dist/graphify/index.d.ts +1 -1
  4. package/dist/graphify/index.d.ts.map +1 -1
  5. package/dist/graphify/index.js.map +1 -1
  6. package/dist/graphify/kind.d.ts +1 -1
  7. package/dist/graphify/kind.d.ts.map +1 -1
  8. package/dist/graphify/kind.js +7 -1
  9. package/dist/graphify/kind.js.map +1 -1
  10. package/dist/index.d.ts +5 -4
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +3 -2
  13. package/dist/index.js.map +1 -1
  14. package/dist/pierre/constructColors.d.ts +24 -0
  15. package/dist/pierre/constructColors.d.ts.map +1 -0
  16. package/dist/pierre/constructColors.js +91 -0
  17. package/dist/pierre/constructColors.js.map +1 -0
  18. package/dist/pierre/index.d.ts +2 -0
  19. package/dist/pierre/index.d.ts.map +1 -1
  20. package/dist/pierre/index.js +1 -0
  21. package/dist/pierre/index.js.map +1 -1
  22. package/dist/pierre/pierreSyntaxTheme.d.ts +11 -0
  23. package/dist/pierre/pierreSyntaxTheme.d.ts.map +1 -0
  24. package/dist/pierre/pierreSyntaxTheme.js +20 -0
  25. package/dist/pierre/pierreSyntaxTheme.js.map +1 -0
  26. package/dist/pierre/syntaxTokens.d.ts +11 -0
  27. package/dist/pierre/syntaxTokens.d.ts.map +1 -0
  28. package/dist/pierre/syntaxTokens.js +51 -0
  29. package/dist/pierre/syntaxTokens.js.map +1 -0
  30. package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts +8 -0
  31. package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts.map +1 -0
  32. package/dist/stories/Subsystem/ComponentGraph/fixtures.js +116 -0
  33. package/dist/stories/Subsystem/ComponentGraph/fixtures.js.map +1 -0
  34. package/dist/subsystem/ComponentDeclaration.d.ts +2 -2
  35. package/dist/subsystem/ComponentDeclaration.d.ts.map +1 -1
  36. package/dist/subsystem/ComponentDeclaration.js +47 -24
  37. package/dist/subsystem/ComponentDeclaration.js.map +1 -1
  38. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  39. package/dist/subsystem/SubsystemComponentGraph.js +23 -2
  40. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  41. package/dist/subsystem/formatDeclaration.d.ts.map +1 -1
  42. package/dist/subsystem/formatDeclaration.js +49 -10
  43. package/dist/subsystem/formatDeclaration.js.map +1 -1
  44. package/dist/subsystem/model.d.ts +57 -12
  45. package/dist/subsystem/model.d.ts.map +1 -1
  46. package/dist/subsystem/model.js +54 -24
  47. package/dist/subsystem/model.js.map +1 -1
  48. package/dist/subsystem/nodes.d.ts +4 -1
  49. package/dist/subsystem/nodes.d.ts.map +1 -1
  50. package/dist/subsystem/nodes.js +70 -21
  51. package/dist/subsystem/nodes.js.map +1 -1
  52. package/dist/subsystem/tokenizeComponent.d.ts +4 -3
  53. package/dist/subsystem/tokenizeComponent.d.ts.map +1 -1
  54. package/dist/subsystem/tokenizeComponent.js +7 -12
  55. package/dist/subsystem/tokenizeComponent.js.map +1 -1
  56. package/dist/subsystem/tokenizeFormatted.d.ts +3 -6
  57. package/dist/subsystem/tokenizeFormatted.d.ts.map +1 -1
  58. package/dist/subsystem/tokenizeFormatted.js +4 -75
  59. package/dist/subsystem/tokenizeFormatted.js.map +1 -1
  60. package/package.json +3 -1
  61. package/src/graphify/consolidated.ts +18 -1
  62. package/src/graphify/index.ts +1 -0
  63. package/src/graphify/kind.ts +7 -1
  64. package/src/index.ts +5 -3
  65. package/src/pierre/constructColors.test.ts +52 -0
  66. package/src/pierre/constructColors.ts +112 -0
  67. package/src/pierre/index.ts +5 -0
  68. package/src/pierre/pierreSyntaxTheme.ts +23 -0
  69. package/src/pierre/syntaxTokens.test.ts +35 -0
  70. package/src/pierre/syntaxTokens.ts +60 -0
  71. package/src/stories/{BoundaryNodes.stories.tsx → Audit/BoundaryNodes.stories.tsx} +1 -1
  72. package/src/stories/{CanvasEdgeTypes.stories.tsx → Audit/CanvasEdgeTypes.stories.tsx} +1 -1
  73. package/src/stories/{CanvasNodeTypes.stories.tsx → Audit/CanvasNodeTypes.stories.tsx} +1 -1
  74. package/src/stories/{ColorPriority.stories.tsx → Audit/ColorPriority.stories.tsx} +1 -1
  75. package/src/stories/{DiamondBadges.stories.tsx → Audit/DiamondBadges.stories.tsx} +1 -1
  76. package/src/stories/{NodeDimensionsTesting.stories.tsx → Audit/NodeDimensionsTesting.stories.tsx} +2 -2
  77. package/src/stories/{NodeFieldsAudit.stories.tsx → Audit/NodeFieldsAudit.stories.tsx} +1 -1
  78. package/src/stories/{NodeFontSizeTesting.stories.tsx → Audit/NodeFontSizeTesting.stories.tsx} +2 -2
  79. package/src/stories/{NodeShapes.stories.tsx → Audit/NodeShapes.stories.tsx} +1 -1
  80. package/src/stories/{WideNodesTesting.stories.tsx → Audit/WideNodesTesting.stories.tsx} +2 -2
  81. package/src/stories/{dashboard → Components}/DashboardRenderer.stories.tsx +2 -2
  82. package/src/stories/{GraphRenderer.stories.tsx → Components/GraphRenderer.stories.tsx} +2 -2
  83. package/src/stories/{MultiCanvasRenderer.stories.tsx → Components/MultiCanvasRenderer.stories.tsx} +2 -2
  84. package/src/stories/{SequenceDiagram.stories.tsx → Components/SequenceDiagram.stories.tsx} +2 -2
  85. package/src/stories/{StateView.stories.tsx → Components/StateView.stories.tsx} +2 -2
  86. package/src/stories/{TraceViewer.stories.tsx → Components/TraceViewer.stories.tsx} +3 -3
  87. package/src/stories/{WorkflowSequenceDiagram.stories.tsx → Components/WorkflowSequenceDiagram.stories.tsx} +1 -1
  88. package/src/stories/{AnimationWorkshop.stories.tsx → Features/AnimationWorkshop.stories.tsx} +5 -5
  89. package/src/stories/{ChangeTypeVisual.stories.tsx → Features/ChangeTypeVisual.stories.tsx} +2 -2
  90. package/src/stories/{EventDrivenAnimations.stories.tsx → Features/EventDrivenAnimations.stories.tsx} +2 -2
  91. package/src/stories/{MultiConfig.stories.tsx → Features/MultiConfig.stories.tsx} +2 -2
  92. package/src/stories/{ElkEdgeRouting.stories.tsx → Layout/ElkEdgeRouting.stories.tsx} +2 -2
  93. package/src/stories/{GraphOrientation.stories.tsx → Layout/GraphOrientation.stories.tsx} +3 -3
  94. package/src/stories/{MultiDirectionalConnections.stories.tsx → Layout/MultiDirectionalConnections.stories.tsx} +2 -2
  95. package/src/stories/{NodeDefinitionComparison.stories.tsx → Nodes/NodeDefinitionComparison.stories.tsx} +2 -2
  96. package/src/stories/{NodeNameFallbacks.stories.tsx → Nodes/NodeNameFallbacks.stories.tsx} +2 -2
  97. package/src/stories/{NumericColors.stories.tsx → Nodes/NumericColors.stories.tsx} +2 -2
  98. package/src/stories/{EventNameDisplay.stories.tsx → OTEL/EventNameDisplay.stories.tsx} +2 -2
  99. package/src/stories/{EventNameFallback.stories.tsx → OTEL/EventNameFallback.stories.tsx} +2 -2
  100. package/src/stories/{OtelComponents.stories.tsx → OTEL/OtelComponents.stories.tsx} +3 -3
  101. package/src/stories/{OtelNodeTypesPrototype.stories.tsx → OTEL/OtelNodeTypesPrototype.stories.tsx} +2 -2
  102. package/src/stories/{RawEventCards.stories.tsx → OTEL/RawEventCards.stories.tsx} +3 -3
  103. package/src/stories/{SpanBadges.stories.tsx → OTEL/SpanBadges.stories.tsx} +1 -1
  104. package/src/stories/{ValidationEventsCanvas.stories.tsx → OTEL/ValidationEventsCanvas.stories.tsx} +3 -3
  105. package/src/stories/{BacklogMdCanvases.stories.tsx → RealWorld/BacklogMdCanvases.stories.tsx} +2 -2
  106. package/src/stories/{FileCitySequence.stories.tsx → RealWorld/FileCitySequence.stories.tsx} +4 -4
  107. package/src/stories/{RealCanvasFiles.stories.tsx → RealWorld/RealCanvasFiles.stories.tsx} +7 -7
  108. package/src/stories/{ComponentDeclarationAudit.stories.tsx → Subsystem/ComponentDeclarationAudit.stories.tsx} +19 -20
  109. package/src/stories/Subsystem/ComponentGraph/Appearance.stories.tsx +365 -0
  110. package/src/stories/Subsystem/ComponentGraph/Basics.stories.tsx +137 -0
  111. package/src/stories/Subsystem/ComponentGraph/Captures.stories.tsx +238 -0
  112. package/src/stories/Subsystem/ComponentGraph/DetailPanel.stories.tsx +315 -0
  113. package/src/stories/Subsystem/ComponentGraph/Scenarios.stories.tsx +959 -0
  114. package/src/stories/Subsystem/ComponentGraph/Spotlights.stories.tsx +646 -0
  115. package/src/stories/Subsystem/ComponentGraph/fixtures.ts +129 -0
  116. package/src/subsystem/ComponentDeclaration.tsx +64 -26
  117. package/src/subsystem/SubsystemComponentGraph.tsx +26 -6
  118. package/src/subsystem/formatDeclaration.ts +57 -11
  119. package/src/subsystem/model.test.ts +38 -6
  120. package/src/subsystem/model.ts +110 -31
  121. package/src/subsystem/nodes.tsx +103 -27
  122. package/src/subsystem/tokenizeComponent.ts +12 -12
  123. package/src/subsystem/tokenizeFormatted.ts +9 -82
  124. package/src/stories/SubsystemComponentGraph.stories.tsx +0 -1225
  125. /package/src/stories/{dashboard/sample-dashboards → data}/activity-feed-analytics.dashboard.json +0 -0
@@ -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 (kind-tagged source units), file refs, integration edges,
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 SubsystemComponentKind =
24
+ export type SubsystemComponentConstruct =
25
25
  | 'class'
26
26
  | 'function'
27
27
  | 'method'
28
- | 'type'
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
  // ---------------------------------------------------------------------------
@@ -45,6 +61,8 @@ export type SubsystemDeclTokenKind =
45
61
  export interface SubsystemDeclToken {
46
62
  text: string;
47
63
  kind: SubsystemDeclTokenKind;
64
+ /** Shiki/Pierre foreground when tokenized client-side; omitted on legacy wire tokens. */
65
+ color?: string;
48
66
  }
49
67
 
50
68
  export type SubsystemEdgeMechanism =
@@ -63,19 +81,48 @@ export type SubsystemEdgeMechanism =
63
81
  | 'contains'
64
82
  | 'feeds'
65
83
  | 'produces'
84
+ | 'writes'
85
+ | 'reads'
86
+ | 'watches'
66
87
  | 'registers-into';
67
88
 
68
- /** A component node — the named unit, kind-tagged; `file` is its location. */
89
+ /** A component node — the named unit, construct-tagged; `file` is its location. */
69
90
  export interface SubsystemComponent {
70
91
  id: string;
71
92
  name: string;
72
- kind: SubsystemComponentKind;
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;
73
103
  /** Source location the component lives in (repo-root-relative path). */
74
104
  file: string;
75
105
  /** PURL identifying the repo or package this component lives in (for subgraph grouping). */
76
106
  purl: string;
77
107
  /** One-line purpose shown on the node. */
78
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;
79
126
  /** A symbol this component exposes / is (the node's identity). */
80
127
  symbol?: string;
81
128
  /**
@@ -94,7 +141,7 @@ export interface SubsystemComponent {
94
141
  capture?: 'edited' | 'analyzed' | 'referenced';
95
142
  /**
96
143
  * Graphify drill-down detail for this component, when the facet is anchored
97
- * to a graphify node. Discriminated by kind (class/function/type/module/
144
+ * to a graphify node. Discriminated by kind (class/function/type/module/store/
98
145
  * external); rendered in the detail panel on click.
99
146
  */
100
147
  detail?: GraphifyComponentDetail;
@@ -131,10 +178,10 @@ export interface SubsystemComponentEdge {
131
178
  }
132
179
 
133
180
  /**
134
- * Derive a consistent display `name` from a code `symbol` + kind.
181
+ * Derive a consistent display `name` from a code `symbol` + construct.
135
182
  *
136
- * `symbol` is the source of truth (fully-qualified code identity). For most
137
- * kinds the name is the symbol itself or its last dotted segment:
183
+ * `symbol` is the source of truth (fully-qualified code identity). The name
184
+ * is the symbol itself:
138
185
  * - class/type/module/function/script/... symbol → symbol (e.g. `SessionReader`)
139
186
  * - method `Owner.method` → last segment (e.g. `SessionReader.normalize` → `normalize`)
140
187
  * - module, no symbol → basename of `file` (e.g. `transcript.ts` → `transcript`) —
@@ -143,20 +190,37 @@ export interface SubsystemComponentEdge {
143
190
  */
144
191
  export function deriveNameFromSymbol(
145
192
  symbol: string | undefined,
146
- kind: SubsystemComponentKind,
193
+ construct: SubsystemComponentConstruct,
147
194
  existingName?: string,
148
195
  file?: string,
149
196
  ): string {
197
+ let name: string | undefined;
150
198
  if (symbol && symbol.trim()) {
151
- return symbol;
152
- }
153
- // Module nodes without a symbol are whole files → use the file basename.
154
- if (kind === 'module' && file) {
199
+ name = symbol;
200
+ } else if (construct === 'module' && file) {
155
201
  const base = file.split('/').pop() ?? '';
156
202
  const clean = base.replace(/\.[^.]+$/, ''); // strip extension
157
- if (clean) return 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}()`;
158
213
  }
159
- return existingName ?? 'untitled';
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;
160
224
  }
161
225
 
162
226
  /**
@@ -235,6 +299,9 @@ export const MECHANISM_COLOR: Record<SubsystemEdgeMechanism, string> = {
235
299
  contains: '#6c5ce7', // indigo
236
300
  feeds: '#22c55e', // green — data-flow into a processor
237
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
238
305
  'registers-into': '#ff6b35', // orange
239
306
  };
240
307
 
@@ -254,6 +321,9 @@ export const MECHANISM_STYLE: Record<SubsystemEdgeMechanism, 'solid' | 'dashed'
254
321
  contains: 'solid',
255
322
  feeds: 'solid',
256
323
  produces: 'solid',
324
+ writes: 'solid',
325
+ reads: 'solid',
326
+ watches: 'dashed',
257
327
  'registers-into': 'dashed',
258
328
  };
259
329
 
@@ -272,14 +342,19 @@ export function packageColor(name: string): string {
272
342
  return palette[h % palette.length];
273
343
  }
274
344
 
275
- /** Color per component kind — the informative signal (rather than package). */
276
- export const KIND_COLOR: Record<SubsystemComponentKind, string> = {
277
- class: '#0893d2', // blue
278
- function: '#6c5ce7', // indigo
279
- method: '#d4a03c', // gold
280
- type: '#e07a5f', // terracotta
281
- module: '#4ec9b0', // teal
282
- external: '#b48ead', // purple
345
+ /** Color per semantic roleapplied 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',
283
358
  };
284
359
 
285
360
  /**
@@ -295,12 +370,16 @@ export function convertSubsystemToNodes(
295
370
  opts: { maxNodeWidth?: number } = {},
296
371
  ): SubsystemGraphNode[] {
297
372
  const { maxNodeWidth } = opts;
298
- // Group components by package.
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.
299
377
  const byPkg = new Map<string, SubsystemComponent[]>();
300
378
  for (const c of doc.components) {
301
- const list = byPkg.get(c.purl) ?? [];
379
+ const regionKey = c.process ?? c.purl;
380
+ const list = byPkg.get(regionKey) ?? [];
302
381
  list.push(c);
303
- byPkg.set(c.purl, list);
382
+ byPkg.set(regionKey, list);
304
383
  }
305
384
 
306
385
  const nodes: SubsystemGraphNode[] = [];
@@ -320,7 +399,7 @@ export function convertSubsystemToNodes(
320
399
  const row = Math.floor(i / COLS);
321
400
  // Estimate rendered node width from the longest text line so ELK reserves
322
401
  // the right space (names can wrap, so we cap at the configurable max).
323
- const text = [c.symbol, c.name, c.file.split('/').pop() ?? '']
402
+ const text = [c.symbol, c.name, c.file?.split('/').pop() ?? '']
324
403
  .filter((t): t is string => !!t)
325
404
  .sort((a, b) => b.length - a.length)[0];
326
405
  const cap = maxNodeWidth ?? 300;
@@ -385,8 +464,8 @@ export function subsystemGraphLayoutKey(
385
464
  doc: Pick<SubsystemGraphDocument, 'components' | 'edges'>,
386
465
  ): string {
387
466
  const components = doc.components
388
- .map(({ id, purl, name, symbol, kind, file, purpose }) =>
389
- [id, purl, name, symbol ?? '', kind, file, purpose ?? ''].join('\0'))
467
+ .map(({ id, purl, name, symbol, construct, file, purpose, process }) =>
468
+ [id, purl, name, symbol ?? '', construct, file, purpose ?? '', process ?? ''].join('\0'))
390
469
  .sort()
391
470
  .join('\n');
392
471
  const edgeKey = doc.edges
@@ -435,7 +514,7 @@ export async function buildSubsystemGraph(
435
514
  component: {
436
515
  id: extId,
437
516
  name: label,
438
- kind: 'external',
517
+ construct: 'external',
439
518
  purl: 'external',
440
519
  file: '',
441
520
  purpose: 'cross-package integration target (not a member node)',
@@ -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 KIND_LABEL: Record<string, string> = {
30
+ export const CONSTRUCT_LABEL: Record<string, string> = {
27
31
  class: 'class',
28
32
  function: 'function',
29
- type: 'type',
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
- const color = KIND_COLOR[c.kind] ?? '#888';
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.kind, c.name, c.file);
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
- {/* Kind + package tooltip, shown at the top on hover */}
121
- {hover && (
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: -26,
126
- left: 0,
127
- zIndex: 1000,
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
- color,
136
- background: theme.colors.background,
137
- border: `1px solid ${theme.colors.border}`,
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: '1px 6px',
178
+ padding: '0 5px',
140
179
  }}
141
180
  >
142
- <span style={{ textTransform: 'uppercase' }}>
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
- {c.symbol && c.symbol !== displayName && (
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
- const isDashed = mechanism === 'registers-into';
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
 
@@ -2,12 +2,13 @@
2
2
  * Tokenize a SubsystemComponent into a flat token stream for the detail panel.
3
3
  *
4
4
  * Pipeline: generate declaration string → format with Prettier → tokenize
5
- * with highlight.js. The `component.tokens` field, when present, overrides
6
- * the entire pipeline (for pre-tokenized data from graphify).
5
+ * with Shiki + Pierre themes. The `component.tokens` field, when present,
6
+ * overrides the entire pipeline (for pre-tokenized data from graphify).
7
7
  *
8
8
  * This function is async because Prettier's format() is async.
9
9
  */
10
10
 
11
+ import type { PierreSyntaxThemeName } from '../pierre/pierreSyntaxTheme';
11
12
  import type { SubsystemComponent, SubsystemDeclToken } from './model';
12
13
  import { generateDeclarationString } from './formatDeclaration';
13
14
  import { tokenizeFormatted } from './tokenizeFormatted';
@@ -40,21 +41,20 @@ async function getPrettier() {
40
41
  * it's returned as-is. Otherwise the pipeline generates a TypeScript
41
42
  * declaration string, formats it with Prettier, and tokenizes the output.
42
43
  */
43
- export async function tokenizeComponent(component: SubsystemComponent, printWidth = 80): Promise<SubsystemDeclToken[]> {
44
+ export async function tokenizeComponent(
45
+ component: SubsystemComponent,
46
+ printWidth = 80,
47
+ themeName: PierreSyntaxThemeName = 'pierre-dark',
48
+ ): Promise<SubsystemDeclToken[]> {
44
49
  // Pre-tokenized tokens from the wire take precedence.
45
50
  if (component.tokens) return component.tokens;
46
51
 
47
52
  // External kind — not valid TypeScript, bypass Prettier.
48
- const kind = component.detail?.kind ?? component.kind;
53
+ const kind = component.detail?.kind ?? component.construct;
49
54
  if (kind === 'external') {
50
55
  const label = component.detail?.kind === 'external' ? component.detail.label : component.name;
51
- return [
52
- { text: 'external', kind: 'keyword' },
53
- { text: ' ', kind: 'punctuation' },
54
- { text: "'", kind: 'punctuation' },
55
- { text: label, kind: 'string' },
56
- { text: "'", kind: 'punctuation' },
57
- ];
56
+ const escaped = label.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
57
+ return tokenizeFormatted(`external '${escaped}'`, themeName);
58
58
  }
59
59
 
60
60
  // Generate → format → tokenize
@@ -66,5 +66,5 @@ export async function tokenizeComponent(component: SubsystemComponent, printWidt
66
66
  printWidth,
67
67
  });
68
68
 
69
- return tokenizeFormatted(formatted);
69
+ return tokenizeFormatted(formatted, themeName);
70
70
  }
@@ -1,92 +1,19 @@
1
1
  /**
2
2
  * Tokenize a formatted TypeScript declaration string into SubsystemDeclToken[]
3
- * using highlight.js.
4
- *
5
- * highlight.js produces HTML with `<span class="hljs-...">` wrappers. This
6
- * module parses that output into our token format so the renderer stays
7
- * language-agnostic.
3
+ * using Shiki + Pierre themes (same highlighter stack as @pierre/diffs).
8
4
  */
9
5
 
10
- import hljs from 'highlight.js/lib/core';
11
- import typescript from 'highlight.js/lib/languages/typescript';
12
- import type { SubsystemDeclToken, SubsystemDeclTokenKind } from './model';
13
-
14
- // Register once — safe to call multiple times.
15
- try {
16
- hljs.registerLanguage('typescript', typescript);
17
- } catch {
18
- // Already registered.
19
- }
20
-
21
- // hljs class → our token kind
22
- const CLASS_MAP: Record<string, SubsystemDeclTokenKind> = {
23
- 'hljs-keyword': 'keyword',
24
- 'hljs-title': 'name',
25
- 'hljs-title.function_': 'name',
26
- 'hljs-params': 'member',
27
- 'hljs-type': 'type',
28
- 'hljs-built_in': 'type',
29
- 'hljs-string': 'string',
30
- 'hljs-number': 'string',
31
- 'hljs-comment': 'punctuation',
32
- };
6
+ import { tokenizeWithPierreSyntax } from '../pierre/syntaxTokens';
7
+ import type { PierreSyntaxThemeName } from '../pierre/pierreSyntaxTheme';
8
+ import type { SubsystemDeclToken } from './model';
33
9
 
34
10
  /**
35
11
  * Tokenize a formatted TypeScript string into SubsystemDeclToken[].
36
12
  * The input should already be formatted by Prettier (or be valid TS).
37
13
  */
38
- export function tokenizeFormatted(code: string): SubsystemDeclToken[] {
39
- const result = hljs.highlight(code, { language: 'typescript' });
40
- return parseHighlightedHtml(result.value);
41
- }
42
-
43
- // ---------------------------------------------------------------------------
44
- // HTML parser — walks the hljs output and extracts tokens
45
- // ---------------------------------------------------------------------------
46
-
47
- function unescapeHtml(text: string): string {
48
- return text
49
- .replace(/&quot;/g, '"')
50
- .replace(/&lt;/g, '<')
51
- .replace(/&gt;/g, '>')
52
- .replace(/&amp;/g, '&')
53
- .replace(/&#39;/g, "'");
54
- }
55
-
56
- function parseHighlightedHtml(html: string): SubsystemDeclToken[] {
57
- const tokens: SubsystemDeclToken[] = [];
58
- // Regex matches either `<span class="hljs-...">`, `</span>`, or text content.
59
- const re = /<span class="([^"]+)">|<\/span>|[^<]+/g;
60
- const classStack: string[] = [];
61
- let match: RegExpExecArray | null;
62
-
63
- while ((match = re.exec(html)) !== null) {
64
- const chunk = match[0];
65
-
66
- if (chunk.startsWith('<span')) {
67
- // Opening tag — push class onto stack.
68
- classStack.push(match[1]);
69
- } else if (chunk === '</span>') {
70
- // Closing tag — pop class.
71
- classStack.pop();
72
- } else {
73
- // Text content — emit tokens for each line segment.
74
- const currentClass = classStack[classStack.length - 1] ?? '';
75
- const kind = CLASS_MAP[currentClass] ?? 'punctuation';
76
- const text = chunk;
77
-
78
- // Split on newlines so each line becomes its own token set.
79
- const lines = text.split('\n');
80
- for (let i = 0; i < lines.length; i++) {
81
- if (i > 0) {
82
- tokens.push({ text: '', kind: 'newline' });
83
- }
84
- if (lines[i]) {
85
- tokens.push({ text: unescapeHtml(lines[i]), kind });
86
- }
87
- }
88
- }
89
- }
90
-
91
- return tokens;
14
+ export function tokenizeFormatted(
15
+ code: string,
16
+ themeName: PierreSyntaxThemeName = 'pierre-dark',
17
+ ): Promise<SubsystemDeclToken[]> {
18
+ return tokenizeWithPierreSyntax(code, themeName);
92
19
  }