From d203ebf1655e9bbaa039ffc34855bbe3396dde35 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Tue, 12 Feb 2019 20:41:39 -0500 Subject: [PATCH] Added new tabs UI and theme/display-density preferences --- package.json | 1 + shells/browser/shared/src/panel.js | 13 +- shells/browser/shared/webpack.config.js | 17 ++- shells/dev/index.html | 1 - shells/dev/src/devtools.js | 7 +- shells/dev/webpack.config.js | 10 ++ src/devtools/views/DevTools.js | 64 +++++++++ src/devtools/views/Elements.js | 41 ++---- src/devtools/views/Icon.js | 22 +++- src/devtools/views/OwnersStack.css | 2 +- src/devtools/views/Profiler.css | 12 ++ src/devtools/views/Profiler.js | 11 ++ src/devtools/views/ReactLogo.css | 5 + src/devtools/views/ReactLogo.js | 22 ++++ src/devtools/views/SearchInput.css | 4 +- src/devtools/views/SelectedElement.css | 4 +- src/devtools/views/Settings.css | 54 ++++++++ src/devtools/views/Settings.js | 93 +++++++++++++ src/devtools/views/SettingsContext.js | 166 ++++++++++++++++++++++++ src/devtools/views/TabBar.css | 51 ++++++++ src/devtools/views/TabBar.js | 71 ++++++++++ src/devtools/views/Tree.css | 3 +- src/devtools/views/hooks.js | 43 ++++++ src/devtools/views/root.css | 46 +++---- 24 files changed, 684 insertions(+), 79 deletions(-) create mode 100644 src/devtools/views/DevTools.js create mode 100644 src/devtools/views/Profiler.css create mode 100644 src/devtools/views/Profiler.js create mode 100644 src/devtools/views/ReactLogo.css create mode 100644 src/devtools/views/ReactLogo.js create mode 100644 src/devtools/views/Settings.css create mode 100644 src/devtools/views/Settings.js create mode 100644 src/devtools/views/SettingsContext.js create mode 100644 src/devtools/views/TabBar.css create mode 100644 src/devtools/views/TabBar.js create mode 100644 src/devtools/views/hooks.js diff --git a/package.json b/package.json index 21434e9d8e..585922882b 100644 --- a/package.json +++ b/package.json @@ -1,4 +1,5 @@ { + "version": "4.0.0", "repository": "bvaughn/react-devtools-experimental", "license": "BSD-3-Clause", "private": true, diff --git a/shells/browser/shared/src/panel.js b/shells/browser/shared/src/panel.js index 19ef2fda46..baf628f01e 100644 --- a/shells/browser/shared/src/panel.js +++ b/shells/browser/shared/src/panel.js @@ -3,7 +3,7 @@ import { createElement } from 'react'; import { createRoot, flushSync } from 'react-dom'; import Bridge from 'src/bridge'; -import Elements from 'src/devtools/views/Elements'; +import DevTools from 'src/devtools/views/DevTools'; import inject from './inject'; const IS_CHROME = navigator.userAgent.indexOf('Firefox') < 0; @@ -16,7 +16,7 @@ if (IS_CHROME) { // chrome.devtools.panels added in Chrome 18. // chrome.devtools.panels.themeName added in Chrome 54. - themeName = chrome.devtools.panels.themeName === 'dark' ? 'Dark' : 'Default'; + themeName = chrome.devtools.panels.themeName === 'dark' ? 'dark' : 'light'; } else { browserName = 'Firefox'; @@ -25,10 +25,10 @@ if (IS_CHROME) { if (chrome.devtools && chrome.devtools.panels) { switch (chrome.devtools.panels.themeName) { case 'dark': - themeName = 'Dark'; + themeName = 'dark'; break; default: - themeName = 'Light'; + themeName = 'light'; break; } } @@ -63,10 +63,11 @@ function injectAndInit() { const root = createRoot(container); root.render( - createElement(Elements, { + createElement(DevTools, { bridge, browserName, - themeName, + browserTheme: themeName, + showTabBar: false, }) ); diff --git a/shells/browser/shared/webpack.config.js b/shells/browser/shared/webpack.config.js index b5984117ea..44e3d5b5c2 100644 --- a/shells/browser/shared/webpack.config.js +++ b/shells/browser/shared/webpack.config.js @@ -1,9 +1,13 @@ const { readFileSync } = require('fs'); const { resolve } = require('path'); -const webpack = require('webpack'); +const { DefinePlugin } = require('webpack'); const __DEV__ = process.env.NODE_ENV !== 'production'; +const DEVTOOLS_VERSION = JSON.parse( + readFileSync(resolve(__dirname, '../../../package.json')) +).version; + module.exports = { mode: __DEV__ ? 'development' : 'production', devtool: __DEV__ ? 'cheap-module-eval-source-map' : false, @@ -24,12 +28,19 @@ module.exports = { }, }, plugins: __DEV__ - ? [] + ? [ + new DefinePlugin({ + 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`, + }), + ] : [ // Ensure we get production React - new webpack.DefinePlugin({ + new DefinePlugin({ 'process.env.NODE_ENV': '"production"', }), + new DefinePlugin({ + 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`, + }), ], module: { rules: [ diff --git a/shells/dev/index.html b/shells/dev/index.html index 021f696b78..caef7119cc 100644 --- a/shells/dev/index.html +++ b/shells/dev/index.html @@ -10,7 +10,6 @@ border: none; } #devtools { - display: flex; height: 400px; max-height: 50%; overflow: hidden; diff --git a/shells/dev/src/devtools.js b/shells/dev/src/devtools.js index 158fb3983b..579fde5494 100644 --- a/shells/dev/src/devtools.js +++ b/shells/dev/src/devtools.js @@ -6,7 +6,7 @@ import { createRoot } from 'react-dom'; import Bridge from 'src/bridge'; import { installHook } from 'src/hook'; import { initDevTools } from 'src/devtools'; -import Elements from 'src/devtools/views/Elements'; +import DevTools from 'src/devtools/views/DevTools'; const iframe = ((document.getElementById('target'): any): HTMLIFrameElement); @@ -56,10 +56,11 @@ inject('./build/App.js', () => { const root = createRoot(container); const batch = root.createBatch(); batch.render( - createElement(Elements, { + createElement(DevTools, { bridge, browserName: 'Chrome', - themeName: 'light', + browserTheme: 'light', + showTabBar: true, }) ); batch.then(() => { diff --git a/shells/dev/webpack.config.js b/shells/dev/webpack.config.js index 887375ce9d..0629609f3b 100644 --- a/shells/dev/webpack.config.js +++ b/shells/dev/webpack.config.js @@ -1,5 +1,10 @@ const { readFileSync } = require('fs'); const { resolve } = require('path'); +const { DefinePlugin } = require('webpack'); + +const DEVTOOLS_VERSION = JSON.parse( + readFileSync(resolve(__dirname, '../../package.json')) +).version; // TODO Share Webpack configs like alias @@ -20,6 +25,11 @@ module.exports = { src: resolve(__dirname, '../../src'), }, }, + plugins: [ + new DefinePlugin({ + 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`, + }), + ], module: { rules: [ { diff --git a/src/devtools/views/DevTools.js b/src/devtools/views/DevTools.js new file mode 100644 index 0000000000..36de0d91e8 --- /dev/null +++ b/src/devtools/views/DevTools.js @@ -0,0 +1,64 @@ +// @flow + +import React, { useMemo, useState } from 'react'; +import Store from '../store'; +import { BridgeContext, StoreContext } from './context'; +import Elements from './Elements'; +import Profiler from './Profiler'; +import Settings from './Settings'; +import TabBar from './TabBar'; +import { SettingsContextController } from './SettingsContext'; +import { TreeContextController } from './TreeContext'; + +import './root.css'; + +import type { Bridge } from '../../types'; + +export type TabID = 'elements' | 'profiler' | 'settings'; +export type BrowserTheme = 'dark' | 'light'; + +export type Props = {| + bridge: Bridge, + browserName: string, + defaultTab?: TabID, + browserTheme: BrowserTheme, + showTabBar?: boolean, +|}; + +export default function DevTools({ + bridge, + browserName, + defaultTab = 'elements', + browserTheme = 'light', + showTabBar = false, +}: Props) { + const store = useMemo(() => new Store(bridge), []); + const [tab, setTab] = useState(defaultTab); + + let tabElement; + switch (tab) { + case 'profiler': + tabElement = ; + break; + case 'settings': + tabElement = ; + break; + case 'elements': + default: + tabElement = ; + break; + } + + return ( + + + + + {showTabBar && } + {tabElement} + + + + + ); +} diff --git a/src/devtools/views/Elements.js b/src/devtools/views/Elements.js index b79f12cc36..cb401ddfdb 100644 --- a/src/devtools/views/Elements.js +++ b/src/devtools/views/Elements.js @@ -1,41 +1,22 @@ // @flow -import React, { useMemo } from 'react'; -import Store from '../store'; +import React from 'react'; import Tree from './Tree'; -import { BridgeContext, StoreContext } from './context'; import SelectedElement from './SelectedElement'; -import { TreeContextController } from './TreeContext'; import styles from './Elements.css'; -import './root.css'; - -import type { Bridge } from '../../types'; - -export type Props = {| - bridge: Bridge, - browserName: string, - themeName: string, -|}; - -export default function Elements({ bridge, browserName, themeName }: Props) { - const store = useMemo(() => new Store(bridge), []); +export type Props = {||}; +export default function Elements(_: Props) { // TODO Flex wrappers below should be user resizable. return ( - - - -
-
- -
-
- -
-
-
-
-
+
+
+ +
+
+ +
+
); } diff --git a/src/devtools/views/Icon.js b/src/devtools/views/Icon.js index 16868464b6..e7b6a3d211 100644 --- a/src/devtools/views/Icon.js +++ b/src/devtools/views/Icon.js @@ -5,7 +5,7 @@ import styles from './Icon.css'; type Props = {| className?: string, - type: 'arrow' | 'search', + type: 'arrow' | 'elements' | 'profiler' | 'search' | 'settings', |}; export default function Icon({ className = '', type }: Props) { @@ -14,9 +14,18 @@ export default function Icon({ className = '', type }: Props) { case 'arrow': pathData = PATH_ARROW; break; + case 'elements': + pathData = PATH_ELEMENTS; + break; + case 'profiler': + pathData = PATH_PROFILER; + break; case 'search': pathData = PATH_SEARCH; break; + case 'settings': + pathData = PATH_SETTINGS; + break; default: console.warn(`Unsupported type "${type}" specified for Icon`); break; @@ -37,8 +46,19 @@ export default function Icon({ className = '', type }: Props) { } const PATH_ARROW = 'M8 5v14l11-7z'; +const PATH_ELEMENTS = + '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'; +const PATH_PROFILER = 'M5 9.2h3V19H5zM10.6 5h2.8v14h-2.8zm5.6 8H19v6h-2.8z'; const PATH_SEARCH = ` 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 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 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z `; +const PATH_SETTINGS = ` + 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 + 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 + 0-.49.18l-1.6 2.77c-.1.18-.06.39.1.51l1.69 + 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 + 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 + .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 +`; diff --git a/src/devtools/views/OwnersStack.css b/src/devtools/views/OwnersStack.css index 5b00134f0c..28ac5ae858 100644 --- a/src/devtools/views/OwnersStack.css +++ b/src/devtools/views/OwnersStack.css @@ -29,7 +29,7 @@ margin-right: 0.5rem; color: var(--color-component-name); font-family: var(--font-family-monospace); - font-size: var(--font-size-normal); + font-size: var(--font-size-monospace-normal); white-space: nowrap; border-radius: 0.125rem; } diff --git a/src/devtools/views/Profiler.css b/src/devtools/views/Profiler.css new file mode 100644 index 0000000000..440c241c03 --- /dev/null +++ b/src/devtools/views/Profiler.css @@ -0,0 +1,12 @@ +.Profiler { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + border-top: 1px solid var(--color-border); + font-family: var(--font-family-sans); + font-size: var(--font-size-sans-normal); + background-color: var(--color-background); + color: var(--color-text-color); +} diff --git a/src/devtools/views/Profiler.js b/src/devtools/views/Profiler.js new file mode 100644 index 0000000000..bd89d54cbb --- /dev/null +++ b/src/devtools/views/Profiler.js @@ -0,0 +1,11 @@ +// @flow + +import React from 'react'; + +import styles from './Profiler.css'; + +export type Props = {||}; + +export default function Profiler(_: Props) { + return
Under development...
; +} diff --git a/src/devtools/views/ReactLogo.css b/src/devtools/views/ReactLogo.css new file mode 100644 index 0000000000..407562437c --- /dev/null +++ b/src/devtools/views/ReactLogo.css @@ -0,0 +1,5 @@ +.ReactLogo { + width: 1.75rem; + height: 1.75rem; + margin: 0 0.75rem 0 0.25rem; +} diff --git a/src/devtools/views/ReactLogo.js b/src/devtools/views/ReactLogo.js new file mode 100644 index 0000000000..d7f33639c7 --- /dev/null +++ b/src/devtools/views/ReactLogo.js @@ -0,0 +1,22 @@ +// @flow + +import React from 'react'; + +import styles from './ReactLogo.css'; + +export default function ReactLogo() { + return ( + + + + + + + + + ); +} diff --git a/src/devtools/views/SearchInput.css b/src/devtools/views/SearchInput.css index b09da991e2..053e70c5cc 100644 --- a/src/devtools/views/SearchInput.css +++ b/src/devtools/views/SearchInput.css @@ -6,7 +6,7 @@ .Input { flex: 1; - font-size: 1rem; + font-size: var(--font-size-sans-large); outline: none; border: none; background-color: var(--color-background); @@ -22,7 +22,7 @@ .IndexLabel { color: var(--color-dim); - font-size: var(--font-size-normal); + font-size: var(--font-size-sans-normal); } .IconButton { diff --git a/src/devtools/views/SelectedElement.css b/src/devtools/views/SelectedElement.css index 54611b8aa5..4b9ee97752 100644 --- a/src/devtools/views/SelectedElement.css +++ b/src/devtools/views/SelectedElement.css @@ -11,7 +11,7 @@ flex: 0 0 42px; display: flex; align-items: center; - font-size: var(--font-size-large); + font-size: var(--font-size-monospace-large); border-bottom: 1px solid var(--color-border); padding: 0.5rem; } @@ -67,7 +67,7 @@ .InspectedElement { overflow: auto; font-family: var(--font-family-monospace); - font-size: var(--font-size-normal); + font-size: var(--font-size-monospace-normal); line-height: var(--line-height-data); } diff --git a/src/devtools/views/Settings.css b/src/devtools/views/Settings.css new file mode 100644 index 0000000000..cb493a402a --- /dev/null +++ b/src/devtools/views/Settings.css @@ -0,0 +1,54 @@ +.Settings { + width: 100%; + height: 100%; + padding: 0.5rem; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + border-top: 1px solid var(--color-border); + font-family: var(--font-family-sans); + font-size: var(--font-size-sans-normal); + background-color: var(--color-background); + color: var(--color-text-color); +} + +.Section { + display: flex; + flex-direction: column; + margin-right: 0.5rem; + margin-bottom: 0.5rem; +} + +.Header { + margin-bottom: 0.5rem; + font-size: var(--font-size-sans-large); +} + +.OptionGroup { + display: flex; + flex-direction: row; + user-select: none; +} + +.Option { + cursor: pointer; + padding: 0.5rem; + border: 1px solid var(--color-border); + border-right: none; +} + +.Option:hover { + background-color: var(--color-tree-node-hover); +} + +.Option:first-of-type { + border-top-left-radius: 0.25rem; + border-bottom-left-radius: 0.25rem; +} + +.Option:last-of-type { + border-top-right-radius: 0.25rem; + border-bottom-right-radius: 0.25rem; + border-right: 1px solid var(--color-border); +} diff --git a/src/devtools/views/Settings.js b/src/devtools/views/Settings.js new file mode 100644 index 0000000000..37e4dd9198 --- /dev/null +++ b/src/devtools/views/Settings.js @@ -0,0 +1,93 @@ +// @flow + +import React, { useCallback, useContext } from 'react'; +import { SettingsContext } from './SettingsContext'; + +import styles from './Settings.css'; + +export type Props = {||}; + +export default function Settings(_: Props) { + const { displayDensity, setDisplayDensity, theme, setTheme } = useContext( + SettingsContext + ); + + const updateDisplayDensity = useCallback( + ({ currentTarget }) => { + setDisplayDensity(currentTarget.value); + }, + [setDisplayDensity] + ); + + const updateTheme = useCallback( + ({ currentTarget }) => { + setTheme(currentTarget.value); + }, + [setTheme] + ); + + return ( +
+
+
Theme
+
+ + + +
+
+
+
Display density
+
+ + +
+
+
+ ); +} diff --git a/src/devtools/views/SettingsContext.js b/src/devtools/views/SettingsContext.js new file mode 100644 index 0000000000..3929ffa41e --- /dev/null +++ b/src/devtools/views/SettingsContext.js @@ -0,0 +1,166 @@ +// @flow + +import React, { createContext, useLayoutEffect, useMemo } from 'react'; +import { useLocalStorage } from './hooks'; + +import type { BrowserTheme } from './DevTools'; + +export type DisplayDensity = 'compact' | 'comfortable'; +export type Theme = 'auto' | 'light' | 'dark'; + +type Context = {| + displayDensity: DisplayDensity, + setDisplayDensity(value: DisplayDensity): void, + theme: Theme, + setTheme(value: Theme): void, +|}; + +const SettingsContext = createContext(((null: any): Context)); +// $FlowFixMe displayName is a valid attribute of React$ConsearchText +SettingsContext.displayName = 'SettingsContext'; + +type Props = {| + browserTheme: BrowserTheme, + children: React$Node, +|}; + +function SettingsContextController({ browserTheme, children }: Props) { + const [displayDensity, setDisplayDensity] = useLocalStorage( + 'displayDensity', + 'compact' + ); + const [theme, setTheme] = useLocalStorage('theme', 'auto'); + + useLayoutEffect(() => { + switch (displayDensity) { + case 'compact': + updateDisplayDensity('compact'); + break; + case 'comfortable': + updateDisplayDensity('comfortable'); + break; + default: + throw Error(`Unsupported displayDensity value "${displayDensity}"`); + } + }, [displayDensity]); + + useLayoutEffect(() => { + switch (theme) { + case 'light': + updateThemeVariables('light'); + break; + case 'dark': + updateThemeVariables('dark'); + break; + case 'auto': + updateThemeVariables(browserTheme); + break; + default: + throw Error(`Unsupported theme value "${theme}"`); + } + }, [theme]); + + const value = useMemo( + () => ({ + displayDensity, + setDisplayDensity, + theme, + setTheme, + }), + [displayDensity, setDisplayDensity, theme, setTheme] + ); + + return ( + + {children} + + ); +} + +function setStyleVariable(name: string, value: string) { + (document.documentElement: any).style.setProperty(name, value); +} + +function updateDisplayDensity(displayDensity: DisplayDensity): void { + setStyleVariable( + '--font-size-monospace-normal', + `var(--${displayDensity}-font-size-monospace-normal)` + ); + setStyleVariable( + '--font-size-monospace-large', + `var(--${displayDensity}-font-size-monospace-large)` + ); + setStyleVariable( + '--font-size-sans-normal', + `var(--${displayDensity}-font-size-sans-normal)` + ); + setStyleVariable( + '--font-size-sans-large', + `var(--${displayDensity}-font-size-sans-large)` + ); + setStyleVariable( + '--line-height-data', + `var(--${displayDensity}-line-height-data)` + ); +} + +function updateThemeVariables(theme: Theme): void { + setStyleVariable('--color-arrow', `var(--theme-${theme}-arrow)`); + setStyleVariable( + '--color-arrow-inverted', + `var(--theme-${theme}-arrow-inverted)` + ); + setStyleVariable( + '--color-attribute-name', + `var(--theme-${theme}-attribute-name)` + ); + setStyleVariable( + '--color-attribute-value', + `var(--theme-${theme}-attribute-value)` + ); + setStyleVariable('--color-background', `var(--theme-${theme}-background)`); + setStyleVariable('--color-border', `var(--theme-${theme}-color-border)`); + setStyleVariable('--color-button', `var(--theme-${theme}-button)`); + setStyleVariable( + '--color-button-hover', + `var(--theme-${theme}-button-hover)` + ); + setStyleVariable( + '--color-component-name', + `var(--theme-${theme}-component-name)` + ); + setStyleVariable( + '--color-component-name-inverted', + `var(--theme-${theme}-component-name-inverted)` + ); + setStyleVariable('--color-dim', `var(--theme-${theme}-dim)`); + setStyleVariable('--color-dimmer', `var(--theme-${theme}-dimmer)`); + setStyleVariable('--color-dimmest', `var(--theme-${theme}-dimmest)`); + setStyleVariable( + '--color-search-match', + `var(--theme-${theme}-search-match)` + ); + setStyleVariable( + '--color-search-match-current', + `var(--theme-${theme}-search-match-current)` + ); + setStyleVariable('--color-text-color', `var(--theme-${theme}-text-color)`); + setStyleVariable( + '--color-tree-jsx-arrow-brackets', + `var(--theme-${theme}-jsx-arrow-brackets)` + ); + setStyleVariable( + '--color-tree-jsx-arrow-brackets-inverted', + `var(--theme-${theme}-jsx-arrow-brackets-inverted)` + ); + setStyleVariable( + '--color-tree-node-selected', + `var(--theme-${theme}-tree-node-selected)` + ); + setStyleVariable( + '--color-tree-node-hover', + `var(--theme-${theme}-tree-node-hover)` + ); +} + +export { SettingsContext, SettingsContextController }; diff --git a/src/devtools/views/TabBar.css b/src/devtools/views/TabBar.css new file mode 100644 index 0000000000..6c9ab7b6d4 --- /dev/null +++ b/src/devtools/views/TabBar.css @@ -0,0 +1,51 @@ +.TabBar { + display: flex; + align-items: center; + padding: 0 0.5rem; + background-color: var(--color-background); + color: var(--color-tree-node-selected); + border-top: 1px solid var(--color-border); + font-family: var(--font-family-sans); + font-size: var(--font-size-sans-large); +} + +.Tabs { + flex: 1 1 auto; + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-end; + overflow-x: auto; +} + +.TabCurrent, +.Tab { + display: flex; + align-items: center; + padding: 0.5rem 1rem; + cursor: pointer; + border-top: 3px solid transparent; + border-bottom: 3px solid transparent; + cursor: pointer; + user-select: none; +} +.TabCurrent:hover, +.Tab:hover { + background-color: var(--color-tree-node-hover); +} + +.TabCurrent { + border-bottom: 3px solid var(--color-tree-node-selected); +} + +.Input { + width: 0; + margin: 0; + opacity: 0; +} + +.Icon { + margin-right: 0.5rem; + width: 1.5rem; + height: 1.5rem; +} diff --git a/src/devtools/views/TabBar.js b/src/devtools/views/TabBar.js new file mode 100644 index 0000000000..6819cb553c --- /dev/null +++ b/src/devtools/views/TabBar.js @@ -0,0 +1,71 @@ +// @flow + +import React, { useCallback } from 'react'; +import Icon from './Icon'; +import ReactLogo from './ReactLogo'; + +import styles from './TabBar.css'; + +import type { TabID } from './DevTools'; + +export type Props = {| + currentTab: TabID, + selectTab: (tabID: TabID) => void, +|}; + +export default function TabBar({ currentTab, selectTab }: Props) { + const onChange = useCallback( + ({ currentTarget }) => selectTab(currentTarget.value), + [selectTab] + ); + + return ( +
+ DevTools {process.env.DEVTOOLS_VERSION} +
+ + + +
+
+ ); +} diff --git a/src/devtools/views/Tree.css b/src/devtools/views/Tree.css index f43c12e7a2..fab177f808 100644 --- a/src/devtools/views/Tree.css +++ b/src/devtools/views/Tree.css @@ -10,7 +10,6 @@ flex: 0 0 42px; display: flex; align-items: center; - font-size: var(--font-size-large); border-bottom: 1px solid var(--color-border); padding: 0.5rem; } @@ -24,7 +23,7 @@ .List { font-family: var(--font-family-monospace); - font-size: var(--font-size-normal); + font-size: var(--font-size-monospace-normal); line-height: var(--line-height-data); } diff --git a/src/devtools/views/hooks.js b/src/devtools/views/hooks.js new file mode 100644 index 0000000000..0e18914863 --- /dev/null +++ b/src/devtools/views/hooks.js @@ -0,0 +1,43 @@ +// @flow + +import { useState } from 'react'; + +// https://usehooks.com/useLocalStorage/ +export function useLocalStorage( + key: string, + initialValue: T +): [T, (value: T | (() => T)) => void] { + // State to store our value + // Pass initial state function to useState so logic is only executed once + const [storedValue, setStoredValue] = useState(() => { + try { + // Get from local storage by key + const item = window.localStorage.getItem(key); + // Parse stored json or if none return initialValue + return item ? JSON.parse(item) : initialValue; + } catch (error) { + // If error also return initialValue + console.log(error); + return initialValue; + } + }); + + // Return a wrapped version of useState's setter function that ... + // ... persists the new value to localStorage. + const setValue = value => { + try { + // Allow value to be a function so we have same API as useState + const valueToStore = + value instanceof Function ? value(storedValue) : value; + // Save state + setStoredValue(valueToStore); + // Save to local storage + window.localStorage.setItem(key, JSON.stringify(valueToStore)); + } catch (error) { + // A more advanced implementation would handle the error case + console.log(error); + } + }; + + return [storedValue, setValue]; +} diff --git a/src/devtools/views/root.css b/src/devtools/views/root.css index 8e9b4fc974..1df9ed50ff 100644 --- a/src/devtools/views/root.css +++ b/src/devtools/views/root.css @@ -1,4 +1,8 @@ :root { + /** + * IMPORTANT: When new theme variables are added below– also add them to SettingsContext updateThemeVariables() + */ + /* Light theme */ --theme-light-arrow: #777d88; --theme-light-arrow-inverted: #ffffff; @@ -6,7 +10,7 @@ --theme-light-attribute-value: #1a1aa6; --theme-light-background: #ffffff; --theme-light-button: #0088fa; - --theme-light-button-hover: #1a1aa6; + --theme-light-button-hover: #3578e5; --theme-light-color-border: #eeeeee; --theme-light-component-name: #8155cb; --theme-light-component-name-inverted: #ffffff; @@ -41,35 +45,21 @@ --theme-dark-search-match-current: #f7923b; --theme-dark-text-color: #ffffff; --theme-dark-tree-node-selected: #31a38d; - --theme-dark-tree-node-hover: #23272f; + --theme-dark-tree-node-hover: #303846; - /* "theme" values referenced by other stylesheets */ - --color-arrow: var(--theme-light-arrow); - --color-arrow-inverted: var(--theme-light-arrow-inverted); - --color-attribute-name: var(--theme-light-attribute-name); - --color-attribute-value: var(--theme-light-attribute-value); - --color-background: var(--theme-light-background); - --color-border: var(--theme-light-color-border); - --color-button: var(--theme-light-button); - --color-button-hover: var(--theme-light-button-hover); - --color-component-name: var(--theme-light-component-name); - --color-component-name-inverted: var(--theme-light-component-name-inverted); - --color-dim: var(--theme-light-dim); - --color-dimmer: var(--theme-light-dimmer); - --color-dimmest: var(--theme-light-dimmest); - --color-search-match: var(--theme-light-search-match); - --color-search-match-current: var(--theme-light-search-match-current); - --color-text-color: var(--theme-light-text-color); - --color-tree-jsx-arrow-brackets: var(--theme-light-jsx-arrow-brackets); - --color-tree-jsx-arrow-brackets-inverted: var( - --theme-light-jsx-arrow-brackets-inverted - ); - --color-tree-node-selected: var(--theme-light-tree-node-selected); - --color-tree-node-hover: var(--theme-light-tree-node-hover); + /* Compact density */ + --compact-font-size-monospace-normal: 11px; + --compact-font-size-monospace-large: 15px; + --compact-font-size-sans-normal: 12px; + --compact-font-size-sans-large: 14px; + --compact-line-height-data: 18px; - --font-size-normal: 11px; - --font-size-large: 15px; - --line-height-data: 18px; + /* Comfortable density */ + --comfortable-font-size-monospace-normal: 13px; + --comfortable-font-size-monospace-large: 17px; + --comfortable-font-size-sans-normal: 14px; + --comfortable-font-size-sans-large: 16px; + --comfortable-line-height-data: 22px; /* GitHub.com system fonts */ --font-family-monospace: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo,