@samitouri / QOS-React-1 / commits / 36c63d4f9c

[DevTools] Layout for Suspense tab (#34042)

Sebastian "Sebbie" Silbermann committed Jul 30, 2025 at 07:12 UTC 36c63d4f9c987899d075fe9befbd25edc40bc75c
3 files changed +620 -4
packages/react-devtools-shared/src/devtools/views/ButtonIcon.js
+63 -1
@@ -34,6 +34,12 @@ export type IconType =
34 | 'save'
35 | 'search'
36 | 'settings'
37 + | 'panel-left-close'
38 + | 'panel-left-open'
39 + | 'panel-right-close'
40 + | 'panel-right-open'
41 + | 'panel-bottom-open'
42 + | 'panel-bottom-close'
43 | 'error'
44 | 'suspend'
45 | 'undo'
@@ -46,8 +52,10 @@ type Props = {
52 type: IconType,
53 };
54
55 +const materialIconsViewBox = '0 -960 960 960';
56 export default function ButtonIcon({className = '', type}: Props): React.Node {
57 let pathData = null;
58 + let viewBox = '0 0 24 24';
59 switch (type) {
60 case 'add':
61 pathData = PATH_ADD;
@@ -121,6 +129,30 @@ export default function ButtonIcon({className = '', type}: Props): React.Node {
129 case 'error':
130 pathData = PATH_ERROR;
131 break;
132 + case 'panel-left-close':
133 + pathData = PATH_MATERIAL_PANEL_LEFT_CLOSE;
134 + viewBox = materialIconsViewBox;
135 + break;
136 + case 'panel-left-open':
137 + pathData = PATH_MATERIAL_PANEL_LEFT_OPEN;
138 + viewBox = materialIconsViewBox;
139 + break;
140 + case 'panel-right-close':
141 + pathData = PATH_MATERIAL_PANEL_RIGHT_CLOSE;
142 + viewBox = materialIconsViewBox;
143 + break;
144 + case 'panel-right-open':
145 + pathData = PATH_MATERIAL_PANEL_RIGHT_OPEN;
146 + viewBox = materialIconsViewBox;
147 + break;
148 + case 'panel-bottom-open':
149 + pathData = PATH_MATERIAL_PANEL_BOTTOM_OPEN;
150 + viewBox = materialIconsViewBox;
151 + break;
152 + case 'panel-bottom-close':
153 + pathData = PATH_MATERIAL_PANEL_BOTTOM_CLOSE;
154 + viewBox = materialIconsViewBox;
155 + break;
156 case 'suspend':
157 pathData = PATH_SUSPEND;
158 break;
@@ -147,7 +179,7 @@ export default function ButtonIcon({className = '', type}: Props): React.Node {
179 className={`${styles.ButtonIcon} ${className}`}
180 width="24"
181 height="24"
150 - viewBox="0 0 24 24">
182 + viewBox={viewBox}>
183 <path d="M0 0h24v24H0z" fill="none" />
184 {typeof pathData === 'string' ? (
185 <path fill="currentColor" d={pathData} />
@@ -276,3 +308,33 @@ const PATH_VIEW_SOURCE = `
308 const PATH_EDITOR = `
309 M7 5h10v2h2V3c0-1.1-.9-1.99-2-1.99L7 1c-1.1 0-2 .9-2 2v4h2V5zm8.41 11.59L20 12l-4.59-4.59L14 8.83 17.17 12 14 15.17l1.41 1.42zM10 15.17L6.83 12 10 8.83 8.59 7.41 4 12l4.59 4.59L10 15.17zM17 19H7v-2H5v4c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2v-4h-2v2z
310 `;
311 +
312 +// Source: Material Design Icons left_panel_close
313 +const PATH_MATERIAL_PANEL_LEFT_CLOSE = `
314 + M648-324v-312L480-480l168 156ZM211-144q-27.64 0-47.32-19.68T144-211v-538q0-27.64 19.68-47.32T211-816h538q27.64 0 47.32 19.68T816-749v538q0 27.64-19.68 47.32T749-144H211Zm125-72v-528H216v528h120Zm72 0h336v-528H408v528Zm-72 0H216h120Z
315 +`;
316 +
317 +// Source: Material Design Icons left_panel_open
318 +const PATH_MATERIAL_PANEL_LEFT_OPEN = `
319 + M504-595v230q0 12.25 10.5 16.62Q525-344 534-352l110-102q11-11.18 11-26.09T644-506L534-608q-8.82-8-19.41-3.5T504-595ZM211-144q-27.64 0-47.32-19.68T144-211v-538q0-27.64 19.68-47.32T211-816h538q27.64 0 47.32 19.68T816-749v538q0 27.64-19.68 47.32T749-144H211Zm125-72v-528H216v528h120Zm72 0h336v-528H408v528Zm-72 0H216h120Z
320 +`;
321 +
322 +// Source: Material Design Icons right_panel_close
323 +const PATH_MATERIAL_PANEL_RIGHT_CLOSE = `
324 + M312-365q0 12.25 10.5 16.62Q333-344 342-352l110-102q11-11.18 11-26.09T452-506L342-608q-8.82-8-19.41-3.5T312-595v230ZM211-144q-27.64 0-47.32-19.68T144-211v-538q0-27.64 19.68-47.32T211-816h538q27.64 0 47.32 19.68T816-749v538q0 27.64-19.68 47.32T749-144H211Zm413-72h120v-528H624v528Zm-72 0v-528H216v528h336Zm72 0h120-120Z
325 +`;
326 +
327 +// Source: Material Design Icons right_panel_open
328 +const PATH_MATERIAL_PANEL_RIGHT_OPEN = `
329 + M456-365v-230q0-12.25-10.5-16.63Q435-616 426-608L316-506q-11 11.18-11 26.09T316-454l110 102q8.82 8 19.41 3.5T456-365ZM211-144q-27.64 0-47.32-19.68T144-211v-538q0-27.64 19.68-47.32T211-816h538q27.64 0 47.32 19.68T816-749v538q0 27.64-19.68 47.32T749-144H211Zm413-72h120v-528H624v528Zm-72 0v-528H216v528h336Zm72 0h120-120Z
330 +`;
331 +
332 +// Source: Material Design Icons bottom_panel_open
333 +const PATH_MATERIAL_PANEL_BOTTOM_OPEN = `
334 + M365-504h230q12.25 0 16.63-10.5Q616-525 608-534L506-644q-11.18-11-26.09-11T454-644L352-534q-8 8.82-3.5 19.41T365-504ZM211-144q-27.64 0-47.32-19.68T144-211v-538q0-27.64 19.68-47.32T211-816h538q27.64 0 47.32 19.68T816-749v538q0 27.64-19.68 47.32T749-144H211Zm5-192v120h528v-120H216Zm0-72h528v-336H216v336Zm0 72v120-120Z
335 +`;
336 +
337 +// Source: Material Design Icons bottom_panel_close
338 +const PATH_MATERIAL_PANEL_BOTTOM_CLOSE = `
339 + m506-508 102-110q8-8.82 3.5-19.41T595-648H365q-12.25 0-16.62 10.5Q344-627 352-618l102 110q11.18 11 26.09 11T506-508Zm243-308q27.64 0 47.32 19.68T816-749v538q0 27.64-19.68 47.32T749-144H211q-27.64 0-47.32-19.68T144-211v-538q0-27.64 19.68-47.32T211-816h538ZM216-336v120h528v-120H216Zm528-72v-336H216v336h528Zm-528 72v120-120Z
340 +`;
packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTab.css new
+117
@@ -0,0 +1,117 @@
1 +.SuspenseTab {
2 + position: relative;
3 + width: 100%;
4 + height: 100%;
5 + display: flex;
6 + flex-direction: row;
7 + background-color: var(--color-background);
8 + color: var(--color-text);
9 + font-family: var(--font-family-sans);
10 +}
11 +
12 +.SuspenseTab, .SuspenseTab * {
13 + box-sizing: border-box;
14 + -webkit-font-smoothing: var(--font-smoothing);
15 +}
16 +
17 +.TreeWrapper {
18 + flex: 1 1 var(--horizontal-resize-tree-percentage);
19 + display: flex;
20 + flex-direction: row;
21 + overflow: auto;
22 + border-top: 1px solid var(--color-border);
23 +}
24 +
25 +.InspectedElementWrapper {
26 + flex: 1 1 calc(100% - var(--horizontal-resize-tree-percentage));
27 + overflow-x: hidden;
28 + overflow-y: auto;
29 +}
30 +
31 +.ResizeBarWrapper {
32 + flex: 0 0 0px;
33 + position: relative;
34 +}
35 +
36 +.ResizeBar {
37 + position: absolute;
38 + /*
39 + * moving the bar out of its bounding box might cause its hitbox to overlap
40 + * with another scrollbar creating disorienting UX where you both resize and scroll
41 + * at the same time.
42 + * If you adjust this value, double check that starting resize right on this edge
43 + * doesn't also cause scroll
44 + */
45 + left: 1px;
46 + width: 5px;
47 + height: 100%;
48 + cursor: ew-resize;
49 +}
50 +
51 +.TreeView footer {
52 + display: none;
53 +}
54 +
55 +
56 +
57 +@container devtools (width < 600px) {
58 + .SuspenseTab {
59 + flex-direction: column;
60 + }
61 +
62 + .TreeWrapper {
63 + border-top: 1px solid var(--color-border);
64 + flex: 1 0 var(--vertical-resize-tree-percentage);
65 + }
66 +
67 + .InspectedElementWrapper {
68 + flex: 1 1 50%;
69 + }
70 +
71 + .TreeWrapper + .ResizeBarWrapper .ResizeBar {
72 + top: 1px;
73 + left: 0;
74 + width: 100%;
75 + height: 5px;
76 + cursor: ns-resize;
77 + }
78 +
79 + .TreeView footer {
80 + display: flex;
81 + justify-content: end;
82 + }
83 +
84 + .ToggleInspectedElement[data-orientation="horizontal"] {
85 + display: none;
86 + }
87 +}
88 +
89 +.TreeList {
90 + flex: 0 0 var(--horizontal-resize-tree-list-percentage);
91 + border-right: 1px solid var(--color-border);
92 + padding: 0.25rem
93 +}
94 +
95 +.TreeView {
96 + flex: 1 1 35%;
97 + display: flex;
98 + flex-direction: column;
99 +}
100 +
101 +
102 +
103 +.Rects {
104 + border-top: 1px solid var(--color-border);
105 + padding: 0.25rem;
106 + flex-grow: 1;
107 +}
108 +
109 +.TimelineWrapper {
110 + padding: 0.25rem;
111 + display: flex;
112 + flex-direction: row;
113 +}
114 +
115 +.Timeline {
116 + flex-grow: 1;
117 +}
packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTab.js
+440 -3
@@ -1,8 +1,445 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 import * as React from 'react';
11 +import {useEffect, useLayoutEffect, useReducer, useRef} from 'react';
12 +
13 +import {
14 + localStorageGetItem,
15 + localStorageSetItem,
16 +} from 'react-devtools-shared/src/storage';
17 +import ButtonIcon, {type IconType} from '../ButtonIcon';
18 +import InspectedElementErrorBoundary from '../Components/InspectedElementErrorBoundary';
19 +import InspectedElement from '../Components/InspectedElement';
20 import portaledContent from '../portaledContent';
21 +import styles from './SuspenseTab.css';
22 +import Button from '../Button';
23 +
24 +type Orientation = 'horizontal' | 'vertical';
25 +
26 +type LayoutActionType =
27 + | 'ACTION_SET_TREE_LIST_TOGGLE'
28 + | 'ACTION_SET_TREE_LIST_HORIZONTAL_FRACTION'
29 + | 'ACTION_SET_INSPECTED_ELEMENT_TOGGLE'
30 + | 'ACTION_SET_INSPECTED_ELEMENT_HORIZONTAL_FRACTION'
31 + | 'ACTION_SET_INSPECTED_ELEMENT_VERTICAL_FRACTION';
32 +type LayoutAction = {
33 + type: LayoutActionType,
34 + payload: any,
35 +};
36 +
37 +type LayoutState = {
38 + treeListHidden: boolean,
39 + treeListHorizontalFraction: number,
40 + inspectedElementHidden: boolean,
41 + inspectedElementHorizontalFraction: number,
42 + inspectedElementVerticalFraction: number,
43 +};
44 +type LayoutDispatch = (action: LayoutAction) => void;
45 +
46 +function SuspenseTreeList() {
47 + return <div>tree list</div>;
48 +}
49 +
50 +function SuspenseTimeline() {
51 + return <div className={styles.Timeline}>timeline</div>;
52 +}
53 +
54 +function SuspenseRects() {
55 + return <div>rects</div>;
56 +}
57 +
58 +function ToggleTreeList({
59 + dispatch,
60 + state,
61 +}: {
62 + dispatch: LayoutDispatch,
63 + state: LayoutState,
64 +}) {
65 + return (
66 + <Button
67 + onClick={() =>
68 + dispatch({
69 + type: 'ACTION_SET_TREE_LIST_TOGGLE',
70 + payload: null,
71 + })
72 + }
73 + title={state.treeListHidden ? 'Show Tree List' : 'Hide Tree List'}>
74 + <ButtonIcon
75 + type={state.treeListHidden ? 'panel-left-open' : 'panel-left-close'}
76 + />
77 + </Button>
78 + );
79 +}
80 +
81 +function ToggleInspectedElement({
82 + dispatch,
83 + state,
84 + orientation,
85 +}: {
86 + dispatch: LayoutDispatch,
87 + state: LayoutState,
88 + orientation: 'horizontal' | 'vertical',
89 +}) {
90 + let iconType: IconType;
91 + if (orientation === 'horizontal') {
92 + iconType = state.inspectedElementHidden
93 + ? 'panel-right-open'
94 + : 'panel-right-close';
95 + } else {
96 + iconType = state.inspectedElementHidden
97 + ? 'panel-bottom-open'
98 + : 'panel-bottom-close';
99 + }
100 + return (
101 + <Button
102 + className={styles.ToggleInspectedElement}
103 + data-orientation={orientation}
104 + onClick={() =>
105 + dispatch({
106 + type: 'ACTION_SET_INSPECTED_ELEMENT_TOGGLE',
107 + payload: null,
108 + })
109 + }
110 + title={
111 + state.inspectedElementHidden
112 + ? 'Show Inspected Element'
113 + : 'Hide Inspected Element'
114 + }>
115 + <ButtonIcon type={iconType} />
116 + </Button>
117 + );
118 +}
119 +
120 +function SuspenseTab(_: {}) {
121 + const [state, dispatch] = useReducer<LayoutState, null, LayoutAction>(
122 + layoutReducer,
123 + null,
124 + initLayoutState,
125 + );
126 +
127 + const wrapperTreeRef = useRef<null | HTMLElement>(null);
128 + const resizeTreeRef = useRef<null | HTMLElement>(null);
129 + const resizeTreeListRef = useRef<null | HTMLElement>(null);
130 +
131 + // TODO: We'll show the recently inspected element in this tab when it should probably
132 + // switch to the nearest Suspense boundary when we switch into this tab.
133 +
134 + const {
135 + inspectedElementHidden,
136 + inspectedElementHorizontalFraction,
137 + inspectedElementVerticalFraction,
138 + treeListHidden,
139 + treeListHorizontalFraction,
140 + } = state;
141 +
142 + useLayoutEffect(() => {
143 + const wrapperElement = wrapperTreeRef.current;
144 +
145 + setResizeCSSVariable(
146 + wrapperElement,
147 + 'tree',
148 + 'horizontal',
149 + inspectedElementHorizontalFraction * 100,
150 + );
151 + setResizeCSSVariable(
152 + wrapperElement,
153 + 'tree',
154 + 'vertical',
155 + inspectedElementVerticalFraction * 100,
156 + );
157 +
158 + const resizeTreeListElement = resizeTreeListRef.current;
159 + setResizeCSSVariable(
160 + resizeTreeListElement,
161 + 'tree-list',
162 + 'horizontal',
163 + treeListHorizontalFraction * 100,
164 + );
165 + }, []);
166 + useEffect(() => {
167 + const timeoutID = setTimeout(() => {
168 + localStorageSetItem(
169 + LOCAL_STORAGE_KEY,
170 + JSON.stringify({
171 + inspectedElementHidden,
172 + inspectedElementHorizontalFraction,
173 + inspectedElementVerticalFraction,
174 + treeListHidden,
175 + treeListHorizontalFraction,
176 + }),
177 + );
178 + }, 500);
179 +
180 + return () => clearTimeout(timeoutID);
181 + }, [
182 + inspectedElementHidden,
183 + inspectedElementHorizontalFraction,
184 + inspectedElementVerticalFraction,
185 + treeListHidden,
186 + treeListHorizontalFraction,
187 + ]);
188 +
189 + const onResizeStart = (event: SyntheticPointerEvent<HTMLElement>) => {
190 + const element = event.currentTarget;
191 + element.setPointerCapture(event.pointerId);
192 + };
193 +
194 + const onResizeEnd = (event: SyntheticPointerEvent<HTMLElement>) => {
195 + const element = event.currentTarget;
196 + element.releasePointerCapture(event.pointerId);
197 + };
198 +
199 + const onResizeTree = (event: SyntheticPointerEvent<HTMLElement>) => {
200 + const element = event.currentTarget;
201 + const isResizing = element.hasPointerCapture(event.pointerId);
202 + if (!isResizing) {
203 + return;
204 + }
205 +
206 + const resizeElement = resizeTreeRef.current;
207 + const wrapperElement = wrapperTreeRef.current;
208 +
209 + if (wrapperElement === null || resizeElement === null) {
210 + return;
211 + }
212 +
213 + event.preventDefault();
214 +
215 + const orientation = getTreeOrientation(wrapperElement);
216 +
217 + const {height, width, left, top} = wrapperElement.getBoundingClientRect();
218 +
219 + const currentMousePosition =
220 + orientation === 'horizontal' ? event.clientX - left : event.clientY - top;
221 +
222 + const boundaryMin = MINIMUM_TREE_SIZE;
223 + const boundaryMax =
224 + orientation === 'horizontal'
225 + ? width - MINIMUM_TREE_SIZE
226 + : height - MINIMUM_TREE_SIZE;
227 +
228 + const isMousePositionInBounds =
229 + currentMousePosition > boundaryMin && currentMousePosition < boundaryMax;
230 +
231 + if (isMousePositionInBounds) {
232 + const resizedElementDimension =
233 + orientation === 'horizontal' ? width : height;
234 + const actionType =
235 + orientation === 'horizontal'
236 + ? 'ACTION_SET_INSPECTED_ELEMENT_HORIZONTAL_FRACTION'
237 + : 'ACTION_SET_INSPECTED_ELEMENT_VERTICAL_FRACTION';
238 + const fraction = currentMousePosition / resizedElementDimension;
239 + const percentage = fraction * 100;
240 +
241 + setResizeCSSVariable(wrapperElement, 'tree', orientation, percentage);
242 +
243 + dispatch({
244 + type: actionType,
245 + payload: fraction,
246 + });
247 + }
248 + };
249 +
250 + const onResizeTreeList = (event: SyntheticPointerEvent<HTMLElement>) => {
251 + const element = event.currentTarget;
252 + const isResizing = element.hasPointerCapture(event.pointerId);
253 + if (!isResizing) {
254 + return;
255 + }
256 +
257 + const resizeElement = resizeTreeListRef.current;
258 + const wrapperElement = resizeTreeRef.current;
259 +
260 + if (wrapperElement === null || resizeElement === null) {
261 + return;
262 + }
263 +
264 + event.preventDefault();
265 +
266 + const orientation = 'horizontal';
267 +
268 + const {height, width, left, top} = wrapperElement.getBoundingClientRect();
269 +
270 + const currentMousePosition =
271 + orientation === 'horizontal' ? event.clientX - left : event.clientY - top;
272 +
273 + const boundaryMin = MINIMUM_TREE_LIST_SIZE;
274 + const boundaryMax =
275 + orientation === 'horizontal'
276 + ? width - MINIMUM_TREE_LIST_SIZE
277 + : height - MINIMUM_TREE_LIST_SIZE;
278 +
279 + const isMousePositionInBounds =
280 + currentMousePosition > boundaryMin && currentMousePosition < boundaryMax;
281 +
282 + if (isMousePositionInBounds) {
283 + const resizedElementDimension =
284 + orientation === 'horizontal' ? width : height;
285 + const actionType = 'ACTION_SET_TREE_LIST_HORIZONTAL_FRACTION';
286 + const percentage = (currentMousePosition / resizedElementDimension) * 100;
287 +
288 + setResizeCSSVariable(resizeElement, 'tree-list', orientation, percentage);
289 +
290 + dispatch({
291 + type: actionType,
292 + payload: currentMousePosition / resizedElementDimension,
293 + });
294 + }
295 + };
296 +
297 + return (
298 + <div className={styles.SuspenseTab} ref={wrapperTreeRef}>
299 + <div className={styles.TreeWrapper} ref={resizeTreeRef}>
300 + <div
301 + className={styles.TreeList}
302 + hidden={treeListHidden}
303 + ref={resizeTreeListRef}>
304 + <SuspenseTreeList />
305 + </div>
306 + <div className={styles.ResizeBarWrapper}>
307 + <div
308 + onPointerDown={onResizeStart}
309 + onPointerMove={onResizeTreeList}
310 + onPointerUp={onResizeEnd}
311 + className={styles.ResizeBar}
312 + />
313 + </div>
314 + <div className={styles.TreeView}>
315 + <div className={styles.TimelineWrapper}>
316 + <ToggleTreeList dispatch={dispatch} state={state} />
317 + <SuspenseTimeline />
318 + <ToggleInspectedElement
319 + dispatch={dispatch}
320 + state={state}
321 + orientation="horizontal"
322 + />
323 + </div>
324 + <div className={styles.Rects}>
325 + <SuspenseRects />
326 + </div>
327 + <footer>
328 + <ToggleInspectedElement
329 + dispatch={dispatch}
330 + state={state}
331 + orientation="vertical"
332 + />
333 + </footer>
334 + </div>
335 + </div>
336 + <div className={styles.ResizeBarWrapper}>
337 + <div
338 + onPointerDown={onResizeStart}
339 + onPointerMove={onResizeTree}
340 + onPointerUp={onResizeEnd}
341 + className={styles.ResizeBar}
342 + />
343 + </div>
344 + <div
345 + className={styles.InspectedElementWrapper}
346 + hidden={inspectedElementHidden}>
347 + <InspectedElementErrorBoundary>
348 + <InspectedElement />
349 + </InspectedElementErrorBoundary>
350 + </div>
351 + </div>
352 + );
353 +}
354 +
355 +const LOCAL_STORAGE_KEY = 'React::DevTools::SuspenseTab::layout';
356 +const VERTICAL_TREE_MODE_MAX_WIDTH = 600;
357 +const MINIMUM_TREE_SIZE = 100;
358 +const MINIMUM_TREE_LIST_SIZE = 100;
359 +
360 +function layoutReducer(state: LayoutState, action: LayoutAction): LayoutState {
361 + switch (action.type) {
362 + case 'ACTION_SET_TREE_LIST_TOGGLE':
363 + return {
364 + ...state,
365 + treeListHidden: !state.treeListHidden,
366 + };
367 + case 'ACTION_SET_TREE_LIST_HORIZONTAL_FRACTION':
368 + return {
369 + ...state,
370 + treeListHorizontalFraction: action.payload,
371 + };
372 + case 'ACTION_SET_INSPECTED_ELEMENT_TOGGLE':
373 + return {
374 + ...state,
375 + inspectedElementHidden: !state.inspectedElementHidden,
376 + };
377 + case 'ACTION_SET_INSPECTED_ELEMENT_HORIZONTAL_FRACTION':
378 + return {
379 + ...state,
380 + inspectedElementHorizontalFraction: action.payload,
381 + };
382 + case 'ACTION_SET_INSPECTED_ELEMENT_VERTICAL_FRACTION':
383 + return {
384 + ...state,
385 + inspectedElementVerticalFraction: action.payload,
386 + };
387 + default:
388 + return state;
389 + }
390 +}
391 +
392 +function initLayoutState(): LayoutState {
393 + let inspectedElementHidden = false;
394 + let inspectedElementHorizontalFraction = 0.65;
395 + let inspectedElementVerticalFraction = 0.5;
396 + let treeListHidden = false;
397 + let treeListHorizontalFraction = 0.35;
398 +
399 + try {
400 + let data = localStorageGetItem(LOCAL_STORAGE_KEY);
401 + if (data != null) {
402 + data = JSON.parse(data);
403 + inspectedElementHidden = data.inspectedElementHidden;
404 + inspectedElementHorizontalFraction =
405 + data.inspectedElementHorizontalFraction;
406 + inspectedElementVerticalFraction = data.inspectedElementVerticalFraction;
407 + treeListHidden = data.treeListHidden;
408 + treeListHorizontalFraction = data.treeListHorizontalFraction;
409 + }
410 + } catch (error) {}
411 +
412 + return {
413 + inspectedElementHidden,
414 + inspectedElementHorizontalFraction,
415 + inspectedElementVerticalFraction,
416 + treeListHidden,
417 + treeListHorizontalFraction,
418 + };
419 +}
420 +
421 +function getTreeOrientation(
422 + wrapperElement: null | HTMLElement,
423 +): null | Orientation {
424 + if (wrapperElement != null) {
425 + const {width} = wrapperElement.getBoundingClientRect();
426 + return width > VERTICAL_TREE_MODE_MAX_WIDTH ? 'horizontal' : 'vertical';
427 + }
428 + return null;
429 +}
430
4 -function SuspenseTab() {
5 - return 'Under construction';
431 +function setResizeCSSVariable(
432 + resizeElement: null | HTMLElement,
433 + name: 'tree' | 'tree-list',
434 + orientation: null | Orientation,
435 + percentage: number,
436 +): void {
437 + if (resizeElement !== null && orientation !== null) {
438 + resizeElement.style.setProperty(
439 + `--${orientation}-resize-${name}-percentage`,
440 + `${percentage}%`,
441 + );
442 + }
443 }
444
8 -export default (portaledContent(SuspenseTab): React.ComponentType<{}>);
445 +export default (portaledContent(SuspenseTab): React$ComponentType<{}>);