diff --git a/shells/dev/index.html b/shells/dev/index.html
index 3d9c03dc2d..021f696b78 100644
--- a/shells/dev/index.html
+++ b/shells/dev/index.html
@@ -8,7 +8,6 @@
#target {
flex: 1;
border: none;
- border-bottom: 1px solid #ccc;
}
#devtools {
display: flex;
diff --git a/src/devtools/store.js b/src/devtools/store.js
index 49bc2d607a..b698afd775 100644
--- a/src/devtools/store.js
+++ b/src/devtools/store.js
@@ -105,14 +105,16 @@ export default class Store extends EventEmitter {
return ((currentElement: any): Element);
}
- getElementByID(id: number): Element {
+ getElementByID(id: number): Element | null {
const element = this._idToElement.get(id);
if (element == null) {
- throw Error(`No element found with id "${id}`);
+ console.warn(`No element found with id "${id}`);
+
+ return null;
}
- return ((element: any): Element);
+ return element;
}
onBridgeOperations = (operations: Uint32Array) => {
diff --git a/src/devtools/views/SearchIcon.css b/src/devtools/views/ButtonIcon.css
similarity index 84%
rename from src/devtools/views/SearchIcon.css
rename to src/devtools/views/ButtonIcon.css
index a3ebe2def4..85c6a98bb7 100644
--- a/src/devtools/views/SearchIcon.css
+++ b/src/devtools/views/ButtonIcon.css
@@ -1,4 +1,4 @@
-.Icon {
+.ButtonIcon {
width: 1rem;
height: 1rem;
margin: 0 0.25rem;
diff --git a/src/devtools/views/ButtonIcon.js b/src/devtools/views/ButtonIcon.js
new file mode 100644
index 0000000000..5174e71e17
--- /dev/null
+++ b/src/devtools/views/ButtonIcon.js
@@ -0,0 +1,55 @@
+// @flow
+
+import React from 'react';
+import styles from './ButtonIcon.css';
+
+type Props = {|
+ type: 'search' | 'view-dom' | 'view-source',
+|};
+
+export default function ButtonIcon({ type }: Props) {
+ let pathData = null;
+ switch (type) {
+ case 'search':
+ pathData = PATH_SEARCH;
+ break;
+ case 'view-dom':
+ pathData = PATH_VIEW_DOM;
+ break;
+ case 'view-source':
+ pathData = PATH_VIEW_SOURCE;
+ break;
+ default:
+ console.warn(`Unsupported type "${type}" specified for ButtonIcon`);
+ break;
+ }
+
+ return (
+
+ );
+}
+
+const PATH_SEARCH = `
+ M20.94 11c-.46-4.17-3.77-7.48-7.94-7.94V1h-2v2.06C6.83 3.52 3.52 6.83 3.06 11H1v2h2.06c.46
+ 4.17 3.77 7.48 7.94 7.94V23h2v-2.06c4.17-.46 7.48-3.77 7.94-7.94H23v-2h-2.06zM12 19c-3.87
+ 0-7-3.13-7-7s3.13-7 7-7 7 3.13 7 7-3.13 7-7 7z
+`;
+
+const PATH_VIEW_DOM = `
+ M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12
+ 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3
+ 3-1.34 3-3-1.34-3-3-3z
+`;
+
+const PATH_VIEW_SOURCE = `
+ 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
+ `;
diff --git a/src/devtools/views/Element.css b/src/devtools/views/Element.css
index 3a33050ceb..24f54d909b 100644
--- a/src/devtools/views/Element.css
+++ b/src/devtools/views/Element.css
@@ -1,14 +1,26 @@
-.Element {
+.Element,
+.SelectedElement {
width: 100%;
border-radius: 0.25em;
cursor: pointer;
position: relative;
white-space: nowrap;
+ line-height: 20px;
}
.Element:hover {
background-color: var(--color-state03);
}
+.SelectedElement {
+ background-color: var(--color-state00);
+
+ /* Invert colors */
+ --color-component: var(--color-base00);
+ --color-tree-tag: rgba(255, 255, 255, 0.8);
+ --color-tree-attr-name: #b3e5fc;
+ --color-tree-attr-value: #fff;
+}
+
.ArrowClosed,
.ArrowOpen {
position: absolute;
diff --git a/src/devtools/views/Element.js b/src/devtools/views/Element.js
index 2f7a9c94bc..b625827fdf 100644
--- a/src/devtools/views/Element.js
+++ b/src/devtools/views/Element.js
@@ -1,7 +1,8 @@
// @flow
-import React, { Fragment, useContext } from 'react';
+import React, { Fragment, useCallback, useContext } from 'react';
import { TreeContext } from './context';
+import { SelectedElementContext } from './SelectedElementContext';
import styles from './Element.css';
@@ -22,13 +23,24 @@ export default function Element({ index, style }: Props) {
return null;
}
- const { children, depth, displayName, key } = element;
+ const { children, depth, displayName, id, key } = element;
- // TODO: Add state for toggling element open/close
+ const selectedElement = useContext(SelectedElementContext);
+ const handleClick = useCallback(
+ ({ metaKey }) => {
+ selectedElement.id = metaKey ? null : id;
+ },
+ [id]
+ );
+
+ // TODO: Add click and key handlers for toggling element open/close state.
return (
store.removeListener('rootCommitted', handler);
}, [store]);
+ // TODO Flex wrappers below should be user resizable.
return (
-
-
-
-
+
+
-
-
-
-
+
);
diff --git a/src/devtools/views/SearchIcon.js b/src/devtools/views/SearchIcon.js
deleted file mode 100644
index 1d4f63eb1f..0000000000
--- a/src/devtools/views/SearchIcon.js
+++ /dev/null
@@ -1,22 +0,0 @@
-// @flow
-
-import React from 'react';
-import styles from './SearchIcon.css';
-
-export default function SearchIcon() {
- return (
-
- );
-}
diff --git a/src/devtools/views/SelectedElement.css b/src/devtools/views/SelectedElement.css
new file mode 100644
index 0000000000..140eb9e032
--- /dev/null
+++ b/src/devtools/views/SelectedElement.css
@@ -0,0 +1,54 @@
+.SelectedElement {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ width: 100%;
+ border-left: 1px solid var(--color-base02);
+ border-top: 1px solid var(--color-base02);
+}
+
+.TitleRow {
+ flex: 0 0 42px;
+ display: flex;
+ align-items: center;
+ font-size: 16px;
+ border-bottom: 1px solid var(--color-base02);
+ padding: 0.5rem;
+}
+
+.IconButton {
+ flex: 0 0 auto;
+ border: none;
+ background: none;
+ display: inline-flex;
+ align-items: center;
+ padding: 0.25rem;
+ cursor: pointer;
+ color: var(--color-state00);
+}
+.IconButton:hover {
+ color: var(--color-special02);
+}
+
+.SelectedComponentName {
+ flex: 1 1 auto;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.Component {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ color: var(--color-component);
+ flex: 1 1 auto;
+}
+.Component:before {
+ white-space: nowrap;
+ content: '<';
+ color: var(--color-tree-tag);
+}
+.Component:after {
+ white-space: nowrap;
+ content: '>';
+ color: var(--color-tree-tag);
+}
diff --git a/src/devtools/views/SelectedElement.js b/src/devtools/views/SelectedElement.js
new file mode 100644
index 0000000000..37f5c3c606
--- /dev/null
+++ b/src/devtools/views/SelectedElement.js
@@ -0,0 +1,50 @@
+// @flow
+
+import React, { useContext } from 'react';
+import { SelectedElementContext } from './SelectedElementContext';
+import { StoreContext } from './context';
+import ButtonIcon from './ButtonIcon';
+import styles from './SelectedElement.css';
+
+export type Props = {||};
+
+export default function SelectedElement(props: Props) {
+ const { id } = useContext(SelectedElementContext);
+ const store = useContext(StoreContext);
+ const element = id !== null ? store.getElementByID(id) : null;
+
+ // TODO Show/hide source button based on whether debug "source" is available.
+
+ if (element === null) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+
+ {element.displayName}
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/devtools/views/SelectedElementContext.js b/src/devtools/views/SelectedElementContext.js
new file mode 100644
index 0000000000..7d7afce7a4
--- /dev/null
+++ b/src/devtools/views/SelectedElementContext.js
@@ -0,0 +1,41 @@
+// @flow
+
+import React, { createContext, useMemo, useState } from 'react';
+
+type SelectedElementContextValue = {|
+ id: number | null,
+|};
+
+const SelectedElementContext = createContext
(
+ ((null: any): SelectedElementContextValue)
+);
+// $FlowFixMe displayName is a valid attribute of React$Context
+SelectedElementContext.displayName = 'SelectedElementContext';
+
+type Props = {|
+ children: React$Node,
+|};
+
+// TODO Remove this wrapper element once global Context.write API exists.
+function SelectedElementController({ children }: Props) {
+ const [id, setID] = useState(null);
+ const value = useMemo(
+ () => ({
+ get id() {
+ return id;
+ },
+ set id(id: number | null) {
+ setID(id);
+ },
+ }),
+ [id]
+ );
+
+ return (
+
+ {children}
+
+ );
+}
+
+export { SelectedElementContext, SelectedElementController };
diff --git a/src/devtools/views/Tree.css b/src/devtools/views/Tree.css
index 73c69432ab..a168eab550 100644
--- a/src/devtools/views/Tree.css
+++ b/src/devtools/views/Tree.css
@@ -1,4 +1,54 @@
.Tree {
+ height: 100%;
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ border-top: 1px solid var(--color-base02);
+}
+
+.SearchRow {
+ flex: 0 0 42px;
+ display: flex;
+ align-items: center;
+ font-size: 16px;
+ border-bottom: 1px solid var(--color-base02);
+ padding: 0.5rem;
+}
+
+.SearchInput {
+ font-size: 1rem;
+ flex: 1;
+ outline: none;
+ border: none;
+ background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath d='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'/%3E%3Cpath d='M0 0h24v24H0z' fill='none'/%3E%3C/svg%3E");
+ background-repeat: no-repeat;
+ background-position: left center;
+ background-size: 16px 16px;
+ padding-left: 1.5rem;
+}
+
+.IconButton {
+ border: none;
+ background: none;
+ display: inline-flex;
+ align-items: center;
+ padding: 0.25rem;
+ cursor: pointer;
+ color: var(--color-state00);
+ flex: 0 0 auto;
+}
+.IconButton:hover {
+ color: var(--color-special02);
+}
+
+.AutoSizerWrapper {
+ width: 100%;
+ overflow: auto;
+ flex: 1 0 auto;
+ padding: 0.25rem;
+}
+
+.List {
font-family: var(--font-family-monospace);
font-size: var(--font-size-tree-compact);
line-height: var(--line-height-tree-compact);
diff --git a/src/devtools/views/Tree.js b/src/devtools/views/Tree.js
index 39a14a3772..92bf94c04c 100644
--- a/src/devtools/views/Tree.js
+++ b/src/devtools/views/Tree.js
@@ -4,6 +4,7 @@ import React, { useContext } from 'react';
import AutoSizer from 'react-virtualized-auto-sizer';
import { FixedSizeList } from 'react-window';
import Element from './Element';
+import ButtonIcon from './ButtonIcon';
import { TreeContext } from './context';
import styles from './Tree.css';
@@ -13,20 +14,38 @@ type Props = {||};
export default function Tree(props: Props) {
const treeContext = useContext(TreeContext);
+ // TODO Add key handlers for selecting previous/next element.
+
return (
-
- {({ height, width }) => (
-
+
+
+
+
+
+
+ {({ height, width }) => (
+
+ {Element}
+
+ )}
+
+
+
);
}
diff --git a/src/devtools/views/root.css b/src/devtools/views/root.css
index 6bd6a48e7d..6e31b7a02e 100644
--- a/src/devtools/views/root.css
+++ b/src/devtools/views/root.css
@@ -33,6 +33,8 @@
/* GitHub.com system fonts */
--font-family-monospace: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo,
Courier, monospace;
+ --font-family-sans: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica,
+ Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;
--font-size-tree-compact: 12px;
--font-size-tree-normal: 14px;
}