[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<{}>);