website v2 skeleton

Summary:
Sets up a skeleton for the new yoga website using gatsby static site generator

allow-large-files

Reviewed By: emilsjolander

Differential Revision: D6952326

fbshipit-source-id: 7579bc80bec21552689da5b78f3d960910ff13bb
This commit is contained in:
Daniel Büchele
2018-02-12 09:28:31 -08:00
committed by Facebook Github Bot
parent b08bd572ef
commit e43bb9da19
49 changed files with 25219 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
.DocsSidebar {
width: 350px;
padding: 20px;
}

View File

@@ -0,0 +1,22 @@
/**
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
* @format
*/
import React from 'react';
import './DocsSidebar.css';
type Props = {
children: any,
};
export default (props: Props) => (
<div className="DocsSidebar">{props.children}</div>
);

View File

@@ -0,0 +1,10 @@
.Footer {
background: #1c2126;
min-height: 100px;
color: rgba(255, 255, 255, 0.6);
padding: 20px 0;
}
.Footer a {
color: white;
}

View File

@@ -0,0 +1,21 @@
/**
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
* @format
*/
import React from 'react';
import Padded from './Padded';
import './Footer.css';
export default () => (
<footer className="Footer">
<Padded>footer</Padded>
</footer>
);

View File

@@ -0,0 +1,7 @@
.Padded {
width: 100%;
max-width: 1000px;
padding: 0 20px;
margin-left: auto;
margin-right: auto;
}

View File

@@ -0,0 +1,23 @@
/**
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
* @format
*/
import React from 'react';
import './Padded.css';
type Props = {
children: any,
className?: string,
};
export default (props: Props) => (
<div className={`Padded ${props.className || ''}`}>{props.children}</div>
);

View File

@@ -0,0 +1,27 @@
@import url('https://fonts.googleapis.com/css?family=Barlow');
html,
body {
margin: 0;
padding: 0;
font-family: 'Barlow', sans-serif;
color: #303845;
font-size: 16px;
}
h1,
h2 {
font-weight: 500;
}
.Page {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.PageContent {
display: flex;
flex-direction: column;
flex-grow: 1;
}

View File

@@ -0,0 +1,38 @@
/**
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
* @format
*/
import React from 'react';
import Toolbar from './Toolbar';
import Footer from './Footer';
import './Page.css';
type Props = {|
children: any,
title?: string,
className?: string,
|};
export default (props: Props) => (
<div className={`Page ${props.className || ''}`}>
{/* <Head>
<link
rel="stylesheet"
href="//cdnjs.cloudflare.com/ajax/libs/antd/3.2.0/antd.min.css"
/>
<link href="//fonts.googleapis.com/css?family=Barlow" rel="stylesheet" />
<title>Yoga Layout{props.title ? ` | ${props.title}` : ''}</title>
</Head> */}
<Toolbar />
<div className="PageContent">{props.children}</div>
<Footer />
</div>
);

View File

@@ -0,0 +1,20 @@
.Code {
display: flex;
flex-direction: column;
max-height: 100%;
}
.CodeContainer {
overflow: scroll;
}
.CodeLanguageSelector.ant-radio-group {
display: flex;
margin: 10px 15px;
}
.CodeLanguageSelector.ant-radio-group .ant-radio-button-wrapper {
flex-grow: 1;
flex-basis: 0;
text-align: center;
}

View File

@@ -0,0 +1,114 @@
/**
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
* @format
*/
import React, {Component} from 'react';
import SyntaxHighlighter, {
registerLanguage,
} from 'react-syntax-highlighter/prism-light';
import styles from 'react-syntax-highlighter/styles/prism/prism';
import jsx from 'react-syntax-highlighter/languages/prism/jsx';
//import javascript from 'react-syntax-highlighter/languages/prism/javascript';
import java from 'react-syntax-highlighter/languages/prism/java';
import objectivec from 'react-syntax-highlighter/languages/prism/objectivec';
import type {LayoutRecordT} from './LayoutRecord';
import {Radio} from 'antd';
import CodeJavaScript from './CodeJavaScript';
import CodeLitho from './CodeLitho';
import CodeReactNative from './CodeReactNative';
import CodeComponentKit from './CodeComponentKit';
import './Code.css';
import type {Yoga$Direction} from 'yoga-layout';
const RadioButton = Radio.Button;
const RadioGroup = Radio.Group;
registerLanguage('jsx', jsx);
//registerLanguage('javascript', javascript);
registerLanguage('java', java);
registerLanguage('objectivec', objectivec);
type Language = 'JavaScript' | 'Litho' | 'ComponentKit' | 'React Native';
type Props = {
layoutDefinition: LayoutRecordT,
direction: Yoga$Direction,
languages: Array<Language>,
};
type State = {
language: Language,
};
export default class Code extends Component<Props, State> {
static defaultProps = {
languages: [/*'JavaScript', */ 'Litho', 'ComponentKit', 'React Native'],
};
state = {
language: 'Litho',
};
generateCode(lang: Language): string {
if (lang === 'JavaScript') {
return CodeJavaScript(this.props.layoutDefinition, this.props.direction);
} else if (lang === 'Litho') {
return CodeLitho(this.props.layoutDefinition, this.props.direction);
} else if (lang === 'ComponentKit') {
return CodeComponentKit(
this.props.layoutDefinition,
this.props.direction,
);
} else if (lang === 'React Native') {
return CodeReactNative(this.props.layoutDefinition, this.props.direction);
} else {
return '';
}
}
getLanguage(): string {
if (this.state.language === 'Litho') {
return 'java';
} else if (this.state.language === 'React Native') {
return 'javascript';
} else if (this.state.language === 'ComponentKit') {
return 'objectivec';
} else {
return this.state.language;
}
}
render() {
return (
<div className="Code">
<RadioGroup
className="CodeLanguageSelector"
onChange={e => this.setState({language: e.target.value})}
value={this.state.language}>
{this.props.languages.map(lang => (
<RadioButton key={lang} value={lang}>
{lang}
</RadioButton>
))}
</RadioGroup>
<div className="CodeContainer">
<SyntaxHighlighter
language={this.getLanguage()}
style={styles}
customStyle={{fontSize: '13px', backgroundColor: 'white'}}
lineNumberStyle={{userSelect: 'none', opacity: 0.5}}
codeTagProps={{style: {tabSize: 4}}}
showLineNumbers>
{this.generateCode(this.state.language)}
</SyntaxHighlighter>
</div>
</div>
);
}
}

View File

@@ -0,0 +1,184 @@
// @flow
import yoga from 'yoga-layout';
import LayoutRecord from './LayoutRecord';
import PositionRecord from './PositionRecord';
import type {LayoutRecordT} from './LayoutRecord';
import type {Yoga$Direction /* Yoga$Node */} from 'yoga-layout';
type Yoga$Node = any;
const enumLookup = {
flexDirection: {
[yoga.FLEX_DIRECTION_COLUMN]: 'CKFlexboxDirectionVertical',
[yoga.FLEX_DIRECTION_ROW]: 'CKFlexboxDirectionHorizontal',
[yoga.FLEX_DIRECTION_COLUMN_REVERSE]: 'CKFlexboxDirectionVerticalReverse',
[yoga.FLEX_DIRECTION_ROW_REVERSE]: 'CKFlexboxDirectionHorizontalReverse',
},
alignItems: {
[yoga.ALIGN_FLEX_START]: 'CKFlexboxAlignItemsStart',
[yoga.ALIGN_FLEX_END]: 'CKFlexboxAlignItemsEnd',
[yoga.ALIGN_CENTER]: 'CKFlexboxAlignItemsCenter',
[yoga.ALIGN_BASELINE]: 'CKFlexboxAlignItemsBaseline',
[yoga.ALIGN_STRETCH]: 'CKFlexboxAlignItemsStretch',
},
alignSelf: {
[yoga.ALIGN_AUTO]: 'CKFlexboxAlignSelfAuto',
[yoga.ALIGN_FLEX_START]: 'CKFlexboxAlignSelfStart',
[yoga.ALIGN_FLEX_END]: 'CKFlexboxAlignSelfEnd',
[yoga.ALIGN_CENTER]: 'CKFlexboxAlignSelfCenter',
[yoga.ALIGN_BASELINE]: 'CKFlexboxAlignSelfBaseline',
[yoga.ALIGN_STRETCH]: 'CKFlexboxAlignSelfStretch',
},
alignContent: {
[yoga.ALIGN_FLEX_START]: 'CKFlexboxAlignContentStart',
[yoga.ALIGN_FLEX_END]: 'CKFlexboxAlignContentEnd',
[yoga.ALIGN_CENTER]: 'CKFlexboxAlignContentCenter',
[yoga.ALIGN_SPACE_BETWEEN]: 'CKFlexboxAlignContentSpaceBetween',
[yoga.ALIGN_SPACE_AROUND]: 'CKFlexboxAlignContentSpaceAround',
[yoga.ALIGN_STRETCH]: 'CKFlexboxAlignContentStretch',
},
justifyContent: {
[yoga.JUSTIFY_FLEX_START]: 'CKFlexboxJustifyContentStart',
[yoga.JUSTIFY_CENTER]: 'CKFlexboxJustifyContentCenter',
[yoga.JUSTIFY_FLEX_END]: 'CKFlexboxJustifyContentEnd',
[yoga.JUSTIFY_SPACE_BETWEEN]: 'CKFlexboxJustifyContentSpaceBetween',
[yoga.JUSTIFY_SPACE_AROUND]: 'CKFlexboxJustifyContentSpaceAround',
},
flexWrap: {
[yoga.WRAP_NO_WRAP]: 'CKFlexboxWrapNoWrap',
[yoga.WRAP_WRAP]: 'CKFlexboxWrapWrap',
[yoga.WRAP_WRAP_REVERSE]: 'CKFlexboxWrapWrapReverse',
},
positionType: {
[yoga.POSITION_TYPE_RELATIVE]: 'CKFlexboxPositionTypeRelative',
[yoga.POSITION_TYPE_ABSOLUTE]: 'CKFlexboxPositionTypeAbsolute',
},
};
const untouchedLayout = LayoutRecord({});
const untouchedPosition = PositionRecord({});
function keyLookup(key: string): string {
const keyLookup = {
flexWrap: 'wrap',
flexDirection: 'direction',
};
return keyLookup[key] || key;
}
function getLayoutCode(
node: LayoutRecordT,
indent: string = '',
isRoot?: boolean,
): string {
const lines = [];
const isFlexbox = node.children.size > 0;
lines.push(
indent +
`${isRoot ? '' : `.component = \n${indent}`}[${
isFlexbox ? 'CKFlexboxComponent' : 'CKComponent'
}`,
);
lines.push(indent + ` newWithView:{}`);
lines.push(indent + ` size:{${node.width},${node.height}}`);
const CKFlexboxComponentStyle = [
'direction',
'margin',
'justifyContent',
'alignItems',
'alignContent',
'wrap',
'padding',
'border',
];
const CKFlexboxComponentChild = [
'margin',
'padding',
'flexGrow',
'flexShrink',
'flexBasis',
'alignSelf',
'position',
];
if (isFlexbox) {
// render styles
lines.push(indent + ` style:{`);
indent += '\t';
CKFlexboxComponentStyle.forEach(key => {
let line = renderKey(node, key, indent);
if (line) {
lines.push(line);
}
});
indent = indent.substr(-1);
lines.push(indent + ` }`);
// render children
lines.push(indent + ' children:{');
lines.push(
...node.children
.toJSON()
.map(
child =>
`${indent}\t{\n${getLayoutCode(
child,
indent + '\t\t',
)}\n${indent}\t},`,
),
);
lines.push(indent + `}]${isRoot ? ';' : ''}`);
} else {
lines[lines.length - 1] += ']';
CKFlexboxComponentChild.forEach(key => {
let line = renderKey(node, key, indent);
if (line) {
lines.push(line);
}
});
}
return lines.join('\n');
}
function renderKey(node: Yoga$Node, key: string, indent: string): ?string {
if (
node[key] instanceof PositionRecord &&
!node[key].equals(untouchedPosition)
) {
const lines = [];
lines.push(indent + `.${key} = {`);
if (key === 'position') {
lines.push(
indent + `\t.type = ${enumLookup.positionType[node.positionType]},`,
);
}
['top', 'left', 'right', 'bottom'].forEach(pKey => {
if (node[key][pKey]) {
lines.push(indent + `\t.${pKey} = ${node[key][pKey]},`);
}
});
lines.push(indent + `},`);
return lines.join('\n');
} else if (node[key] !== untouchedLayout[key]) {
if (enumLookup[key]) {
return indent + `.${keyLookup(key)} = ${enumLookup[key][node[key]]},`;
} else {
console.error(`Unknown property ${key}`);
}
}
}
export default function generateCode(
root: LayoutRecordT,
direction: Yoga$Direction,
): string {
return ['CKFlexboxComponent *c =', getLayoutCode(root, '\t', true)].join(
'\n',
);
}

View File

@@ -0,0 +1,138 @@
/**
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
* @format
*/
import yoga from 'yoga-layout';
import LayoutRecord from './LayoutRecord';
import PositionRecord from './PositionRecord';
import type {LayoutRecordT} from './LayoutRecord';
import type {Yoga$Direction} from 'yoga-layout';
export const JSEnumLookup = {
justifyContent: 'JUSTIFY_',
alignItems: 'ALIGN_',
alignContent: 'ALIGN_',
alignSelf: 'ALIGN_',
position: 'POSITION_',
flexDirection: 'DIRECTION_',
flexWrap: 'WRAP_',
positionType: 'POSITION_TYPE_',
direction: 'DIRECTION_',
};
function getEnum(yogaEnum: string, value: string | number): string {
return `yoga.${Object.keys(yoga)
.filter(key => key.toLowerCase().startsWith(yogaEnum.toLowerCase()))
.find(key => yoga[key] === value) || value}`;
}
function setProperty(
name: string,
key: string,
value: string,
enumValue?: string,
): string {
return [
name,
'.set',
key[0].toUpperCase() + key.substr(1),
'(',
enumValue ? `${enumValue}, ` : '',
JSEnumLookup[key] ? getEnum(JSEnumLookup[key], value) : value,
');',
].join('');
}
function getLayoutCode(
node: LayoutRecordT,
name: string,
index: number,
): string {
const lines = [];
const childName = (i: number) => `${name === 'root' ? 'node' : name}_${i}`;
lines.push(
...node.children.map((node, i) => getLayoutCode(node, childName(i), i)),
);
lines.push('', `// create node ${name}`, `const ${name} = Node.create();`);
const untouchedNode = LayoutRecord({width: '', height: ''});
Object.keys(untouchedNode.toJS()).forEach(key => {
if (key !== 'children' && untouchedNode[key] !== node[key]) {
if (node[key] instanceof PositionRecord) {
// iterate through position record
const {top, left, right, bottom} = node[key].toJS();
if (
top !== untouchedNode[key].top &&
top === left &&
top === right &&
top === bottom
) {
// all edges
lines.push(setProperty(name, key, node[key].top, getEnum('edge', 8)));
return;
}
const alreadySet = [];
if (top !== untouchedNode[key].top && top === bottom) {
lines.push(setProperty(name, key, node[key].top, getEnum('edge', 7)));
alreadySet.push('top', 'bottom');
}
if (left !== untouchedNode[key].left && left === right) {
lines.push(
setProperty(name, key, node[key].left, getEnum('edge', 6)),
);
alreadySet.push('left', 'right');
}
['left', 'top', 'right', 'bottom'].forEach((pKey, i) => {
if (
node[key][pKey] !== untouchedNode[key][pKey] &&
alreadySet.indexOf(pKey) === -1
) {
lines.push(
setProperty(name, key, node[key][pKey], getEnum('edge', i)),
);
}
});
} else {
lines.push(setProperty(name, key, node[key]));
}
}
});
if (node.children && node.children.size > 0) {
lines.push(
'',
'// insert children',
...node.children.map(
(_, i) => `${name}.insertChild(${childName(i)}, ${i});`,
),
);
}
return lines.join('\n');
}
export default function generateCode(
root: LayoutRecordT,
direction: Yoga$Direction,
): string {
const rootNodeName = 'root';
return [
`import yoga, {Node} from 'yoga-layout';`,
getLayoutCode(root, rootNodeName, 0),
'',
`${rootNodeName}.calculateLayout(${root.width}, ${root.height}, ${getEnum(
'direction',
direction,
)});`,
`${rootNodeName}.getComputedLayout();`,
].join('\n');
}

View File

@@ -0,0 +1,172 @@
/**
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
* @format
*/
import yoga from 'yoga-layout';
import LayoutRecord from './LayoutRecord';
import PositionRecord from './PositionRecord';
import {JSEnumLookup} from './CodeJavaScript';
import type {LayoutRecordT} from './LayoutRecord';
import type {Yoga$Direction} from 'yoga-layout';
function getEnum(yogaEnum: string, value: string | number): string {
const enumLookup = {
justifyContent: 'Justify',
alignItems: 'Align',
alignContent: 'Align',
alignSelf: 'Align',
position: 'Position',
flexWrap: 'Wrap',
positionType: 'PositionType',
direction: 'Driection',
};
if (!enumLookup[yogaEnum]) {
return String(value);
} else {
const enumValue = Object.keys(yoga)
.filter(key =>
key.toLowerCase().startsWith(JSEnumLookup[yogaEnum].toLowerCase()),
)
.find(key => yoga[key] === value);
return `Yoga${enumLookup[yogaEnum]}.${
enumValue ? enumValue.replace(/^([A-Z]+)_/, '') : value
}`;
}
}
function getLayoutCode(
node: LayoutRecordT,
indent: string = '',
isReturning?: boolean,
): string {
const lines = [];
const flexDirection = {
[yoga.FLEX_DIRECTION_ROW]: 'Row',
[yoga.FLEX_DIRECTION_ROW_REVERSE]: 'RowReverse',
[yoga.FLEX_DIRECTION_COLUMN]: 'Column',
[yoga.FLEX_DIRECTION_COLUMN_REVERSE]: 'ColumnReverse',
};
lines.push(
indent +
`${isReturning ? 'return ' : ''}${
flexDirection[node.flexDirection]
}.create(c)`,
);
if (node.children.size > 0) {
lines.push(
...node.children
.toJSON()
.map(
child =>
`${indent}\t.child(\n${getLayoutCode(child, indent + '\t\t')})`,
),
);
}
const untouchedLayout = LayoutRecord({width: '', height: ''});
const untouchedPosition = PositionRecord({});
Object.keys(node.toJSON()).forEach(key => {
if (
node[key] instanceof PositionRecord &&
!node[key].equals(untouchedPosition)
) {
if (key === 'border') {
lines.push(indent + '\t.border(', indent + '\t\tBorder.create(c)');
}
const {top, left, right, bottom} = node[key].toJS();
if (
top !== untouchedPosition.top &&
top === left &&
top === right &&
top === bottom
) {
// all edges
lines.push(
indent +
(key === 'border'
? `\t\t\t.widthDip(YogaEdge.ALL, ${node[key].top})`
: `\t.${key}Dip(YogaEdge.ALL, ${node[key].top})`),
);
return;
}
const alreadySet = [];
if (top !== untouchedPosition.top && top === bottom) {
lines.push(
indent +
(key === 'border'
? `\t\t\t.widthDip(YogaEdge.VERTICAL, ${node[key].top})`
: `\t.${key}Dip(YogaEdge.VERTICAL, ${node[key].top})`),
);
alreadySet.push('top', 'bottom');
}
if (left !== untouchedPosition.left && left === right) {
lines.push(
indent +
(key === 'border'
? `\t\t\t.widthDip(YogaEdge.HORIZONTAL, ${node[key].left})`
: `\t.${key}Dip(YogaEdge.HORIZONTAL, ${node[key].left})`),
);
alreadySet.push('left', 'right');
}
['left', 'top', 'right', 'bottom'].forEach((pKey, i) => {
if (
node[key][pKey] !== untouchedPosition[pKey] &&
alreadySet.indexOf(pKey) === -1
) {
lines.push(
indent +
(key === 'border'
? `\t\t\t.widthDip(YogaEdge.${pKey.toUpperCase()}, ${
node.border[pKey]
})`
: `\t.${key}Dip(YogaEdge.${pKey.toUpperCase()}, ${
node[key][pKey]
})`),
);
}
});
if (key === 'border') {
lines.push(
indent + '\t\t\t.color(YogaEdge.ALL, 0xfff36b7f)',
indent + '\t\t\t.build())',
);
}
} else if (
key !== 'children' &&
key !== 'flexDirection' &&
node[key] !== untouchedLayout[key]
) {
lines.push(indent + `\t.${key}(${getEnum(key, node[key])})`);
}
});
return lines.join('\n');
}
export default function generateCode(
root: LayoutRecordT,
direction: Yoga$Direction,
): string {
return [
'@LayoutSpec',
'public class PlaygroundComponentSpec {',
'\t@OnCreateLayout',
'\tstatic Component onCreateLayout(ComponentContext c) {',
getLayoutCode(root, '\t\t', true),
'\t\t\t.build();',
'\t}',
'}',
].join('\n');
}

View File

@@ -0,0 +1,133 @@
/**
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
* @format
*/
import yoga from 'yoga-layout';
import LayoutRecord from './LayoutRecord';
import PositionRecord from './PositionRecord';
import {JSEnumLookup} from './CodeJavaScript';
import type {LayoutRecordT} from './LayoutRecord';
import type {Yoga$Direction} from 'yoga-layout';
function getEnum(yogaEnum: string, value: string | number): string {
const enumValue = Object.keys(yoga)
.filter(
key =>
JSEnumLookup[yogaEnum] &&
key.toLowerCase().startsWith(JSEnumLookup[yogaEnum].toLowerCase()),
)
.find(key => yoga[key] === value);
return enumValue
? "'" +
enumValue
.replace(/^([A-Z]+)_/, '')
.replace('_', '-')
.toLowerCase() +
"'"
: String(value);
}
function getLayoutCode(node: LayoutRecordT, indent: string = ''): string {
const lines = [];
const untouchedLayout = LayoutRecord({width: '', height: ''});
const untouchedPosition = PositionRecord({});
lines.push(indent + '<View style={{');
lines.push(indent + ' flex: 1,');
Object.keys(node.toJSON()).forEach(key => {
if (key === 'border' && !node.border.equals(untouchedPosition)) {
['Top', 'Left', 'Right', 'Bottom'].forEach(pKey => {
if (
untouchedPosition[pKey.toLowerCase()] !==
node.border[pKey.toLowerCase()]
) {
lines.push(
indent +
` border${pKey}Width: ${node.border[pKey.toLowerCase()]},`,
);
}
});
} else if (
node[key] instanceof PositionRecord &&
!node[key].equals(untouchedPosition)
) {
const {top, left, right, bottom} = node[key].toJS();
if (
top !== untouchedPosition.top &&
top === left &&
top === right &&
top === bottom
) {
// all edges
lines.push(indent + ` ${key}: ${node[key].top},`);
return;
}
const alreadySet = [];
if (top !== untouchedPosition.top && top === bottom) {
lines.push(indent + ` ${key}Vertical: ${node[key].top},`);
alreadySet.push('top', 'bottom');
}
if (left !== untouchedPosition.left && left === right) {
lines.push(indent + ` ${key}Horizontal: ${node[key].left},`);
alreadySet.push('left', 'right');
}
['left', 'top', 'right', 'bottom'].forEach((pKey, i) => {
if (
node[key][pKey] !== untouchedPosition[pKey] &&
alreadySet.indexOf(pKey) === -1
) {
lines.push(
indent +
` ${key}${pKey[0].toUpperCase()}${pKey.substr(1)}: ${
node[key][pKey]
},`,
);
}
});
} else if (key !== 'children' && node[key] !== untouchedLayout[key]) {
lines.push(indent + ` ${key}: ${getEnum(key, node[key])},`);
}
});
if (node.children.size > 0) {
lines.push(indent + '}}>');
} else {
lines.push(indent + '}} />');
}
if (node.children.size > 0) {
lines.push(
...node.children
.toJSON()
.map(child => getLayoutCode(child, indent + ' ')),
);
}
if (node.children.size > 0) {
lines.push(indent + '</View>');
}
return lines.join('\n');
}
export default function generateCode(
root: LayoutRecordT,
direction: Yoga$Direction,
): string {
return [
`import React, {Component} from 'react';`,
`import {View} from 'react-native';`,
'',
'export default class MyLayout extends Component {',
' render() {',
' return (',
getLayoutCode(root, ' '),
' );',
' }',
'};',
].join('\n');
}

View File

@@ -0,0 +1,49 @@
.Editor {
display: flex;
flex-direction: column;
height: 100%;
}
.Editor .field {
display: flex;
}
.Editor .ant-btn {
width: 100%;
}
.Editor h2 {
font-size: 16px;
margin-bottom: 8px;
margin-top: 30px;
}
.Editor h2:first-child {
margin-top: 0;
}
.Editor .ant-tabs-nav .ant-tabs-tab {
margin-left: 16px;
}
.Editor .EditorTabs {
flex-grow: 1;
}
.Editor .ant-tabs {
display: flex;
flex-direction: column;
}
.Editor .ant-tabs-bar {
margin-bottom: 0;
}
.Editor .ant-tabs-tabpane {
overflow-y: scroll;
padding: 15px;
}
.Editor .EditorButtons {
padding: 15px;
}

View File

@@ -0,0 +1,284 @@
/**
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
* @format
*/
import React, {Component} from 'react';
import {Row, Col, Button, Tabs, Input} from 'antd';
import YogaEnumSelect from './YogaEnumSelect';
import type {LayoutRecordT} from './LayoutRecord';
import type {Yoga$Direction} from 'yoga-layout';
import InfoText from './InfoText';
import YogaPositionEditor from './YogaPositionEditor';
import './Editor.css';
const TabPane = Tabs.TabPane;
type Props = {
node: ?LayoutRecordT,
onChangeLayout: (key: string, value: any) => void,
onChangeSetting: (key: string, value: any) => void,
direction: Yoga$Direction,
selectedNodeIsRoot: boolean,
onRemove?: () => void,
onAdd?: () => void,
};
export default class Editor extends Component<Props> {
componentDidMount() {
document.addEventListener('keydown', this.onKeyDown);
}
componentWillUnmount() {
document.removeEventListener('keydown', this.onKeyDown);
}
onKeyDown = (e: KeyboardEvent) => {
if (
(e.key === 'Delete' || e.key === 'Backspace') &&
this.props.onRemove &&
!(e.target instanceof HTMLInputElement)
) {
this.props.onRemove();
}
};
render() {
const {node, selectedNodeIsRoot} = this.props;
const disabled = !Boolean(node);
return (
<div className="Editor">
<Tabs defaultActiveKey="1" className="EditorTabs">
<TabPane tab="Flex" key="1">
<h2>
Direction
<InfoText>
The direction property specifies the text direction/writing
direction
</InfoText>
</h2>
<YogaEnumSelect
property="DIRECTION"
value={this.props.direction}
onChange={e => this.props.onChangeSetting('direction', e)}
/>
<h2>
Flex direction
<InfoText>Defining the direction of the main-axis</InfoText>
</h2>
<YogaEnumSelect
disabled={disabled}
property="FLEX_DIRECTION"
value={node ? node.flexDirection : ''}
onChange={e => this.props.onChangeLayout('flexDirection', e)}
/>
<Row gutter={15} style={{marginTop: 30}}>
<Col span={12}>
<h2>
Flex grow
<InfoText>
Grow factor defined how much space this element should take
up, relative to it's siblings
</InfoText>
</h2>
<Input
type="text"
disabled={disabled || selectedNodeIsRoot}
value={node ? node.flexGrow : ''}
onChange={e =>
this.props.onChangeLayout('flexGrow', e.target.value)
}
/>
</Col>
<Col span={12}>
<h2>
Flex shrink
<InfoText>
Shrink factor if elements don't fit into the parent node
anymore.
</InfoText>
</h2>
<Input
type="text"
disabled={disabled || selectedNodeIsRoot}
value={node ? node.flexShrink : ''}
onChange={e =>
this.props.onChangeLayout('flexShrink', e.target.value)
}
/>
</Col>
</Row>
<h2>
Flex wrap
<InfoText>
Wrapping behaviour when child nodes don't fit into a single line
</InfoText>
</h2>
<YogaEnumSelect
disabled={disabled}
property="WRAP"
value={node ? node.flexWrap : ''}
onChange={e => this.props.onChangeLayout('flexWrap', e)}
/>
</TabPane>
<TabPane tab="Alignment" key="2">
<h2>
Justify content
<InfoText>Aligns child nodes along the main-axis</InfoText>
</h2>
<YogaEnumSelect
disabled={disabled}
property="JUSTIFY"
value={node ? node.justifyContent : ''}
onChange={e => this.props.onChangeLayout('justifyContent', e)}
/>
<h2>
Align items
<InfoText>Aligns child nodes along the cross-axis</InfoText>
</h2>
<YogaEnumSelect
disabled={disabled}
property="ALIGN"
value={node ? node.alignItems : ''}
onChange={e => this.props.onChangeLayout('alignItems', e)}
/>
<h2>
Align self
<InfoText>
Specifies alignment on the cross-axis for the node itself
</InfoText>
</h2>
<YogaEnumSelect
disabled={disabled || selectedNodeIsRoot}
property="ALIGN"
value={node ? node.alignSelf : ''}
onChange={e => this.props.onChangeLayout('alignSelf', e)}
/>
<h2>
Align content
<InfoText>
Alignment of lines along the cross-axis when wrapping
</InfoText>
</h2>
<YogaEnumSelect
disabled={disabled}
property="ALIGN"
value={node ? node.alignContent : ''}
onChange={e => this.props.onChangeLayout('alignContent', e)}
/>
</TabPane>
<TabPane tab="Layout" key="3">
<h2>
Width &times; height
<InfoText>Dimensions of the node</InfoText>
</h2>
<Row gutter={15}>
<Col span={12}>
<Input
type="text"
placeholder="width"
disabled={disabled}
value={node ? node.width : ''}
onChange={e =>
this.props.onChangeLayout('width', e.target.value)
}
/>
</Col>
<Col span={12}>
<Input
type="text"
placeholder="height"
disabled={disabled}
value={node ? node.height : ''}
onChange={e =>
this.props.onChangeLayout('height', e.target.value)
}
/>
</Col>
</Row>
<h2>
Aspect ratio
<InfoText>
Aspect radio is an additon by Yoga which is handy e.g. for nodes
displaying videos
</InfoText>
</h2>
<Input
type="text"
placeholder="Aspect ratio"
disabled={disabled}
value={node ? node.aspectRatio : ''}
onChange={e =>
this.props.onChangeLayout('aspectRatio', e.target.value)
}
/>
<h2>Box model</h2>
{['padding', 'border', 'margin'].map(property => (
<YogaPositionEditor
property={property}
key={property}
value={node ? node[property] : undefined}
onChange={value => this.props.onChangeLayout(property, value)}
/>
))}
<h2>
Position
<InfoText>
Relative position offsets the node from it's calculated
position. Absolute position removes the node from the flexbox
flow and positions it at the given position.
</InfoText>
</h2>
<YogaEnumSelect
disabled={disabled}
property="POSITION_TYPE"
value={node ? node.positionType : ''}
onChange={e => this.props.onChangeLayout('positionType', e)}
/>
<YogaPositionEditor
property="position"
value={node ? node.position : undefined}
onChange={value => this.props.onChangeLayout('position', value)}
/>
</TabPane>
</Tabs>
<Row gutter={15} className="EditorButtons">
<Col span={12}>
<Button
icon="plus-circle-o"
disabled={!Boolean(this.props.onAdd)}
onClick={this.props.onAdd}
type="primary">
add child node
</Button>
</Col>
<Col span={12}>
<Button
icon="close-circle-o"
disabled={!Boolean(this.props.onRemove)}
onClick={this.props.onRemove}
type="danger">
remove node
</Button>
</Col>
</Row>
</div>
);
}
}

View File

@@ -0,0 +1,9 @@
.InfoText {
max-width: 230px;
line-height: 130%;
}
.InfoTextIcon {
margin-left: 5px;
opacity: 0.5;
}

View File

@@ -0,0 +1,31 @@
/**
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
* @format
*/
import React, {Component} from 'react';
import {Popover, Icon} from 'antd';
import './InfoText.css';
type Props = {
children: any,
};
export default class InfoText extends Component<Props> {
render() {
return (
<Popover
content={<div className="InfoText">{this.props.children}</div>}
trigger="hover">
<Icon className="InfoTextIcon" type="info-circle-o" />
</Popover>
);
}
}

View File

@@ -0,0 +1,68 @@
/**
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
* @format
*/
import {Record, List} from 'immutable';
import type {RecordOf} from 'immutable';
import PositionRecord from './PositionRecord';
import type {PositionRecordT} from './PositionRecord';
import yoga from 'yoga-layout';
import type {
Yoga$Align,
Yoga$JustifyContent,
Yoga$FlexDirection,
Yoga$FlexWrap,
Yoga$Yoga$PositionType,
} from 'yoga-layout';
export type LayoutRecordT = RecordOf<{
width?: ?number,
height?: ?number,
justifyContent?: Yoga$JustifyContent,
padding: PositionRecordT,
border: PositionRecordT,
margin: PositionRecordT,
position: PositionRecordT,
positionType: Yoga$Yoga$PositionType,
alignItems?: Yoga$Align,
alignSelf?: Yoga$Align,
alignContent?: Yoga$Align,
flexDirection?: Yoga$FlexDirection,
flexGrow?: number,
flexShrink?: number,
padding?: number,
flexWrap?: Yoga$FlexWrap,
aspectRatio?: number,
children?: List<LayoutDefinition>,
}>;
const r: LayoutRecordT = Record({
width: 100,
height: 100,
justifyContent: yoga.JUSTIFY_FLEX_START,
alignItems: yoga.ALIGN_STRETCH,
alignSelf: yoga.ALIGN_AUTO,
alignContent: yoga.ALIGN_STRETCH,
flexDirection: yoga.FLEX_DIRECTION_ROW,
padding: PositionRecord(),
margin: PositionRecord(),
border: PositionRecord(),
position: PositionRecord(),
positionType: yoga.POSITION_TYPE_RELATIVE,
flexWrap: yoga.WRAP_NO_WRAP,
flexGrow: 0,
flexShrink: 1,
children: List(),
aspectRatio: 'auto',
});
export default r;

View File

@@ -0,0 +1,9 @@
.PositionGuide {
position: absolute;
pointer-events: none;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
user-select: none;
}

View File

@@ -0,0 +1,148 @@
/**
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
* @format
*/
import React, {Component} from 'react';
import PositionRecord from './PositionRecord';
import type {PositionRecordT} from './PositionRecord';
import './PositionGuide.css';
type Props = {
inset?: boolean,
reverse?: boolean,
position: PositionRecordT,
offset: PositionRecordT,
color: string,
};
export default class PositionGuide extends Component<Props> {
static defaultProps = {
offset: PositionRecord({}),
};
render() {
const {position, offset, inset, color, reverse} = this.props;
let {top, left, right, bottom} = position;
let {top: oTop, left: oLeft, right: oRight, bottom: oBottom} = offset;
if (reverse) {
let temp1 = left;
left = right;
right = temp1;
temp1 = oLeft;
oLeft = oRight;
oRight = temp1;
}
if (!top) {
top = 0;
}
if (!left) {
left = 0;
}
if (!right) {
right = 0;
}
if (!bottom) {
bottom = 0;
}
if (!oTop) {
oTop = 0;
}
if (!oLeft) {
oLeft = 0;
}
if (!oRight) {
oRight = 0;
}
if (!oBottom) {
oBottom = 0;
}
if (!inset) {
if (top < 0) {
bottom -= top;
top = 0;
}
if (bottom < 0) {
top -= bottom;
bottom = 0;
}
if (left < 0) {
right -= left;
left = 0;
}
if (right < 0) {
left -= right;
right = 0;
}
}
return [
top !== 0 ? (
<div
key="top"
className="PositionGuide"
style={{
backgroundColor: color,
height: top,
top: inset ? oTop : -top - oTop,
left: inset ? left + oLeft : -left - oLeft,
right: inset ? right + oRight : -right - oRight,
}}>
{top}
</div>
) : null,
left !== 0 ? (
<div
key="left"
className="PositionGuide"
style={{
backgroundColor: color,
width: left,
top: inset ? oTop : -oTop,
bottom: inset ? oBottom : -oBottom,
left: inset ? oLeft : -left - oLeft,
}}>
{left}
</div>
) : null,
right !== 0 ? (
<div
key="right"
className="PositionGuide"
style={{
backgroundColor: color,
width: right,
top: inset ? oTop : -oTop,
bottom: inset ? oBottom : -oBottom,
right: inset ? oRight : -right - oRight,
}}>
{right}
</div>
) : null,
bottom !== 0 ? (
<div
key="bottom"
className="PositionGuide"
style={{
backgroundColor: color,
height: bottom,
bottom: inset ? oBottom : -bottom - oBottom,
left: inset ? left + oLeft : -left - oLeft,
right: inset ? right + oRight : -right - oRight,
}}>
{bottom}
</div>
) : null,
];
}
}

View File

@@ -0,0 +1,30 @@
/**
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
* @format
*/
import {Record} from 'immutable';
import type {RecordOf} from 'immutable';
export type PositionRecordT = RecordOf<{
top: string,
right: string,
bottom: string,
left: string,
}>;
const r: PositionRecordT = Record({
top: '',
right: '',
bottom: '',
left: '',
});
export default r;

View File

@@ -0,0 +1,52 @@
.Sidebar {
position: absolute;
z-index: 3;
top: 0;
right: 0;
bottom: 0;
width: 350px;
background: white;
border-left: 1px solid #dddfe2;
transform: translateX(100%);
transition: 0.2s transform;
display: flex;
flex-direction: column;
pointer-events: none;
}
.Sidebar.floating {
margin: 25px;
max-height: calc(100% - 50px);
border-radius: 6px;
bottom: auto;
box-shadow: 3px 3px 15px rgba(0, 0, 0, 0.15);
opacity: 0;
transform: translateY(15px);
transition: 0.2s all;
transition-timing-function: ease-out;
}
.Sidebar.visible {
transform: translateX(0);
pointer-events: all;
}
.Sidebar.floating.visible {
transform: translateY(0);
opacity: 1;
}
.SidebarClose {
text-align: right;
padding: 15px;
padding-bottom: 0;
}
.SidebarClose i {
cursor: pointer;
opacity: 0.4;
}
.SidebarClose i:hover {
opacity: 0.6;
}

View File

@@ -0,0 +1,41 @@
/**
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
* @format
*/
import React, {Component} from 'react';
import {Icon} from 'antd';
import './Sidebar.css';
type Props = {
onClose?: () => void,
width?: number,
children: any,
floating?: boolean,
};
export default class Sidebar extends Component<Props> {
render() {
return (
<div
className={`Sidebar ${this.props.visible ? 'visible' : ''} ${
this.props.floating ? 'floating' : ''
}`}
style={{width: this.props.width}}>
{this.props.onClose && (
<div className="SidebarClose">
<Icon type="close" onClick={this.props.onClose} />
</div>
)}
{this.props.children}
</div>
);
}
}

View File

@@ -0,0 +1,9 @@
.YogaEnumSelect.ant-radio-group {
display: flex;
}
.YogaEnumSelect.ant-radio-group .ant-radio-button-wrapper {
flex-grow: 1;
flex-basis: 0;
text-align: center;
}

View File

@@ -0,0 +1,82 @@
/**
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
* @format
*/
import React, {Component} from 'react';
import yoga from 'yoga-layout';
import {Radio, Menu, Dropdown, Button, Icon} from 'antd';
import './YogaEnumSelect.css';
const RadioButton = Radio.Button;
const RadioGroup = Radio.Group;
type Props = {
property: string,
disabled?: boolean,
value: string | number,
onChange: (value: number) => void,
};
export default class YogaEnumSelect extends Component<Props> {
values: Array<{key: string, value: number}>;
constructor({property}: Props) {
super();
// $FlowFixMe
this.values = Object.keys(yoga)
.map(key => ({key, value: yoga[key]}))
.filter(
({key}) => key.startsWith(property) && key !== `${property}_COUNT`,
);
}
handleMenuClick = ({key}: {key: string}) => {
this.props.onChange(yoga[key]);
};
render() {
const {property, ...props} = this.props;
const selected = this.values.find(
({key, value}) => value === this.props.value,
);
const replacer = new RegExp(`^${property}_`);
return this.values.length > 3 ? (
<Dropdown
disabled={props.disabled}
overlay={
<Menu onClick={this.handleMenuClick}>
{this.values.map(({key, value}) => (
<Menu.Item key={key} value={value}>
{key.replace(replacer, '')}
</Menu.Item>
))}
</Menu>
}>
<Button>
{selected ? selected.key.replace(replacer, '') : 'undefiend'}{' '}
<Icon type="down" />
</Button>
</Dropdown>
) : (
<RadioGroup
{...props}
onChange={e => this.props.onChange(e.target.value)}
defaultValue="a"
className="YogaEnumSelect">
{this.values.map(({key, value}) => (
<RadioButton key={key} value={value}>
{key.replace(new RegExp(`^${property}_`), '')}
</RadioButton>
))}
</RadioGroup>
);
}
}

View File

@@ -0,0 +1,51 @@
.YogaNode {
background: white;
position: absolute;
transform: scale(1);
box-shadow: inset 0 0 0px 1px rgba(48, 56, 69, 0.2);
transition: 0.2s all, 0s outline, 0s box-shadow;
}
.YogaNode .YogaNode {
background: rgba(255, 255, 255, 0.7);
}
.YogaNode > .info {
position: absolute;
opacity: 0;
font-family: monospace;
}
.YogaNode:hover > .info {
opacity: 1;
}
.YogaNode:focus {
outline: 0;
}
.YogaNode.focused {
box-shadow: 0 0 0 2px #95ddcf, 0 0 15px rgba(0, 0, 0, 0.2),
inset 0 0 0px 1px rgba(255, 255, 255, 0.2);
z-index: 2;
}
.YogaNode.invisible {
transform: scale(0);
}
.YogaNode .label {
user-select: none;
pointer-events: none;
position: absolute;
left: 0;
bottom: 0;
right: 0;
top: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
font-weight: 300;
letter-spacing: 1px;
}

View File

@@ -0,0 +1,283 @@
/**
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
* @format
*/
import React, {Component} from 'react';
import yoga, {Node} from 'yoga-layout';
import PositionGuide from './PositionGuide';
import PositionRecord from './PositionRecord';
import type {LayoutRecordT} from './LayoutRecord';
import type {Yoga$Direction} from 'yoga-layout';
import './YogaNode.css';
type Yoga$Node = any;
type ComputedLayout = {|
left: number,
top: number,
width: number,
height: number,
children: Array<ComputedLayout>,
node: Yoga$Node,
|};
type Props = {|
layoutDefinition: LayoutRecordT,
className?: string,
computedLayout?: ComputedLayout,
path: Array<number>,
selectedNodePath?: ?Array<number>,
direction?: Yoga$Direction,
label?: string,
showGuides: boolean,
onClick?: (path: Array<number>) => void,
onDoubleClick?: (path: Array<number>) => void,
|};
type State = {
visible?: boolean,
};
export default class YogaNode extends Component<Props, State> {
node: Yoga$Node;
static defaultProps = {
path: [],
label: 'root',
showGuides: true,
};
state = {};
computedLayout: ?ComputedLayout;
rootNode: ?Yoga$Node;
constructor(props: Props) {
super();
if (!props.computedLayout) {
// is root node
this.calculateLayout(props);
this.state = {
visible: !Boolean(props.computedLayout),
};
}
}
componentDidMount() {
setTimeout(() => this.setState({visible: true}), 200);
}
componentWillReceiveProps(nextProps: Props) {
if (
!nextProps.computedLayout &&
(!this.props.layoutDefinition.equals(nextProps.layoutDefinition) ||
this.props.direction !== nextProps.direction)
) {
// is root node and the layout definition or settings changed
this.calculateLayout(nextProps);
}
}
componentWillUnmount() {
if (this.rootNode) {
this.rootNode.freeRecursive();
}
}
calculateLayout(props: Props) {
const root = this.createYogaNodes(props.layoutDefinition);
root.calculateLayout(
props.layoutDefinition.width,
props.layoutDefinition.height,
props.direction,
);
this.computedLayout = this.getComputedLayout(root);
this.rootNode = root;
}
createYogaNodes = (layoutDefinition: LayoutRecordT): Yoga$Node => {
const root = Node.create();
root.setWidth(layoutDefinition.width);
root.setHeight(layoutDefinition.height);
root.setJustifyContent(layoutDefinition.justifyContent);
root.setAlignItems(layoutDefinition.alignItems);
root.setAlignSelf(layoutDefinition.alignSelf);
root.setFlexGrow(layoutDefinition.flexGrow);
root.setFlexShrink(layoutDefinition.flexShrink);
root.setPadding(yoga.EDGE_TOP, layoutDefinition.padding.top);
root.setPadding(yoga.EDGE_RIGHT, layoutDefinition.padding.right);
root.setPadding(yoga.EDGE_BOTTOM, layoutDefinition.padding.bottom);
root.setPadding(yoga.EDGE_LEFT, layoutDefinition.padding.left);
root.setBorder(yoga.EDGE_TOP, layoutDefinition.border.top);
root.setBorder(yoga.EDGE_RIGHT, layoutDefinition.border.right);
root.setBorder(yoga.EDGE_BOTTOM, layoutDefinition.border.bottom);
root.setBorder(yoga.EDGE_LEFT, layoutDefinition.border.left);
root.setMargin(yoga.EDGE_TOP, layoutDefinition.margin.top);
root.setMargin(yoga.EDGE_RIGHT, layoutDefinition.margin.right);
root.setMargin(yoga.EDGE_BOTTOM, layoutDefinition.margin.bottom);
root.setMargin(yoga.EDGE_LEFT, layoutDefinition.margin.left);
root.setPosition(yoga.EDGE_TOP, layoutDefinition.position.top);
root.setPosition(yoga.EDGE_RIGHT, layoutDefinition.position.right);
root.setPosition(yoga.EDGE_BOTTOM, layoutDefinition.position.bottom);
root.setPosition(yoga.EDGE_LEFT, layoutDefinition.position.left);
root.setPositionType(layoutDefinition.positionType);
root.setDisplay(yoga.DISPLAY_FLEX);
root.setAspectRatio(layoutDefinition.aspectRatio);
root.setFlexWrap(layoutDefinition.flexWrap);
root.setFlexDirection(layoutDefinition.flexDirection);
(layoutDefinition.children || [])
.map(this.createYogaNodes)
.forEach((node, i) => {
root.insertChild(node, i);
});
return root;
};
getComputedLayout = (node: Yoga$Node): ComputedLayout => {
return {
...node.getComputedLayout(),
node,
children: Array.apply(null, Array(node.getChildCount())).map((_, i) =>
this.getComputedLayout(node.getChild(i)),
),
};
};
onClick = (e: SyntheticMouseEvent<>) => {
const {onClick} = this.props;
if (onClick) {
e.stopPropagation();
onClick(this.props.path);
}
};
onDoubleClick = (e: SyntheticMouseEvent<>) => {
const {onDoubleClick} = this.props;
if (onDoubleClick) {
e.stopPropagation();
onDoubleClick(this.props.path);
}
};
showPositionGuides({node}: ComputedLayout) {
const padding = PositionRecord({
top: node.getComputedPadding(yoga.EDGE_TOP),
left: node.getComputedPadding(yoga.EDGE_LEFT),
right: node.getComputedPadding(yoga.EDGE_RIGHT),
bottom: node.getComputedPadding(yoga.EDGE_BOTTOM),
});
const border = PositionRecord({
top: node.getComputedBorder(yoga.EDGE_TOP),
left: node.getComputedBorder(yoga.EDGE_LEFT),
right: node.getComputedBorder(yoga.EDGE_RIGHT),
bottom: node.getComputedBorder(yoga.EDGE_BOTTOM),
});
const margin = PositionRecord({
top: node.getComputedMargin(yoga.EDGE_TOP),
left: node.getComputedMargin(yoga.EDGE_LEFT),
right: node.getComputedMargin(yoga.EDGE_RIGHT),
bottom: node.getComputedMargin(yoga.EDGE_BOTTOM),
});
const position = PositionRecord({
top: node.getPosition(yoga.EDGE_TOP).value,
left: node.getPosition(yoga.EDGE_LEFT).value,
right: node.getPosition(yoga.EDGE_RIGHT).value,
bottom: node.getPosition(yoga.EDGE_BOTTOM).value,
});
return [
<PositionGuide
key="border"
inset
position={border}
color="rgba(251, 170, 51, 0.15)"
reverse={node.getFlexWrap() === yoga.WRAP_WRAP_REVERSE}
/>,
<PositionGuide
key="padding"
inset
offset={border}
position={padding}
color="rgba(123, 179, 41, 0.1)"
reverse={node.getFlexWrap() === yoga.WRAP_WRAP_REVERSE}
/>,
<PositionGuide
key="margin"
position={margin}
color="rgba(214, 43, 28, 0.1)"
/>,
<PositionGuide
key="position"
offset={margin}
position={position}
color="rgba(115, 51, 205, 0.1)"
/>,
];
}
render() {
const {
layoutDefinition,
className,
path,
selectedNodePath,
label,
} = this.props;
// $FlowFixMe
const computedLayout: ComputedLayout =
this.props.computedLayout || this.computedLayout;
const {left, top, width, height, children} = computedLayout;
const isFocused = selectedNodePath && selectedNodePath.length === 0;
return (
<div
className={`YogaNode ${isFocused ? 'focused' : ''} ${className || ''} ${
this.state.visible ? '' : 'invisible'
}`}
style={{left, top, width, height}}
onDoubleClick={this.onDoubleClick}
onClick={this.onClick}>
{label && <div className="label">{label}</div>}
{isFocused &&
this.props.showGuides &&
this.showPositionGuides(computedLayout)}
{(children || []).map((child: ComputedLayout, i) => (
<YogaNode
key={i}
computedLayout={child}
label={String(i + 1)}
layoutDefinition={layoutDefinition.children.get(i)}
selectedNodePath={
selectedNodePath &&
selectedNodePath.length > 0 &&
selectedNodePath[0] === i
? selectedNodePath.slice(1)
: null
}
path={path.concat(i)}
onClick={this.props.onClick}
onDoubleClick={this.props.onDoubleClick}
showGuides={this.props.showGuides}
/>
))}
</div>
);
}
}

View File

@@ -0,0 +1,22 @@
.YogaPositionEditor {
text-align: center;
margin-top: 20px;
margin-bottom: 20px;
width: 60%;
margin-left: auto;
margin-right: auto;
}
.YogaPositionEditor input {
width: 55px;
text-align: center;
}
.YogaPositionEditorRow {
display: flex;
justify-content: space-between;
align-items: center;
flex-direction: row;
font-size: 16px;
font-weight: bold;
}

View File

@@ -0,0 +1,60 @@
/**
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
* @format
*/
import React, {Component} from 'react';
import {Input} from 'antd';
import PositionRecord from './PositionRecord';
import type {PositionRecordT} from './PositionRecord';
import './YogaPositionEditor.css';
type Props = {
value: PositionRecordT,
property: string,
onChange: (value: PositionRecordT) => void,
};
export default class YogaPositionEditor extends Component<Props> {
static defaultProps = {
value: PositionRecord(),
};
render() {
const {onChange, value, property} = this.props;
return (
<div className="YogaPositionEditor">
<Input
type="text"
value={value.top}
onChange={e => onChange(value.set('top', e.target.value))}
/>
<div className="YogaPositionEditorRow">
<Input
type="text"
value={value.left}
onChange={e => onChange(value.set('left', e.target.value))}
/>
{property}
<Input
type="text"
value={value.right}
onChange={e => onChange(value.set('right', e.target.value))}
/>
</div>
<Input
type="text"
value={value.bottom}
onChange={e => onChange(value.set('bottom', e.target.value))}
/>
</div>
);
}
}

View File

@@ -0,0 +1,34 @@
.PlaygroundContainer {
display: flex;
flex-direction: row;
flex-grow: 1;
width: 100%;
}
.Playground {
flex-grow: 1;
position: relative;
width: 100%;
overflow: hidden;
background: linear-gradient(-90deg, rgba(0, 0, 0, 0.02) 1px, transparent 1px),
linear-gradient(rgba(0, 0, 0, 0.02) 1px, transparent 1px),
linear-gradient(-90deg, rgba(0, 0, 0, 0.03) 1px, transparent 1px),
linear-gradient(rgba(0, 0, 0, 0.03) 1px, transparent 1px),
linear-gradient(
transparent 4px,
#f5f5f5 4px,
#f5f5f5 97px,
transparent 97px
),
linear-gradient(-90deg, #e5e5e5 1px, transparent 1px),
linear-gradient(
-90deg,
transparent 4px,
#f5f5f5 4px,
#f5f5f5 97px,
transparent 97px
),
linear-gradient(#e5e5e5 1px, transparent 1px), #f5f5f5;
background-size: 10px 10px, 10px 10px, 100px 100px, 100px 100px, 100px 100px,
100px 100px, 100px 100px, 100px 100px;
}

View File

@@ -0,0 +1,320 @@
/**
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
* @format
*/
import React, {Component} from 'react';
import yoga from 'yoga-layout';
import YogaNode from './YogaNode';
import Editor from './Editor';
import {List, setIn} from 'immutable';
import PositionRecord from './PositionRecord';
import LayoutRecord from './LayoutRecord';
// import Toolbar from './Toolbar';
import Code from './Code';
import Sidebar from './Sidebar';
import type {LayoutRecordT} from './LayoutRecord';
import type {Yoga$Direction} from 'yoga-layout';
import './index.css';
type Props = {
layoutDefinition: LayoutRecordT,
direction: Yoga$Direction,
maxDepth: number,
maxChildren?: number,
minChildren?: number,
selectedNodePath?: Array<number>,
showGuides: boolean,
className?: string,
height?: string | number,
renderSidebar?: (layoutDefinition: LayoutRecordT, onChange: Function) => any,
};
type State = {
selectedNodePath: ?Array<number>,
layoutDefinition: LayoutRecordT,
direction: Yoga$Direction,
showCode: boolean,
};
function getPath(path: Array<number>): Array<mixed> {
return path.reduce((acc, cv) => acc.concat('children', cv), []);
}
export default class Playground extends Component<Props, State> {
_containerRef: ?HTMLElement;
static defaultProps = {
layoutDefinition: LayoutRecord({
width: 800,
height: 400,
justifyContent: yoga.JUSTIFY_SPACE_BETWEEN,
alignItems: yoga.ALIGN_FLEX_START,
children: List([LayoutRecord(), LayoutRecord()]),
padding: PositionRecord({
left: '10',
top: '10',
right: '10',
bottom: '10',
}),
margin: PositionRecord({
left: '20',
top: '70',
}),
}),
direction: yoga.DIRECTION_LTR,
maxDepth: 3,
showCode: false,
showGuides: true,
};
state = {
selectedNodePath: this.props.selectedNodePath,
layoutDefinition: this.props.layoutDefinition,
direction: this.props.direction,
showCode: false,
};
componentDidMount() {
document.addEventListener('keydown', this.onKeyDown);
// rehydrate
if (window.location.hash && window.location.hash.length > 1) {
try {
const restoredState = JSON.parse(atob(window.location.hash.substr(1)));
this.setState({layoutDefinition: this.rehydrate(restoredState)});
} catch (e) {
window.location.hash = '';
}
}
}
componentWillUnmount() {
document.removeEventListener('keydown', this.onKeyDown);
}
rehydrate = (node: Object): LayoutRecord => {
let record = LayoutRecord(node);
record = record.set('padding', PositionRecord(record.padding));
record = record.set('border', PositionRecord(record.border));
record = record.set('margin', PositionRecord(record.margin));
record = record.set('position', PositionRecord(record.position));
record = record.set('children', List(record.children.map(this.rehydrate)));
return record;
};
onKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
this.hideSidePanes();
}
};
onMouseDown = (e: MouseEvent) => {
if (e.target === this._containerRef) {
this.hideSidePanes();
}
};
hideSidePanes() {
if (!Boolean(this.props.renderSidebar)) {
// only unselect if we don't have an external sidebar, otherwise the
// sidebar may rely on a certain node to be selected
this.setState({
selectedNodePath: null,
showCode: false,
});
}
}
onChangeLayout = (key: string, value: any) => {
const {selectedNodePath} = this.state;
if (selectedNodePath) {
this.modifyAtPath([...getPath(selectedNodePath), key], value);
}
};
onRemove = () => {
const {selectedNodePath, layoutDefinition} = this.state;
if (selectedNodePath) {
const index = selectedNodePath.pop();
const path = getPath(selectedNodePath).concat('children');
const updatedChildren = layoutDefinition.getIn(path).delete(index);
this.modifyAtPath(path, updatedChildren);
this.setState({selectedNodePath: null});
}
};
onAdd = () => {
const {selectedNodePath, layoutDefinition} = this.state;
if (selectedNodePath) {
const path = getPath(selectedNodePath).concat('children');
const updatedChildren = layoutDefinition.getIn(path).push(LayoutRecord());
this.modifyAtPath(
path,
updatedChildren,
selectedNodePath.concat(updatedChildren.size - 1),
);
}
};
modifyAtPath(
path: Array<any>,
value: any,
selectedNodePath?: ?Array<number> = this.state.selectedNodePath,
) {
console.log(setIn);
// $FlowFixMe
const layoutDefinition = setIn(this.state.layoutDefinition, path, value);
this.setState({
layoutDefinition,
selectedNodePath,
});
window.location.hash = btoa(
JSON.stringify(this.removeUnchangedProperties(layoutDefinition)),
);
}
removeUnchangedProperties = (node: LayoutRecordT): Object => {
const untouchedLayout = LayoutRecord({});
const untouchedPosition = PositionRecord({});
const result = {};
if (!node.equals(untouchedLayout)) {
Object.keys(node.toJS()).forEach(key => {
if (key === 'children' && node.children.size > 0) {
result.children = node.children
.toJSON()
.map(this.removeUnchangedProperties);
} else if (
node[key] instanceof PositionRecord &&
!node[key].equals(untouchedPosition)
) {
result[key] = {};
Object.keys(untouchedPosition.toJS()).forEach(position => {
if (node[key][position] !== untouchedPosition[position]) {
result[key][position] = node[key][position];
}
});
} else if (node[key] !== untouchedLayout[key]) {
result[key] = node[key];
}
});
}
return result;
};
getChildrenCountForSelectedPath = (): number => {
const selectedNode: ?LayoutRecordT = (
this.state.selectedNodePath || []
).reduce(
(node: LayoutRecordT, cv) => node.children.get(cv),
this.state.layoutDefinition,
);
return selectedNode ? selectedNode.children.size : 0;
};
onToggleCode = () => {
this.setState({
selectedNodePath: this.state.showCode
? this.state.selectedNodePath
: null,
showCode: !this.state.showCode,
});
};
render() {
const {layoutDefinition, selectedNodePath} = this.state;
const {height} = this.props;
const selectedNode: ?LayoutRecordT = selectedNodePath
? layoutDefinition.getIn(getPath(selectedNodePath))
: null;
const playground = (
<div
className="Playground"
onMouseDown={this.onMouseDown}
style={{height, maxHeight: height}}
ref={ref => {
this._containerRef = ref;
}}>
{/* <Toolbar
onShowCode={
!this.state.showCode
? () => this.setState({selectedNodePath: null, showCode: true})
: undefined
}
/> */}
<YogaNode
layoutDefinition={layoutDefinition}
selectedNodePath={selectedNodePath}
onClick={selectedNodePath =>
this.setState({selectedNodePath, showCode: false})
}
onDoubleClick={this.onAdd}
direction={this.state.direction}
showGuides={this.props.showGuides}
/>
<Sidebar
visible={
Boolean(selectedNodePath) &&
!this.state.showCode &&
!this.props.renderSidebar
}
floating>
<Editor
node={selectedNode}
selectedNodeIsRoot={
selectedNodePath ? selectedNodePath.length === 0 : false
}
onChangeLayout={this.onChangeLayout}
onChangeSetting={(key, value) => this.setState({[key]: value})}
direction={this.state.direction}
onRemove={
selectedNodePath && selectedNodePath.length > 0
? this.onRemove
: undefined
}
onAdd={
selectedNodePath && selectedNodePath.length < this.props.maxDepth
? this.onAdd
: undefined
}
/>
</Sidebar>
<Sidebar
width={500}
visible={this.state.showCode}
onClose={() => this.setState({showCode: false})}>
{/* <Code
layoutDefinition={layoutDefinition}
direction={this.state.direction}
/> */}
</Sidebar>
</div>
);
if (this.props.renderSidebar) {
return (
<div className={`PlaygroundContainer ${this.props.className || ''}`}>
<div>
{this.props.renderSidebar(
this.state.layoutDefinition,
this.onChangeLayout,
)}
</div>
{playground}
</div>
);
} else {
return playground;
}
}
}

View File

@@ -0,0 +1,43 @@
.Toolbar {
border-bottom: 1px solid #dddfe2;
box-shadow: 0 1px 6px 0 rgba(0, 0, 0, 0.1);
background: white;
height: 50px;
padding: 0 10px;
padding-bottom: 1px;
z-index: 4;
display: flex;
align-items: center;
}
.Toolbar .logo {
display: flex;
height: 100%;
align-items: center;
text-decoration: none;
margin-left: 5px;
}
.Toolbar a {
margin: 0 15px;
}
.Toolbar h1 {
font-size: 20px;
margin: 0;
padding: 0;
margin-left: 3px;
white-space: nowrap;
}
.Toolbar .ToolbarSpacer {
flex-grow: 1;
}
.Toolbar .ToolbarToggle {
font-size: 16px;
}
.Toolbar .ToolbarToggle i {
margin-right: 5px;
}

View File

@@ -0,0 +1,47 @@
/**
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
* @format
*/
import React, {Component} from 'react';
import Link from 'gatsby-link';
import {Icon} from 'antd';
import './Toolbar.css';
type Props = {
onShowCode?: () => void,
};
export default class Toolbar extends Component<Props> {
render() {
return (
<div className="Toolbar">
<Link to="/" className="logo">
<img
src="https://facebook.github.io/yoga/static/logo.svg"
width="42"
alt="Yoga logo"
/>
<h1>Yoga Layout</h1>
</Link>
<div className="ToolbarSpacer" />
<Link to="/docs">Docs</Link>
<Link to="/playground">Playground</Link>
<a href="https://github.com/facebook/yoga">GitHub</a>
{this.props.onShowCode && (
<a className="ToolbarToggle" onClick={this.props.onShowCode}>
<Icon type={'code-o'} />
Code
</a>
)}
</div>
);
}
}