@socprime/react-ui 0.0.10 → 0.0.12
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/domain.d.ts +14 -1
- package/index.d.ts +1 -1
- package/package.json +1 -1
- package/ui/AttackTimeline/AttackTimeline.js +66 -12
- package/ui/AttackTimeline/CyberpunkCorner.js +3 -4
- package/ui/AttackTimeline/TimelineBranchNode.js +46 -0
- package/ui/AttackTimeline/TimelineBranches.js +99 -0
- package/ui/AttackTimeline/TimelineSegments.js +6 -9
- package/ui/AttackTimeline/TimelineStepItem.js +38 -54
- package/ui/AttackTimeline/TimelineStepTooltip.js +31 -0
- package/ui/AttackTimeline/branchGeometry.js +65 -0
- package/ui/AttackTimeline/index.js +3 -1
- package/ui/AttackTimeline/mapChainGraphBranches.js +29 -0
- package/ui/AttackTimeline/timelineConstants.js +30 -0
- package/ui/AttackTimeline/useTimeline.js +15 -1
package/domain.d.ts
CHANGED
|
@@ -7,9 +7,22 @@ interface AttackTimelineStep {
|
|
|
7
7
|
hit: boolean;
|
|
8
8
|
code: string;
|
|
9
9
|
}
|
|
10
|
+
interface ChainGraphBranch {
|
|
11
|
+
from_code: string;
|
|
12
|
+
to_code: string;
|
|
13
|
+
node: AttackTimelineStep;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type ChainGraphBranchView = {
|
|
17
|
+
fromIndex: number;
|
|
18
|
+
toIndex: number;
|
|
19
|
+
step: ChainGraphBranch['node'];
|
|
20
|
+
};
|
|
21
|
+
declare const mapChainGraphBranches: (branches: ChainGraphBranch[] | undefined, chainGraph: AttackTimelineStep[]) => ChainGraphBranchView[];
|
|
10
22
|
|
|
11
23
|
interface AttackTimelineProps {
|
|
12
24
|
steps?: AttackTimelineStep[];
|
|
25
|
+
branches?: ChainGraphBranchView[];
|
|
13
26
|
}
|
|
14
27
|
declare const AttackTimeline: React.FC<AttackTimelineProps>;
|
|
15
28
|
|
|
@@ -32,4 +45,4 @@ declare const SyncProcessBar: () => React$1.JSX.Element;
|
|
|
32
45
|
|
|
33
46
|
type SyncProcessBarProps = ComponentProps<typeof SyncProcessBar>;
|
|
34
47
|
|
|
35
|
-
export { AttackTimeline, type AttackTimelineProps, type AttackTimelineStep, CorrelationTimer, type CorrelationTimerProps, Severity, type SeverityProps, SyncProcessBar, type SyncProcessBarProps, type TSeverity };
|
|
48
|
+
export { AttackTimeline, type AttackTimelineProps, type AttackTimelineStep, type ChainGraphBranch, type ChainGraphBranchView, CorrelationTimer, type CorrelationTimerProps, Severity, type SeverityProps, SyncProcessBar, type SyncProcessBarProps, type TSeverity, mapChainGraphBranches };
|
package/index.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,12 +1,37 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useMemo } from "react";
|
|
2
3
|
import { CyberpunkCorner } from "./CyberpunkCorner.js";
|
|
4
|
+
import { TimelineBranches } from "./TimelineBranches.js";
|
|
5
|
+
import { BRANCH_LAYOUT_PADDING_TOP } from "./timelineConstants.js";
|
|
3
6
|
import { TimelineDividers } from "./TimelineDividers.js";
|
|
4
7
|
import { TimelineSegments } from "./TimelineSegments.js";
|
|
5
8
|
import { TimelineStepItem } from "./TimelineStepItem.js";
|
|
6
9
|
import { useTimeline } from "./useTimeline.js";
|
|
7
|
-
const AttackTimeline = ({ steps }) => {
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
+
const AttackTimeline = ({ steps, branches = [] }) => {
|
|
11
|
+
const timelineSteps = steps ?? [];
|
|
12
|
+
const lastHitIndex = useMemo(
|
|
13
|
+
() => timelineSteps.reduce((last, step, index) => step.hit ? index : last, -1),
|
|
14
|
+
[timelineSteps]
|
|
15
|
+
);
|
|
16
|
+
const {
|
|
17
|
+
containerRef,
|
|
18
|
+
hoveredIndex,
|
|
19
|
+
hoveredBranchIndex,
|
|
20
|
+
setHoveredIndex,
|
|
21
|
+
setHoveredBranchIndex,
|
|
22
|
+
mouseX,
|
|
23
|
+
timelineWidth,
|
|
24
|
+
handleMouseMove,
|
|
25
|
+
handleMouseLeave
|
|
26
|
+
} = useTimeline();
|
|
27
|
+
const makeHoverHandlers = useCallback(
|
|
28
|
+
(index) => ({
|
|
29
|
+
onMouseEnter: () => setHoveredIndex(index),
|
|
30
|
+
onMouseLeave: () => setHoveredIndex(null)
|
|
31
|
+
}),
|
|
32
|
+
[setHoveredIndex]
|
|
33
|
+
);
|
|
34
|
+
if (timelineSteps.length === 0) {
|
|
10
35
|
return null;
|
|
11
36
|
}
|
|
12
37
|
return /* @__PURE__ */ jsxs("div", { className: "relative p-[12px]", children: [
|
|
@@ -15,17 +40,46 @@ const AttackTimeline = ({ steps }) => {
|
|
|
15
40
|
/* @__PURE__ */ jsx(CyberpunkCorner, { position: "bottom-left" }),
|
|
16
41
|
/* @__PURE__ */ jsx(CyberpunkCorner, { position: "bottom-right" }),
|
|
17
42
|
/* @__PURE__ */ jsx("div", { className: "mb-4 flex items-center gap-2" }),
|
|
18
|
-
/* @__PURE__ */
|
|
43
|
+
/* @__PURE__ */ jsx(
|
|
19
44
|
"div",
|
|
20
45
|
{
|
|
21
|
-
className: "
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
46
|
+
className: "overflow-visible",
|
|
47
|
+
style: { paddingTop: branches.length > 0 ? BRANCH_LAYOUT_PADDING_TOP : void 0 },
|
|
48
|
+
children: /* @__PURE__ */ jsxs(
|
|
49
|
+
"div",
|
|
50
|
+
{
|
|
51
|
+
ref: containerRef,
|
|
52
|
+
className: "relative flex w-full items-start justify-between pt-2.5",
|
|
53
|
+
onMouseMove: handleMouseMove,
|
|
54
|
+
onMouseLeave: handleMouseLeave,
|
|
55
|
+
children: [
|
|
56
|
+
/* @__PURE__ */ jsx(TimelineSegments, { steps: timelineSteps }),
|
|
57
|
+
/* @__PURE__ */ jsx(TimelineDividers, { mouseX, timelineWidth }),
|
|
58
|
+
/* @__PURE__ */ jsx(
|
|
59
|
+
TimelineBranches,
|
|
60
|
+
{
|
|
61
|
+
branches,
|
|
62
|
+
totalSteps: timelineSteps.length,
|
|
63
|
+
hoveredBranchIndex,
|
|
64
|
+
onBranchMouseEnter: setHoveredBranchIndex,
|
|
65
|
+
onBranchMouseLeave: () => setHoveredBranchIndex(null)
|
|
66
|
+
}
|
|
67
|
+
),
|
|
68
|
+
timelineSteps.map((step, index) => /* @__PURE__ */ jsx(
|
|
69
|
+
TimelineStepItem,
|
|
70
|
+
{
|
|
71
|
+
step,
|
|
72
|
+
index,
|
|
73
|
+
totalSteps: timelineSteps.length,
|
|
74
|
+
isCurrent: index === lastHitIndex && step.hit,
|
|
75
|
+
isHovered: hoveredIndex === index,
|
|
76
|
+
...makeHoverHandlers(index)
|
|
77
|
+
},
|
|
78
|
+
`${step.code}-${index}`
|
|
79
|
+
))
|
|
80
|
+
]
|
|
81
|
+
}
|
|
82
|
+
)
|
|
29
83
|
}
|
|
30
84
|
)
|
|
31
85
|
] });
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useThemeColors } from "../../lib/useThemeColors.js";
|
|
3
2
|
const GLOW_SHADOW = "0 0 8px rgba(74, 193, 142, 0.6)";
|
|
3
|
+
const COLOR = "#4ac18e";
|
|
4
4
|
const CyberpunkCorner = ({ position }) => {
|
|
5
|
-
const { success } = useThemeColors();
|
|
6
5
|
const isTop = position.startsWith("top");
|
|
7
6
|
const isLeft = position.endsWith("left");
|
|
8
7
|
const edge = isTop ? "top-0" : "bottom-0";
|
|
@@ -12,14 +11,14 @@ const CyberpunkCorner = ({ position }) => {
|
|
|
12
11
|
"div",
|
|
13
12
|
{
|
|
14
13
|
className: `absolute ${edge} ${side} h-[1px] w-3`,
|
|
15
|
-
style: { backgroundColor:
|
|
14
|
+
style: { backgroundColor: COLOR, boxShadow: GLOW_SHADOW }
|
|
16
15
|
}
|
|
17
16
|
),
|
|
18
17
|
/* @__PURE__ */ jsx(
|
|
19
18
|
"div",
|
|
20
19
|
{
|
|
21
20
|
className: `absolute ${edge} ${side} h-3 w-[1px]`,
|
|
22
|
-
style: { backgroundColor:
|
|
21
|
+
style: { backgroundColor: COLOR, boxShadow: GLOW_SHADOW }
|
|
23
22
|
}
|
|
24
23
|
)
|
|
25
24
|
] });
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { memo } from "react";
|
|
3
|
+
import { cn } from "../../lib/cn.js";
|
|
4
|
+
import { TimelineStepTooltip } from "./TimelineStepTooltip.js";
|
|
5
|
+
import { DETECTION_BYPASS_LABEL, TIMELINE_BRANCH_ORANGE_START } from "./timelineConstants.js";
|
|
6
|
+
const TimelineBranchNode = memo(
|
|
7
|
+
({ step, leftPercent, topPx, isHovered, onMouseEnter, onMouseLeave }) => {
|
|
8
|
+
const markerBoxShadow = isHovered ? `0 0 0 4px #252838, 0 0 16px color-mix(in srgb, ${TIMELINE_BRANCH_ORANGE_START} 60%, transparent)` : "0 0 0 4px #252838";
|
|
9
|
+
return /* @__PURE__ */ jsxs(
|
|
10
|
+
"div",
|
|
11
|
+
{
|
|
12
|
+
className: "pointer-events-auto absolute z-[2] flex -translate-x-1/2 flex-col items-center text-center",
|
|
13
|
+
style: { left: `${leftPercent}%`, top: `${topPx}px` },
|
|
14
|
+
onMouseEnter,
|
|
15
|
+
onMouseLeave,
|
|
16
|
+
children: [
|
|
17
|
+
/* @__PURE__ */ jsx(TimelineStepTooltip, { step, codeClassName: "text-california", className: "max-w-200", children: /* @__PURE__ */ jsx("div", { className: "relative mb-4", children: /* @__PURE__ */ jsx(
|
|
18
|
+
"div",
|
|
19
|
+
{
|
|
20
|
+
className: cn(
|
|
21
|
+
"from-california to-international-orange flex h-6 w-6 rotate-45 cursor-pointer bg-gradient-to-b p-[1.5px] transition-all duration-300",
|
|
22
|
+
isHovered && "scale-120"
|
|
23
|
+
),
|
|
24
|
+
style: { boxShadow: markerBoxShadow },
|
|
25
|
+
children: /* @__PURE__ */ jsx(
|
|
26
|
+
"div",
|
|
27
|
+
{
|
|
28
|
+
className: cn(
|
|
29
|
+
"relative h-full w-full bg-[#2b2419]",
|
|
30
|
+
isHovered && "from-california/25 to-international-orange/25 bg-gradient-to-b"
|
|
31
|
+
),
|
|
32
|
+
children: /* @__PURE__ */ jsx("div", { className: "from-california to-international-orange absolute top-1/2 left-1/2 h-3 w-3 -translate-x-1/2 -translate-y-1/2 bg-gradient-to-b" })
|
|
33
|
+
}
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
) }) }),
|
|
37
|
+
/* @__PURE__ */ jsx("span", { className: "text-default max-w-[120px] cursor-pointer text-[11px] font-medium", children: DETECTION_BYPASS_LABEL })
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
);
|
|
43
|
+
TimelineBranchNode.displayName = "TimelineBranchNode";
|
|
44
|
+
export {
|
|
45
|
+
TimelineBranchNode
|
|
46
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { memo, useId } from "react";
|
|
3
|
+
import { getBranchGeometry } from "./branchGeometry.js";
|
|
4
|
+
import { TimelineBranchNode } from "./TimelineBranchNode.js";
|
|
5
|
+
import {
|
|
6
|
+
BRANCH_AREA_HEIGHT,
|
|
7
|
+
BRANCH_NODE_OFFSET_Y,
|
|
8
|
+
BRANCH_OFFSET_TOP,
|
|
9
|
+
BRANCH_TOP_Y,
|
|
10
|
+
TIMELINE_BRANCH_ORANGE_END,
|
|
11
|
+
TIMELINE_BRANCH_ORANGE_START,
|
|
12
|
+
TIMELINE_GREEN
|
|
13
|
+
} from "./timelineConstants.js";
|
|
14
|
+
const TimelineBranches = memo(
|
|
15
|
+
({ branches, totalSteps, hoveredBranchIndex, onBranchMouseEnter, onBranchMouseLeave }) => {
|
|
16
|
+
const baseId = useId();
|
|
17
|
+
if (branches.length === 0) return null;
|
|
18
|
+
return /* @__PURE__ */ jsxs(
|
|
19
|
+
"div",
|
|
20
|
+
{
|
|
21
|
+
className: "pointer-events-none absolute inset-x-0 overflow-visible",
|
|
22
|
+
style: { top: BRANCH_OFFSET_TOP, height: BRANCH_AREA_HEIGHT, zIndex: 0 },
|
|
23
|
+
children: [
|
|
24
|
+
/* @__PURE__ */ jsxs(
|
|
25
|
+
"svg",
|
|
26
|
+
{
|
|
27
|
+
className: "absolute inset-0 h-full w-full overflow-visible",
|
|
28
|
+
viewBox: `0 0 100 ${BRANCH_AREA_HEIGHT}`,
|
|
29
|
+
preserveAspectRatio: "none",
|
|
30
|
+
children: [
|
|
31
|
+
/* @__PURE__ */ jsx("defs", { children: branches.map((branch, index) => {
|
|
32
|
+
const { fromXPercent, toXPercent } = getBranchGeometry(
|
|
33
|
+
branch.fromIndex,
|
|
34
|
+
branch.toIndex,
|
|
35
|
+
totalSteps
|
|
36
|
+
);
|
|
37
|
+
return /* @__PURE__ */ jsxs(
|
|
38
|
+
"linearGradient",
|
|
39
|
+
{
|
|
40
|
+
id: `${baseId}-branch-gradient-${index}`,
|
|
41
|
+
gradientUnits: "userSpaceOnUse",
|
|
42
|
+
x1: fromXPercent,
|
|
43
|
+
y1: 0,
|
|
44
|
+
x2: toXPercent,
|
|
45
|
+
y2: 0,
|
|
46
|
+
children: [
|
|
47
|
+
/* @__PURE__ */ jsx("stop", { offset: "0%", stopColor: TIMELINE_GREEN }),
|
|
48
|
+
/* @__PURE__ */ jsx("stop", { offset: "20%", style: { stopColor: TIMELINE_BRANCH_ORANGE_START } }),
|
|
49
|
+
/* @__PURE__ */ jsx("stop", { offset: "80%", style: { stopColor: TIMELINE_BRANCH_ORANGE_END } }),
|
|
50
|
+
/* @__PURE__ */ jsx("stop", { offset: "100%", stopColor: TIMELINE_GREEN })
|
|
51
|
+
]
|
|
52
|
+
},
|
|
53
|
+
`gradient-${index}`
|
|
54
|
+
);
|
|
55
|
+
}) }),
|
|
56
|
+
branches.map((branch, index) => {
|
|
57
|
+
const geometry = getBranchGeometry(branch.fromIndex, branch.toIndex, totalSteps);
|
|
58
|
+
const gradientId = `${baseId}-branch-gradient-${index}`;
|
|
59
|
+
return /* @__PURE__ */ jsx(
|
|
60
|
+
"path",
|
|
61
|
+
{
|
|
62
|
+
d: geometry.outlinePath,
|
|
63
|
+
fill: "none",
|
|
64
|
+
stroke: `url(#${gradientId})`,
|
|
65
|
+
strokeWidth: 1,
|
|
66
|
+
strokeLinecap: "round",
|
|
67
|
+
strokeLinejoin: "round",
|
|
68
|
+
vectorEffect: "non-scaling-stroke"
|
|
69
|
+
},
|
|
70
|
+
`branch-shape-${index}`
|
|
71
|
+
);
|
|
72
|
+
})
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
),
|
|
76
|
+
branches.map((branch, index) => {
|
|
77
|
+
const { nodeXPercent } = getBranchGeometry(branch.fromIndex, branch.toIndex, totalSteps);
|
|
78
|
+
return /* @__PURE__ */ jsx(
|
|
79
|
+
TimelineBranchNode,
|
|
80
|
+
{
|
|
81
|
+
step: branch.step,
|
|
82
|
+
leftPercent: nodeXPercent,
|
|
83
|
+
topPx: BRANCH_TOP_Y + BRANCH_NODE_OFFSET_Y,
|
|
84
|
+
isHovered: hoveredBranchIndex === index,
|
|
85
|
+
onMouseEnter: () => onBranchMouseEnter(index),
|
|
86
|
+
onMouseLeave: onBranchMouseLeave
|
|
87
|
+
},
|
|
88
|
+
`branch-node-${index}`
|
|
89
|
+
);
|
|
90
|
+
})
|
|
91
|
+
]
|
|
92
|
+
}
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
TimelineBranches.displayName = "TimelineBranches";
|
|
97
|
+
export {
|
|
98
|
+
TimelineBranches
|
|
99
|
+
};
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { jsx } from "react/jsx-runtime";
|
|
1
|
+
import { Fragment, jsx } from "react/jsx-runtime";
|
|
2
2
|
const getSegmentColor = (current, next) => {
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
const nextActive = isActiveStatus(next);
|
|
3
|
+
const currentActive = current.hit;
|
|
4
|
+
const nextActive = next.hit;
|
|
6
5
|
if (currentActive && nextActive) {
|
|
7
6
|
return "#4ac18e";
|
|
8
7
|
}
|
|
@@ -16,10 +15,8 @@ const getSegmentColor = (current, next) => {
|
|
|
16
15
|
};
|
|
17
16
|
const TimelineSegments = ({ steps }) => {
|
|
18
17
|
const segmentWidth = 100 / steps.length;
|
|
19
|
-
return steps.map((step, index) => {
|
|
20
|
-
if (index === steps.length - 1)
|
|
21
|
-
return null;
|
|
22
|
-
}
|
|
18
|
+
return /* @__PURE__ */ jsx(Fragment, { children: steps.map((step, index) => {
|
|
19
|
+
if (index === steps.length - 1) return null;
|
|
23
20
|
const leftPosition = index * segmentWidth + segmentWidth / 2;
|
|
24
21
|
const lineColor = getSegmentColor(step, steps[index + 1]);
|
|
25
22
|
return /* @__PURE__ */ jsx(
|
|
@@ -35,7 +32,7 @@ const TimelineSegments = ({ steps }) => {
|
|
|
35
32
|
},
|
|
36
33
|
`segment-${index}`
|
|
37
34
|
);
|
|
38
|
-
});
|
|
35
|
+
}) });
|
|
39
36
|
};
|
|
40
37
|
export {
|
|
41
38
|
TimelineSegments
|
|
@@ -1,58 +1,42 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
const TimelineStepItem = (
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
children: step.hit && /* @__PURE__ */ jsx("div", { className: "bg-success absolute top-1/2 left-1/2 h-1 w-1 -translate-x-1/2 -translate-y-1/2 transform" })
|
|
41
|
-
}
|
|
42
|
-
) })
|
|
43
|
-
}
|
|
44
|
-
),
|
|
45
|
-
/* @__PURE__ */ jsx(
|
|
46
|
-
"span",
|
|
47
|
-
{
|
|
48
|
-
className: `text-3xs font-medium ${step.hit ? "text-default" : "text-gray-chateau/80"}`,
|
|
49
|
-
children: step.name
|
|
50
|
-
}
|
|
51
|
-
)
|
|
52
|
-
]
|
|
53
|
-
}
|
|
54
|
-
);
|
|
55
|
-
};
|
|
2
|
+
import { memo } from "react";
|
|
3
|
+
import { TimelineStepTooltip } from "./TimelineStepTooltip.js";
|
|
4
|
+
const TimelineStepItem = memo(
|
|
5
|
+
({ step, totalSteps, isCurrent, isHovered, onMouseEnter, onMouseLeave }) => {
|
|
6
|
+
const isActive = step.hit;
|
|
7
|
+
const markerBoxShadow = isHovered ? "0 0 0 4px #252838, 0 0 16px rgba(74, 193, 142, 0.6)" : isActive ? "0 0 0 4px #252838" : "none";
|
|
8
|
+
return /* @__PURE__ */ jsxs(
|
|
9
|
+
"div",
|
|
10
|
+
{
|
|
11
|
+
className: "group relative flex flex-col items-center text-center",
|
|
12
|
+
style: { width: `${100 / totalSteps}%`, zIndex: 1 },
|
|
13
|
+
onMouseEnter,
|
|
14
|
+
onMouseLeave,
|
|
15
|
+
children: [
|
|
16
|
+
/* @__PURE__ */ jsx(TimelineStepTooltip, { step, className: "max-w-200", children: /* @__PURE__ */ jsx("div", { className: "relative mb-4", children: /* @__PURE__ */ jsx(
|
|
17
|
+
"div",
|
|
18
|
+
{
|
|
19
|
+
className: `h-[13px] w-[13px] rotate-45 transform cursor-pointer transition-all duration-300 ${isActive ? "border-success border-[1.5px] bg-[#1f2b27]" : "border-[1.5px] border-[#57586E] bg-[#252838]"} ${isHovered ? "border-success bg-success scale-150" : ""}`,
|
|
20
|
+
style: { boxShadow: markerBoxShadow },
|
|
21
|
+
children: isActive && /* @__PURE__ */ jsx("div", { className: "bg-success absolute top-1/2 left-1/2 h-1 w-1 -translate-x-1/2 -translate-y-1/2 transform" })
|
|
22
|
+
}
|
|
23
|
+
) }) }),
|
|
24
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-col items-center gap-1", children: /* @__PURE__ */ jsx(
|
|
25
|
+
"span",
|
|
26
|
+
{
|
|
27
|
+
className: `cursor-pointer text-[11px] font-medium transition-colors ${isActive ? "text-default" : "text-[#8C9290]"}`,
|
|
28
|
+
style: {
|
|
29
|
+
textShadow: isCurrent ? "0 0 20px rgba(255,255,255,0.1)" : "none"
|
|
30
|
+
},
|
|
31
|
+
children: step.name
|
|
32
|
+
}
|
|
33
|
+
) })
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
);
|
|
39
|
+
TimelineStepItem.displayName = "TimelineStepItem";
|
|
56
40
|
export {
|
|
57
41
|
TimelineStepItem
|
|
58
42
|
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "../../lib/cn.js";
|
|
3
|
+
import { Tooltip } from "../Tooltip/index.js";
|
|
4
|
+
const TimelineStepTooltip = ({
|
|
5
|
+
step,
|
|
6
|
+
className,
|
|
7
|
+
codeClassName = "text-success",
|
|
8
|
+
children
|
|
9
|
+
}) => /* @__PURE__ */ jsx(
|
|
10
|
+
Tooltip,
|
|
11
|
+
{
|
|
12
|
+
className: cn("border-border bg-primary max-w-100 rounded-lg border p-3 shadow-xl", className),
|
|
13
|
+
classNameArrow: "bg-primary fill-primary",
|
|
14
|
+
content: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
15
|
+
/* @__PURE__ */ jsxs("div", { className: "text-default mb-1 text-xs font-medium", children: [
|
|
16
|
+
step.name,
|
|
17
|
+
" ",
|
|
18
|
+
/* @__PURE__ */ jsxs("span", { className: `text-xs ${codeClassName}`, children: [
|
|
19
|
+
"(",
|
|
20
|
+
step.code,
|
|
21
|
+
")"
|
|
22
|
+
] })
|
|
23
|
+
] }),
|
|
24
|
+
/* @__PURE__ */ jsx("div", { className: "text-subdued text-2xs break-words", children: /* @__PURE__ */ jsx("span", { dangerouslySetInnerHTML: { __html: step.description ?? "" } }) })
|
|
25
|
+
] }),
|
|
26
|
+
children
|
|
27
|
+
}
|
|
28
|
+
);
|
|
29
|
+
export {
|
|
30
|
+
TimelineStepTooltip
|
|
31
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { BRANCH_BOTTOM_Y, BRANCH_TOP_Y } from "./timelineConstants.js";
|
|
2
|
+
const CHAMFER_RADIUS = 0;
|
|
3
|
+
const distance = (ax, ay, bx, by) => Math.hypot(bx - ax, by - ay);
|
|
4
|
+
const buildChamferedPath = (points, chamfer) => {
|
|
5
|
+
if (points.length < 2) return "";
|
|
6
|
+
const segments = [];
|
|
7
|
+
for (let i = 0; i < points.length; i++) {
|
|
8
|
+
const [cx, cy] = points[i];
|
|
9
|
+
const prev = points[i - 1];
|
|
10
|
+
const next = points[i + 1];
|
|
11
|
+
if (!prev || !next) {
|
|
12
|
+
if (i === 0) {
|
|
13
|
+
segments.push(`M ${cx} ${cy}`);
|
|
14
|
+
} else {
|
|
15
|
+
segments.push(`L ${cx} ${cy}`);
|
|
16
|
+
}
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
const [px, py] = prev;
|
|
20
|
+
const [nx, ny] = next;
|
|
21
|
+
const inLen = distance(px, py, cx, cy);
|
|
22
|
+
const outLen = distance(cx, cy, nx, ny);
|
|
23
|
+
const radius = Math.min(chamfer, inLen * 0.35, outLen * 0.35);
|
|
24
|
+
const inX = cx + (px - cx) / inLen * radius;
|
|
25
|
+
const inY = cy + (py - cy) / inLen * radius;
|
|
26
|
+
const outX = cx + (nx - cx) / outLen * radius;
|
|
27
|
+
const outY = cy + (ny - cy) / outLen * radius;
|
|
28
|
+
if (i === 0) {
|
|
29
|
+
segments.push(`M ${inX} ${inY}`);
|
|
30
|
+
} else {
|
|
31
|
+
segments.push(`L ${inX} ${inY}`);
|
|
32
|
+
}
|
|
33
|
+
segments.push(`L ${outX} ${outY}`);
|
|
34
|
+
}
|
|
35
|
+
return segments.join(" ");
|
|
36
|
+
};
|
|
37
|
+
const getBranchGeometry = (fromIndex, toIndex, totalSteps) => {
|
|
38
|
+
const segmentWidth = 100 / totalSteps;
|
|
39
|
+
const from = Math.min(fromIndex, toIndex);
|
|
40
|
+
const to = Math.max(fromIndex, toIndex);
|
|
41
|
+
const fromXPercent = (from + 0.5) * segmentWidth;
|
|
42
|
+
const toXPercent = (to + 0.5) * segmentWidth;
|
|
43
|
+
const midXPercent = (fromXPercent + toXPercent) / 2;
|
|
44
|
+
const topHalfWidth = Math.min((toXPercent - fromXPercent) * 0.36, 26);
|
|
45
|
+
const topLeftX = midXPercent - topHalfWidth;
|
|
46
|
+
const topRightX = midXPercent + topHalfWidth;
|
|
47
|
+
const outlinePath = buildChamferedPath(
|
|
48
|
+
[
|
|
49
|
+
[fromXPercent, BRANCH_BOTTOM_Y],
|
|
50
|
+
[topLeftX, BRANCH_TOP_Y],
|
|
51
|
+
[topRightX, BRANCH_TOP_Y],
|
|
52
|
+
[toXPercent, BRANCH_BOTTOM_Y]
|
|
53
|
+
],
|
|
54
|
+
CHAMFER_RADIUS
|
|
55
|
+
);
|
|
56
|
+
return {
|
|
57
|
+
fromXPercent,
|
|
58
|
+
toXPercent,
|
|
59
|
+
nodeXPercent: midXPercent,
|
|
60
|
+
outlinePath
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
export {
|
|
64
|
+
getBranchGeometry
|
|
65
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const buildCodeToIndexMap = (chainGraph) => {
|
|
2
|
+
const codeToIndex = /* @__PURE__ */ new Map();
|
|
3
|
+
chainGraph.forEach((node, index) => {
|
|
4
|
+
if (!codeToIndex.has(node.code)) {
|
|
5
|
+
codeToIndex.set(node.code, index);
|
|
6
|
+
}
|
|
7
|
+
});
|
|
8
|
+
return codeToIndex;
|
|
9
|
+
};
|
|
10
|
+
const mapChainGraphBranches = (branches, chainGraph) => {
|
|
11
|
+
const codeToIndex = buildCodeToIndexMap(chainGraph);
|
|
12
|
+
return (branches ?? []).flatMap((branch) => {
|
|
13
|
+
const fromIndex = codeToIndex.get(branch.from_code);
|
|
14
|
+
const toIndex = codeToIndex.get(branch.to_code);
|
|
15
|
+
if (fromIndex === void 0 || toIndex === void 0 || fromIndex === toIndex) {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
return [
|
|
19
|
+
{
|
|
20
|
+
fromIndex,
|
|
21
|
+
toIndex,
|
|
22
|
+
step: branch.node
|
|
23
|
+
}
|
|
24
|
+
];
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
export {
|
|
28
|
+
mapChainGraphBranches
|
|
29
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const TIMELINE_GREEN = "#4ac18e";
|
|
2
|
+
const TIMELINE_INACTIVE = "#3b3d4f";
|
|
3
|
+
const TIMELINE_BRANCH_ORANGE_START = "var(--bg-california)";
|
|
4
|
+
const TIMELINE_BRANCH_ORANGE_END = "var(--bg-international-orange)";
|
|
5
|
+
const BRANCH_HEIGHT_ABOVE = 75;
|
|
6
|
+
const TIMELINE_PADDING_TOP = 10;
|
|
7
|
+
const MARKER_CENTER_Y = TIMELINE_PADDING_TOP + 6.5;
|
|
8
|
+
const BRANCH_OFFSET_TOP = MARKER_CENTER_Y - BRANCH_HEIGHT_ABOVE;
|
|
9
|
+
const BRANCH_AREA_HEIGHT = BRANCH_HEIGHT_ABOVE;
|
|
10
|
+
const BRANCH_TOP_Y = 0;
|
|
11
|
+
const BRANCH_NODE_OFFSET_Y = -12;
|
|
12
|
+
const BRANCH_BOTTOM_Y = BRANCH_HEIGHT_ABOVE;
|
|
13
|
+
const BRANCH_LAYOUT_PADDING_TOP = BRANCH_HEIGHT_ABOVE;
|
|
14
|
+
const DETECTION_BYPASS_LABEL = "Detection Bypass";
|
|
15
|
+
export {
|
|
16
|
+
BRANCH_AREA_HEIGHT,
|
|
17
|
+
BRANCH_BOTTOM_Y,
|
|
18
|
+
BRANCH_HEIGHT_ABOVE,
|
|
19
|
+
BRANCH_LAYOUT_PADDING_TOP,
|
|
20
|
+
BRANCH_NODE_OFFSET_Y,
|
|
21
|
+
BRANCH_OFFSET_TOP,
|
|
22
|
+
BRANCH_TOP_Y,
|
|
23
|
+
DETECTION_BYPASS_LABEL,
|
|
24
|
+
MARKER_CENTER_Y,
|
|
25
|
+
TIMELINE_BRANCH_ORANGE_END,
|
|
26
|
+
TIMELINE_BRANCH_ORANGE_START,
|
|
27
|
+
TIMELINE_GREEN,
|
|
28
|
+
TIMELINE_INACTIVE,
|
|
29
|
+
TIMELINE_PADDING_TOP
|
|
30
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useState } from "react";
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
2
|
const calculateLineHeight = (linePositionPercent, mouseX, timelineWidth) => {
|
|
3
3
|
if (mouseX === null || timelineWidth === 0) return 24;
|
|
4
4
|
const linePositionPx = linePositionPercent / 100 * timelineWidth;
|
|
@@ -10,9 +10,20 @@ const calculateLineHeight = (linePositionPercent, mouseX, timelineWidth) => {
|
|
|
10
10
|
return 24 + (maxHeight - 24) * factor;
|
|
11
11
|
};
|
|
12
12
|
const useTimeline = () => {
|
|
13
|
+
const containerRef = useRef(null);
|
|
13
14
|
const [hoveredIndex, setHoveredIndex] = useState(null);
|
|
15
|
+
const [hoveredBranchIndex, setHoveredBranchIndex] = useState(null);
|
|
14
16
|
const [mouseX, setMouseX] = useState(null);
|
|
15
17
|
const [timelineWidth, setTimelineWidth] = useState(0);
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const element = containerRef.current;
|
|
20
|
+
if (!element) return;
|
|
21
|
+
const updateWidth = () => setTimelineWidth(element.getBoundingClientRect().width);
|
|
22
|
+
updateWidth();
|
|
23
|
+
const observer = new ResizeObserver(updateWidth);
|
|
24
|
+
observer.observe(element);
|
|
25
|
+
return () => observer.disconnect();
|
|
26
|
+
}, []);
|
|
16
27
|
const handleMouseMove = useCallback((e) => {
|
|
17
28
|
const rect = e.currentTarget.getBoundingClientRect();
|
|
18
29
|
setMouseX(e.clientX - rect.left);
|
|
@@ -20,10 +31,13 @@ const useTimeline = () => {
|
|
|
20
31
|
}, []);
|
|
21
32
|
const handleMouseLeave = useCallback(() => setMouseX(null), []);
|
|
22
33
|
return {
|
|
34
|
+
containerRef,
|
|
23
35
|
mouseX,
|
|
24
36
|
hoveredIndex,
|
|
37
|
+
hoveredBranchIndex,
|
|
25
38
|
timelineWidth,
|
|
26
39
|
setHoveredIndex,
|
|
40
|
+
setHoveredBranchIndex,
|
|
27
41
|
handleMouseMove,
|
|
28
42
|
handleMouseLeave
|
|
29
43
|
};
|