@principal-ai/principal-view-react 0.14.22 → 0.14.24

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 (64) hide show
  1. package/dist/components/GraphRenderer.d.ts.map +1 -1
  2. package/dist/components/GraphRenderer.js +11 -7
  3. package/dist/components/GraphRenderer.js.map +1 -1
  4. package/dist/components/SequenceDiagramRenderer.d.ts.map +1 -1
  5. package/dist/components/SequenceDiagramRenderer.js +62 -39
  6. package/dist/components/SequenceDiagramRenderer.js.map +1 -1
  7. package/dist/components/state-view/PipelineView.d.ts +13 -0
  8. package/dist/components/state-view/PipelineView.d.ts.map +1 -0
  9. package/dist/components/state-view/PipelineView.js +195 -0
  10. package/dist/components/state-view/PipelineView.js.map +1 -0
  11. package/dist/components/state-view/index.d.ts +14 -0
  12. package/dist/components/state-view/index.d.ts.map +1 -0
  13. package/dist/components/state-view/index.js +12 -0
  14. package/dist/components/state-view/index.js.map +1 -0
  15. package/dist/components/state-view/types.d.ts +188 -0
  16. package/dist/components/state-view/types.d.ts.map +1 -0
  17. package/dist/components/state-view/types.js +10 -0
  18. package/dist/components/state-view/types.js.map +1 -0
  19. package/dist/components/state-view/useStateView.d.ts +32 -0
  20. package/dist/components/state-view/useStateView.d.ts.map +1 -0
  21. package/dist/components/state-view/useStateView.js +129 -0
  22. package/dist/components/state-view/useStateView.js.map +1 -0
  23. package/dist/hooks/useSequenceLayout.d.ts +1 -1
  24. package/dist/hooks/useSequenceLayout.d.ts.map +1 -1
  25. package/dist/hooks/useSequenceLayout.js +28 -4
  26. package/dist/hooks/useSequenceLayout.js.map +1 -1
  27. package/dist/index.d.ts +2 -0
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +2 -0
  30. package/dist/index.js.map +1 -1
  31. package/dist/nodes/CustomNode.js +8 -8
  32. package/dist/nodes/CustomNode.js.map +1 -1
  33. package/dist/nodes/otel/OtelBoundaryNode.d.ts.map +1 -1
  34. package/dist/nodes/otel/OtelBoundaryNode.js +5 -3
  35. package/dist/nodes/otel/OtelBoundaryNode.js.map +1 -1
  36. package/dist/nodes/otel/OtelEventNode.d.ts.map +1 -1
  37. package/dist/nodes/otel/OtelEventNode.js +5 -3
  38. package/dist/nodes/otel/OtelEventNode.js.map +1 -1
  39. package/dist/nodes/otel/OtelResourceNode.d.ts.map +1 -1
  40. package/dist/nodes/otel/OtelResourceNode.js +5 -3
  41. package/dist/nodes/otel/OtelResourceNode.js.map +1 -1
  42. package/dist/nodes/otel/OtelScopeNode.d.ts.map +1 -1
  43. package/dist/nodes/otel/OtelScopeNode.js +5 -3
  44. package/dist/nodes/otel/OtelScopeNode.js.map +1 -1
  45. package/dist/nodes/otel/OtelSpanConventionNode.d.ts.map +1 -1
  46. package/dist/nodes/otel/OtelSpanConventionNode.js +5 -3
  47. package/dist/nodes/otel/OtelSpanConventionNode.js.map +1 -1
  48. package/package.json +3 -3
  49. package/src/components/GraphRenderer.tsx +12 -6
  50. package/src/components/SequenceDiagramRenderer.tsx +84 -45
  51. package/src/components/state-view/PipelineView.tsx +347 -0
  52. package/src/components/state-view/index.ts +14 -0
  53. package/src/components/state-view/types.ts +261 -0
  54. package/src/components/state-view/useStateView.ts +205 -0
  55. package/src/hooks/useSequenceLayout.ts +34 -5
  56. package/src/index.ts +36 -0
  57. package/src/nodes/CustomNode.tsx +8 -8
  58. package/src/nodes/otel/OtelBoundaryNode.tsx +5 -3
  59. package/src/nodes/otel/OtelEventNode.tsx +5 -3
  60. package/src/nodes/otel/OtelResourceNode.tsx +5 -3
  61. package/src/nodes/otel/OtelScopeNode.tsx +5 -3
  62. package/src/nodes/otel/OtelSpanConventionNode.tsx +5 -4
  63. package/src/stories/RealCanvasFiles.stories.tsx +336 -0
  64. package/src/stories/StateView.stories.tsx +417 -0
@@ -0,0 +1,205 @@
1
+ /**
2
+ * useStateView Hook
3
+ *
4
+ * Manages state accumulation from events and provides replay controls.
5
+ */
6
+
7
+ import { useState, useEffect, useRef, useCallback } from 'react';
8
+ import type {
9
+ StateEvent,
10
+ EventSource,
11
+ StateReducer,
12
+ StateDiff,
13
+ ReplayControls,
14
+ ActiveAnimation,
15
+ TransitionDefinition,
16
+ } from './types';
17
+
18
+ export interface UseStateViewOptions<TState, TEvent extends StateEvent> {
19
+ /** Initial state */
20
+ initialState: TState;
21
+ /** Reducer to apply events */
22
+ reducer: StateReducer<TState, TEvent>;
23
+ /** Event source (live or replay) */
24
+ eventSource: EventSource<TEvent>;
25
+ /** Transition definitions for animations */
26
+ transitions?: TransitionDefinition[];
27
+ /** Callback when state changes */
28
+ onStateChange?: (diff: StateDiff<TState>) => void;
29
+ }
30
+
31
+ export interface UseStateViewResult<TState> {
32
+ /** Current state */
33
+ state: TState;
34
+ /** Active animations */
35
+ animations: ActiveAnimation[];
36
+ /** Whether we're in replay mode */
37
+ isReplay: boolean;
38
+ /** Replay controls (if in replay mode) */
39
+ replayControls?: ReplayControls;
40
+ /** Reset state to initial */
41
+ reset: () => void;
42
+ }
43
+
44
+ /**
45
+ * Get changed paths between two objects (shallow comparison)
46
+ */
47
+ function getChangedPaths(prev: unknown, next: unknown, prefix = ''): string[] {
48
+ const paths: string[] = [];
49
+
50
+ if (prev === next) return paths;
51
+
52
+ if (
53
+ typeof prev !== 'object' ||
54
+ typeof next !== 'object' ||
55
+ prev === null ||
56
+ next === null
57
+ ) {
58
+ return prefix ? [prefix] : [];
59
+ }
60
+
61
+ const prevObj = prev as Record<string, unknown>;
62
+ const nextObj = next as Record<string, unknown>;
63
+
64
+ const allKeys = new Set([...Object.keys(prevObj), ...Object.keys(nextObj)]);
65
+
66
+ for (const key of allKeys) {
67
+ const path = prefix ? `${prefix}.${key}` : key;
68
+ if (prevObj[key] !== nextObj[key]) {
69
+ paths.push(path);
70
+ }
71
+ }
72
+
73
+ return paths;
74
+ }
75
+
76
+ /**
77
+ * Check if a transition should fire based on state change
78
+ */
79
+ function shouldTriggerTransition(
80
+ transition: TransitionDefinition,
81
+ prev: unknown,
82
+ next: unknown,
83
+ changedPaths: string[]
84
+ ): boolean {
85
+ // Check if the watched path changed
86
+ if (!changedPaths.some((p) => p.startsWith(transition.watch))) {
87
+ return false;
88
+ }
89
+
90
+ const prevValue = getValueAtPath(prev, transition.watch);
91
+ const nextValue = getValueAtPath(next, transition.watch);
92
+
93
+ switch (transition.condition) {
94
+ case 'changed':
95
+ return prevValue !== nextValue;
96
+ case 'increased':
97
+ return typeof nextValue === 'number' && typeof prevValue === 'number' && nextValue > prevValue;
98
+ case 'decreased':
99
+ return typeof nextValue === 'number' && typeof prevValue === 'number' && nextValue < prevValue;
100
+ case 'added':
101
+ return prevValue === undefined && nextValue !== undefined;
102
+ case 'removed':
103
+ return prevValue !== undefined && nextValue === undefined;
104
+ default:
105
+ return false;
106
+ }
107
+ }
108
+
109
+ function getValueAtPath(obj: unknown, path: string): unknown {
110
+ const parts = path.split('.');
111
+ let current = obj;
112
+ for (const part of parts) {
113
+ if (current === null || current === undefined) return undefined;
114
+ current = (current as Record<string, unknown>)[part];
115
+ }
116
+ return current;
117
+ }
118
+
119
+ export function useStateView<TState, TEvent extends StateEvent>({
120
+ initialState,
121
+ reducer,
122
+ eventSource,
123
+ transitions = [],
124
+ onStateChange,
125
+ }: UseStateViewOptions<TState, TEvent>): UseStateViewResult<TState> {
126
+ const [state, setState] = useState<TState>(initialState);
127
+ const [animations, setAnimations] = useState<ActiveAnimation[]>([]);
128
+ const prevStateRef = useRef<TState>(initialState);
129
+ const animationIdRef = useRef(0);
130
+
131
+ const triggerAnimation = useCallback(
132
+ (transition: TransitionDefinition, _event: StateEvent) => {
133
+ const id = `anim-${++animationIdRef.current}`;
134
+ const duration = transition.duration ?? 500;
135
+
136
+ const animation: ActiveAnimation = {
137
+ id,
138
+ type: transition.animate,
139
+ target: transition.target ?? transition.watch,
140
+ startTime: Date.now(),
141
+ duration,
142
+ params: transition.params,
143
+ };
144
+
145
+ setAnimations((prev) => [...prev, animation]);
146
+
147
+ // Auto-remove after duration
148
+ setTimeout(() => {
149
+ setAnimations((prev) => prev.filter((a) => a.id !== id));
150
+ }, duration);
151
+ },
152
+ []
153
+ );
154
+
155
+ const handleEvent = useCallback(
156
+ (event: TEvent) => {
157
+ setState((prevState) => {
158
+ const nextState = reducer(prevState, event);
159
+ const changedPaths = getChangedPaths(prevState, nextState);
160
+
161
+ // Trigger animations based on transitions
162
+ for (const transition of transitions) {
163
+ if (shouldTriggerTransition(transition, prevState, nextState, changedPaths)) {
164
+ triggerAnimation(transition, event);
165
+ }
166
+ }
167
+
168
+ // Notify listener
169
+ if (onStateChange && changedPaths.length > 0) {
170
+ onStateChange({
171
+ prev: prevState,
172
+ next: nextState,
173
+ changedPaths,
174
+ event,
175
+ });
176
+ }
177
+
178
+ prevStateRef.current = nextState;
179
+ return nextState;
180
+ });
181
+ },
182
+ [reducer, transitions, onStateChange, triggerAnimation]
183
+ );
184
+
185
+ // Subscribe to event source
186
+ useEffect(() => {
187
+ const unsubscribe = eventSource.subscribe(handleEvent);
188
+ return unsubscribe;
189
+ }, [eventSource, handleEvent]);
190
+
191
+ const reset = useCallback(() => {
192
+ setState(initialState);
193
+ setAnimations([]);
194
+ prevStateRef.current = initialState;
195
+ }, [initialState]);
196
+
197
+ const isReplay = eventSource.mode === 'replay';
198
+
199
+ return {
200
+ state,
201
+ animations,
202
+ isReplay,
203
+ reset,
204
+ };
205
+ }
@@ -86,7 +86,7 @@ export interface UseSequenceLayoutOptions {
86
86
 
87
87
  /**
88
88
  * Gap between swimlanes
89
- * @default 50
89
+ * @default 10
90
90
  */
91
91
  laneGap?: number;
92
92
 
@@ -207,7 +207,7 @@ export function useSequenceLayout(
207
207
  const {
208
208
  namespaceStrategy = 'all-but-last',
209
209
  laneWidth = 200,
210
- laneGap = 50,
210
+ laneGap = 10,
211
211
  eventSpacing = 80,
212
212
  headerHeight = 60,
213
213
  collapsedNamespaces = [],
@@ -316,7 +316,8 @@ export function useSequenceLayout(
316
316
  const lane = swimlaneByNamespace.get(visibleNamespace)!;
317
317
 
318
318
  // Global Y position based on event order (time layer)
319
- const y = headerHeight + i * eventSpacing;
319
+ // Start first event closer to header with small offset
320
+ const y = headerHeight + 40 + i * eventSpacing;
320
321
 
321
322
  nodes.push({
322
323
  id: event.id,
@@ -369,9 +370,37 @@ export function useSequenceLayout(
369
370
 
370
371
  // Step 6: Compute total dimensions
371
372
  const totalWidth =
372
- swimlanes.length * (laneWidth + laneGap) - laneGap + laneWidth;
373
+ swimlanes.length * laneWidth + (swimlanes.length - 1) * laneGap;
373
374
  // Total height based on number of events (time layers)
374
- const totalHeight = headerHeight + events.length * eventSpacing + eventSpacing;
375
+ const totalHeight = headerHeight + 40 + events.length * eventSpacing;
376
+
377
+ // Step 7: Add invisible boundary nodes to ensure fitView includes full diagram width
378
+ if (swimlanes.length > 0) {
379
+ const leftmostLane = swimlanes[0];
380
+ const rightmostLane = swimlanes[swimlanes.length - 1];
381
+
382
+ // Add boundary nodes at the corners
383
+ nodes.push(
384
+ {
385
+ id: '__boundary_left__',
386
+ type: 'default',
387
+ position: { x: leftmostLane.x - laneWidth / 2, y: 0 },
388
+ data: {},
389
+ style: { width: 1, height: 1, opacity: 0 },
390
+ draggable: false,
391
+ selectable: false,
392
+ },
393
+ {
394
+ id: '__boundary_right__',
395
+ type: 'default',
396
+ position: { x: rightmostLane.x + laneWidth / 2, y: totalHeight },
397
+ data: {},
398
+ style: { width: 1, height: 1, opacity: 0 },
399
+ draggable: false,
400
+ selectable: false,
401
+ }
402
+ );
403
+ }
375
404
 
376
405
  return {
377
406
  nodes,
package/src/index.ts CHANGED
@@ -164,3 +164,39 @@ export type {
164
164
  SourceLinkProps,
165
165
  TimeRangeSelectorProps,
166
166
  } from './components/dashboard';
167
+
168
+ // State View components for event-driven state visualization
169
+ export { PipelineView, useStateView } from './components/state-view';
170
+ export type {
171
+ // Core types
172
+ StateEvent,
173
+ EventSource,
174
+ StateReducer,
175
+ StateDiff,
176
+ TransitionDefinition,
177
+ ActiveAnimation,
178
+ AnimationType,
179
+ ReplayControls,
180
+ StateViewDefinition,
181
+ // Pipeline view types
182
+ PipelineState,
183
+ PipelineStage,
184
+ PipelineRepoState,
185
+ PipelineEvent,
186
+ PipelineEventType,
187
+ // Activity view types
188
+ ActivityState,
189
+ RoomState,
190
+ ActivityEvent,
191
+ ActivityEventType,
192
+ // Quota view types
193
+ QuotaDistributionState,
194
+ UserQuotaState,
195
+ QuotaEvent,
196
+ QuotaEventType,
197
+ // Hook types
198
+ UseStateViewOptions,
199
+ UseStateViewResult,
200
+ // Component props
201
+ PipelineViewProps,
202
+ } from './components/state-view';
@@ -442,8 +442,8 @@ export const CustomNode: React.FC<NodeProps<Node<CustomNodeData>>> = (props) =>
442
442
  }
443
443
 
444
444
  // Color Contract:
445
- // - scopeColor: Used as BORDER color (from library.yaml owned-scopes)
446
- // - spanColor: Used as FILL color (from .spans.canvas based on workflow context)
445
+ // - scopeColor: Used as FILL/background color (from library.yaml scopes)
446
+ // - spanColor: Used as BORDER color in workflow context (from .spans.canvas)
447
447
  // - For non-event canvases, falls back to legacy behavior (node color or type color)
448
448
 
449
449
  // Get colors from node data (injected by GraphRenderer)
@@ -458,15 +458,15 @@ export const CustomNode: React.FC<NodeProps<Node<CustomNodeData>>> = (props) =>
458
458
  const stateColor =
459
459
  state && (nodeDataStates?.[state]?.color || typeDefinition.states?.[state]?.color);
460
460
 
461
- // Fill color priority: state color > span color > node data color > type definition color > default
462
- // spanColor is the new primary source for fill (from .spans.canvas)
463
- const baseFillColor = spanColor || nodeDataColor || typeDefinition.color || '#888';
461
+ // Fill color priority: state color > node data color > scope color > type definition color > default
462
+ // scopeColor provides background color from the scope definition in library.yaml
463
+ const baseFillColor = nodeDataColor || scopeColor || typeDefinition.color || '#888';
464
464
  const fillColor = stateColor || baseFillColor;
465
465
 
466
- // Stroke/border color priority: explicit stroke > scope color > fill color
467
- // scopeColor is now the primary source for border (from library.yaml owned-scopes)
466
+ // Stroke/border color priority: explicit stroke > span color (workflow context) > fill color
467
+ // spanColor provides border color in workflow context (from .spans.canvas)
468
468
  const nodeDataStroke = nodeData.stroke as string | undefined;
469
- const baseStrokeColor = nodeDataStroke || scopeColor || fillColor;
469
+ const baseStrokeColor = nodeDataStroke || spanColor || fillColor;
470
470
 
471
471
  // Apply status-based border styling
472
472
  const status = nodeData?.status as 'draft' | 'approved' | 'implemented' | undefined;
@@ -97,13 +97,15 @@ export const OtelBoundaryNode: React.FC<NodeProps<Node<OtelBoundaryNodeData>>> =
97
97
  const nodeOpacity = isHidden ? 0.4 : isActive ? 1 : 0.1;
98
98
 
99
99
  // Color resolution
100
+ const scopeColor = nodeData.scopeColor as string | undefined;
100
101
  const spanColor = nodeData.spanColor as string | undefined;
101
102
  const nodeDataColor = nodeData.color as string | undefined;
102
- const baseFillColor = spanColor || nodeDataColor || typeDefinition.color || '#06b6d4';
103
+ // Fill color priority: explicit color > scope color > type definition color > default cyan
104
+ const baseFillColor = nodeDataColor || scopeColor || typeDefinition.color || '#06b6d4';
103
105
  const fillColor = baseFillColor;
104
- const scopeColor = nodeData.scopeColor as string | undefined;
106
+ // Stroke color priority: explicit stroke > span color (workflow context) > fill color
105
107
  const nodeDataStroke = nodeData.stroke as string | undefined;
106
- const strokeColor = nodeDataStroke || scopeColor || fillColor;
108
+ const strokeColor = nodeDataStroke || spanColor || fillColor;
107
109
 
108
110
  // Display info
109
111
  const displayName = nodeProps.name;
@@ -97,13 +97,15 @@ export const OtelEventNode: React.FC<NodeProps<Node<OtelEventNodeData>>> = ({
97
97
  const nodeOpacity = isHidden ? 0.4 : isActive ? 1 : 0.1;
98
98
 
99
99
  // Color resolution
100
+ const scopeColor = nodeData.scopeColor as string | undefined;
100
101
  const spanColor = nodeData.spanColor as string | undefined;
101
102
  const nodeDataColor = nodeData.color as string | undefined;
102
- const baseFillColor = spanColor || nodeDataColor || typeDefinition.color || '#3b82f6';
103
+ // Fill color priority: explicit color > scope color > type definition color > default blue
104
+ const baseFillColor = nodeDataColor || scopeColor || typeDefinition.color || '#3b82f6';
103
105
  const fillColor = baseFillColor;
104
- const scopeColor = nodeData.scopeColor as string | undefined;
106
+ // Stroke color priority: explicit stroke > span color (workflow context) > fill color
105
107
  const nodeDataStroke = nodeData.stroke as string | undefined;
106
- const strokeColor = nodeDataStroke || scopeColor || fillColor;
108
+ const strokeColor = nodeDataStroke || spanColor || fillColor;
107
109
 
108
110
  // Display info
109
111
  const displayName = nodeProps.name;
@@ -97,13 +97,15 @@ export const OtelResourceNode: React.FC<NodeProps<Node<OtelResourceNodeData>>> =
97
97
  const nodeOpacity = isHidden ? 0.4 : isActive ? 1 : 0.1;
98
98
 
99
99
  // Color resolution
100
+ const scopeColor = nodeData.scopeColor as string | undefined;
100
101
  const spanColor = nodeData.spanColor as string | undefined;
101
102
  const nodeDataColor = nodeData.color as string | undefined;
102
- const baseFillColor = spanColor || nodeDataColor || typeDefinition.color || '#f97316';
103
+ // Fill color priority: explicit color > scope color > type definition color > default orange
104
+ const baseFillColor = nodeDataColor || scopeColor || typeDefinition.color || '#f97316';
103
105
  const fillColor = baseFillColor;
104
- const scopeColor = nodeData.scopeColor as string | undefined;
106
+ // Stroke color priority: explicit stroke > span color (workflow context) > fill color
105
107
  const nodeDataStroke = nodeData.stroke as string | undefined;
106
- const strokeColor = nodeDataStroke || scopeColor || fillColor;
108
+ const strokeColor = nodeDataStroke || spanColor || fillColor;
107
109
 
108
110
  // Display info
109
111
  const displayName = nodeProps.name;
@@ -94,13 +94,15 @@ export const OtelScopeNode: React.FC<NodeProps<Node<OtelScopeNodeData>>> = ({
94
94
  const nodeOpacity = isHidden ? 0.4 : isActive ? 1 : 0.1;
95
95
 
96
96
  // Color resolution
97
+ const scopeColor = nodeData.scopeColor as string | undefined;
97
98
  const spanColor = nodeData.spanColor as string | undefined;
98
99
  const nodeDataColor = nodeData.color as string | undefined;
99
- const baseFillColor = spanColor || nodeDataColor || typeDefinition.color || '#22c55e';
100
+ // Fill color priority: explicit color > scope color > type definition color > default green
101
+ const baseFillColor = nodeDataColor || scopeColor || typeDefinition.color || '#22c55e';
100
102
  const fillColor = baseFillColor;
101
- const scopeColor = nodeData.scopeColor as string | undefined;
103
+ // Stroke color priority: explicit stroke > span color (workflow context) > fill color
102
104
  const nodeDataStroke = nodeData.stroke as string | undefined;
103
- const strokeColor = nodeDataStroke || scopeColor || fillColor;
105
+ const strokeColor = nodeDataStroke || spanColor || fillColor;
104
106
 
105
107
  // Display info
106
108
  const displayName = nodeProps.name;
@@ -113,14 +113,15 @@ export const OtelSpanConventionNode: React.FC<
113
113
  const nodeOpacity = isHidden ? 0.4 : isActive ? 1 : 0.1;
114
114
 
115
115
  // Color resolution
116
+ const scopeColor = nodeData.scopeColor as string | undefined;
116
117
  const spanColor = nodeData.spanColor as string | undefined;
117
118
  const nodeDataColor = nodeData.color as string | undefined;
118
- const baseFillColor = spanColor || nodeDataColor || typeDefinition.color || '#8b5cf6';
119
+ // Fill color priority: explicit color > scope color > type definition color > default purple
120
+ const baseFillColor = nodeDataColor || scopeColor || typeDefinition.color || '#8b5cf6';
119
121
  const fillColor = baseFillColor;
120
-
121
- const scopeColor = nodeData.scopeColor as string | undefined;
122
+ // Stroke color priority: explicit stroke > span color (workflow context) > fill color
122
123
  const nodeDataStroke = nodeData.stroke as string | undefined;
123
- const strokeColor = nodeDataStroke || scopeColor || fillColor;
124
+ const strokeColor = nodeDataStroke || spanColor || fillColor;
124
125
 
125
126
  // Get display info
126
127
  const displayName = nodeProps.name;