@samitouri / QOS-React / commits / 2e68dc76a4

[DevTools] Give a distinct color to the root (#34690)

Stacked on #34654. The root is special since it represents "Initial Paint" (or a "Transition" when an Activity is selected). This gives it a different color in the timeline as well as gives it an outline that's clickable. Hovering the timeline now shows "Initial Paint" or "Suspense". Also made the cursor a pointer to invite you to try to click things and some rounded corners. <img width="1219" height="420" alt="Screenshot 2025-10-02 at 1 26 38 PM" src="https://github.com/user-attachments/assets/12451f93-8917-4f3b-8f01-930129e5fc13" /> <img width="1217" height="419" alt="Screenshot 2025-10-02 at 1 26 54 PM" src="https://github.com/user-attachments/assets/02b5e94c-3fbe-488d-b0f2-225b73578608" /> <img width="1215" height="419" alt="Screenshot 2025-10-02 at 1 27 24 PM" src="https://github.com/user-attachments/assets/c24e8861-e74a-4ccc-8643-ee9d04bef43c" /> <img width="1216" height="419" alt="Screenshot 2025-10-02 at 1 27 10 PM" src="https://github.com/user-attachments/assets/d5cc2b62-fa64-41bf-b485-116b1cd67467" />

Sebastian Markbåge committed Oct 2, 2025 at 14:37 UTC 2e68dc76a41165d16b35d6eb2e7bf13aafed9aef
4 files changed +53 -20
packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.css
+10 -1
@@ -1,5 +1,12 @@
1 .SuspenseRectsContainer {
2 padding: .25rem;
3 + cursor: pointer;
4 + outline: 1px solid var(--color-component-name);
5 + border-radius: 0.25rem;
6 +}
7 +
8 +.SuspenseRectsContainer[data-highlighted='true'] {
9 + background: var(--color-dimmest);
10 }
11
12 .SuspenseRectsViewBox {
@@ -28,6 +35,8 @@
35 pointer-events: all;
36 outline-style: solid;
37 outline-width: 1px;
38 + border-radius: 0.125rem;
39 + cursor: pointer;
40 }
41
42 .SuspenseRectsScaledRect {
@@ -42,7 +51,7 @@
51
52 /* highlight this boundary */
53 .SuspenseRectsBoundary:hover:not(:has(.SuspenseRectsBoundary:hover)) > .SuspenseRectsRect, .SuspenseRectsBoundary[data-highlighted='true'] > .SuspenseRectsRect {
45 - background-color: var(--color-background-hover);
54 + background-color: var(--color-background-hover);
55 }
56
57 .SuspenseRectsRect[data-highlighted='true'] {
packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js
+7 -1
@@ -295,6 +295,7 @@ const ViewBox = createContext<Rect>((null: any));
295
296 function SuspenseRectsContainer(): React$Node {
297 const store = useContext(StoreContext);
298 + const {inspectedElementID} = useContext(TreeStateContext);
299 const treeDispatch = useContext(TreeDispatcherContext);
300 const suspenseTreeDispatch = useContext(SuspenseTreeDispatcherContext);
301 // TODO: This relies on a full re-render of all children when the Suspense tree changes.
@@ -329,8 +330,13 @@ function SuspenseRectsContainer(): React$Node {
330 });
331 }
332
333 + const isRootSelected = roots.includes(inspectedElementID);
334 +
335 return (
333 - <div className={styles.SuspenseRectsContainer} onClick={handleClick}>
336 + <div
337 + className={styles.SuspenseRectsContainer}
338 + onClick={handleClick}
339 + data-highlighted={isRootSelected}>
340 <ViewBox.Provider value={boundingBox}>
341 <div
342 className={styles.SuspenseRectsViewBox}
packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseScrubber.css
+6 -4
@@ -37,7 +37,7 @@
37 padding-right: 0;
38 }
39
40 -.SuspenseScrubberBead, .SuspenseScrubberBeadSelected {
40 +.SuspenseScrubberBead {
41 flex: 1;
42 height: 0.5rem;
43 background: var(--color-background-selected);
@@ -51,9 +51,11 @@
51 background: var(--color-background-selected);
52 }
53
54 +.SuspenseScrubberBeadTransition {
55 + background: var(--color-component-name);
56 +}
57 +
58 .SuspenseScrubberStepHighlight > .SuspenseScrubberBead,
55 -.SuspenseScrubberStepHighlight > .SuspenseScrubberBeadSelected,
56 -.SuspenseScrubberStep:hover > .SuspenseScrubberBead,
57 -.SuspenseScrubberStep:hover > .SuspenseScrubberBeadSelected {
59 +.SuspenseScrubberStep:hover > .SuspenseScrubberBead {
60 height: 0.75rem;
61 }
packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseScrubber.js
+30 -14
@@ -14,6 +14,8 @@ import {useRef} from 'react';
14
15 import styles from './SuspenseScrubber.css';
16
17 +import Tooltip from '../Components/reach-ui/tooltip';
18 +
19 export default function SuspenseScrubber({
20 min,
21 max,
@@ -53,24 +55,38 @@ export default function SuspenseScrubber({
55 const steps = [];
56 for (let index = min; index <= max; index++) {
57 steps.push(
56 - <div
58 + <Tooltip
59 key={index}
58 - className={
59 - styles.SuspenseScrubberStep +
60 - (highlight === index
61 - ? ' ' + styles.SuspenseScrubberStepHighlight
62 - : '')
63 - }
64 - onPointerDown={handlePress.bind(null, index)}
65 - onMouseEnter={onHoverSegment.bind(null, index)}>
60 + label={
61 + index === min
62 + ? // The first step in the timeline is always a Transition (Initial Paint).
63 + // TODO: Support multiple environments.
64 + 'Initial Paint'
65 + : // TODO: Consider adding the name of this specific boundary if this step has only one.
66 + 'Suspense'
67 + }>
68 <div
69 className={
68 - index <= value
69 - ? styles.SuspenseScrubberBeadSelected
70 - : styles.SuspenseScrubberBead
70 + styles.SuspenseScrubberStep +
71 + (highlight === index
72 + ? ' ' + styles.SuspenseScrubberStepHighlight
73 + : '')
74 }
72 - />
73 - </div>,
75 + onPointerDown={handlePress.bind(null, index)}
76 + onMouseEnter={onHoverSegment.bind(null, index)}>
77 + <div
78 + className={
79 + styles.SuspenseScrubberBead +
80 + (index === min
81 + ? // The first step in the timeline is always a Transition (Initial Paint).
82 + // TODO: Support multiple environments.
83 + ' ' + styles.SuspenseScrubberBeadTransition
84 + : '') +
85 + (index <= value ? ' ' + styles.SuspenseScrubberBeadSelected : '')
86 + }
87 + />
88 + </div>
89 + </Tooltip>,
90 );
91 }
92