@samitouri / QOS-React-2 / commits / d203ebf165

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,