@principal-ai/principal-view-react 0.16.64 → 0.16.66

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 (36) hide show
  1. package/dist/pierre/PierreFileView.d.ts.map +1 -1
  2. package/dist/pierre/PierreFileView.js +4 -1
  3. package/dist/pierre/PierreFileView.js.map +1 -1
  4. package/dist/pierre/PierreSnippetView.d.ts.map +1 -1
  5. package/dist/pierre/PierreSnippetView.js +8 -1
  6. package/dist/pierre/PierreSnippetView.js.map +1 -1
  7. package/dist/pierre/PierreThroughlineCodeView.d.ts +3 -1
  8. package/dist/pierre/PierreThroughlineCodeView.d.ts.map +1 -1
  9. package/dist/pierre/PierreThroughlineCodeView.js +53 -13
  10. package/dist/pierre/PierreThroughlineCodeView.js.map +1 -1
  11. package/dist/pierre/index.d.ts +1 -0
  12. package/dist/pierre/index.d.ts.map +1 -1
  13. package/dist/pierre/index.js +1 -0
  14. package/dist/pierre/index.js.map +1 -1
  15. package/dist/pierre/pierreFileLang.d.ts +23 -0
  16. package/dist/pierre/pierreFileLang.d.ts.map +1 -0
  17. package/dist/pierre/pierreFileLang.js +48 -0
  18. package/dist/pierre/pierreFileLang.js.map +1 -0
  19. package/dist/subsystem/ComponentDeclaration.d.ts.map +1 -1
  20. package/dist/subsystem/ComponentDeclaration.js +2 -1
  21. package/dist/subsystem/ComponentDeclaration.js.map +1 -1
  22. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  23. package/dist/subsystem/SubsystemComponentGraph.js +56 -1
  24. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  25. package/package.json +1 -1
  26. package/src/components/PendingChanges.test.tsx +12 -0
  27. package/src/pierre/PierreFileView.tsx +6 -2
  28. package/src/pierre/PierreSnippetView.tsx +10 -2
  29. package/src/pierre/PierreThroughlineCodeView.tsx +62 -14
  30. package/src/pierre/index.ts +5 -0
  31. package/src/pierre/pierreFileLang.test.ts +26 -0
  32. package/src/pierre/pierreFileLang.ts +51 -0
  33. package/src/stories/Pierre/CodeView.stories.tsx +40 -17
  34. package/src/subsystem/ComponentDeclaration.tsx +2 -1
  35. package/src/subsystem/SubsystemComponentGraph.tsx +57 -0
  36. package/src/components/GraphRenderer.test.tsx +0 -118
@@ -739,6 +739,50 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
739
739
  [fitFocusBounds, renderThroughlineViewer],
740
740
  );
741
741
 
742
+ // Arrow keys step through the focused throughline once a step is active
743
+ // (sidebar click or drawer open). Ignores typing targets and chords.
744
+ useEffect(() => {
745
+ if (focusedThroughlineId == null || focusedStepIndex == null || !throughlines) {
746
+ return;
747
+ }
748
+ const tl = throughlines.find((t) => t.id === focusedThroughlineId);
749
+ if (!tl || tl.steps.length === 0) return;
750
+
751
+ const onKeyDown = (e: KeyboardEvent) => {
752
+ const el = e.target as HTMLElement | null;
753
+ if (
754
+ el &&
755
+ (el.tagName === 'INPUT' ||
756
+ el.tagName === 'TEXTAREA' ||
757
+ el.tagName === 'SELECT' ||
758
+ el.isContentEditable)
759
+ ) {
760
+ return;
761
+ }
762
+ if (e.metaKey || e.ctrlKey || e.altKey) return;
763
+
764
+ let next: number | null = null;
765
+ if (e.key === 'ArrowDown' || e.key === 'ArrowRight') {
766
+ next = Math.min(tl.steps.length - 1, focusedStepIndex + 1);
767
+ } else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') {
768
+ next = Math.max(0, focusedStepIndex - 1);
769
+ } else {
770
+ return;
771
+ }
772
+ if (next === focusedStepIndex) return;
773
+ e.preventDefault();
774
+ focusThroughlineStep(tl, next);
775
+ };
776
+
777
+ window.addEventListener('keydown', onKeyDown);
778
+ return () => window.removeEventListener('keydown', onKeyDown);
779
+ }, [
780
+ focusedThroughlineId,
781
+ focusedStepIndex,
782
+ throughlines,
783
+ focusThroughlineStep,
784
+ ]);
785
+
742
786
  const toggleThroughlineCollapsed = useCallback((tlId: string) => {
743
787
  setExpandedThroughlines((prev) => {
744
788
  const next = new Set(prev);
@@ -1363,6 +1407,15 @@ function ThroughlineFlow({
1363
1407
  const [headerHover, setHeaderHover] = useState(false);
1364
1408
  const [closeHover, setCloseHover] = useState(false);
1365
1409
  const [hoveredStep, setHoveredStep] = useState<number | null>(null);
1410
+ const stepButtonRefs = useRef<(HTMLButtonElement | null)[]>([]);
1411
+
1412
+ // Keep DOM focus on the active step so the browser focus ring (and
1413
+ // subsequent arrow keys) follow arrow navigation, not the originally
1414
+ // clicked button.
1415
+ useEffect(() => {
1416
+ if (active?.stepIndex == null) return;
1417
+ stepButtonRefs.current[active.stepIndex]?.focus({ preventScroll: true });
1418
+ }, [active?.stepIndex]);
1366
1419
 
1367
1420
  return (
1368
1421
  <div style={{ margin: '4px 0', borderRadius: 8 }}>
@@ -1460,6 +1513,9 @@ function ThroughlineFlow({
1460
1513
  return (
1461
1514
  <button
1462
1515
  key={`${step.edgeId}-${i}`}
1516
+ ref={(el) => {
1517
+ stepButtonRefs.current[i] = el;
1518
+ }}
1463
1519
  type="button"
1464
1520
  onMouseEnter={() => setHoveredStep(i)}
1465
1521
  onMouseLeave={() => setHoveredStep(null)}
@@ -1473,6 +1529,7 @@ function ThroughlineFlow({
1473
1529
  textAlign: 'left',
1474
1530
  borderRadius: 6,
1475
1531
  border: 'none',
1532
+ outline: 'none',
1476
1533
  background: stepActive || hoveredStep === i ? hoverBg : 'transparent',
1477
1534
  cursor: 'pointer',
1478
1535
  transition: 'background 120ms ease',
@@ -1,118 +0,0 @@
1
- import React from 'react';
2
- import { render, screen } from '@testing-library/react';
3
- import { GraphRenderer } from './GraphRenderer';
4
- import type { GraphConfiguration, NodeState, EdgeState } from '@principal-ai/principal-view-core';
5
-
6
- describe('GraphRenderer', () => {
7
- const testConfig: GraphConfiguration = {
8
- metadata: {
9
- name: 'Test Graph',
10
- version: '1.0.0',
11
- },
12
- nodeTypes: {
13
- user: {
14
- shape: 'circle',
15
- color: '#4CAF50',
16
- dataSchema: {
17
- userId: { type: 'string', required: true },
18
- },
19
- },
20
- },
21
- edgeTypes: {
22
- connection: {
23
- style: 'solid',
24
- directed: true,
25
- },
26
- },
27
- allowedConnections: [{ from: 'user', to: 'user', via: 'connection' }],
28
- };
29
-
30
- const testNodes: NodeState[] = [
31
- {
32
- id: 'user-1',
33
- type: 'user',
34
- data: { userId: 'alice' },
35
- createdAt: Date.now(),
36
- updatedAt: Date.now(),
37
- },
38
- {
39
- id: 'user-2',
40
- type: 'user',
41
- data: { userId: 'bob' },
42
- createdAt: Date.now(),
43
- updatedAt: Date.now(),
44
- },
45
- ];
46
-
47
- const testEdges: EdgeState[] = [
48
- {
49
- id: 'conn-1',
50
- type: 'connection',
51
- from: 'user-1',
52
- to: 'user-2',
53
- createdAt: Date.now(),
54
- updatedAt: Date.now(),
55
- },
56
- ];
57
-
58
- it('should render without crashing', () => {
59
- render(<GraphRenderer configuration={testConfig} nodes={testNodes} edges={testEdges} />);
60
-
61
- expect(screen.getByText(/Graph Renderer/i)).toBeDefined();
62
- });
63
-
64
- it('should display configuration name', () => {
65
- render(<GraphRenderer configuration={testConfig} nodes={testNodes} edges={testEdges} />);
66
-
67
- expect(screen.getByText(/Test Graph/i)).toBeDefined();
68
- });
69
-
70
- it('should display node count', () => {
71
- render(<GraphRenderer configuration={testConfig} nodes={testNodes} edges={testEdges} />);
72
-
73
- expect(screen.getByText(/Nodes: 2/i)).toBeDefined();
74
- });
75
-
76
- it('should display edge count', () => {
77
- render(<GraphRenderer configuration={testConfig} nodes={testNodes} edges={testEdges} />);
78
-
79
- expect(screen.getByText(/Edges: 1/i)).toBeDefined();
80
- });
81
-
82
- it('should render with empty nodes and edges', () => {
83
- render(<GraphRenderer configuration={testConfig} nodes={[]} edges={[]} />);
84
-
85
- expect(screen.getByText(/Nodes: 0/i)).toBeDefined();
86
- expect(screen.getByText(/Edges: 0/i)).toBeDefined();
87
- });
88
-
89
- it('should apply custom className', () => {
90
- const { container } = render(
91
- <GraphRenderer
92
- configuration={testConfig}
93
- nodes={testNodes}
94
- edges={testEdges}
95
- className="custom-class"
96
- />
97
- );
98
-
99
- const element = container.querySelector('.custom-class');
100
- expect(element).toBeDefined();
101
- });
102
-
103
- it('should apply custom width and height', () => {
104
- const { container } = render(
105
- <GraphRenderer
106
- configuration={testConfig}
107
- nodes={testNodes}
108
- edges={testEdges}
109
- width="500px"
110
- height="400px"
111
- />
112
- );
113
-
114
- const element = container.querySelector('div');
115
- expect(element?.style.width).toBe('500px');
116
- expect(element?.style.height).toBe('400px');
117
- });
118
- });