@principal-ai/principal-view-react 0.16.54 → 0.16.56

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 (46) hide show
  1. package/dist/index.d.ts +2 -2
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +1 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/pierre/index.d.ts +2 -0
  6. package/dist/pierre/index.d.ts.map +1 -1
  7. package/dist/pierre/index.js +1 -0
  8. package/dist/pierre/index.js.map +1 -1
  9. package/dist/pierre/pierreSyntaxTheme.d.ts +11 -0
  10. package/dist/pierre/pierreSyntaxTheme.d.ts.map +1 -0
  11. package/dist/pierre/pierreSyntaxTheme.js +20 -0
  12. package/dist/pierre/pierreSyntaxTheme.js.map +1 -0
  13. package/dist/pierre/syntaxTokens.d.ts +11 -0
  14. package/dist/pierre/syntaxTokens.d.ts.map +1 -0
  15. package/dist/pierre/syntaxTokens.js +51 -0
  16. package/dist/pierre/syntaxTokens.js.map +1 -0
  17. package/dist/subsystem/ComponentDeclaration.d.ts.map +1 -1
  18. package/dist/subsystem/ComponentDeclaration.js +35 -13
  19. package/dist/subsystem/ComponentDeclaration.js.map +1 -1
  20. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  21. package/dist/subsystem/SubsystemComponentGraph.js +12 -5
  22. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  23. package/dist/subsystem/model.d.ts +4 -0
  24. package/dist/subsystem/model.d.ts.map +1 -1
  25. package/dist/subsystem/model.js +12 -0
  26. package/dist/subsystem/model.js.map +1 -1
  27. package/dist/subsystem/tokenizeComponent.d.ts +4 -3
  28. package/dist/subsystem/tokenizeComponent.d.ts.map +1 -1
  29. package/dist/subsystem/tokenizeComponent.js +6 -11
  30. package/dist/subsystem/tokenizeComponent.js.map +1 -1
  31. package/dist/subsystem/tokenizeFormatted.d.ts +3 -6
  32. package/dist/subsystem/tokenizeFormatted.d.ts.map +1 -1
  33. package/dist/subsystem/tokenizeFormatted.js +4 -75
  34. package/dist/subsystem/tokenizeFormatted.js.map +1 -1
  35. package/package.json +3 -1
  36. package/src/index.ts +2 -1
  37. package/src/pierre/index.ts +5 -0
  38. package/src/pierre/pierreSyntaxTheme.ts +23 -0
  39. package/src/pierre/syntaxTokens.test.ts +35 -0
  40. package/src/pierre/syntaxTokens.ts +60 -0
  41. package/src/subsystem/ComponentDeclaration.tsx +51 -13
  42. package/src/subsystem/SubsystemComponentGraph.tsx +16 -4
  43. package/src/subsystem/model.test.ts +22 -0
  44. package/src/subsystem/model.ts +18 -0
  45. package/src/subsystem/tokenizeComponent.ts +11 -11
  46. package/src/subsystem/tokenizeFormatted.ts +9 -82
@@ -37,6 +37,7 @@ import { IndustryMarkdownSlide } from 'themed-markdown';
37
37
  import {
38
38
  buildSubsystemGraph,
39
39
  MECHANISM_COLOR,
40
+ subsystemGraphLayoutKey,
40
41
  type SubsystemComponentEdge,
41
42
  type SubsystemComponent,
42
43
  type SubsystemEdgeMechanism,
@@ -163,19 +164,30 @@ function Inner({ components, edges, onSelect, onEdgeSelect, measured: _measured,
163
164
  }, [components, selected]);
164
165
 
165
166
  // Pass 1: build with estimated widths so React Flow can measure the DOM.
166
- // The pane stays hidden until Pass 2 completes.
167
+ // The pane stays hidden until Pass 2 completes. Key off layout-affecting
168
+ // fields only — declarationRef updates after verify must not re-run ELK.
169
+ const layoutKey = useMemo(
170
+ () => subsystemGraphLayoutKey({ components, edges }),
171
+ [components, edges],
172
+ );
173
+ const componentsRef = useRef(components);
174
+ const edgesRef = useRef(edges);
175
+ componentsRef.current = components;
176
+ edgesRef.current = edges;
177
+
167
178
  useEffect(() => {
168
179
  let alive = true;
169
180
  pass2DoneRef.current = false;
170
181
  measuredDimsRef.current = new Map();
171
182
  prevMeasuredSigRef.current = '';
172
- void buildSubsystemGraph({ components, edges }, { maxNodeWidth, showEdgeLabels }).then(({ nodes, edges: e }) => {
183
+ const doc = { components: componentsRef.current, edges: edgesRef.current };
184
+ void buildSubsystemGraph(doc, { maxNodeWidth, showEdgeLabels }).then(({ nodes, edges: e }) => {
173
185
  if (!alive) return;
174
186
  setBuilt({ nodes, edges: e as Edge[] });
175
187
  setLayoutReady(false);
176
188
  });
177
189
  return () => { alive = false; };
178
- }, [components, edges]);
190
+ }, [layoutKey, maxNodeWidth, showEdgeLabels]);
179
191
 
180
192
  // Track measured dimensions from React Flow's dimension changes.
181
193
  // These arrive as { type: 'dimensions', id, dimensions } in onNodesChange.
@@ -722,7 +734,7 @@ function Inner({ components, edges, onSelect, onEdgeSelect, measured: _measured,
722
734
  top: 10,
723
735
  right: 10,
724
736
  zIndex: 8,
725
- maxWidth: 'min(560px, 70%)',
737
+ width: 'min(560px, 70%)',
726
738
  maxHeight: '46%',
727
739
  overflowY: 'auto',
728
740
  background: theme.colors.background,
@@ -6,6 +6,7 @@ import {
6
6
  deriveNameFromSymbol,
7
7
  formatPurl,
8
8
  packageColor,
9
+ subsystemGraphLayoutKey,
9
10
  } from './model';
10
11
  import type { SubsystemComponent, SubsystemComponentEdge } from './model';
11
12
 
@@ -87,4 +88,25 @@ describe('subsystem graph model', () => {
87
88
  // Malformed purls pass through untouched.
88
89
  expect(formatPurl('not-a-purl')).toBe('not-a-purl');
89
90
  });
91
+
92
+ test('subsystemGraphLayoutKey ignores declarationRef-only changes', () => {
93
+ const base = { components: comps, edges };
94
+ const withRef = {
95
+ components: comps.map((c, i) =>
96
+ i === 0
97
+ ? {
98
+ ...c,
99
+ declarationRef: {
100
+ file: c.file,
101
+ startLine: 42,
102
+ lineHash: 'abc',
103
+ capturedAt: new Date(0).toISOString(),
104
+ },
105
+ }
106
+ : c,
107
+ ),
108
+ edges,
109
+ };
110
+ expect(subsystemGraphLayoutKey(base)).toBe(subsystemGraphLayoutKey(withRef));
111
+ });
90
112
  });
@@ -45,6 +45,8 @@ export type SubsystemDeclTokenKind =
45
45
  export interface SubsystemDeclToken {
46
46
  text: string;
47
47
  kind: SubsystemDeclTokenKind;
48
+ /** Shiki/Pierre foreground when tokenized client-side; omitted on legacy wire tokens. */
49
+ color?: string;
48
50
  }
49
51
 
50
52
  export type SubsystemEdgeMechanism =
@@ -380,6 +382,22 @@ export function convertSubsystemToEdges(doc: SubsystemGraphDocument): SubsystemG
380
382
  return edges;
381
383
  }
382
384
 
385
+ /** Stable key for layout-affecting graph fields (ignores declarationRef, etc.). */
386
+ export function subsystemGraphLayoutKey(
387
+ doc: Pick<SubsystemGraphDocument, 'components' | 'edges'>,
388
+ ): string {
389
+ const components = doc.components
390
+ .map(({ id, purl, name, symbol, kind, file, purpose }) =>
391
+ [id, purl, name, symbol ?? '', kind, file, purpose ?? ''].join('\0'))
392
+ .sort()
393
+ .join('\n');
394
+ const edgeKey = doc.edges
395
+ .map(({ id, from, to, mechanism }) => [id, from, to, mechanism].join('\0'))
396
+ .sort()
397
+ .join('\n');
398
+ return `${components}|${edgeKey}`;
399
+ }
400
+
383
401
  /**
384
402
  * Build the full React Flow graph (nodes + edges) using **ELK auto-layout** to
385
403
  * position every node (including external stub targets), so the graph is
@@ -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,7 +41,11 @@ 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
 
@@ -48,13 +53,8 @@ export async function tokenizeComponent(component: SubsystemComponent, printWidt
48
53
  const kind = component.detail?.kind ?? component.kind;
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
  }