Added new tabs UI and theme/display-density preferences
Brian Vaughn committed
Feb 12, 2019 at 20:41 UTC
d203ebf1655e9bbaa039ffc34855bbe3396dde35
24 files changed
+684
-79
package.json
+1
@@ -1,4 +1,5 @@
1
{
2
+ "version": "4.0.0",
3
"repository": "bvaughn/react-devtools-experimental",
4
"license": "BSD-3-Clause",
5
"private": true,
shells/browser/shared/src/panel.js
+7
-6
@@ -3,7 +3,7 @@
3
import { createElement } from 'react';
4
import { createRoot, flushSync } from 'react-dom';
5
import Bridge from 'src/bridge';
6
-import Elements from 'src/devtools/views/Elements';
6
+import DevTools from 'src/devtools/views/DevTools';
7
import inject from './inject';
8
9
const IS_CHROME = navigator.userAgent.indexOf('Firefox') < 0;
@@ -16,7 +16,7 @@ if (IS_CHROME) {
16
17
// chrome.devtools.panels added in Chrome 18.
18
// chrome.devtools.panels.themeName added in Chrome 54.
19
- themeName = chrome.devtools.panels.themeName === 'dark' ? 'Dark' : 'Default';
19
+ themeName = chrome.devtools.panels.themeName === 'dark' ? 'dark' : 'light';
20
} else {
21
browserName = 'Firefox';
22
@@ -25,10 +25,10 @@ if (IS_CHROME) {
25
if (chrome.devtools && chrome.devtools.panels) {
26
switch (chrome.devtools.panels.themeName) {
27
case 'dark':
28
- themeName = 'Dark';
28
+ themeName = 'dark';
29
break;
30
default:
31
- themeName = 'Light';
31
+ themeName = 'light';
32
break;
33
}
34
}
@@ -63,10 +63,11 @@ function injectAndInit() {
63
64
const root = createRoot(container);
65
root.render(
66
- createElement(Elements, {
66
+ createElement(DevTools, {
67
bridge,
68
browserName,
69
- themeName,
69
+ browserTheme: themeName,
70
+ showTabBar: false,
71
})
72
);
73
shells/browser/shared/webpack.config.js
+14
-3
@@ -1,9 +1,13 @@
1
const { readFileSync } = require('fs');
2
const { resolve } = require('path');
3
-const webpack = require('webpack');
3
+const { DefinePlugin } = require('webpack');
4
5
const __DEV__ = process.env.NODE_ENV !== 'production';
6
7
+const DEVTOOLS_VERSION = JSON.parse(
8
+ readFileSync(resolve(__dirname, '../../../package.json'))
9
+).version;
10
+
11
module.exports = {
12
mode: __DEV__ ? 'development' : 'production',
13
devtool: __DEV__ ? 'cheap-module-eval-source-map' : false,
@@ -24,12 +28,19 @@ module.exports = {
28
},
29
},
30
plugins: __DEV__
27
- ? []
31
+ ? [
32
+ new DefinePlugin({
33
+ 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
34
+ }),
35
+ ]
36
: [
37
// Ensure we get production React
30
- new webpack.DefinePlugin({
38
+ new DefinePlugin({
39
'process.env.NODE_ENV': '"production"',
40
}),
41
+ new DefinePlugin({
42
+ 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
43
+ }),
44
],
45
module: {
46
rules: [
shells/dev/index.html
-1
@@ -10,7 +10,6 @@
10
border: none;
11
}
12
#devtools {
13
- display: flex;
13
height: 400px;
14
max-height: 50%;
15
overflow: hidden;
shells/dev/src/devtools.js
+4
-3
@@ -6,7 +6,7 @@ import { createRoot } from 'react-dom';
6
import Bridge from 'src/bridge';
7
import { installHook } from 'src/hook';
8
import { initDevTools } from 'src/devtools';
9
-import Elements from 'src/devtools/views/Elements';
9
+import DevTools from 'src/devtools/views/DevTools';
10
11
const iframe = ((document.getElementById('target'): any): HTMLIFrameElement);
12
@@ -56,10 +56,11 @@ inject('./build/App.js', () => {
56
const root = createRoot(container);
57
const batch = root.createBatch();
58
batch.render(
59
- createElement(Elements, {
59
+ createElement(DevTools, {
60
bridge,
61
browserName: 'Chrome',
62
- themeName: 'light',
62
+ browserTheme: 'light',
63
+ showTabBar: true,
64
})
65
);
66
batch.then(() => {
shells/dev/webpack.config.js
+10
@@ -1,5 +1,10 @@
1
const { readFileSync } = require('fs');
2
const { resolve } = require('path');
3
+const { DefinePlugin } = require('webpack');
4
+
5
+const DEVTOOLS_VERSION = JSON.parse(
6
+ readFileSync(resolve(__dirname, '../../package.json'))
7
+).version;
8
9
// TODO Share Webpack configs like alias
10
@@ -20,6 +25,11 @@ module.exports = {
25
src: resolve(__dirname, '../../src'),
26
},
27
},
28
+ plugins: [
29
+ new DefinePlugin({
30
+ 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
31
+ }),
32
+ ],
33
module: {
34
rules: [
35
{
src/devtools/views/DevTools.js
new
+64
@@ -0,0 +1,64 @@
1
+// @flow
2
+
3
+import React, { useMemo, useState } from 'react';
4
+import Store from '../store';
5
+import { BridgeContext, StoreContext } from './context';
6
+import Elements from './Elements';
7
+import Profiler from './Profiler';
8
+import Settings from './Settings';
9
+import TabBar from './TabBar';
10
+import { SettingsContextController } from './SettingsContext';
11
+import { TreeContextController } from './TreeContext';
12
+
13
+import './root.css';
14
+
15
+import type { Bridge } from '../../types';
16
+
17
+export type TabID = 'elements' | 'profiler' | 'settings';
18
+export type BrowserTheme = 'dark' | 'light';
19
+
20
+export type Props = {|
21
+ bridge: Bridge,
22
+ browserName: string,
23
+ defaultTab?: TabID,
24
+ browserTheme: BrowserTheme,
25
+ showTabBar?: boolean,
26
+|};
27
+
28
+export default function DevTools({
29
+ bridge,
30
+ browserName,
31
+ defaultTab = 'elements',
32
+ browserTheme = 'light',
33
+ showTabBar = false,
34
+}: Props) {
35
+ const store = useMemo<Store>(() => new Store(bridge), []);
36
+ const [tab, setTab] = useState(defaultTab);
37
+
38
+ let tabElement;
39
+ switch (tab) {
40
+ case 'profiler':
41
+ tabElement = <Profiler />;
42
+ break;
43
+ case 'settings':
44
+ tabElement = <Settings />;
45
+ break;
46
+ case 'elements':
47
+ default:
48
+ tabElement = <Elements />;
49
+ break;
50
+ }
51
+
52
+ return (
53
+ <BridgeContext.Provider value={bridge}>
54
+ <StoreContext.Provider value={store}>
55
+ <SettingsContextController browserTheme={browserTheme}>
56
+ <TreeContextController>
57
+ {showTabBar && <TabBar currentTab={tab} selectTab={setTab} />}
58
+ {tabElement}
59
+ </TreeContextController>
60
+ </SettingsContextController>
61
+ </StoreContext.Provider>
62
+ </BridgeContext.Provider>
63
+ );
64
+}
src/devtools/views/Elements.js
+11
-30
@@ -1,41 +1,22 @@
1
// @flow
2
3
-import React, { useMemo } from 'react';
4
-import Store from '../store';
3
+import React from 'react';
4
import Tree from './Tree';
6
-import { BridgeContext, StoreContext } from './context';
5
import SelectedElement from './SelectedElement';
8
-import { TreeContextController } from './TreeContext';
6
import styles from './Elements.css';
7
11
-import './root.css';
12
-
13
-import type { Bridge } from '../../types';
14
-
15
-export type Props = {|
16
- bridge: Bridge,
17
- browserName: string,
18
- themeName: string,
19
-|};
20
-
21
-export default function Elements({ bridge, browserName, themeName }: Props) {
22
- const store = useMemo<Store>(() => new Store(bridge), []);
8
+export type Props = {||};
9
10
+export default function Elements(_: Props) {
11
// TODO Flex wrappers below should be user resizable.
12
return (
26
- <BridgeContext.Provider value={bridge}>
27
- <StoreContext.Provider value={store}>
28
- <TreeContextController>
29
- <div className={styles.Elements}>
30
- <div className={styles.TreeWrapper}>
31
- <Tree />
32
- </div>
33
- <div className={styles.SelectedElementWrapper}>
34
- <SelectedElement />
35
- </div>
36
- </div>
37
- </TreeContextController>
38
- </StoreContext.Provider>
39
- </BridgeContext.Provider>
13
+ <div className={styles.Elements}>
14
+ <div className={styles.TreeWrapper}>
15
+ <Tree />
16
+ </div>
17
+ <div className={styles.SelectedElementWrapper}>
18
+ <SelectedElement />
19
+ </div>
20
+ </div>
21
);
22
}
src/devtools/views/Icon.js
+21
-1
@@ -5,7 +5,7 @@ import styles from './Icon.css';
5
6
type Props = {|
7
className?: string,
8
- type: 'arrow' | 'search',
8
+ type: 'arrow' | 'elements' | 'profiler' | 'search' | 'settings',
9
|};
10
11
export default function Icon({ className = '', type }: Props) {
@@ -14,9 +14,18 @@ export default function Icon({ className = '', type }: Props) {
14
case 'arrow':
15
pathData = PATH_ARROW;
16
break;
17
+ case 'elements':
18
+ pathData = PATH_ELEMENTS;
19
+ break;
20
+ case 'profiler':
21
+ pathData = PATH_PROFILER;
22
+ break;
23
case 'search':
24
pathData = PATH_SEARCH;
25
break;
26
+ case 'settings':
27
+ pathData = PATH_SETTINGS;
28
+ break;
29
default:
30
console.warn(`Unsupported type "${type}" specified for Icon`);
31
break;
@@ -37,8 +46,19 @@ export default function Icon({ className = '', type }: Props) {
46
}
47
48
const PATH_ARROW = 'M8 5v14l11-7z';
49
+const PATH_ELEMENTS =
50
+ 'M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z';
51
+const PATH_PROFILER = 'M5 9.2h3V19H5zM10.6 5h2.8v14h-2.8zm5.6 8H19v6h-2.8z';
52
const PATH_SEARCH = `
53
M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91
54
16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99
55
5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z
56
`;
57
+const PATH_SETTINGS = `
58
+ M15.95 10.78c.03-.25.05-.51.05-.78s-.02-.53-.06-.78l1.69-1.32c.15-.12.19-.34.1-.51l-1.6-2.77c-.1-.18-.31-.24-.49-.18l-1.99.8c-.42-.32-.86-.58-1.35-.78L12
59
+ 2.34c-.03-.2-.2-.34-.4-.34H8.4c-.2 0-.36.14-.39.34l-.3 2.12c-.49.2-.94.47-1.35.78l-1.99-.8c-.18-.07-.39
60
+ 0-.49.18l-1.6 2.77c-.1.18-.06.39.1.51l1.69
61
+ 1.32c-.04.25-.07.52-.07.78s.02.53.06.78L2.37 12.1c-.15.12-.19.34-.1.51l1.6 2.77c.1.18.31.24.49.18l1.99-.8c.42.32.86.58
62
+ 1.35.78l.3 2.12c.04.2.2.34.4.34h3.2c.2 0 .37-.14.39-.34l.3-2.12c.49-.2.94-.47 1.35-.78l1.99.8c.18.07.39 0
63
+ .49-.18l1.6-2.77c.1-.18.06-.39-.1-.51l-1.67-1.32zM10 13c-1.65 0-3-1.35-3-3s1.35-3 3-3 3 1.35 3 3-1.35 3-3 3z
64
+`;
src/devtools/views/OwnersStack.css
+1
-1
@@ -29,7 +29,7 @@
29
margin-right: 0.5rem;
30
color: var(--color-component-name);
31
font-family: var(--font-family-monospace);
32
- font-size: var(--font-size-normal);
32
+ font-size: var(--font-size-monospace-normal);
33
white-space: nowrap;
34
border-radius: 0.125rem;
35
}
src/devtools/views/Profiler.css
new
+12
@@ -0,0 +1,12 @@
1
+.Profiler {
2
+ width: 100%;
3
+ height: 100%;
4
+ display: flex;
5
+ align-items: center;
6
+ justify-content: center;
7
+ border-top: 1px solid var(--color-border);
8
+ font-family: var(--font-family-sans);
9
+ font-size: var(--font-size-sans-normal);
10
+ background-color: var(--color-background);
11
+ color: var(--color-text-color);
12
+}
src/devtools/views/Profiler.js
new
+11
@@ -0,0 +1,11 @@
1
+// @flow
2
+
3
+import React from 'react';
4
+
5
+import styles from './Profiler.css';
6
+
7
+export type Props = {||};
8
+
9
+export default function Profiler(_: Props) {
10
+ return <div className={styles.Profiler}>Under development...</div>;
11
+}
src/devtools/views/ReactLogo.css
new
+5
@@ -0,0 +1,5 @@
1
+.ReactLogo {
2
+ width: 1.75rem;
3
+ height: 1.75rem;
4
+ margin: 0 0.75rem 0 0.25rem;
5
+}
src/devtools/views/ReactLogo.js
new
+22
@@ -0,0 +1,22 @@
1
+// @flow
2
+
3
+import React from 'react';
4
+
5
+import styles from './ReactLogo.css';
6
+
7
+export default function ReactLogo() {
8
+ return (
9
+ <svg
10
+ xmlns="http://www.w3.org/2000/svg"
11
+ className={styles.ReactLogo}
12
+ viewBox="-11.5 -10.23174 23 20.46348"
13
+ >
14
+ <circle cx="0" cy="0" r="2.05" fill="currentColor" />
15
+ <g stroke="currentColor" strokeWidth="1" fill="none">
16
+ <ellipse rx="11" ry="4.2" />
17
+ <ellipse rx="11" ry="4.2" transform="rotate(60)" />
18
+ <ellipse rx="11" ry="4.2" transform="rotate(120)" />
19
+ </g>
20
+ </svg>
21
+ );
22
+}
src/devtools/views/SearchInput.css
+2
-2
@@ -6,7 +6,7 @@
6
7
.Input {
8
flex: 1;
9
- font-size: 1rem;
9
+ font-size: var(--font-size-sans-large);
10
outline: none;
11
border: none;
12
background-color: var(--color-background);
@@ -22,7 +22,7 @@
22
23
.IndexLabel {
24
color: var(--color-dim);
25
- font-size: var(--font-size-normal);
25
+ font-size: var(--font-size-sans-normal);
26
}
27
28
.IconButton {
src/devtools/views/SelectedElement.css
+2
-2
@@ -11,7 +11,7 @@
11
flex: 0 0 42px;
12
display: flex;
13
align-items: center;
14
- font-size: var(--font-size-large);
14
+ font-size: var(--font-size-monospace-large);
15
border-bottom: 1px solid var(--color-border);
16
padding: 0.5rem;
17
}
@@ -67,7 +67,7 @@
67
.InspectedElement {
68
overflow: auto;
69
font-family: var(--font-family-monospace);
70
- font-size: var(--font-size-normal);
70
+ font-size: var(--font-size-monospace-normal);
71
line-height: var(--line-height-data);
72
}
73
src/devtools/views/Settings.css
new
+54
@@ -0,0 +1,54 @@
1
+.Settings {
2
+ width: 100%;
3
+ height: 100%;
4
+ padding: 0.5rem;
5
+ display: flex;
6
+ flex-direction: column;
7
+ align-items: flex-start;
8
+ justify-content: flex-start;
9
+ border-top: 1px solid var(--color-border);
10
+ font-family: var(--font-family-sans);
11
+ font-size: var(--font-size-sans-normal);
12
+ background-color: var(--color-background);
13
+ color: var(--color-text-color);
14
+}
15
+
16
+.Section {
17
+ display: flex;
18
+ flex-direction: column;
19
+ margin-right: 0.5rem;
20
+ margin-bottom: 0.5rem;
21
+}
22
+
23
+.Header {
24
+ margin-bottom: 0.5rem;
25
+ font-size: var(--font-size-sans-large);
26
+}
27
+
28
+.OptionGroup {
29
+ display: flex;
30
+ flex-direction: row;
31
+ user-select: none;
32
+}
33
+
34
+.Option {
35
+ cursor: pointer;
36
+ padding: 0.5rem;
37
+ border: 1px solid var(--color-border);
38
+ border-right: none;
39
+}
40
+
41
+.Option:hover {
42
+ background-color: var(--color-tree-node-hover);
43
+}
44
+
45
+.Option:first-of-type {
46
+ border-top-left-radius: 0.25rem;
47
+ border-bottom-left-radius: 0.25rem;
48
+}
49
+
50
+.Option:last-of-type {
51
+ border-top-right-radius: 0.25rem;
52
+ border-bottom-right-radius: 0.25rem;
53
+ border-right: 1px solid var(--color-border);
54
+}
src/devtools/views/Settings.js
new
+93
@@ -0,0 +1,93 @@
1
+// @flow
2
+
3
+import React, { useCallback, useContext } from 'react';
4
+import { SettingsContext } from './SettingsContext';
5
+
6
+import styles from './Settings.css';
7
+
8
+export type Props = {||};
9
+
10
+export default function Settings(_: Props) {
11
+ const { displayDensity, setDisplayDensity, theme, setTheme } = useContext(
12
+ SettingsContext
13
+ );
14
+
15
+ const updateDisplayDensity = useCallback(
16
+ ({ currentTarget }) => {
17
+ setDisplayDensity(currentTarget.value);
18
+ },
19
+ [setDisplayDensity]
20
+ );
21
+
22
+ const updateTheme = useCallback(
23
+ ({ currentTarget }) => {
24
+ setTheme(currentTarget.value);
25
+ },
26
+ [setTheme]
27
+ );
28
+
29
+ return (
30
+ <div className={styles.Settings}>
31
+ <div className={styles.Section}>
32
+ <div className={styles.Header}>Theme</div>
33
+ <div className={styles.OptionGroup}>
34
+ <label className={styles.Option}>
35
+ <input
36
+ type="radio"
37
+ name="Settings-Settings-theme"
38
+ checked={theme === 'auto'}
39
+ value="auto"
40
+ onChange={updateTheme}
41
+ />{' '}
42
+ Auto
43
+ </label>
44
+ <label className={styles.Option}>
45
+ <input
46
+ type="radio"
47
+ name="Settings-theme"
48
+ checked={theme === 'light'}
49
+ value="light"
50
+ onChange={updateTheme}
51
+ />{' '}
52
+ Light
53
+ </label>
54
+ <label className={styles.Option}>
55
+ <input
56
+ type="radio"
57
+ name="Settings-theme"
58
+ checked={theme === 'dark'}
59
+ value="dark"
60
+ onChange={updateTheme}
61
+ />{' '}
62
+ Dark
63
+ </label>
64
+ </div>
65
+ </div>
66
+ <div className={styles.Section}>
67
+ <div className={styles.Header}>Display density</div>
68
+ <div className={styles.OptionGroup}>
69
+ <label className={styles.Option}>
70
+ <input
71
+ type="radio"
72
+ name="Settings-displayDensity"
73
+ checked={displayDensity === 'compact'}
74
+ value="compact"
75
+ onChange={updateDisplayDensity}
76
+ />{' '}
77
+ Compact
78
+ </label>
79
+ <label className={styles.Option}>
80
+ <input
81
+ type="radio"
82
+ name="Settings-displayDensity"
83
+ checked={displayDensity === 'comfortable'}
84
+ value="comfortable"
85
+ onChange={updateDisplayDensity}
86
+ />{' '}
87
+ Comfortable
88
+ </label>
89
+ </div>
90
+ </div>
91
+ </div>
92
+ );
93
+}
src/devtools/views/SettingsContext.js
new
+166
@@ -0,0 +1,166 @@
1
+// @flow
2
+
3
+import React, { createContext, useLayoutEffect, useMemo } from 'react';
4
+import { useLocalStorage } from './hooks';
5
+
6
+import type { BrowserTheme } from './DevTools';
7
+
8
+export type DisplayDensity = 'compact' | 'comfortable';
9
+export type Theme = 'auto' | 'light' | 'dark';
10
+
11
+type Context = {|
12
+ displayDensity: DisplayDensity,
13
+ setDisplayDensity(value: DisplayDensity): void,
14
+ theme: Theme,
15
+ setTheme(value: Theme): void,
16
+|};
17
+
18
+const SettingsContext = createContext<Context>(((null: any): Context));
19
+// $FlowFixMe displayName is a valid attribute of React$ConsearchText
20
+SettingsContext.displayName = 'SettingsContext';
21
+
22
+type Props = {|
23
+ browserTheme: BrowserTheme,
24
+ children: React$Node,
25
+|};
26
+
27
+function SettingsContextController({ browserTheme, children }: Props) {
28
+ const [displayDensity, setDisplayDensity] = useLocalStorage<DisplayDensity>(
29
+ 'displayDensity',
30
+ 'compact'
31
+ );
32
+ const [theme, setTheme] = useLocalStorage<Theme>('theme', 'auto');
33
+
34
+ useLayoutEffect(() => {
35
+ switch (displayDensity) {
36
+ case 'compact':
37
+ updateDisplayDensity('compact');
38
+ break;
39
+ case 'comfortable':
40
+ updateDisplayDensity('comfortable');
41
+ break;
42
+ default:
43
+ throw Error(`Unsupported displayDensity value "${displayDensity}"`);
44
+ }
45
+ }, [displayDensity]);
46
+
47
+ useLayoutEffect(() => {
48
+ switch (theme) {
49
+ case 'light':
50
+ updateThemeVariables('light');
51
+ break;
52
+ case 'dark':
53
+ updateThemeVariables('dark');
54
+ break;
55
+ case 'auto':
56
+ updateThemeVariables(browserTheme);
57
+ break;
58
+ default:
59
+ throw Error(`Unsupported theme value "${theme}"`);
60
+ }
61
+ }, [theme]);
62
+
63
+ const value = useMemo(
64
+ () => ({
65
+ displayDensity,
66
+ setDisplayDensity,
67
+ theme,
68
+ setTheme,
69
+ }),
70
+ [displayDensity, setDisplayDensity, theme, setTheme]
71
+ );
72
+
73
+ return (
74
+ <SettingsContext.Provider value={value}>
75
+ {children}
76
+ </SettingsContext.Provider>
77
+ );
78
+}
79
+
80
+function setStyleVariable(name: string, value: string) {
81
+ (document.documentElement: any).style.setProperty(name, value);
82
+}
83
+
84
+function updateDisplayDensity(displayDensity: DisplayDensity): void {
85
+ setStyleVariable(
86
+ '--font-size-monospace-normal',
87
+ `var(--${displayDensity}-font-size-monospace-normal)`
88
+ );
89
+ setStyleVariable(
90
+ '--font-size-monospace-large',
91
+ `var(--${displayDensity}-font-size-monospace-large)`
92
+ );
93
+ setStyleVariable(
94
+ '--font-size-sans-normal',
95
+ `var(--${displayDensity}-font-size-sans-normal)`
96
+ );
97
+ setStyleVariable(
98
+ '--font-size-sans-large',
99
+ `var(--${displayDensity}-font-size-sans-large)`
100
+ );
101
+ setStyleVariable(
102
+ '--line-height-data',
103
+ `var(--${displayDensity}-line-height-data)`
104
+ );
105
+}
106
+
107
+function updateThemeVariables(theme: Theme): void {
108
+ setStyleVariable('--color-arrow', `var(--theme-${theme}-arrow)`);
109
+ setStyleVariable(
110
+ '--color-arrow-inverted',
111
+ `var(--theme-${theme}-arrow-inverted)`
112
+ );
113
+ setStyleVariable(
114
+ '--color-attribute-name',
115
+ `var(--theme-${theme}-attribute-name)`
116
+ );
117
+ setStyleVariable(
118
+ '--color-attribute-value',
119
+ `var(--theme-${theme}-attribute-value)`
120
+ );
121
+ setStyleVariable('--color-background', `var(--theme-${theme}-background)`);
122
+ setStyleVariable('--color-border', `var(--theme-${theme}-color-border)`);
123
+ setStyleVariable('--color-button', `var(--theme-${theme}-button)`);
124
+ setStyleVariable(
125
+ '--color-button-hover',
126
+ `var(--theme-${theme}-button-hover)`
127
+ );
128
+ setStyleVariable(
129
+ '--color-component-name',
130
+ `var(--theme-${theme}-component-name)`
131
+ );
132
+ setStyleVariable(
133
+ '--color-component-name-inverted',
134
+ `var(--theme-${theme}-component-name-inverted)`
135
+ );
136
+ setStyleVariable('--color-dim', `var(--theme-${theme}-dim)`);
137
+ setStyleVariable('--color-dimmer', `var(--theme-${theme}-dimmer)`);
138
+ setStyleVariable('--color-dimmest', `var(--theme-${theme}-dimmest)`);
139
+ setStyleVariable(
140
+ '--color-search-match',
141
+ `var(--theme-${theme}-search-match)`
142
+ );
143
+ setStyleVariable(
144
+ '--color-search-match-current',
145
+ `var(--theme-${theme}-search-match-current)`
146
+ );
147
+ setStyleVariable('--color-text-color', `var(--theme-${theme}-text-color)`);
148
+ setStyleVariable(
149
+ '--color-tree-jsx-arrow-brackets',
150
+ `var(--theme-${theme}-jsx-arrow-brackets)`
151
+ );
152
+ setStyleVariable(
153
+ '--color-tree-jsx-arrow-brackets-inverted',
154
+ `var(--theme-${theme}-jsx-arrow-brackets-inverted)`
155
+ );
156
+ setStyleVariable(
157
+ '--color-tree-node-selected',
158
+ `var(--theme-${theme}-tree-node-selected)`
159
+ );
160
+ setStyleVariable(
161
+ '--color-tree-node-hover',
162
+ `var(--theme-${theme}-tree-node-hover)`
163
+ );
164
+}
165
+
166
+export { SettingsContext, SettingsContextController };
src/devtools/views/TabBar.css
new
+51
@@ -0,0 +1,51 @@
1
+.TabBar {
2
+ display: flex;
3
+ align-items: center;
4
+ padding: 0 0.5rem;
5
+ background-color: var(--color-background);
6
+ color: var(--color-tree-node-selected);
7
+ border-top: 1px solid var(--color-border);
8
+ font-family: var(--font-family-sans);
9
+ font-size: var(--font-size-sans-large);
10
+}
11
+
12
+.Tabs {
13
+ flex: 1 1 auto;
14
+ display: flex;
15
+ flex-direction: row;
16
+ align-items: center;
17
+ justify-content: flex-end;
18
+ overflow-x: auto;
19
+}
20
+
21
+.TabCurrent,
22
+.Tab {
23
+ display: flex;
24
+ align-items: center;
25
+ padding: 0.5rem 1rem;
26
+ cursor: pointer;
27
+ border-top: 3px solid transparent;
28
+ border-bottom: 3px solid transparent;
29
+ cursor: pointer;
30
+ user-select: none;
31
+}
32
+.TabCurrent:hover,
33
+.Tab:hover {
34
+ background-color: var(--color-tree-node-hover);
35
+}
36
+
37
+.TabCurrent {
38
+ border-bottom: 3px solid var(--color-tree-node-selected);
39
+}
40
+
41
+.Input {
42
+ width: 0;
43
+ margin: 0;
44
+ opacity: 0;
45
+}
46
+
47
+.Icon {
48
+ margin-right: 0.5rem;
49
+ width: 1.5rem;
50
+ height: 1.5rem;
51
+}
src/devtools/views/TabBar.js
new
+71
@@ -0,0 +1,71 @@
1
+// @flow
2
+
3
+import React, { useCallback } from 'react';
4
+import Icon from './Icon';
5
+import ReactLogo from './ReactLogo';
6
+
7
+import styles from './TabBar.css';
8
+
9
+import type { TabID } from './DevTools';
10
+
11
+export type Props = {|
12
+ currentTab: TabID,
13
+ selectTab: (tabID: TabID) => void,
14
+|};
15
+
16
+export default function TabBar({ currentTab, selectTab }: Props) {
17
+ const onChange = useCallback(
18
+ ({ currentTarget }) => selectTab(currentTarget.value),
19
+ [selectTab]
20
+ );
21
+
22
+ return (
23
+ <div className={styles.TabBar}>
24
+ <ReactLogo /> DevTools {process.env.DEVTOOLS_VERSION}
25
+ <div className={styles.Tabs}>
26
+ <label
27
+ className={currentTab === 'elements' ? styles.TabCurrent : styles.Tab}
28
+ >
29
+ <input
30
+ type="radio"
31
+ name="TabBar-tab"
32
+ className={styles.Input}
33
+ checked={currentTab === 'elements'}
34
+ value="elements"
35
+ onChange={onChange}
36
+ />
37
+ <Icon className={styles.Icon} type="elements" />
38
+ Elements
39
+ </label>
40
+ <label
41
+ className={currentTab === 'profiler' ? styles.TabCurrent : styles.Tab}
42
+ >
43
+ <input
44
+ type="radio"
45
+ name="TabBar-tab"
46
+ className={styles.Input}
47
+ checked={currentTab === 'profiler'}
48
+ value="profiler"
49
+ onChange={onChange}
50
+ />
51
+ <Icon className={styles.Icon} type="profiler" />
52
+ Profiler
53
+ </label>
54
+ <label
55
+ className={currentTab === 'settings' ? styles.TabCurrent : styles.Tab}
56
+ >
57
+ <input
58
+ type="radio"
59
+ name="TabBar-tab"
60
+ className={styles.Input}
61
+ checked={currentTab === 'settings'}
62
+ value="settings"
63
+ onChange={onChange}
64
+ />
65
+ <Icon className={styles.Icon} type="settings" />
66
+ Settings
67
+ </label>
68
+ </div>
69
+ </div>
70
+ );
71
+}
src/devtools/views/Tree.css
+1
-2
@@ -10,7 +10,6 @@
10
flex: 0 0 42px;
11
display: flex;
12
align-items: center;
13
- font-size: var(--font-size-large);
13
border-bottom: 1px solid var(--color-border);
14
padding: 0.5rem;
15
}
@@ -24,7 +23,7 @@
23
24
.List {
25
font-family: var(--font-family-monospace);
27
- font-size: var(--font-size-normal);
26
+ font-size: var(--font-size-monospace-normal);
27
line-height: var(--line-height-data);
28
}
29
src/devtools/views/hooks.js
new
+43
@@ -0,0 +1,43 @@
1
+// @flow
2
+
3
+import { useState } from 'react';
4
+
5
+// https://usehooks.com/useLocalStorage/
6
+export function useLocalStorage<T>(
7
+ key: string,
8
+ initialValue: T
9
+): [T, (value: T | (() => T)) => void] {
10
+ // State to store our value
11
+ // Pass initial state function to useState so logic is only executed once
12
+ const [storedValue, setStoredValue] = useState(() => {
13
+ try {
14
+ // Get from local storage by key
15
+ const item = window.localStorage.getItem(key);
16
+ // Parse stored json or if none return initialValue
17
+ return item ? JSON.parse(item) : initialValue;
18
+ } catch (error) {
19
+ // If error also return initialValue
20
+ console.log(error);
21
+ return initialValue;
22
+ }
23
+ });
24
+
25
+ // Return a wrapped version of useState's setter function that ...
26
+ // ... persists the new value to localStorage.
27
+ const setValue = value => {
28
+ try {
29
+ // Allow value to be a function so we have same API as useState
30
+ const valueToStore =
31
+ value instanceof Function ? value(storedValue) : value;
32
+ // Save state
33
+ setStoredValue(valueToStore);
34
+ // Save to local storage
35
+ window.localStorage.setItem(key, JSON.stringify(valueToStore));
36
+ } catch (error) {
37
+ // A more advanced implementation would handle the error case
38
+ console.log(error);
39
+ }
40
+ };
41
+
42
+ return [storedValue, setValue];
43
+}
src/devtools/views/root.css
+18
-28
@@ -1,4 +1,8 @@
1
:root {
2
+ /**
3
+ * IMPORTANT: When new theme variables are added below– also add them to SettingsContext updateThemeVariables()
4
+ */
5
+
6
/* Light theme */
7
--theme-light-arrow: #777d88;
8
--theme-light-arrow-inverted: #ffffff;
@@ -6,7 +10,7 @@
10
--theme-light-attribute-value: #1a1aa6;
11
--theme-light-background: #ffffff;
12
--theme-light-button: #0088fa;
9
- --theme-light-button-hover: #1a1aa6;
13
+ --theme-light-button-hover: #3578e5;
14
--theme-light-color-border: #eeeeee;
15
--theme-light-component-name: #8155cb;
16
--theme-light-component-name-inverted: #ffffff;
@@ -41,35 +45,21 @@
45
--theme-dark-search-match-current: #f7923b;
46
--theme-dark-text-color: #ffffff;
47
--theme-dark-tree-node-selected: #31a38d;
44
- --theme-dark-tree-node-hover: #23272f;
48
+ --theme-dark-tree-node-hover: #303846;
49
46
- /* "theme" values referenced by other stylesheets */
47
- --color-arrow: var(--theme-light-arrow);
48
- --color-arrow-inverted: var(--theme-light-arrow-inverted);
49
- --color-attribute-name: var(--theme-light-attribute-name);
50
- --color-attribute-value: var(--theme-light-attribute-value);
51
- --color-background: var(--theme-light-background);
52
- --color-border: var(--theme-light-color-border);
53
- --color-button: var(--theme-light-button);
54
- --color-button-hover: var(--theme-light-button-hover);
55
- --color-component-name: var(--theme-light-component-name);
56
- --color-component-name-inverted: var(--theme-light-component-name-inverted);
57
- --color-dim: var(--theme-light-dim);
58
- --color-dimmer: var(--theme-light-dimmer);
59
- --color-dimmest: var(--theme-light-dimmest);
60
- --color-search-match: var(--theme-light-search-match);
61
- --color-search-match-current: var(--theme-light-search-match-current);
62
- --color-text-color: var(--theme-light-text-color);
63
- --color-tree-jsx-arrow-brackets: var(--theme-light-jsx-arrow-brackets);
64
- --color-tree-jsx-arrow-brackets-inverted: var(
65
- --theme-light-jsx-arrow-brackets-inverted
66
- );
67
- --color-tree-node-selected: var(--theme-light-tree-node-selected);
68
- --color-tree-node-hover: var(--theme-light-tree-node-hover);
50
+ /* Compact density */
51
+ --compact-font-size-monospace-normal: 11px;
52
+ --compact-font-size-monospace-large: 15px;
53
+ --compact-font-size-sans-normal: 12px;
54
+ --compact-font-size-sans-large: 14px;
55
+ --compact-line-height-data: 18px;
56
70
- --font-size-normal: 11px;
71
- --font-size-large: 15px;
72
- --line-height-data: 18px;
57
+ /* Comfortable density */
58
+ --comfortable-font-size-monospace-normal: 13px;
59
+ --comfortable-font-size-monospace-large: 17px;
60
+ --comfortable-font-size-sans-normal: 14px;
61
+ --comfortable-font-size-sans-large: 16px;
62
+ --comfortable-line-height-data: 22px;
63
64
/* GitHub.com system fonts */
65
--font-family-monospace: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo,