@principal-ai/subsystems-react 0.51.2 → 0.51.4
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 +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/subsystem/C4Graph.d.ts +12 -1
- package/dist/subsystem/C4Graph.d.ts.map +1 -1
- package/dist/subsystem/C4Graph.js +115 -22
- package/dist/subsystem/C4Graph.js.map +1 -1
- package/dist/subsystem/C4NodeCard.d.ts +22 -22
- package/dist/subsystem/C4NodeCard.d.ts.map +1 -1
- package/dist/subsystem/C4NodeCard.js +56 -77
- package/dist/subsystem/C4NodeCard.js.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +32 -13
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/TrailsPanel.d.ts.map +1 -1
- package/dist/subsystem/TrailsPanel.js +8 -4
- package/dist/subsystem/TrailsPanel.js.map +1 -1
- package/dist/subsystem/c4.d.ts +24 -86
- package/dist/subsystem/c4.d.ts.map +1 -1
- package/dist/subsystem/c4.js +6 -285
- package/dist/subsystem/c4.js.map +1 -1
- package/dist/subsystem/c4Evidence.js +1 -1
- package/dist/subsystem/c4Evidence.js.map +1 -1
- package/dist/subsystem/model.d.ts +14 -0
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js.map +1 -1
- package/dist/subsystem/nodes.d.ts.map +1 -1
- package/dist/subsystem/nodes.js +4 -2
- package/dist/subsystem/nodes.js.map +1 -1
- package/dist/subsystem/techIcons/bun.d.ts +11 -0
- package/dist/subsystem/techIcons/bun.d.ts.map +1 -0
- package/dist/subsystem/techIcons/bun.js +11 -0
- package/dist/subsystem/techIcons/bun.js.map +1 -0
- package/dist/subsystem/techIcons/index.d.ts +36 -0
- package/dist/subsystem/techIcons/index.d.ts.map +1 -0
- package/dist/subsystem/techIcons/index.js +55 -0
- package/dist/subsystem/techIcons/index.js.map +1 -0
- package/dist/subsystem/techIcons/node.d.ts +12 -0
- package/dist/subsystem/techIcons/node.d.ts.map +1 -0
- package/dist/subsystem/techIcons/node.js +39 -0
- package/dist/subsystem/techIcons/node.js.map +1 -0
- package/dist/subsystem/techIcons/react.d.ts +12 -0
- package/dist/subsystem/techIcons/react.d.ts.map +1 -0
- package/dist/subsystem/techIcons/react.js +19 -0
- package/dist/subsystem/techIcons/react.js.map +1 -0
- package/dist/utils/edgeLabel.d.ts +1 -1
- package/dist/utils/edgeLabel.d.ts.map +1 -1
- package/dist/utils/edgeLabel.js +1 -1
- package/dist/utils/edgeLabel.js.map +1 -1
- package/dist/utils/elkLayout.d.ts +18 -0
- package/dist/utils/elkLayout.d.ts.map +1 -1
- package/dist/utils/elkLayout.js +90 -9
- package/dist/utils/elkLayout.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +4 -6
- package/src/stories/Subsystem/C4Graph/C4Container.stories.tsx +212 -0
- package/src/stories/Subsystem/C4Graph/C4NodeGallery.stories.tsx +20 -10
- package/src/stories/Subsystem/ComponentGraph/IssueOverlay.stories.tsx +4 -2
- package/src/stories/Subsystem/ComponentGraph/ProposedTrails.stories.tsx +21 -1
- package/src/subsystem/C4Graph.tsx +171 -19
- package/src/subsystem/C4NodeCard.test.ts +71 -77
- package/src/subsystem/C4NodeCard.tsx +65 -95
- package/src/subsystem/SubsystemComponentGraph.tsx +41 -13
- package/src/subsystem/TrailsPanel.tsx +10 -5
- package/src/subsystem/c4.ts +23 -357
- package/src/subsystem/c4Evidence.test.ts +1 -1
- package/src/subsystem/c4Evidence.ts +1 -1
- package/src/subsystem/model.ts +14 -0
- package/src/subsystem/nodes.tsx +5 -2
- package/src/subsystem/techIcons/bun.ts +10 -0
- package/src/subsystem/techIcons/index.tsx +73 -0
- package/src/subsystem/techIcons/node.ts +38 -0
- package/src/subsystem/techIcons/react.ts +18 -0
- package/src/subsystem/techIcons/techIcons.test.ts +39 -0
- package/src/utils/edgeLabel.ts +1 -1
- package/src/utils/elkLayout.test.ts +34 -0
- package/src/utils/elkLayout.ts +88 -9
- package/dist/stories/Subsystem/C4Graph/c4ProposedElements.fixture.d.ts +0 -493
- package/dist/stories/Subsystem/C4Graph/c4ProposedElements.fixture.d.ts.map +0 -1
- package/dist/stories/Subsystem/C4Graph/c4ProposedElements.fixture.js +0 -329
- package/dist/stories/Subsystem/C4Graph/c4ProposedElements.fixture.js.map +0 -1
- package/src/stories/Subsystem/C4Graph/ProposedElements.stories.tsx +0 -293
- package/src/stories/Subsystem/C4Graph/c4ProposedElements.fixture.ts +0 -366
- package/src/subsystem/c4.test.ts +0 -573
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React mark — the official logo, vendored verbatim.
|
|
3
|
+
*
|
|
4
|
+
* Source: https://react.dev (the React logo; mirrored at
|
|
5
|
+
* https://commons.wikimedia.org/wiki/File:React-icon.svg).
|
|
6
|
+
* React and the React logo are trademarks of Meta Platforms, Inc. Used here
|
|
7
|
+
* for identification only, with no endorsement implied.
|
|
8
|
+
*
|
|
9
|
+
* Root sized to 100% so the card's wrapper controls the box.
|
|
10
|
+
*/
|
|
11
|
+
export const reactSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="-11.5 -10.23174 23 20.46348">
|
|
12
|
+
<circle cx="0" cy="0" r="2.05" fill="#61dafb"/>
|
|
13
|
+
<g stroke="#61dafb" stroke-width="1" fill="none">
|
|
14
|
+
<ellipse rx="11" ry="4.2"/>
|
|
15
|
+
<ellipse rx="11" ry="4.2" transform="rotate(60)"/>
|
|
16
|
+
<ellipse rx="11" ry="4.2" transform="rotate(120)"/>
|
|
17
|
+
</g>
|
|
18
|
+
</svg>`;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { TECH_BRANDS, technologyBrand } from './index';
|
|
3
|
+
|
|
4
|
+
describe('technologyBrand', () => {
|
|
5
|
+
test('matches the brand named in the technology string', () => {
|
|
6
|
+
expect(technologyBrand('React 19')?.name).toBe('React');
|
|
7
|
+
expect(technologyBrand('React + ELK')?.name).toBe('React');
|
|
8
|
+
expect(technologyBrand('Bun + Electrobun host')?.name).toBe('Bun');
|
|
9
|
+
expect(technologyBrand('Bun fs')?.name).toBe('Bun');
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('does not match a brand name inside another word', () => {
|
|
13
|
+
// "Electrobun" contains "bun" but is not Bun — no word boundary.
|
|
14
|
+
expect(technologyBrand('Electrobun host')).toBeUndefined();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('returns nothing for technologies we hold no official mark for', () => {
|
|
18
|
+
for (const tech of ['HTTP', 'REST', 'JSON files under ~/.principal', 'Electrobun host']) {
|
|
19
|
+
expect(technologyBrand(tech)).toBeUndefined();
|
|
20
|
+
}
|
|
21
|
+
expect(technologyBrand('')).toBeUndefined();
|
|
22
|
+
expect(technologyBrand(undefined)).toBeUndefined();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('matches Node and Node.js', () => {
|
|
26
|
+
expect(technologyBrand('Node')?.name).toBe('Node.js');
|
|
27
|
+
expect(technologyBrand('Node.js 20')?.name).toBe('Node.js');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('every vendored brand carries a source and an SVG', () => {
|
|
31
|
+
for (const brand of TECH_BRANDS) {
|
|
32
|
+
expect(brand.source).toMatch(/^https?:\/\//);
|
|
33
|
+
expect(brand.svg.startsWith('<svg')).toBe(true);
|
|
34
|
+
// Root sized to 100%, so the card wrapper controls the box.
|
|
35
|
+
expect(brand.svg).toContain('width="100%"');
|
|
36
|
+
expect(brand.svg).toContain('height="100%"');
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
});
|
package/src/utils/edgeLabel.ts
CHANGED
|
@@ -33,7 +33,7 @@ export const EDGE_LABEL_FONT_SIZE = 14;
|
|
|
33
33
|
* "min edge length" here double-counts (it is applied twice), which is how the
|
|
34
34
|
* gap silently ballooned before.
|
|
35
35
|
*/
|
|
36
|
-
export const EDGE_LABEL_SIDE_PADDING =
|
|
36
|
+
export const EDGE_LABEL_SIDE_PADDING = 8;
|
|
37
37
|
|
|
38
38
|
/** Actual end-to-end gap ELK leaves for a labelled edge (box + both clearances). */
|
|
39
39
|
export const EDGE_LABEL_EDGE_GAP =
|
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
pointsToPath,
|
|
13
13
|
pointsToSmoothPath,
|
|
14
14
|
calculatePathMidpoint,
|
|
15
|
+
pointAlongPath,
|
|
16
|
+
fractionAlongPath,
|
|
15
17
|
planCompoundGroups,
|
|
16
18
|
type Point,
|
|
17
19
|
} from './elkLayout';
|
|
@@ -129,6 +131,38 @@ describe('planCompoundGroups', () => {
|
|
|
129
131
|
});
|
|
130
132
|
|
|
131
133
|
describe('elkLayout helper functions', () => {
|
|
134
|
+
describe('pointAlongPath / fractionAlongPath', () => {
|
|
135
|
+
const bent: Point[] = [
|
|
136
|
+
{ x: 0, y: 0 },
|
|
137
|
+
{ x: 100, y: 0 },
|
|
138
|
+
{ x: 100, y: 100 },
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
test('half is the arc-length midpoint, not a corner', () => {
|
|
142
|
+
// Total length 200; half is 100 along, which is the corner (100,0).
|
|
143
|
+
const mid = pointAlongPath(bent, 0.5);
|
|
144
|
+
expect(mid).toEqual({ x: 100, y: 0 });
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('a fraction lands on the correct segment', () => {
|
|
148
|
+
// 0.75 of 200 = 150 → 50 down the second segment.
|
|
149
|
+
expect(pointAlongPath(bent, 0.75)).toEqual({ x: 100, y: 50 });
|
|
150
|
+
// 0.25 of 200 = 50 → half along the first segment.
|
|
151
|
+
expect(pointAlongPath(bent, 0.25)).toEqual({ x: 50, y: 0 });
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('clamps and handles degenerate input', () => {
|
|
155
|
+
expect(pointAlongPath(bent, -1)).toEqual({ x: 0, y: 0 });
|
|
156
|
+
expect(pointAlongPath(bent, 2)).toEqual({ x: 100, y: 100 });
|
|
157
|
+
expect(pointAlongPath([{ x: 5, y: 5 }], 0.5)).toEqual({ x: 5, y: 5 });
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('fractionAlongPath inverts pointAlongPath', () => {
|
|
161
|
+
const at = pointAlongPath(bent, 0.75);
|
|
162
|
+
expect(fractionAlongPath(bent, at)).toBeCloseTo(0.75, 5);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
132
166
|
describe('pointsToPath', () => {
|
|
133
167
|
test('should return empty string for empty array', () => {
|
|
134
168
|
expect(pointsToPath([])).toBe('');
|
package/src/utils/elkLayout.ts
CHANGED
|
@@ -10,7 +10,7 @@ import type { Node, Edge } from '@xyflow/react';
|
|
|
10
10
|
import {
|
|
11
11
|
EDGE_LABEL_WIDTH,
|
|
12
12
|
EDGE_LABEL_HEIGHT,
|
|
13
|
-
|
|
13
|
+
EDGE_LABEL_EDGE_GAP,
|
|
14
14
|
} from './edgeLabel';
|
|
15
15
|
|
|
16
16
|
/** ELK layout options for different routing styles */
|
|
@@ -330,6 +330,70 @@ export function calculatePathLength(points: Point[]): number {
|
|
|
330
330
|
return total;
|
|
331
331
|
}
|
|
332
332
|
|
|
333
|
+
/**
|
|
334
|
+
* Point a given fraction along a polyline, by arc length.
|
|
335
|
+
*
|
|
336
|
+
* Using arc length, not the nearest-point snap, is what keeps a label chip on
|
|
337
|
+
* the visual middle of a *bent* route. Nearest-point (`closestPointOnPath`)
|
|
338
|
+
* jumps to whichever segment happens to be nearest the target, so a fanned-out
|
|
339
|
+
* orthogonal route put the chip on a corner near the source node.
|
|
340
|
+
*
|
|
341
|
+
* @public Exported for testing
|
|
342
|
+
*/
|
|
343
|
+
export function pointAlongPath(points: Point[], fraction: number): Point {
|
|
344
|
+
if (points.length === 0) return { x: 0, y: 0 };
|
|
345
|
+
if (points.length === 1) return points[0];
|
|
346
|
+
const total = calculatePathLength(points);
|
|
347
|
+
if (total === 0) return points[0];
|
|
348
|
+
|
|
349
|
+
let remaining = Math.max(0, Math.min(1, fraction)) * total;
|
|
350
|
+
for (let i = 1; i < points.length; i++) {
|
|
351
|
+
const a = points[i - 1];
|
|
352
|
+
const b = points[i];
|
|
353
|
+
const seg = Math.hypot(b.x - a.x, b.y - a.y);
|
|
354
|
+
if (seg === 0) continue;
|
|
355
|
+
if (remaining <= seg) {
|
|
356
|
+
const t = remaining / seg;
|
|
357
|
+
return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t };
|
|
358
|
+
}
|
|
359
|
+
remaining -= seg;
|
|
360
|
+
}
|
|
361
|
+
return points[points.length - 1];
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* How far along a polyline a point lies, as a 0…1 fraction of arc length.
|
|
366
|
+
* The point is assumed to lie on (or near) the path.
|
|
367
|
+
*
|
|
368
|
+
* @public Exported for testing
|
|
369
|
+
*/
|
|
370
|
+
export function fractionAlongPath(points: Point[], at: Point): number {
|
|
371
|
+
if (points.length < 2) return 0.5;
|
|
372
|
+
const total = calculatePathLength(points);
|
|
373
|
+
if (total === 0) return 0.5;
|
|
374
|
+
|
|
375
|
+
let travelled = 0;
|
|
376
|
+
let bestDist = Infinity;
|
|
377
|
+
let bestTravelled = 0;
|
|
378
|
+
for (let i = 1; i < points.length; i++) {
|
|
379
|
+
const a = points[i - 1];
|
|
380
|
+
const b = points[i];
|
|
381
|
+
const seg = Math.hypot(b.x - a.x, b.y - a.y);
|
|
382
|
+
const dx = b.x - a.x;
|
|
383
|
+
const dy = b.y - a.y;
|
|
384
|
+
const t = seg === 0 ? 0 : Math.max(0, Math.min(1, ((at.x - a.x) * dx + (at.y - a.y) * dy) / (seg * seg)));
|
|
385
|
+
const px = a.x + dx * t;
|
|
386
|
+
const py = a.y + dy * t;
|
|
387
|
+
const dist = Math.hypot(at.x - px, at.y - py);
|
|
388
|
+
if (dist < bestDist) {
|
|
389
|
+
bestDist = dist;
|
|
390
|
+
bestTravelled = travelled + seg * t;
|
|
391
|
+
}
|
|
392
|
+
travelled += seg;
|
|
393
|
+
}
|
|
394
|
+
return bestTravelled / total;
|
|
395
|
+
}
|
|
396
|
+
|
|
333
397
|
/**
|
|
334
398
|
* Get ELK layout options based on configuration
|
|
335
399
|
*/
|
|
@@ -707,13 +771,24 @@ export async function computeElkLayout(
|
|
|
707
771
|
// edge by ~13px) plus a gap before the first child.
|
|
708
772
|
'elk.padding': '[top=64,left=24,bottom=24,right=24]',
|
|
709
773
|
'elk.spacing.nodeNode': '40',
|
|
710
|
-
// Edges inside a frame host labels too
|
|
711
|
-
// the
|
|
712
|
-
//
|
|
774
|
+
// Edges inside a frame host labels too, so the frame's own layout has to
|
|
775
|
+
// reserve the same room the root does — otherwise a nested graph (C4 puts
|
|
776
|
+
// every box in the system frame) lays its edges out with the label space
|
|
777
|
+
// ignored, and a 140px chip overflows a ~72px gap. `EDGE_LABEL_EDGE_GAP` is
|
|
778
|
+
// the full reserved width (label box + clearance at each end); labels off
|
|
779
|
+
// falls back to a plain between-layer gap.
|
|
713
780
|
'elk.layered.spacing.nodeNodeBetweenLayers': String(
|
|
714
|
-
edgeLabels?.enabled === false ? 40 :
|
|
781
|
+
edgeLabels?.enabled === false ? 40 : EDGE_LABEL_EDGE_GAP,
|
|
715
782
|
),
|
|
716
783
|
};
|
|
784
|
+
// Match the root's inline-label reservation, so ELK inside the frame leaves a
|
|
785
|
+
// dedicated label layer rather than routing straight through the chip.
|
|
786
|
+
if (edgeLabels?.enabled !== false) {
|
|
787
|
+
const placement = edgeLabels?.placement ?? 'CENTER';
|
|
788
|
+
compoundLayoutOptions['elk.edgeLabels.inline'] = 'true';
|
|
789
|
+
compoundLayoutOptions['elk.edgeLabels.inlinePlacement'] = placement;
|
|
790
|
+
compoundLayoutOptions['elk.layered.edgeLabels.centerLabelPlacementStrategy'] = 'CENTER_LAYER';
|
|
791
|
+
}
|
|
717
792
|
|
|
718
793
|
const plan = planCompoundGroups(groupDefs, elkById.keys(), keepSingletonGroups);
|
|
719
794
|
const skippedGroups = new Set(plan.skipped);
|
|
@@ -947,10 +1022,14 @@ export async function computeElkLayout(
|
|
|
947
1022
|
lx += sourceOffset.x + (targetOffset.x - sourceOffset.x) * t;
|
|
948
1023
|
ly += sourceOffset.y + (targetOffset.y - sourceOffset.y) * t;
|
|
949
1024
|
}
|
|
950
|
-
//
|
|
951
|
-
//
|
|
952
|
-
//
|
|
953
|
-
|
|
1025
|
+
// Place the chip by arc length, using ELK's label position only as a
|
|
1026
|
+
// hint for *how far along* the route it belongs. Snapping to the
|
|
1027
|
+
// nearest point (the old behaviour) jumped to whichever segment was
|
|
1028
|
+
// nearest the target, which put fanned-out orthogonal labels on a
|
|
1029
|
+
// corner near the source instead of the middle of the line.
|
|
1030
|
+
const hint = closestPointOnPath(allPoints, { x: lx, y: ly });
|
|
1031
|
+
const hintFraction = fractionAlongPath(allPoints, hint);
|
|
1032
|
+
const onPath = pointAlongPath(allPoints, hintFraction);
|
|
954
1033
|
edgeLabelPositions.set(edge.id, onPath);
|
|
955
1034
|
}
|
|
956
1035
|
|