Initial commit

This commit is contained in:
Brian Vaughn committed 2019-01-22 11:04:37 -08:00
commit 5e0dfdac54
43 files changed
+12581

No files matched your search

+14
View File
@@ -0,0 +1,14 @@
// @flow
import type { Bridge } from '../types';
type Shell = {|
connect: (callback: Function) => void,
onReload: (reloadFn: Function) => void,
|};
export function initDevTools(shell: Shell) {
shell.connect((bridge: Bridge) => {
// TODO ...
});
}
+78
View File
@@ -0,0 +1,78 @@
// @flow
import EventEmitter from 'events';
import type { Element } from './types';
import type { Bridge } from '../types';
/**
* The store is the single source of truth for updates from the backend.
* ContextProviders can subscribe to the Store for specific things they want to provide.
*/
class Store extends EventEmitter {
_idToElement: Map<string, Element> = new Map();
_idToParentID: Map<string, string> = new Map();
roots: Set<string> = new Set();
constructor(bridge: Bridge) {
super();
bridge.on('root', this.onBridgeRoot);
bridge.on('mount', this.onBridgeMount);
bridge.on('update', this.onBridgeUpdated);
bridge.on('unmount', this.onBridgeUnmounted);
}
getElement(id: string) {
return this._idToElement.get(id);
}
geParent(id: string) {
return this._idToParentID.get(id);
}
onBridgeMount = (element: Element) => {
const {id} = element;
console.log('%cStore%c onBridgeMount()', 'color: red; font-weight: bold;', 'font-weight: bold;', element);
this._idToElement.set(id, element);
element.children.forEach(childID => {
this._idToParentID.set(childID, id);
});
this.emit(id);
};
onBridgeRoot = (id: string) => {
console.log('%cStore%c onBridgeRoot()', 'color: red; font-weight: bold;', 'font-weight: bold;', id);
if (!this.roots.has(id)) {
this.roots.add(id);
this.emit('roots');
}
};
onBridgeUnmounted = (id: string) => {
console.log('%cStore%c onBridgeUnmounted()', 'color: red; font-weight: bold;', 'font-weight: bold;', id);
this._idToElement.delete(id);
if (this._idToParentID.has(id)) {
this._idToParentID.delete(id);
}
if (this.roots.has(id)) {
this.roots.delete(id);
this.emit('roots');
}
};
onBridgeUpdated = (element: Element) => {
const {id} = element;
console.log('%cStore%c onBridgeUpdated()', 'color: red; font-weight: bold;', 'font-weight: bold;', element);
this._idToElement.set(id, element);
this.emit(id);
};
}
export default Store;
+38
View File
@@ -0,0 +1,38 @@
// @flow
export const ElementTypeClassOrFunction = 1;
export const ElementTypeContext = 2;
export const ElementTypeForwardRef = 3;
export const ElementTypeMemo = 4;
export const ElementTypeOtherOrUnknown = 5;
export const ElementTypeProfiler = 6;
export const ElementTypeSuspense = 7;
export type ElementType =
| 1
| 2
| 3
| 4
| 5
| 6
| 7;
// TODO: Add profiling node
export type Element = {|
id: string,
type: ElementType,
key: React$Key | null,
displayName: string | null,
children: Array<string>,
|};
export type InspectedElement = {|
id: string,
context: Object | null,
hooks: Object | null,
props: Object | null,
state: Object | null,
canEditProps: boolean,
source: Object,
|};
+49
View File
@@ -0,0 +1,49 @@
:root {
/* TODO */
--color-base00: #ffffff;
--color-base01: #f3f3f3;
--color-base02: #eeeeee;
--color-base03: #dadada;
--color-base04: #333333;
--color-base05: #5a5a5a;
--color-special00: #8155cb;
--color-special01: #222222;
--color-special02: #1a1aa6;
--color-special03: #c80000;
--color-special04: #236e25;
--color-special05: #aa0d91;
--color-special06: #ef6632;
--color-special07: #777d88;
--color-state00: #0088FA;
--color-state01: #dadada;
--color-state02: #ffffff;
--color-state03: #ebf1fb;
--color-state04: #FFFF00;
--color-state05: #222222;
--color-state06: #222222;
--color-component: var(--color-special00);
--color-tree-tag: var(--color-base04);
--color-tree-attr-name: var(--color-special06);
--color-tree-attr-value: var(--color-special02);
--line-height-tree-compact: 1.5;
--line-height-tree-normal: 1.5;
/* GitHub.com system fonts */
--font-family-monospace: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
--font-size-tree-compact: 12px;
--font-size-tree-normal: 14px;
}
.App {
width: 100%;
height: 100%;
overflow: auto;
-webkit-font-smoothing: antialiased;
}
* {
box-sizing: border-box;
}
+25
View File
@@ -0,0 +1,25 @@
// @flow
import React, { useMemo } from 'react';
import Store from '../store';
import Tree from './Tree';
import {StoreContext} from './contexts';
import styles from './App.css';
import type {Bridge} from '../../types';
type Props = {|
bridge: Bridge,
|};
export default function App({ bridge }: Props) {
const store = useMemo(() => new Store(bridge), []);
return (
<StoreContext.Provider value={store}>
<div className={styles.App}>
<Tree />
</div>
</StoreContext.Provider>
);
}
+46
View File
@@ -0,0 +1,46 @@
.Element {
width: 100%;
border-radius: 0.25em;
cursor: pointer;
position: relative;
}
.Element:hover {
background-color: var(--color-state03);
}
.ArrowClosed,
.ArrowOpen {
position: absolute;
margin-left: -1rem;
width: 1rem;
height: 1rem;
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 fill='%23777d88' d='M8 5v14l11-7z'/%3E%3Cpath fill='none' d='M0 24V0h24v24H0z'/%3E%3C/svg%3E");
background-size: 12px 12px;
background-repeat: no-repeat;
background-position: center;
}
.ArrowOpen {
transform: rotate(90deg);
}
.Component {
color: var(--color-component);
}
.Component:before {
white-space: nowrap;
content: "<";
color: var(--color-tree-tag);
}
.Component:after {
white-space: nowrap;
content: ">";
color: var(--color-tree-tag);
}
.AttributeName {
color: var(--color-tree-attr-name);
}
.AttributeValue {
color: var(--color-tree-attr-value);
}
+43
View File
@@ -0,0 +1,43 @@
// @flow
import React, {Fragment, useContext} from 'react';
import {useElement} from './hooks';
import {StoreContext} from './contexts';
import styles from './Element.css';
type Props = {|
depth: number,
id: string,
|};
export default function Element({depth, id}: Props) {
const store = useContext(StoreContext);
const element = useElement(store, id);
const {children, displayName, key} = element;
// TODO: Toggle open/close state
return (
<Fragment>
<div className={styles.Element} style={{paddingLeft: `${1 + depth}rem`}}>
{children.length > 0 && (
<span className={styles.ArrowOpen}></span>
)}
<span className={styles.Component}>
{displayName}{key && (
<Fragment>
&nbsp;<span className={styles.AttributeName}>key</span>=<span className={styles.AttributeValue}>"{key}"</span>
</Fragment>
)}
</span>
</div>
{children.map(childID => (
<Element key={childID} depth={depth + 1} id={childID} />
))}
</Fragment>
);
}
+11
View File
@@ -0,0 +1,11 @@
.Tree {
width: 100%;
padding: 0.25rem;
overflow: auto;
font-family: var(--font-family-monospace);
font-size: var(--font-size-tree-compact);
/* TODO */
line-height: var(--line-height-tree-compact);
}
+34
View File
@@ -0,0 +1,34 @@
// @flow
import React, {useContext} from 'react';
import Element from './Element';
import {StoreContext} from './contexts';
import {useElement, useRoots} from './hooks';
import styles from './Tree.css';
type TreeProps = {||};
export default function Tree({}: TreeProps) {
const store = useContext(StoreContext);
const roots = useRoots(store);
return (
<div className={styles.Tree}>
{roots.map(id => <Root key={id} id={id} />)}
</div>
);
}
type RootProps = {|
id: string,
|};
function Root({id}: RootProps) {
const store = useContext(StoreContext);
const element = useElement(store, id);
return element.children.map(childID => (
<Element key={childID} depth={0} id={childID} />
));
}
+8
View File
@@ -0,0 +1,8 @@
// @flow
import { createContext } from 'react';
import Store from '../store';
export const RootsContext = createContext<Array<string>>([]);
export const StoreContext = createContext<Store>(((null: any): Store));
+30
View File
@@ -0,0 +1,30 @@
// @flow
import {createContext, useLayoutEffect, useRef, useState} from 'react';
import type {Element} from '../types';
import Store from '../store';
export function useElement(store: Store, id: string): Element {
const [element, setElement] = useState<Element>(((store.getElement(id): any): Element));
useLayoutEffect(() => {
const handler = () => setElement(((store.getElement(id): any): Element));
store.addListener(id, handler);
return () => store.removeListener(id, handler);
}, [store, id]);
return element;
}
export function useRoots(store: Store): Array<string> {
const [roots, setRoots] = useState<Array<string>>(Array.from(store.roots));
useLayoutEffect(() => {
const handler = () => setRoots(Array.from(store.roots));
store.addListener('roots', handler);
return () => store.removeListener('roots', handler);
}, [store]);
return roots;
}