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; }