mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Added new tabs UI and theme/display-density preferences
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"version": "4.0.0",
|
||||
"repository": "bvaughn/react-devtools-experimental",
|
||||
"license": "BSD-3-Clause",
|
||||
"private": true,
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
border: none;
|
||||
}
|
||||
#devtools {
|
||||
display: flex;
|
||||
height: 400px;
|
||||
max-height: 50%;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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: [
|
||||
{
|
||||
|
||||
@@ -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<Store>(() => new Store(bridge), []);
|
||||
const [tab, setTab] = useState(defaultTab);
|
||||
|
||||
let tabElement;
|
||||
switch (tab) {
|
||||
case 'profiler':
|
||||
tabElement = <Profiler />;
|
||||
break;
|
||||
case 'settings':
|
||||
tabElement = <Settings />;
|
||||
break;
|
||||
case 'elements':
|
||||
default:
|
||||
tabElement = <Elements />;
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<BridgeContext.Provider value={bridge}>
|
||||
<StoreContext.Provider value={store}>
|
||||
<SettingsContextController browserTheme={browserTheme}>
|
||||
<TreeContextController>
|
||||
{showTabBar && <TabBar currentTab={tab} selectTab={setTab} />}
|
||||
{tabElement}
|
||||
</TreeContextController>
|
||||
</SettingsContextController>
|
||||
</StoreContext.Provider>
|
||||
</BridgeContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -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<Store>(() => new Store(bridge), []);
|
||||
export type Props = {||};
|
||||
|
||||
export default function Elements(_: Props) {
|
||||
// TODO Flex wrappers below should be user resizable.
|
||||
return (
|
||||
<BridgeContext.Provider value={bridge}>
|
||||
<StoreContext.Provider value={store}>
|
||||
<TreeContextController>
|
||||
<div className={styles.Elements}>
|
||||
<div className={styles.TreeWrapper}>
|
||||
<Tree />
|
||||
</div>
|
||||
<div className={styles.SelectedElementWrapper}>
|
||||
<SelectedElement />
|
||||
</div>
|
||||
</div>
|
||||
</TreeContextController>
|
||||
</StoreContext.Provider>
|
||||
</BridgeContext.Provider>
|
||||
<div className={styles.Elements}>
|
||||
<div className={styles.TreeWrapper}>
|
||||
<Tree />
|
||||
</div>
|
||||
<div className={styles.SelectedElementWrapper}>
|
||||
<SelectedElement />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
`;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import styles from './Profiler.css';
|
||||
|
||||
export type Props = {||};
|
||||
|
||||
export default function Profiler(_: Props) {
|
||||
return <div className={styles.Profiler}>Under development...</div>;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
.ReactLogo {
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
margin: 0 0.75rem 0 0.25rem;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import styles from './ReactLogo.css';
|
||||
|
||||
export default function ReactLogo() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={styles.ReactLogo}
|
||||
viewBox="-11.5 -10.23174 23 20.46348"
|
||||
>
|
||||
<circle cx="0" cy="0" r="2.05" fill="currentColor" />
|
||||
<g stroke="currentColor" strokeWidth="1" fill="none">
|
||||
<ellipse rx="11" ry="4.2" />
|
||||
<ellipse rx="11" ry="4.2" transform="rotate(60)" />
|
||||
<ellipse rx="11" ry="4.2" transform="rotate(120)" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 (
|
||||
<div className={styles.Settings}>
|
||||
<div className={styles.Section}>
|
||||
<div className={styles.Header}>Theme</div>
|
||||
<div className={styles.OptionGroup}>
|
||||
<label className={styles.Option}>
|
||||
<input
|
||||
type="radio"
|
||||
name="Settings-Settings-theme"
|
||||
checked={theme === 'auto'}
|
||||
value="auto"
|
||||
onChange={updateTheme}
|
||||
/>{' '}
|
||||
Auto
|
||||
</label>
|
||||
<label className={styles.Option}>
|
||||
<input
|
||||
type="radio"
|
||||
name="Settings-theme"
|
||||
checked={theme === 'light'}
|
||||
value="light"
|
||||
onChange={updateTheme}
|
||||
/>{' '}
|
||||
Light
|
||||
</label>
|
||||
<label className={styles.Option}>
|
||||
<input
|
||||
type="radio"
|
||||
name="Settings-theme"
|
||||
checked={theme === 'dark'}
|
||||
value="dark"
|
||||
onChange={updateTheme}
|
||||
/>{' '}
|
||||
Dark
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.Section}>
|
||||
<div className={styles.Header}>Display density</div>
|
||||
<div className={styles.OptionGroup}>
|
||||
<label className={styles.Option}>
|
||||
<input
|
||||
type="radio"
|
||||
name="Settings-displayDensity"
|
||||
checked={displayDensity === 'compact'}
|
||||
value="compact"
|
||||
onChange={updateDisplayDensity}
|
||||
/>{' '}
|
||||
Compact
|
||||
</label>
|
||||
<label className={styles.Option}>
|
||||
<input
|
||||
type="radio"
|
||||
name="Settings-displayDensity"
|
||||
checked={displayDensity === 'comfortable'}
|
||||
value="comfortable"
|
||||
onChange={updateDisplayDensity}
|
||||
/>{' '}
|
||||
Comfortable
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<Context>(((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>(
|
||||
'displayDensity',
|
||||
'compact'
|
||||
);
|
||||
const [theme, setTheme] = useLocalStorage<Theme>('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 (
|
||||
<SettingsContext.Provider value={value}>
|
||||
{children}
|
||||
</SettingsContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
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 };
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 (
|
||||
<div className={styles.TabBar}>
|
||||
<ReactLogo /> DevTools {process.env.DEVTOOLS_VERSION}
|
||||
<div className={styles.Tabs}>
|
||||
<label
|
||||
className={currentTab === 'elements' ? styles.TabCurrent : styles.Tab}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="TabBar-tab"
|
||||
className={styles.Input}
|
||||
checked={currentTab === 'elements'}
|
||||
value="elements"
|
||||
onChange={onChange}
|
||||
/>
|
||||
<Icon className={styles.Icon} type="elements" />
|
||||
Elements
|
||||
</label>
|
||||
<label
|
||||
className={currentTab === 'profiler' ? styles.TabCurrent : styles.Tab}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="TabBar-tab"
|
||||
className={styles.Input}
|
||||
checked={currentTab === 'profiler'}
|
||||
value="profiler"
|
||||
onChange={onChange}
|
||||
/>
|
||||
<Icon className={styles.Icon} type="profiler" />
|
||||
Profiler
|
||||
</label>
|
||||
<label
|
||||
className={currentTab === 'settings' ? styles.TabCurrent : styles.Tab}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="TabBar-tab"
|
||||
className={styles.Input}
|
||||
checked={currentTab === 'settings'}
|
||||
value="settings"
|
||||
onChange={onChange}
|
||||
/>
|
||||
<Icon className={styles.Icon} type="settings" />
|
||||
Settings
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
// @flow
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
// https://usehooks.com/useLocalStorage/
|
||||
export function useLocalStorage<T>(
|
||||
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];
|
||||
}
|
||||
+18
-28
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user