@principal-ai/principal-view-react 0.7.39 → 0.7.40

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 CHANGED
@@ -28,4 +28,5 @@ export { convertToXYFlowNodes, convertToXYFlowEdges, } from './utils/graphConver
28
28
  export type { EdgeStateWithHandles } from './utils/graphConverter';
29
29
  export { Icon, resolveIcon } from './utils/iconResolver';
30
30
  export type { IconProps } from './utils/iconResolver';
31
+ export { swapGraphOrientation, swapNodePositions, swapEdgeSides, } from './utils/orientationUtils';
31
32
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,YAAY,EACV,kBAAkB,EAClB,UAAU,EACV,YAAY,EACZ,SAAS,EACT,OAAO,EACP,gBAAgB,EAChB,WAAW,EACX,kBAAkB,EAClB,kBAAkB,EAClB,cAAc,EACd,SAAS,EACT,SAAS,EACT,iBAAiB,EACjB,uBAAuB,EAEvB,gBAAgB,EAChB,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,2CAA2C,CAAC;AAGnD,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,YAAY,EACV,kBAAkB,EAClB,mBAAmB,EACnB,kBAAkB,EAClB,cAAc,GACf,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,YAAY,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAErE,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,YAAY,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAErE,OAAO,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AAC3E,YAAY,EAAE,0BAA0B,EAAE,MAAM,oCAAoC,CAAC;AAGrF,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,YAAY,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5D,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,YAAY,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEzD,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,YAAY,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5D,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,YAAY,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAGzD,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,YAAY,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAG3E,OAAO,EACL,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,wBAAwB,CAAC;AAChC,YAAY,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AACnE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACzD,YAAY,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,YAAY,EACV,kBAAkB,EAClB,UAAU,EACV,YAAY,EACZ,SAAS,EACT,OAAO,EACP,gBAAgB,EAChB,WAAW,EACX,kBAAkB,EAClB,kBAAkB,EAClB,cAAc,EACd,SAAS,EACT,SAAS,EACT,iBAAiB,EACjB,uBAAuB,EAEvB,gBAAgB,EAChB,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,2CAA2C,CAAC;AAGnD,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,YAAY,EACV,kBAAkB,EAClB,mBAAmB,EACnB,kBAAkB,EAClB,cAAc,GACf,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,YAAY,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAErE,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,YAAY,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAErE,OAAO,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AAC3E,YAAY,EAAE,0BAA0B,EAAE,MAAM,oCAAoC,CAAC;AAGrF,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,YAAY,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5D,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,YAAY,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEzD,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,YAAY,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5D,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,YAAY,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAGzD,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,YAAY,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAG3E,OAAO,EACL,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,wBAAwB,CAAC;AAChC,YAAY,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AACnE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACzD,YAAY,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EACL,oBAAoB,EACpB,iBAAiB,EACjB,aAAa,GACd,MAAM,0BAA0B,CAAC"}
package/dist/index.js CHANGED
@@ -20,4 +20,5 @@ export { NodeTooltip } from './components/NodeTooltip';
20
20
  // Export utilities
21
21
  export { convertToXYFlowNodes, convertToXYFlowEdges, } from './utils/graphConverter';
22
22
  export { Icon, resolveIcon } from './utils/iconResolver';
23
+ export { swapGraphOrientation, swapNodePositions, swapEdgeSides, } from './utils/orientationUtils';
23
24
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAwBH,oBAAoB;AACpB,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAQ3D,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAG3D,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAG3D,OAAO,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AAG3E,6BAA6B;AAC7B,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAGlD,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAGhD,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAGlD,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAGhD,2BAA2B;AAC3B,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAGvD,mBAAmB;AACnB,OAAO,EACL,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAwBH,oBAAoB;AACpB,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAQ3D,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAG3D,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAG3D,OAAO,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AAG3E,6BAA6B;AAC7B,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAGlD,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAGhD,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAGlD,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAGhD,2BAA2B;AAC3B,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAGvD,mBAAmB;AACnB,OAAO,EACL,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAEzD,OAAO,EACL,oBAAoB,EACpB,iBAAiB,EACjB,aAAa,GACd,MAAM,0BAA0B,CAAC"}
@@ -0,0 +1,19 @@
1
+ import type { NodeState, EdgeState } from '@principal-ai/principal-view-core';
2
+ /**
3
+ * Swaps the orientation of a graph from horizontal to vertical or vice versa.
4
+ * This swaps x/y coordinates and adjusts edge connection sides accordingly.
5
+ */
6
+ export declare function swapGraphOrientation(nodes: NodeState[], edges: EdgeState[]): {
7
+ nodes: NodeState[];
8
+ edges: EdgeState[];
9
+ };
10
+ /**
11
+ * Swaps x and y positions for all nodes
12
+ */
13
+ export declare function swapNodePositions(nodes: NodeState[]): NodeState[];
14
+ /**
15
+ * Swaps edge connection sides to match the new orientation
16
+ * Both sides rotate clockwise: top → right → bottom → left → top
17
+ */
18
+ export declare function swapEdgeSides(edges: EdgeState[]): EdgeState[];
19
+ //# sourceMappingURL=orientationUtils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"orientationUtils.d.ts","sourceRoot":"","sources":["../../src/utils/orientationUtils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAc,MAAM,mCAAmC,CAAC;AAE1F;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,SAAS,EAAE,EAClB,KAAK,EAAE,SAAS,EAAE,GACjB;IAAE,KAAK,EAAE,SAAS,EAAE,CAAC;IAAC,KAAK,EAAE,SAAS,EAAE,CAAA;CAAE,CAK5C;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,SAAS,EAAE,CAQjE;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,SAAS,EAAE,CAuB7D"}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Swaps the orientation of a graph from horizontal to vertical or vice versa.
3
+ * This swaps x/y coordinates and adjusts edge connection sides accordingly.
4
+ */
5
+ export function swapGraphOrientation(nodes, edges) {
6
+ return {
7
+ nodes: swapNodePositions(nodes),
8
+ edges: swapEdgeSides(edges),
9
+ };
10
+ }
11
+ /**
12
+ * Swaps x and y positions for all nodes
13
+ */
14
+ export function swapNodePositions(nodes) {
15
+ return nodes.map(node => ({
16
+ ...node,
17
+ position: node.position ? {
18
+ x: node.position.y,
19
+ y: node.position.x,
20
+ } : undefined,
21
+ }));
22
+ }
23
+ /**
24
+ * Swaps edge connection sides to match the new orientation
25
+ * Both sides rotate clockwise: top → right → bottom → left → top
26
+ */
27
+ export function swapEdgeSides(edges) {
28
+ return edges.map(edge => {
29
+ const data = edge.data;
30
+ const fromSide = data?.fromSide;
31
+ const toSide = data?.toSide;
32
+ const rotatedFromSide = rotateClockwise(fromSide);
33
+ const rotatedToSide = rotateClockwise(toSide);
34
+ // Only include fromSide/toSide if they have defined values (not undefined)
35
+ const newData = { ...edge.data };
36
+ if (rotatedFromSide !== undefined) {
37
+ newData.fromSide = rotatedFromSide;
38
+ }
39
+ if (rotatedToSide !== undefined) {
40
+ newData.toSide = rotatedToSide;
41
+ }
42
+ return {
43
+ ...edge,
44
+ data: newData,
45
+ };
46
+ });
47
+ }
48
+ /**
49
+ * Rotates a side clockwise: top → right → bottom → left → top
50
+ */
51
+ function rotateClockwise(side) {
52
+ if (!side)
53
+ return undefined;
54
+ const clockwiseMap = {
55
+ top: 'right',
56
+ right: 'bottom',
57
+ bottom: 'left',
58
+ left: 'top',
59
+ };
60
+ return clockwiseMap[side];
61
+ }
62
+ //# sourceMappingURL=orientationUtils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"orientationUtils.js","sourceRoot":"","sources":["../../src/utils/orientationUtils.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAClC,KAAkB,EAClB,KAAkB;IAElB,OAAO;QACL,KAAK,EAAE,iBAAiB,CAAC,KAAK,CAAC;QAC/B,KAAK,EAAE,aAAa,CAAC,KAAK,CAAC;KAC5B,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAkB;IAClD,OAAO,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACxB,GAAG,IAAI;QACP,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;YACxB,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;YAClB,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;SACnB,CAAC,CAAC,CAAC,SAAS;KACd,CAAC,CAAC,CAAC;AACN,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,KAAkB;IAC9C,OAAO,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE;QACtB,MAAM,IAAI,GAAG,IAAI,CAAC,IAA2C,CAAC;QAC9D,MAAM,QAAQ,GAAG,IAAI,EAAE,QAAkC,CAAC;QAC1D,MAAM,MAAM,GAAG,IAAI,EAAE,MAAgC,CAAC;QAEtD,MAAM,eAAe,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAC;QAClD,MAAM,aAAa,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;QAE9C,2EAA2E;QAC3E,MAAM,OAAO,GAAG,EAAE,GAAG,IAAI,CAAC,IAAI,EAA6B,CAAC;QAC5D,IAAI,eAAe,KAAK,SAAS,EAAE;YACjC,OAAO,CAAC,QAAQ,GAAG,eAAe,CAAC;SACpC;QACD,IAAI,aAAa,KAAK,SAAS,EAAE;YAC/B,OAAO,CAAC,MAAM,GAAG,aAAa,CAAC;SAChC;QAED,OAAO;YACL,GAAG,IAAI;YACP,IAAI,EAAE,OAA4B;SACnC,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;GAEG;AACH,SAAS,eAAe,CAAC,IAAiB;IACxC,IAAI,CAAC,IAAI;QAAE,OAAO,SAAS,CAAC;IAE5B,MAAM,YAAY,GAAmC;QACnD,GAAG,EAAE,OAAO;QACZ,KAAK,EAAE,QAAQ;QACf,MAAM,EAAE,MAAM;QACd,IAAI,EAAE,KAAK;KACZ,CAAC;IAEF,OAAO,YAAY,CAAC,IAAI,CAAC,CAAC;AAC5B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@principal-ai/principal-view-react",
3
- "version": "0.7.39",
3
+ "version": "0.7.40",
4
4
  "description": "React components for graph-based principal view framework",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -18,7 +18,6 @@
18
18
  "@principal-ai/codebase-composition": "^0.2.41",
19
19
  "@principal-ai/principal-view-core": "0.11.7",
20
20
  "@xyflow/react": "12.0.0",
21
- "elkjs": "0.9.0",
22
21
  "js-yaml": "4.1.1",
23
22
  "lucide-react": "0.554.0",
24
23
  "zustand": "4.5.0"
package/src/index.ts CHANGED
@@ -71,3 +71,8 @@ export {
71
71
  export type { EdgeStateWithHandles } from './utils/graphConverter';
72
72
  export { Icon, resolveIcon } from './utils/iconResolver';
73
73
  export type { IconProps } from './utils/iconResolver';
74
+ export {
75
+ swapGraphOrientation,
76
+ swapNodePositions,
77
+ swapEdgeSides,
78
+ } from './utils/orientationUtils';
@@ -0,0 +1,500 @@
1
+ import React, { useState, useMemo } from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { GraphRenderer } from '../components/GraphRenderer';
4
+ import type { ExtendedCanvas } from '@principal-ai/principal-view-core/browser';
5
+ import { ThemeProvider, defaultEditorTheme } from '@principal-ade/industry-theme';
6
+ import { swapGraphOrientation } from '../utils/orientationUtils';
7
+ import { CanvasConverter } from '@principal-ai/principal-view-core/browser';
8
+
9
+ const meta = {
10
+ title: 'Features/Graph Orientation',
11
+ component: GraphRenderer,
12
+ parameters: {
13
+ layout: 'centered',
14
+ },
15
+ tags: ['autodocs'],
16
+ decorators: [
17
+ (Story) => (
18
+ <ThemeProvider theme={defaultEditorTheme}>
19
+ <Story />
20
+ </ThemeProvider>
21
+ ),
22
+ ],
23
+ } satisfies Meta<typeof GraphRenderer>;
24
+
25
+ export default meta;
26
+ type Story = StoryObj<typeof meta>;
27
+
28
+ /**
29
+ * Canvas with horizontal flow (left to right)
30
+ */
31
+ const horizontalCanvas: ExtendedCanvas = {
32
+ nodes: [
33
+ {
34
+ id: 'start',
35
+ type: 'text',
36
+ x: 100,
37
+ y: 200,
38
+ width: 120,
39
+ height: 80,
40
+ text: 'Start',
41
+ color: 4,
42
+ pv: {
43
+ nodeType: 'process',
44
+ shape: 'circle',
45
+ icon: 'Play',
46
+ },
47
+ },
48
+ {
49
+ id: 'process-1',
50
+ type: 'text',
51
+ x: 300,
52
+ y: 200,
53
+ width: 140,
54
+ height: 80,
55
+ text: 'Process 1',
56
+ color: 6,
57
+ pv: {
58
+ nodeType: 'process',
59
+ shape: 'rectangle',
60
+ icon: 'Box',
61
+ },
62
+ },
63
+ {
64
+ id: 'decision',
65
+ type: 'text',
66
+ x: 520,
67
+ y: 200,
68
+ width: 100,
69
+ height: 100,
70
+ text: 'Decision',
71
+ color: 2,
72
+ pv: {
73
+ nodeType: 'decision',
74
+ shape: 'diamond',
75
+ icon: 'GitBranch',
76
+ },
77
+ },
78
+ {
79
+ id: 'process-2a',
80
+ type: 'text',
81
+ x: 700,
82
+ y: 120,
83
+ width: 140,
84
+ height: 80,
85
+ text: 'Process 2A',
86
+ color: 6,
87
+ pv: {
88
+ nodeType: 'process',
89
+ shape: 'rectangle',
90
+ icon: 'Box',
91
+ },
92
+ },
93
+ {
94
+ id: 'process-2b',
95
+ type: 'text',
96
+ x: 700,
97
+ y: 280,
98
+ width: 140,
99
+ height: 80,
100
+ text: 'Process 2B',
101
+ color: 6,
102
+ pv: {
103
+ nodeType: 'process',
104
+ shape: 'rectangle',
105
+ icon: 'Box',
106
+ },
107
+ },
108
+ {
109
+ id: 'end',
110
+ type: 'text',
111
+ x: 920,
112
+ y: 200,
113
+ width: 120,
114
+ height: 80,
115
+ text: 'End',
116
+ color: 5,
117
+ pv: {
118
+ nodeType: 'process',
119
+ shape: 'circle',
120
+ icon: 'CheckCircle',
121
+ },
122
+ },
123
+ ],
124
+ edges: [
125
+ {
126
+ id: 'e1',
127
+ fromNode: 'start',
128
+ toNode: 'process-1',
129
+ fromSide: 'right',
130
+ toSide: 'left',
131
+ pv: {
132
+ edgeType: 'flow',
133
+ },
134
+ },
135
+ {
136
+ id: 'e2',
137
+ fromNode: 'process-1',
138
+ toNode: 'decision',
139
+ fromSide: 'right',
140
+ toSide: 'left',
141
+ pv: {
142
+ edgeType: 'flow',
143
+ },
144
+ },
145
+ {
146
+ id: 'e3',
147
+ fromNode: 'decision',
148
+ toNode: 'process-2a',
149
+ fromSide: 'top',
150
+ toSide: 'left',
151
+ pv: {
152
+ edgeType: 'flow',
153
+ },
154
+ },
155
+ {
156
+ id: 'e4',
157
+ fromNode: 'decision',
158
+ toNode: 'process-2b',
159
+ fromSide: 'bottom',
160
+ toSide: 'left',
161
+ pv: {
162
+ edgeType: 'flow',
163
+ },
164
+ },
165
+ {
166
+ id: 'e5',
167
+ fromNode: 'process-2a',
168
+ toNode: 'end',
169
+ fromSide: 'right',
170
+ toSide: 'top',
171
+ pv: {
172
+ edgeType: 'flow',
173
+ },
174
+ },
175
+ {
176
+ id: 'e6',
177
+ fromNode: 'process-2b',
178
+ toNode: 'end',
179
+ fromSide: 'right',
180
+ toSide: 'bottom',
181
+ pv: {
182
+ edgeType: 'flow',
183
+ },
184
+ },
185
+ ],
186
+ pv: {
187
+ version: '1.0.0',
188
+ name: 'Horizontal Flow',
189
+ description: 'Graph with horizontal (left-to-right) orientation',
190
+ edgeTypes: {
191
+ flow: {
192
+ description: 'Flow connection',
193
+ style: 'solid',
194
+ color: '#888',
195
+ },
196
+ },
197
+ },
198
+ };
199
+
200
+ /**
201
+ * Interactive story demonstrating orientation swap
202
+ */
203
+ const OrientationSwapTemplate = () => {
204
+ const [isVertical, setIsVertical] = useState(false);
205
+
206
+ const canvas = useMemo(() => {
207
+ if (!isVertical) {
208
+ return horizontalCanvas;
209
+ }
210
+
211
+ // Convert canvas to internal format
212
+ const { nodes: nodeStates, edges: edgeStates } =
213
+ CanvasConverter.canvasToGraph(horizontalCanvas);
214
+
215
+ // Swap orientation
216
+ const { nodes: swappedNodes, edges: swappedEdges } = swapGraphOrientation(
217
+ nodeStates,
218
+ edgeStates
219
+ );
220
+
221
+ // Convert back to canvas format for rendering
222
+ return {
223
+ ...horizontalCanvas,
224
+ nodes: swappedNodes.map((node, idx) => ({
225
+ ...horizontalCanvas.nodes[idx],
226
+ x: node.position?.x ?? 0,
227
+ y: node.position?.y ?? 0,
228
+ })),
229
+ edges: swappedEdges.map((edge, idx) => {
230
+ const data = edge.data as Record<string, unknown> | undefined;
231
+ return {
232
+ ...horizontalCanvas.edges[idx],
233
+ fromSide: (data?.fromSide as 'top' | 'right' | 'bottom' | 'left') ?? 'bottom',
234
+ toSide: (data?.toSide as 'top' | 'right' | 'bottom' | 'left') ?? 'top',
235
+ };
236
+ }),
237
+ };
238
+ }, [isVertical]);
239
+
240
+ return (
241
+ <div style={{ padding: 20 }}>
242
+ <div style={{ marginBottom: 20, display: 'flex', alignItems: 'center', gap: 20 }}>
243
+ <h2 style={{ margin: 0, fontFamily: 'system-ui' }}>Graph Orientation</h2>
244
+ <button
245
+ onClick={() => setIsVertical(!isVertical)}
246
+ style={{
247
+ padding: '8px 16px',
248
+ fontSize: 14,
249
+ fontFamily: 'system-ui',
250
+ backgroundColor: '#3b82f6',
251
+ color: 'white',
252
+ border: 'none',
253
+ borderRadius: 6,
254
+ cursor: 'pointer',
255
+ }}
256
+ >
257
+ Switch to {isVertical ? 'Horizontal' : 'Vertical'}
258
+ </button>
259
+ <div style={{ fontSize: 14, color: '#666', fontFamily: 'system-ui' }}>
260
+ Current: <strong>{isVertical ? 'Vertical (Top-to-Bottom)' : 'Horizontal (Left-to-Right)'}</strong>
261
+ </div>
262
+ </div>
263
+
264
+ <GraphRenderer
265
+ key={isVertical ? 'vertical' : 'horizontal'}
266
+ canvas={canvas}
267
+ width={isVertical ? 500 : 1100}
268
+ height={isVertical ? 1100 : 500}
269
+ />
270
+
271
+ <div style={{ marginTop: 30, padding: 16, backgroundColor: '#f5f5f5', borderRadius: 8 }}>
272
+ <h4 style={{ marginBottom: 10, fontFamily: 'system-ui' }}>How It Works</h4>
273
+ <ul style={{ fontSize: 13, lineHeight: 1.8, margin: 0, paddingLeft: 20 }}>
274
+ <li>
275
+ <strong>Position Swap</strong>: Swaps x and y coordinates of all nodes
276
+ </li>
277
+ <li>
278
+ <strong>Edge Side Rotation</strong>: Both sides rotate <strong>clockwise</strong>
279
+ <ul style={{ marginTop: 5 }}>
280
+ <li><code>top → right → bottom → left → top</code></li>
281
+ <li>Example: <code>right → left</code> becomes <code>bottom → top</code></li>
282
+ </ul>
283
+ </li>
284
+ <li>
285
+ <strong>Usage</strong>: <code>swapGraphOrientation(nodes, edges)</code>
286
+ </li>
287
+ </ul>
288
+ </div>
289
+ </div>
290
+ );
291
+ };
292
+
293
+ export const InteractiveOrientationSwap: Story = {
294
+ render: () => <OrientationSwapTemplate />,
295
+ parameters: {
296
+ docs: {
297
+ description: {
298
+ story: `
299
+ **Interactive Orientation Swap**
300
+
301
+ Click the button to toggle between horizontal (left-to-right) and vertical (top-to-bottom) orientations.
302
+
303
+ The \`swapGraphOrientation\` utility:
304
+ 1. Swaps x/y coordinates for all node positions
305
+ 2. Rotates edge connection sides clockwise (both fromSide and toSide)
306
+ - Rotation: top → right → bottom → left → top
307
+ - Example: \`right → left\` becomes \`bottom → top\`
308
+ 3. Preserves the graph structure while rotating it 90 degrees
309
+ `,
310
+ },
311
+ },
312
+ },
313
+ };
314
+
315
+ /**
316
+ * Horizontal flow example
317
+ */
318
+ export const HorizontalFlow: Story = {
319
+ args: {
320
+ canvas: horizontalCanvas,
321
+ width: 1100,
322
+ height: 450,
323
+ },
324
+ parameters: {
325
+ docs: {
326
+ description: {
327
+ story: `
328
+ **Horizontal Flow (Left-to-Right)**
329
+
330
+ Traditional left-to-right flowchart layout:
331
+ - Primary flow uses \`right → left\` connections
332
+ - Branches use \`top\` and \`bottom\` connections for splitting
333
+ - Typical for flowcharts and process diagrams
334
+ `,
335
+ },
336
+ },
337
+ },
338
+ };
339
+
340
+ /**
341
+ * Vertical flow example (using swapped canvas)
342
+ */
343
+ const VerticalFlowTemplate = () => {
344
+ const verticalCanvas = useMemo(() => {
345
+ const { nodes: nodeStates, edges: edgeStates } =
346
+ CanvasConverter.canvasToGraph(horizontalCanvas);
347
+
348
+ const { nodes: swappedNodes, edges: swappedEdges } = swapGraphOrientation(
349
+ nodeStates,
350
+ edgeStates
351
+ );
352
+
353
+ return {
354
+ ...horizontalCanvas,
355
+ pv: {
356
+ ...horizontalCanvas.pv,
357
+ name: 'Vertical Flow',
358
+ description: 'Graph with vertical (top-to-bottom) orientation',
359
+ },
360
+ nodes: swappedNodes.map((node, idx) => ({
361
+ ...horizontalCanvas.nodes[idx],
362
+ x: node.position?.x ?? 0,
363
+ y: node.position?.y ?? 0,
364
+ })),
365
+ edges: swappedEdges.map((edge, idx) => {
366
+ const data = edge.data as Record<string, unknown> | undefined;
367
+ return {
368
+ ...horizontalCanvas.edges[idx],
369
+ fromSide: (data?.fromSide as 'top' | 'right' | 'bottom' | 'left') ?? 'bottom',
370
+ toSide: (data?.toSide as 'top' | 'right' | 'bottom' | 'left') ?? 'top',
371
+ };
372
+ }),
373
+ };
374
+ }, []);
375
+
376
+ return <GraphRenderer canvas={verticalCanvas} width={500} height={1100} />;
377
+ };
378
+
379
+ export const VerticalFlow: Story = {
380
+ render: () => <VerticalFlowTemplate />,
381
+ parameters: {
382
+ docs: {
383
+ description: {
384
+ story: `
385
+ **Vertical Flow (Top-to-Bottom)**
386
+
387
+ Top-to-bottom layout using \`swapGraphOrientation\`:
388
+ - Primary flow uses \`bottom → top\` connections
389
+ - Branches use \`left\` and \`right\` connections for splitting
390
+ - Common for hierarchical diagrams and org charts
391
+ `,
392
+ },
393
+ },
394
+ },
395
+ };
396
+
397
+ /**
398
+ * Side-by-side comparison
399
+ */
400
+ const SideBySideTemplate = () => {
401
+ const verticalCanvas = useMemo(() => {
402
+ const { nodes: nodeStates, edges: edgeStates } =
403
+ CanvasConverter.canvasToGraph(horizontalCanvas);
404
+
405
+ const { nodes: swappedNodes, edges: swappedEdges } = swapGraphOrientation(
406
+ nodeStates,
407
+ edgeStates
408
+ );
409
+
410
+ return {
411
+ ...horizontalCanvas,
412
+ nodes: swappedNodes.map((node, idx) => ({
413
+ ...horizontalCanvas.nodes[idx],
414
+ x: node.position?.x ?? 0,
415
+ y: node.position?.y ?? 0,
416
+ })),
417
+ edges: swappedEdges.map((edge, idx) => {
418
+ const data = edge.data as Record<string, unknown> | undefined;
419
+ return {
420
+ ...horizontalCanvas.edges[idx],
421
+ fromSide: (data?.fromSide as 'top' | 'right' | 'bottom' | 'left') ?? 'bottom',
422
+ toSide: (data?.toSide as 'top' | 'right' | 'bottom' | 'left') ?? 'top',
423
+ };
424
+ }),
425
+ };
426
+ }, []);
427
+
428
+ return (
429
+ <div style={{ padding: 20 }}>
430
+ <h2 style={{ marginBottom: 20, fontFamily: 'system-ui' }}>Orientation Comparison</h2>
431
+
432
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 40 }}>
433
+ <div>
434
+ <h3 style={{ fontFamily: 'system-ui', marginBottom: 10 }}>
435
+ Horizontal (Left-to-Right)
436
+ </h3>
437
+ <div style={{ border: '1px solid #ddd', borderRadius: 8, overflow: 'hidden' }}>
438
+ <GraphRenderer canvas={horizontalCanvas} width={550} height={400} />
439
+ </div>
440
+ <div style={{ marginTop: 10, fontSize: 13, color: '#666' }}>
441
+ Original orientation with right→left primary flow
442
+ </div>
443
+ </div>
444
+
445
+ <div>
446
+ <h3 style={{ fontFamily: 'system-ui', marginBottom: 10 }}>
447
+ Vertical (Top-to-Bottom)
448
+ </h3>
449
+ <div style={{ border: '1px solid #ddd', borderRadius: 8, overflow: 'hidden' }}>
450
+ <GraphRenderer canvas={verticalCanvas} width={550} height={400} />
451
+ </div>
452
+ <div style={{ marginTop: 10, fontSize: 13, color: '#666' }}>
453
+ Swapped orientation with bottom→top primary flow
454
+ </div>
455
+ </div>
456
+ </div>
457
+
458
+ <div style={{ marginTop: 30, padding: 16, backgroundColor: '#f5f5f5', borderRadius: 8 }}>
459
+ <h4 style={{ marginBottom: 10, fontFamily: 'system-ui' }}>
460
+ Edge Side Rotation (Both Clockwise)
461
+ </h4>
462
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 20 }}>
463
+ <div>
464
+ <strong>Clockwise Rotation:</strong>
465
+ <ul style={{ fontSize: 13, lineHeight: 1.8, marginTop: 8, paddingLeft: 20 }}>
466
+ <li><code>top</code> → <code>right</code></li>
467
+ <li><code>right</code> → <code>bottom</code></li>
468
+ <li><code>bottom</code> → <code>left</code></li>
469
+ <li><code>left</code> → <code>top</code></li>
470
+ </ul>
471
+ </div>
472
+ <div>
473
+ <strong>Examples:</strong>
474
+ <ul style={{ fontSize: 13, lineHeight: 1.8, marginTop: 8, paddingLeft: 20 }}>
475
+ <li><code>right → left</code> becomes <code>bottom → top</code></li>
476
+ <li><code>top → left</code> becomes <code>right → top</code></li>
477
+ <li><code>bottom → left</code> becomes <code>left → top</code></li>
478
+ </ul>
479
+ </div>
480
+ </div>
481
+ </div>
482
+ </div>
483
+ );
484
+ };
485
+
486
+ export const SideBySideComparison: Story = {
487
+ render: () => <SideBySideTemplate />,
488
+ parameters: {
489
+ docs: {
490
+ description: {
491
+ story: `
492
+ **Side-by-Side Comparison**
493
+
494
+ Visual comparison of the same graph in both orientations, showing how \`swapGraphOrientation\`
495
+ transforms the layout while preserving the graph structure and flow logic.
496
+ `,
497
+ },
498
+ },
499
+ },
500
+ };
@@ -0,0 +1,73 @@
1
+ import type { NodeState, EdgeState, CanvasSide } from '@principal-ai/principal-view-core';
2
+
3
+ /**
4
+ * Swaps the orientation of a graph from horizontal to vertical or vice versa.
5
+ * This swaps x/y coordinates and adjusts edge connection sides accordingly.
6
+ */
7
+ export function swapGraphOrientation(
8
+ nodes: NodeState[],
9
+ edges: EdgeState[]
10
+ ): { nodes: NodeState[]; edges: EdgeState[] } {
11
+ return {
12
+ nodes: swapNodePositions(nodes),
13
+ edges: swapEdgeSides(edges),
14
+ };
15
+ }
16
+
17
+ /**
18
+ * Swaps x and y positions for all nodes
19
+ */
20
+ export function swapNodePositions(nodes: NodeState[]): NodeState[] {
21
+ return nodes.map(node => ({
22
+ ...node,
23
+ position: node.position ? {
24
+ x: node.position.y,
25
+ y: node.position.x,
26
+ } : undefined,
27
+ }));
28
+ }
29
+
30
+ /**
31
+ * Swaps edge connection sides to match the new orientation
32
+ * Both sides rotate clockwise: top → right → bottom → left → top
33
+ */
34
+ export function swapEdgeSides(edges: EdgeState[]): EdgeState[] {
35
+ return edges.map(edge => {
36
+ const data = edge.data as Record<string, unknown> | undefined;
37
+ const fromSide = data?.fromSide as CanvasSide | undefined;
38
+ const toSide = data?.toSide as CanvasSide | undefined;
39
+
40
+ const rotatedFromSide = rotateClockwise(fromSide);
41
+ const rotatedToSide = rotateClockwise(toSide);
42
+
43
+ // Only include fromSide/toSide if they have defined values (not undefined)
44
+ const newData = { ...edge.data } as Record<string, unknown>;
45
+ if (rotatedFromSide !== undefined) {
46
+ newData.fromSide = rotatedFromSide;
47
+ }
48
+ if (rotatedToSide !== undefined) {
49
+ newData.toSide = rotatedToSide;
50
+ }
51
+
52
+ return {
53
+ ...edge,
54
+ data: newData as EdgeState['data'],
55
+ };
56
+ });
57
+ }
58
+
59
+ /**
60
+ * Rotates a side clockwise: top → right → bottom → left → top
61
+ */
62
+ function rotateClockwise(side?: CanvasSide): CanvasSide | undefined {
63
+ if (!side) return undefined;
64
+
65
+ const clockwiseMap: Record<CanvasSide, CanvasSide> = {
66
+ top: 'right',
67
+ right: 'bottom',
68
+ bottom: 'left',
69
+ left: 'top',
70
+ };
71
+
72
+ return clockwiseMap[side];
73
+ }
@@ -1,404 +0,0 @@
1
- import React, { useState } from 'react';
2
- import type { Meta, StoryObj } from '@storybook/react';
3
- import { GraphRenderer } from '../components/GraphRenderer';
4
- import { TestEventPanel } from '../components/TestEventPanel';
5
- import type { ViewMode } from '../components/TestEventPanel';
6
- import type { ExtendedCanvas, GraphEvent, JsonObject, NarrativeTemplate } from '@principal-ai/principal-view-core/browser';
7
- import { ThemeProvider, defaultEditorTheme } from '@principal-ade/industry-theme';
8
- import testSpans from './data/graph-converter-test-execution.json';
9
- import narrativeTemplate from './data/graph-converter-test.narrative.json';
10
-
11
- const meta = {
12
- title: 'Features/Real Test Execution',
13
- component: GraphRenderer,
14
- parameters: {
15
- layout: 'fullscreen',
16
- docs: {
17
- description: {
18
- component:
19
- 'Visualizes REAL test execution data from instrumented Bun tests using the "wide event" pattern. Shows actual spans with file/line information collected from running GraphConverter.test.ts. Hover over graph nodes to highlight related events in the panel.',
20
- },
21
- },
22
- },
23
- tags: ['autodocs'],
24
- decorators: [
25
- (Story) => (
26
- <ThemeProvider theme={defaultEditorTheme}>
27
- <div style={{ width: '100vw', height: '100vh', background: '#0a0a0a' }}>
28
- <Story />
29
- </div>
30
- </ThemeProvider>
31
- ),
32
- ],
33
- } satisfies Meta<typeof GraphRenderer>;
34
-
35
- export default meta;
36
- type Story = StoryObj<typeof meta>;
37
-
38
- // ============================================================================
39
- // Test Execution Flow Canvas
40
- // ============================================================================
41
-
42
- const testExecutionCanvas: ExtendedCanvas = {
43
- nodes: [
44
- // Test Suite
45
- {
46
- id: 'test-suite',
47
- type: 'text',
48
- text: 'GraphConverter Test Suite',
49
- x: -100,
50
- y: -100,
51
- width: 240,
52
- height: 80,
53
- pv: {
54
- nodeType: 'test-suite',
55
- name: 'Test Suite',
56
- description: 'Collection of GraphConverter tests',
57
- shape: 'rectangle',
58
- fill: '#3b82f6',
59
- },
60
- },
61
-
62
- // Test Phase Nodes
63
- {
64
- id: 'setup-phase',
65
- type: 'text',
66
- text: 'Setup',
67
- x: -250,
68
- y: 50,
69
- width: 120,
70
- height: 80,
71
- pv: {
72
- nodeType: 'test-phase',
73
- name: 'Setup Phase',
74
- description: 'Test data preparation',
75
- shape: 'hexagon',
76
- fill: '#10b981',
77
- },
78
- },
79
- {
80
- id: 'execution-phase',
81
- type: 'text',
82
- text: 'Execution',
83
- x: -80,
84
- y: 50,
85
- width: 120,
86
- height: 80,
87
- pv: {
88
- nodeType: 'test-phase',
89
- name: 'Execution Phase',
90
- description: 'Code under test runs',
91
- shape: 'hexagon',
92
- fill: '#f59e0b',
93
- },
94
- },
95
- {
96
- id: 'assertion-phase',
97
- type: 'text',
98
- text: 'Assertion',
99
- x: 90,
100
- y: 50,
101
- width: 120,
102
- height: 80,
103
- pv: {
104
- nodeType: 'test-phase',
105
- name: 'Assertion Phase',
106
- description: 'Verify results',
107
- shape: 'hexagon',
108
- fill: '#8b5cf6',
109
- },
110
- },
111
-
112
- // Result Node
113
- {
114
- id: 'test-result',
115
- type: 'text',
116
- text: 'Test Result',
117
- x: -100,
118
- y: 200,
119
- width: 240,
120
- height: 80,
121
- pv: {
122
- nodeType: 'result',
123
- name: 'Test Result',
124
- description: 'Pass/Fail outcome',
125
- shape: 'rectangle',
126
- fill: '#10b981',
127
- },
128
- },
129
- ],
130
- edges: [
131
- {
132
- id: 'suite-to-setup',
133
- fromNode: 'test-suite',
134
- toNode: 'setup-phase',
135
- fromSide: 'bottom',
136
- toSide: 'top',
137
- label: 'start test',
138
- pv: {
139
- edgeType: 'flow',
140
- style: 'solid',
141
- },
142
- },
143
- {
144
- id: 'setup-to-execution',
145
- fromNode: 'setup-phase',
146
- toNode: 'execution-phase',
147
- fromSide: 'right',
148
- toSide: 'left',
149
- label: 'data ready',
150
- pv: {
151
- edgeType: 'flow',
152
- style: 'solid',
153
- },
154
- },
155
- {
156
- id: 'execution-to-assertion',
157
- fromNode: 'execution-phase',
158
- toNode: 'assertion-phase',
159
- fromSide: 'right',
160
- toSide: 'left',
161
- label: 'got result',
162
- pv: {
163
- edgeType: 'flow',
164
- style: 'solid',
165
- },
166
- },
167
- {
168
- id: 'assertion-to-result',
169
- fromNode: 'assertion-phase',
170
- toNode: 'test-result',
171
- fromSide: 'bottom',
172
- toSide: 'top',
173
- label: 'complete',
174
- pv: {
175
- edgeType: 'flow',
176
- style: 'solid',
177
- },
178
- },
179
- ],
180
- pv: {
181
- version: '1.0.0',
182
- name: 'Test Execution Flow',
183
- description: 'Visualizes the flow of test execution through phases',
184
- },
185
- };
186
-
187
- // ============================================================================
188
- // Interactive Story (No Animation)
189
- // ============================================================================
190
-
191
- const AnimatedTestExecution = () => {
192
- const [events] = useState<GraphEvent[]>([]);
193
- const [currentSpanIndex, setCurrentSpanIndex] = useState(0);
194
- // Show all events by default - set to a large number
195
- const [currentEventIndex] = useState(999);
196
- const [highlightedPhase, setHighlightedPhase] = useState<string | undefined>();
197
-
198
- // Extract spans and logs from test data
199
- const testData = testSpans as JsonObject;
200
- const spans = Array.isArray(testData) ? testData : testData.spans || testData;
201
- const logs = testData.logs || [];
202
-
203
- return (
204
- <div style={{ display: 'flex', width: '100vw', height: '100vh' }}>
205
- {/* Event Panel - Left Side */}
206
- <div style={{ flex: '0 0 50%', height: '100%', borderRight: `1px solid #333`, overflow: 'hidden' }}>
207
- <TestEventPanel
208
- spans={spans}
209
- logs={logs}
210
- currentSpanIndex={currentSpanIndex}
211
- currentEventIndex={currentEventIndex}
212
- highlightedPhase={highlightedPhase}
213
- onSpanIndexChange={setCurrentSpanIndex}
214
- />
215
- </div>
216
-
217
- {/* Graph Visualization - Right Side */}
218
- <div
219
- style={{ flex: '0 0 50%', height: '100%', position: 'relative' }}
220
- onMouseLeave={() => setHighlightedPhase(undefined)}
221
- >
222
- <div
223
- style={{ width: '100%', height: '100%' }}
224
- onMouseOver={(e) => {
225
- // Check if hovering over a phase node
226
- const target = e.target as HTMLElement;
227
- const textContent = target.textContent;
228
- if (textContent === 'Setup') setHighlightedPhase('setup');
229
- else if (textContent === 'Execution') setHighlightedPhase('execution');
230
- else if (textContent === 'Assertion') setHighlightedPhase('assertion');
231
- }}
232
- >
233
- <GraphRenderer
234
- canvas={testExecutionCanvas}
235
- showControls={true}
236
- events={events}
237
- />
238
- </div>
239
- </div>
240
- </div>
241
- );
242
- };
243
-
244
- /**
245
- * Interactive visualization of real test execution data using the "wide event" pattern.
246
- *
247
- * This demonstrates the key concept from loggingsucks.com:
248
- * - ONE comprehensive span per test (not multiple child spans)
249
- * - Events show the narrative of what happened during execution
250
- * - Context accumulates through event attributes with file/line information
251
- * - Easy to search by test.name to get full execution story
252
- *
253
- * **Interaction:**
254
- * - Hover over graph nodes (Setup, Execution, Assertion) to highlight related events
255
- * - Watch the code journey: blue = test file, green = code under test
256
- * - All events are shown immediately for easy review
257
- */
258
- export const Animated: Story = {
259
- render: () => <AnimatedTestExecution />,
260
- };
261
-
262
- /**
263
- * Static view of the test execution flow showing phases.
264
- */
265
- export const StaticView: Story = {
266
- args: {
267
- canvas: testExecutionCanvas,
268
- showControls: true,
269
- },
270
- };
271
-
272
- /**
273
- * Event panel component showing test execution narrative with file/line information.
274
- *
275
- * Shows how events and logs are interleaved in chronological order, with automatic
276
- * file/line capture from stack traces and severity-based color coding for logs.
277
- */
278
- export const EventPanelOnly: StoryObj = {
279
- render: () => {
280
- const testData = testSpans as JsonObject;
281
- const spans = Array.isArray(testData) ? testData : testData.spans || testData;
282
- const logs = testData.logs || [];
283
-
284
- return (
285
- <div style={{ width: '600px', height: '100vh' }}>
286
- <TestEventPanel
287
- spans={spans}
288
- logs={logs}
289
- currentSpanIndex={0}
290
- currentEventIndex={999} // Show all events
291
- highlightedPhase={undefined}
292
- />
293
- </div>
294
- );
295
- },
296
- };
297
-
298
- /**
299
- * Event panel with narrative view toggle
300
- *
301
- * Demonstrates the new narrative view mode that transforms raw OTEL events into
302
- * human-readable execution narratives. Toggle between "Raw Events" and "Narrative"
303
- * to see both views.
304
- *
305
- * Features:
306
- * - Human-readable narrative generated from template
307
- * - Scenario-based rendering (test-passed vs test-failed)
308
- * - Syntax highlighting for better readability
309
- * - Optional metadata display
310
- */
311
- export const WithNarrativeToggle: StoryObj = {
312
- render: () => {
313
- const [viewMode, setViewMode] = useState<ViewMode>('narrative');
314
- const [currentSpanIndex, setCurrentSpanIndex] = useState(0);
315
- const [showMetadata, setShowMetadata] = useState(false);
316
-
317
- const testData = testSpans as JsonObject;
318
- const spans = Array.isArray(testData) ? testData : testData.spans || testData;
319
- const logs = testData.logs || [];
320
-
321
- return (
322
- <div style={{ width: '100vw', height: '100vh', display: 'flex', flexDirection: 'column' }}>
323
- {/* Controls */}
324
- <div
325
- style={{
326
- padding: '12px 20px',
327
- background: '#1a1a1a',
328
- borderBottom: '1px solid #333',
329
- display: 'flex',
330
- gap: '12px',
331
- alignItems: 'center',
332
- }}
333
- >
334
- <label style={{ color: '#999', fontSize: '14px' }}>
335
- <input
336
- type="checkbox"
337
- checked={showMetadata}
338
- onChange={(e) => setShowMetadata(e.target.checked)}
339
- style={{ marginRight: '6px' }}
340
- />
341
- Show Metadata
342
- </label>
343
- </div>
344
-
345
- {/* Event Panel */}
346
- <div style={{ flex: 1, overflow: 'hidden' }}>
347
- <TestEventPanel
348
- spans={spans}
349
- logs={logs}
350
- currentSpanIndex={currentSpanIndex}
351
- currentEventIndex={999}
352
- onSpanIndexChange={setCurrentSpanIndex}
353
- viewMode={viewMode}
354
- narrativeTemplate={narrativeTemplate as NarrativeTemplate}
355
- onViewModeChange={setViewMode}
356
- showNarrativeMetadata={showMetadata}
357
- />
358
- </div>
359
- </div>
360
- );
361
- },
362
- };
363
-
364
- /**
365
- * Split view with narrative on left, graph on right
366
- *
367
- * Shows how the narrative view integrates with the graph visualization,
368
- * providing a complete picture of test execution.
369
- */
370
- export const NarrativeWithGraph: StoryObj = {
371
- render: () => {
372
- const [viewMode, setViewMode] = useState<ViewMode>('narrative');
373
- const [currentSpanIndex, setCurrentSpanIndex] = useState(0);
374
- const [events] = useState<GraphEvent[]>([]);
375
-
376
- const testData = testSpans as JsonObject;
377
- const spans = Array.isArray(testData) ? testData : testData.spans || testData;
378
- const logs = testData.logs || [];
379
-
380
- return (
381
- <div style={{ display: 'flex', width: '100vw', height: '100vh' }}>
382
- {/* Event Panel - Left Side */}
383
- <div style={{ flex: '0 0 50%', height: '100%', borderRight: '1px solid #333', overflow: 'hidden' }}>
384
- <TestEventPanel
385
- spans={spans}
386
- logs={logs}
387
- currentSpanIndex={currentSpanIndex}
388
- currentEventIndex={999}
389
- onSpanIndexChange={setCurrentSpanIndex}
390
- viewMode={viewMode}
391
- narrativeTemplate={narrativeTemplate as NarrativeTemplate}
392
- onViewModeChange={setViewMode}
393
- showNarrativeMetadata={true}
394
- />
395
- </div>
396
-
397
- {/* Graph Visualization - Right Side */}
398
- <div style={{ flex: '0 0 50%', height: '100%', position: 'relative' }}>
399
- <GraphRenderer canvas={testExecutionCanvas} showControls={true} events={events} />
400
- </div>
401
- </div>
402
- );
403
- },
404
- };
@@ -1,158 +0,0 @@
1
- import React from 'react';
2
- import type { Meta, StoryObj } from '@storybook/react';
3
- import { GraphRenderer } from '../components/GraphRenderer';
4
- import { TestEventPanel } from '../components/TestEventPanel';
5
- import type { ExtendedCanvas, JsonValue } from '@principal-ai/principal-view-core/browser';
6
- import { ThemeProvider, defaultEditorTheme } from '@principal-ade/industry-theme';
7
- import executionCanvas from '../../../../.principal-views/graph-converter-execution.otel.canvas';
8
- import validatedSpans from './data/graph-converter-validated-execution.json';
9
-
10
- const meta = {
11
- title: 'Features/Validated Execution',
12
- component: GraphRenderer,
13
- parameters: {
14
- layout: 'fullscreen',
15
- docs: {
16
- description: {
17
- component:
18
- 'Demonstrates type-safe event emission with schema validation. The canvas defines expected events, and production code is validated against this schema. Shows how events match the schema defined in graph-converter-execution.otel.canvas.',
19
- },
20
- },
21
- },
22
- tags: ['autodocs'],
23
- decorators: [
24
- (Story) => (
25
- <ThemeProvider theme={defaultEditorTheme}>
26
- <div style={{ width: '100vw', height: '100vh', background: '#0a0a0a' }}>
27
- <Story />
28
- </div>
29
- </ThemeProvider>
30
- ),
31
- ],
32
- } satisfies Meta<typeof GraphRenderer>;
33
-
34
- export default meta;
35
- type Story = StoryObj<typeof meta>;
36
-
37
- /**
38
- * Graph visualization of the execution flow with event schema definitions.
39
- *
40
- * This canvas defines:
41
- * - `graph-converter` node with 5 event types
42
- * - `validation` node with 2 event types
43
- * - `graph-output` node with 1 event type
44
- *
45
- * Each event type has a schema defining:
46
- * - Required/optional fields
47
- * - Field types (string, number, boolean, etc.)
48
- * - Field descriptions
49
- *
50
- * See `.principal-views/graph-converter-execution.otel.canvas` for the full schema.
51
- */
52
- export const ExecutionFlow: Story = {
53
- args: {
54
- canvas: executionCanvas as ExtendedCanvas,
55
- showControls: true,
56
- },
57
- };
58
-
59
- /**
60
- * Event panel showing validated execution data.
61
- *
62
- * These events were emitted using `createValidatedSpanEmitter()` which:
63
- * - Validates events against the canvas schema
64
- * - Ensures required fields are present
65
- * - Checks field types match the schema
66
- * - Throws errors in strict mode if validation fails
67
- *
68
- * All events in this panel passed schema validation.
69
- */
70
- export const ValidatedEvents: Story = {
71
- render: () => (
72
- <div style={{ width: '800px', height: '100vh' }}>
73
- <TestEventPanel
74
- spans={validatedSpans as JsonValue[]}
75
- currentSpanIndex={0}
76
- currentEventIndex={10} // Show all events
77
- highlightedPhase={undefined}
78
- />
79
- </div>
80
- ),
81
- };
82
-
83
- /**
84
- * Side-by-side view of execution flow and validated events.
85
- *
86
- * **How it works:**
87
- * 1. Canvas defines event schemas (what events should be emitted)
88
- * 2. Tests use `createValidatedSpanEmitter()` to emit events
89
- * 3. Events are validated against the schema in strict mode
90
- * 4. If validation fails, test throws `EventValidationError`
91
- * 5. If validation passes, events are emitted and collected
92
- *
93
- * This ensures production code emits events that match the architecture.
94
- */
95
- export const FlowWithValidation: Story = {
96
- render: () => (
97
- <div style={{ display: 'flex', width: '100%', height: '100%' }}>
98
- {/* Graph Visualization - Left Side */}
99
- <div style={{ flex: '0 0 60%', height: '100%', position: 'relative' }}>
100
- <GraphRenderer
101
- canvas={executionCanvas as ExtendedCanvas}
102
- showControls={true}
103
- />
104
- </div>
105
-
106
- {/* Event Panel - Right Side */}
107
- <div style={{ flex: '0 0 40%', height: '100%', borderLeft: '1px solid #333' }}>
108
- <div style={{ padding: '20px', color: '#fff', borderBottom: '1px solid #333' }}>
109
- <h3 style={{ margin: '0 0 10px 0', fontSize: '16px' }}>
110
- Type-Safe Validated Events
111
- </h3>
112
- <p style={{ margin: 0, fontSize: '12px', color: '#888' }}>
113
- Events validated against canvas schema. See{' '}
114
- <code>.principal-views/graph-converter-execution.otel.canvas</code>
115
- </p>
116
- </div>
117
- <TestEventPanel
118
- spans={validatedSpans as JsonValue[]}
119
- currentSpanIndex={0}
120
- currentEventIndex={10}
121
- highlightedPhase={undefined}
122
- />
123
- </div>
124
- </div>
125
- ),
126
- };
127
-
128
- /**
129
- * Canvas with event schema definitions (JSON view).
130
- *
131
- * Shows the raw canvas structure including event schemas.
132
- * Notice the `pv.events` property on each node defining:
133
- * - Event names (e.g., "conversion.started")
134
- * - Event descriptions
135
- * - Field schemas with types and requirements
136
- */
137
- export const CanvasSchema: Story = {
138
- render: () => (
139
- <div
140
- style={{
141
- padding: '20px',
142
- color: '#fff',
143
- fontFamily: 'monospace',
144
- fontSize: '12px',
145
- overflow: 'auto',
146
- height: '100vh',
147
- }}
148
- >
149
- <h2>Event Schema Definition</h2>
150
- <p>
151
- This canvas defines event schemas for type-safe telemetry validation.
152
- </p>
153
- <pre style={{ background: '#1e1e1e', padding: '20px', borderRadius: '8px' }}>
154
- {JSON.stringify(executionCanvas, null, 2)}
155
- </pre>
156
- </div>
157
- ),
158
- };