@principal-ai/principal-view-react 0.16.57 → 0.16.58
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/subsystem/SubsystemComponentGraph.d.ts +10 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +379 -39
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/model.d.ts +34 -0
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js +1 -1
- package/dist/subsystem/model.js.map +1 -1
- package/dist/subsystem/nodes.d.ts +28 -0
- package/dist/subsystem/nodes.d.ts.map +1 -1
- package/dist/subsystem/nodes.js +52 -3
- package/dist/subsystem/nodes.js.map +1 -1
- package/dist/utils/elkLayout.d.ts.map +1 -1
- package/dist/utils/elkLayout.js +4 -3
- package/dist/utils/elkLayout.js.map +1 -1
- package/package.json +1 -1
- package/src/stories/Subsystem/ComponentGraph/Flows.stories.tsx +181 -0
- package/src/subsystem/SubsystemComponentGraph.tsx +533 -39
- package/src/subsystem/model.ts +37 -1
- package/src/subsystem/nodes.test.ts +56 -0
- package/src/subsystem/nodes.tsx +58 -4
- package/src/utils/elkLayout.ts +4 -3
package/src/subsystem/model.ts
CHANGED
|
@@ -177,6 +177,38 @@ export interface SubsystemComponentEdge {
|
|
|
177
177
|
refs?: string[];
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
+
/**
|
|
181
|
+
* A single site on an existing edge — the exact `file:line` where that edge's
|
|
182
|
+
* seam manifests for a given flow. The edge stays the abstract contract
|
|
183
|
+
* (`from`, `to`, `mechanism`); a throughline step picks the concrete
|
|
184
|
+
* manifestation. One edge can appear in many steps.
|
|
185
|
+
*/
|
|
186
|
+
export interface SubsystemThroughlineStep {
|
|
187
|
+
/** Id of the existing edge this hop traverses. */
|
|
188
|
+
edgeId: string;
|
|
189
|
+
/** Repo-root-relative path of the file where the edge fires. */
|
|
190
|
+
file: string;
|
|
191
|
+
/** 1-based line of the site within `file`. */
|
|
192
|
+
line: number;
|
|
193
|
+
/**
|
|
194
|
+
* Frame name for this hop — the function/method/symbol on the stack at
|
|
195
|
+
* this site. Optional so existing throughlines keep working; when set the
|
|
196
|
+
* flows list shows it instead of mechanism + filename.
|
|
197
|
+
*/
|
|
198
|
+
symbol?: string;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* An ordered execution story over a graph's edges — each step references an
|
|
203
|
+
* existing edge and the exact site where that relationship fires for a flow;
|
|
204
|
+
* ordering is the array. One throughline per flow (save flow, load flow, …).
|
|
205
|
+
*/
|
|
206
|
+
export interface SubsystemThroughline {
|
|
207
|
+
id: string;
|
|
208
|
+
title: string;
|
|
209
|
+
steps: SubsystemThroughlineStep[];
|
|
210
|
+
}
|
|
211
|
+
|
|
180
212
|
/**
|
|
181
213
|
* Derive a consistent display `name` from a code `symbol` + construct.
|
|
182
214
|
*
|
|
@@ -250,6 +282,8 @@ export function formatPurl(purl: string): string {
|
|
|
250
282
|
export interface SubsystemGraphDocument {
|
|
251
283
|
components: SubsystemComponent[];
|
|
252
284
|
edges: SubsystemComponentEdge[];
|
|
285
|
+
/** Ordered execution stories over the graph's edges (one per flow). */
|
|
286
|
+
throughlines?: SubsystemThroughline[];
|
|
253
287
|
}
|
|
254
288
|
|
|
255
289
|
// ---------------------------------------------------------------------------
|
|
@@ -264,6 +298,8 @@ export interface SubsystemGraphNodeData extends Record<string, unknown> {
|
|
|
264
298
|
* lives in that file (spotlighted), false otherwise (dimmed). Absent when
|
|
265
299
|
* no file is open — render neutrally. */
|
|
266
300
|
fileMatch?: boolean;
|
|
301
|
+
/** True while this node is on an opened-but-unselected flow. */
|
|
302
|
+
dimmed?: boolean;
|
|
267
303
|
}
|
|
268
304
|
|
|
269
305
|
export type SubsystemGraphNode = Node<SubsystemGraphNodeData, SubsystemGraphNodeType>;
|
|
@@ -447,7 +483,7 @@ export function convertSubsystemToEdges(doc: SubsystemGraphDocument): SubsystemG
|
|
|
447
483
|
target: targetId,
|
|
448
484
|
data: { mechanism: e.mechanism, refs: e.refs },
|
|
449
485
|
type: 'subsystem-edge',
|
|
450
|
-
markerEnd: { type: MarkerType.ArrowClosed, color, width:
|
|
486
|
+
markerEnd: { type: MarkerType.ArrowClosed, color, width: 32, height: 32 },
|
|
451
487
|
style: { color, stroke: color, strokeDasharray: style === 'dashed' ? '6 4' : undefined },
|
|
452
488
|
// `label` feeds ELK's label-space reservation only; the visible label is
|
|
453
489
|
// rendered by the custom SubsystemEdge as an HTML overlay.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { EDGE_DIM_ALPHA, fileMatchForNode, flowElementVisibility, hexWithAlpha } from './nodes';
|
|
3
|
+
|
|
4
|
+
describe('hexWithAlpha', () => {
|
|
5
|
+
test('appends a two-digit alpha to #rrggbb', () => {
|
|
6
|
+
expect(hexWithAlpha('#4ec9b0', 1)).toBe('#4ec9b0ff');
|
|
7
|
+
expect(hexWithAlpha('#4ec9b0', EDGE_DIM_ALPHA)).toBe('#4ec9b026');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test('expands #rgb', () => {
|
|
11
|
+
expect(hexWithAlpha('#abc', 1)).toBe('#aabbccff');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('leaves non-hex values alone', () => {
|
|
15
|
+
expect(hexWithAlpha('teal', 0.15)).toBe('teal');
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('fileMatchForNode', () => {
|
|
20
|
+
test('no open file → neutral', () => {
|
|
21
|
+
expect(fileMatchForNode('src/a.ts', null, false)).toBeUndefined();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('open file spotlights matches and dims others', () => {
|
|
25
|
+
expect(fileMatchForNode('src/a.ts', 'src/a.ts', false)).toBe(true);
|
|
26
|
+
expect(fileMatchForNode('src/b.ts', 'src/a.ts', false)).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('focused-edge endpoint is not dimmed when it lives in another file', () => {
|
|
30
|
+
expect(fileMatchForNode('src/target.ts', 'src/source.ts', true)).toBeUndefined();
|
|
31
|
+
expect(fileMatchForNode('src/source.ts', 'src/source.ts', true)).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('flowElementVisibility', () => {
|
|
36
|
+
test('nothing open or selected → everything full', () => {
|
|
37
|
+
expect(flowElementVisibility({ inOpened: false, inSelected: false, anyOpened: false, anySelected: false }))
|
|
38
|
+
.toEqual({ hidden: false, dimmed: false });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('opened, nothing selected → opened members full, rest hidden', () => {
|
|
42
|
+
expect(flowElementVisibility({ inOpened: true, inSelected: false, anyOpened: true, anySelected: false }))
|
|
43
|
+
.toEqual({ hidden: false, dimmed: false });
|
|
44
|
+
expect(flowElementVisibility({ inOpened: false, inSelected: false, anyOpened: true, anySelected: false }))
|
|
45
|
+
.toEqual({ hidden: true, dimmed: false });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('selected flow or step full; other opened members dimmed; rest hidden', () => {
|
|
49
|
+
expect(flowElementVisibility({ inOpened: true, inSelected: true, anyOpened: true, anySelected: true }))
|
|
50
|
+
.toEqual({ hidden: false, dimmed: false });
|
|
51
|
+
expect(flowElementVisibility({ inOpened: true, inSelected: false, anyOpened: true, anySelected: true }))
|
|
52
|
+
.toEqual({ hidden: false, dimmed: true });
|
|
53
|
+
expect(flowElementVisibility({ inOpened: false, inSelected: false, anyOpened: true, anySelected: true }))
|
|
54
|
+
.toEqual({ hidden: true, dimmed: false });
|
|
55
|
+
});
|
|
56
|
+
});
|
package/src/subsystem/nodes.tsx
CHANGED
|
@@ -126,7 +126,7 @@ export function SubsystemComponentNode(props: NodeProps<SubsystemGraphNode>) {
|
|
|
126
126
|
boxShadow: fileMatch
|
|
127
127
|
? `0 1px 4px rgba(0,0,0,0.25), 0 0 12px ${theme.colors.primary}55`
|
|
128
128
|
: '0 1px 4px rgba(0,0,0,0.25)',
|
|
129
|
-
opacity: fileMatch === false ? 0.18 : 1,
|
|
129
|
+
opacity: fileMatch === false || data.dimmed === true ? 0.18 : 1,
|
|
130
130
|
transition: 'opacity 150ms ease',
|
|
131
131
|
cursor: 'pointer',
|
|
132
132
|
fontFamily: theme.fonts.body,
|
|
@@ -267,6 +267,58 @@ export function SubsystemComponentNode(props: NodeProps<SubsystemGraphNode>) {
|
|
|
267
267
|
);
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
+
/** `#rrggbb` + alpha → `#rrggbbaa`. Used to dim a stroke/marker by color so
|
|
271
|
+
* each opacity gets its own SVG marker id — path `opacity` leaks across every
|
|
272
|
+
* edge that shares a `url(#marker)` (the focused edge's arrowhead dims). */
|
|
273
|
+
export function hexWithAlpha(hex: string, alpha: number): string {
|
|
274
|
+
const raw = hex.replace('#', '');
|
|
275
|
+
const full = raw.length === 3 ? [...raw].map((c) => c + c).join('') : raw;
|
|
276
|
+
if (!/^[0-9a-fA-F]{6}$/.test(full)) return hex;
|
|
277
|
+
const a = Math.round(Math.min(1, Math.max(0, alpha)) * 255)
|
|
278
|
+
.toString(16)
|
|
279
|
+
.padStart(2, '0');
|
|
280
|
+
return `#${full}${a}`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* File-open spotlight flag for a node. `true` = lives in the open file,
|
|
285
|
+
* `false` = dim, `undefined` = render neutrally.
|
|
286
|
+
*
|
|
287
|
+
* Focused-edge endpoints stay neutral when they don't live in the open file
|
|
288
|
+
* (the target of a focused edge must not dim with the rest).
|
|
289
|
+
*/
|
|
290
|
+
export function fileMatchForNode(
|
|
291
|
+
nodeFile: string | undefined,
|
|
292
|
+
openFile: string | null,
|
|
293
|
+
isFocusEndpoint: boolean,
|
|
294
|
+
): boolean | undefined {
|
|
295
|
+
if (!openFile) return undefined;
|
|
296
|
+
if (nodeFile === openFile) return true;
|
|
297
|
+
if (isFocusEndpoint) return undefined;
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Hide / dim a node or edge while flows are open.
|
|
303
|
+
* - not in any opened flow → hidden
|
|
304
|
+
* - in an opened flow, but not the selected flow/step → dimmed
|
|
305
|
+
* - in the selected flow or step (or opened with nothing selected) → full
|
|
306
|
+
*/
|
|
307
|
+
export function flowElementVisibility(opts: {
|
|
308
|
+
inOpened: boolean;
|
|
309
|
+
inSelected: boolean;
|
|
310
|
+
anyOpened: boolean;
|
|
311
|
+
anySelected: boolean;
|
|
312
|
+
}): { hidden: boolean; dimmed: boolean } {
|
|
313
|
+
const { inOpened, inSelected, anyOpened, anySelected } = opts;
|
|
314
|
+
if (!anyOpened && !anySelected) return { hidden: false, dimmed: false };
|
|
315
|
+
if (!inOpened && !inSelected) return { hidden: true, dimmed: false };
|
|
316
|
+
if (anySelected && !inSelected) return { hidden: false, dimmed: true };
|
|
317
|
+
return { hidden: false, dimmed: false };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export const EDGE_DIM_ALPHA = 0.15;
|
|
321
|
+
|
|
270
322
|
/** Subsystem edge — SVG path only. The mechanism label is rendered as an
|
|
271
323
|
* absolutely-positioned HTML overlay OUTSIDE the ReactFlow tree (by the
|
|
272
324
|
* parent Inner component) so it sits above the pane and receives pointer
|
|
@@ -282,7 +334,10 @@ export function SubsystemEdge({
|
|
|
282
334
|
// observational relationships: hierarchy, registration, watches).
|
|
283
335
|
const isDashed = MECHANISM_STYLE[mechanism] === 'dashed';
|
|
284
336
|
const dimmed = data?.dimmed === true;
|
|
285
|
-
|
|
337
|
+
// Dim the stroke by color, never via path `opacity`. SVG markers are shared
|
|
338
|
+
// by id; opacity on the referencing path paints every arrowhead that uses
|
|
339
|
+
// the same marker — including the focused edge's target.
|
|
340
|
+
const stroke = dimmed ? hexWithAlpha(color, EDGE_DIM_ALPHA) : color;
|
|
286
341
|
|
|
287
342
|
return (
|
|
288
343
|
<>
|
|
@@ -301,10 +356,9 @@ export function SubsystemEdge({
|
|
|
301
356
|
<path
|
|
302
357
|
d={path}
|
|
303
358
|
fill="none"
|
|
304
|
-
stroke={
|
|
359
|
+
stroke={stroke}
|
|
305
360
|
strokeWidth={1.5}
|
|
306
361
|
strokeDasharray={isDashed ? '6 4' : undefined}
|
|
307
|
-
opacity={opacity}
|
|
308
362
|
markerEnd={markerEnd}
|
|
309
363
|
style={{ pointerEvents: 'none' }}
|
|
310
364
|
/>
|
package/src/utils/elkLayout.ts
CHANGED
|
@@ -529,9 +529,10 @@ export async function computeElkLayout(
|
|
|
529
529
|
// and apply the same coordinate offset.
|
|
530
530
|
if (edge.labels && edge.labels.length > 0) {
|
|
531
531
|
const elkLabel = edge.labels[0];
|
|
532
|
-
//
|
|
533
|
-
|
|
534
|
-
let
|
|
532
|
+
// ELK reports the label's top-left; convert to center so screen-space
|
|
533
|
+
// overlays can anchor with translate(-50%, -50%) at any zoom.
|
|
534
|
+
let lx = (elkLabel.x ?? 0) + (elkLabel.width ?? 0) / 2;
|
|
535
|
+
let ly = (elkLabel.y ?? 0) + (elkLabel.height ?? 0) / 2;
|
|
535
536
|
if (preserveNodePositions && sourceOriginal && sourceElk && targetOriginal && targetElk) {
|
|
536
537
|
const sourceOffset = {
|
|
537
538
|
x: sourceOriginal.x - sourceElk.x,
|