Replace Playground with JSX Editor (#1500)

Summary:
Pull Request resolved: https://github.com/facebook/yoga/pull/1500

Inspired by the frequent usage of Expo snacks to run RN, to repro Yoga issues, this replaces the Playground port with a new ground up Playground UI. This UI right now is pretty simple, with a JSX editor which creates a Yoga tree, which is then rendered using the WebAssembly variant of Yoga.

There are a lot of ways we can continue to improve this, but this merges the foundation. Subjectively, I find this more useful as a tool to play with Yoga behavior than the GUI.

This also replaces some of the bits of the homepage, and adds a playground entrypoint (though it's pretty identical to the one I've been testing on the home page).

Reviewed By: yungsters

Differential Revision: D51963201

fbshipit-source-id: 1265cb1784151b685686e189d47ecd42cbacdf8f
This commit is contained in:
Nick Gerleman
2023-12-12 09:06:58 -08:00
committed by Facebook GitHub Bot
parent 0d03d8a06d
commit 77742af676
37 changed files with 1591 additions and 2305 deletions

View File

@@ -71,8 +71,8 @@ export type MeasureFunction = (
export type Node = {
calculateLayout(
width?: number | 'auto',
height?: number | 'auto',
width: number | 'auto' | undefined,
height: number | 'auto' | undefined,
direction?: Direction,
): void;
copyStyle(node: Node): void;
@@ -124,45 +124,48 @@ export type Node = {
setAlignContent(alignContent: Align): void;
setAlignItems(alignItems: Align): void;
setAlignSelf(alignSelf: Align): void;
setAspectRatio(aspectRatio: number): void;
setBorder(edge: Edge, borderWidth: number): void;
setAspectRatio(aspectRatio: number | undefined): void;
setBorder(edge: Edge, borderWidth: number | undefined): void;
setDisplay(display: Display): void;
setFlex(flex: number): void;
setFlexBasis(flexBasis: number | 'auto' | `${number}%`): void;
setFlexBasisPercent(flexBasis: number): void;
setFlex(flex: number | undefined): void;
setFlexBasis(flexBasis: number | 'auto' | `${number}%` | undefined): void;
setFlexBasisPercent(flexBasis: number | undefined): void;
setFlexBasisAuto(): void;
setFlexDirection(flexDirection: FlexDirection): void;
setFlexGrow(flexGrow: number): void;
setFlexShrink(flexShrink: number): void;
setFlexGrow(flexGrow: number | undefined): void;
setFlexShrink(flexShrink: number | undefined): void;
setFlexWrap(flexWrap: Wrap): void;
setHeight(height: number | 'auto' | `${number}%`): void;
setHeight(height: number | 'auto' | `${number}%` | undefined): void;
setIsReferenceBaseline(isReferenceBaseline: boolean): void;
setHeightAuto(): void;
setHeightPercent(height: number): void;
setHeightPercent(height: number | undefined): void;
setJustifyContent(justifyContent: Justify): void;
setGap(gutter: Gutter, gapLength: number): Value;
setMargin(edge: Edge, margin: number | 'auto' | `${number}%`): void;
setGap(gutter: Gutter, gapLength: number | undefined): Value;
setMargin(
edge: Edge,
margin: number | 'auto' | `${number}%` | undefined,
): void;
setMarginAuto(edge: Edge): void;
setMarginPercent(edge: Edge, margin: number): void;
setMaxHeight(maxHeight: number | `${number}%`): void;
setMaxHeightPercent(maxHeight: number): void;
setMaxWidth(maxWidth: number | `${number}%`): void;
setMaxWidthPercent(maxWidth: number): void;
setMarginPercent(edge: Edge, margin: number | undefined): void;
setMaxHeight(maxHeight: number | `${number}%` | undefined): void;
setMaxHeightPercent(maxHeight: number | undefined): void;
setMaxWidth(maxWidth: number | `${number}%` | undefined): void;
setMaxWidthPercent(maxWidth: number | undefined): void;
setDirtiedFunc(dirtiedFunc: DirtiedFunction | null): void;
setMeasureFunc(measureFunc: MeasureFunction | null): void;
setMinHeight(minHeight: number | `${number}%`): void;
setMinHeightPercent(minHeight: number): void;
setMinWidth(minWidth: number | `${number}%`): void;
setMinWidthPercent(minWidth: number): void;
setMinHeight(minHeight: number | `${number}%` | undefined): void;
setMinHeightPercent(minHeight: number | undefined): void;
setMinWidth(minWidth: number | `${number}%` | undefined): void;
setMinWidthPercent(minWidth: number | undefined): void;
setOverflow(overflow: Overflow): void;
setPadding(edge: Edge, padding: number | `${number}%`): void;
setPaddingPercent(edge: Edge, padding: number): void;
setPosition(edge: Edge, position: number | `${number}%`): void;
setPositionPercent(edge: Edge, position: number): void;
setPadding(edge: Edge, padding: number | `${number}%` | undefined): void;
setPaddingPercent(edge: Edge, padding: number | undefined): void;
setPosition(edge: Edge, position: number | `${number}%` | undefined): void;
setPositionPercent(edge: Edge, position: number | undefined): void;
setPositionType(positionType: PositionType): void;
setWidth(width: number | 'auto' | `${number}%`): void;
setWidth(width: number | 'auto' | `${number}%` | undefined): void;
setWidthAuto(): void;
setWidthPercent(width: number): void;
setWidthPercent(width: number | undefined): void;
unsetDirtiedFunc(): void;
unsetMeasureFunc(): void;
};
@@ -227,7 +230,11 @@ export default function wrapAssembly(lib: any): Yoga {
? Unit.Percent
: Unit.Point;
asNumber = parseFloat(value);
if (!Number.isNaN(value) && Number.isNaN(asNumber)) {
if (
value !== undefined &&
!Number.isNaN(value) &&
Number.isNaN(asNumber)
) {
throw new Error(`Invalid value ${value} for ${fnName}`);
}
}

View File

@@ -66,6 +66,7 @@ export default {
position: 'left',
label: 'Documentation',
},
{to: '/playground', label: 'Playground', position: 'left'},
{to: '/blog', label: 'Blog', position: 'left'},
{
href: 'https://github.com/facebook/yoga',
@@ -124,7 +125,11 @@ export default {
},
prism: {
theme: prismThemes.github,
darkTheme: prismThemes.dracula,
darkTheme: prismThemes.oneDark,
},
colorMode: {
defaultMode: 'dark',
respectPrefersColorScheme: true,
},
}),
};

View File

@@ -17,14 +17,15 @@
"lint:fix": "eslint . --fix"
},
"dependencies": {
"@docusaurus/core": "3.0.0",
"@docusaurus/preset-classic": "3.0.0",
"@docusaurus/core": "3.0.1",
"@docusaurus/preset-classic": "3.0.1",
"@mdx-js/react": "^3.0.0",
"clsx": "^1.2.1",
"immutable": "^4.0.0",
"prism-react-renderer": "^2.1.0",
"clsx": "^2.0.0",
"nullthrows": "^1.1.1",
"prism-react-renderer": "^2.3.0",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-live": "^4.1.5",
"yoga-layout": "0.0.0"
},
"devDependencies": {

View File

@@ -22,19 +22,6 @@
const sidebars = {
// By default, Docusaurus generates a sidebar from the docs folder structure
docsSidebar: [{type: 'autogenerated', dirName: '.'}],
// But you can create a sidebar manually
/*
tutorialSidebar: [
'intro',
'hello',
{
type: 'category',
label: 'Tutorial',
items: ['tutorial-basics/create-a-document'],
},
],
*/
};
module.exports = sidebars;

View File

@@ -0,0 +1,80 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
.toolbar {
display: flex;
column-gap: 8px;
position: absolute;
top: 10px;
right: 10px;
}
.toolbar button {
display: flex;
align-items: center;
background: var(--prism-background-color);
color: var(--prism-color);
border: 1px solid var(--ifm-color-emphasis-300);
border-radius: var(--ifm-global-radius);
padding: 0.4rem;
line-height: 0;
transition: opacity var(--ifm-transition-fast) ease-in-out;
opacity: 0.4;
}
.toolbar button:focus-visible,
.toolbar button:hover {
opacity: 1 !important;
}
.icon {
width: 24px;
height: 24px;
}
.iconSwitcher {
position: relative;
width: 24px;
height: 24px;
}
.actionIcon,
.successIcon {
position: absolute;
top: 0;
left: 0;
fill: currentColor;
opacity: inherit;
width: inherit;
height: inherit;
transition: all var(--ifm-transition-fast) ease;
}
.successIcon {
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0.33);
opacity: 0;
color: #00d600;
}
.clicked .actionIcon {
transform: scale(0.33);
opacity: 0;
}
.clicked .successIcon {
transform: translate(-50%, -50%) scale(1);
opacity: 1;
transition-delay: 0.075s;
}
@media (max-width: 996px) {
.toolbar {
display: none;
}
}

View File

@@ -0,0 +1,82 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {useCallback, useEffect, useRef, useState} from 'react';
import clsx from 'clsx';
import CopyIcon from '../../static/img/copy.svg';
import LinkIcon from '../../static/img/link.svg';
import SuccessIcon from '@theme/Icon/Success';
import styles from './EditorToolbar.module.css';
export type Props = Readonly<{
getCode: () => string;
}>;
export default function EditorToolbar({getCode}: Props): JSX.Element {
const handleCopy = useCallback(
() => navigator.clipboard.writeText(getCode()),
[],
);
const handleShare = useCallback(
() =>
navigator.clipboard.writeText(
window.location.origin +
`/playground?code=${encodeURIComponent(btoa(getCode()))}`,
),
[],
);
return (
<div className={clsx(styles.toolbar)}>
<ToolbarButton Icon={CopyIcon} label="Copy" onClick={handleCopy} />
<ToolbarButton Icon={LinkIcon} label="Share" onClick={handleShare} />
</div>
);
}
type ToolbarButtonProps = Readonly<{
onClick: () => void;
Icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;
label?: string;
}>;
function ToolbarButton({
onClick,
Icon,
label,
}: ToolbarButtonProps): JSX.Element {
const [isSuccess, setIsSuccess] = useState(false);
const copyTimeout = useRef<number | undefined>(undefined);
useEffect(() => () => window.clearTimeout(copyTimeout.current), []);
const handleClick = useCallback(() => {
onClick();
setIsSuccess(true);
copyTimeout.current = window.setTimeout(() => {
setIsSuccess(false);
}, 1000);
}, []);
return (
<button
type="button"
className={clsx('clean-btn', isSuccess && styles.clicked)}
onClick={handleClick}
aria-label={label}>
<span className={styles.iconSwitcher} aria-hidden="true">
<Icon className={styles.actionIcon} />
<SuccessIcon className={styles.successIcon} />
</span>
</button>
);
}

View File

@@ -0,0 +1,411 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {
Align,
Display,
Edge,
FlexDirection,
Gutter,
Justify,
Overflow,
PositionType,
Wrap,
Node as YogaNode,
} from 'yoga-layout';
export type AlignContent =
| 'flex-start'
| 'flex-end'
| 'center'
| 'stretch'
| 'space-between'
| 'space-around'
| 'space-evenly';
export type AlignItems =
| 'flex-start'
| 'flex-end'
| 'center'
| 'stretch'
| 'baseline';
export type JustifyContent =
| 'flex-start'
| 'flex-end'
| 'center'
| 'space-between'
| 'space-around'
| 'space-evenly';
export type FlexStyle = {
alignContent?: AlignContent;
alignItems?: AlignItems;
alignSelf?: AlignItems;
aspectRatio?: number;
borderBottomWidth?: number;
borderEndWidth?: number;
borderLeftWidth?: number;
borderRightWidth?: number;
borderStartWidth?: number;
borderTopWidth?: number;
borderWidth?: number;
borderInlineWidth?: number;
borderBlockWidth?: number;
bottom?: number | `${number}%`;
display?: 'none' | 'flex';
end?: number | `${number}%`;
flex?: number;
flexBasis?: number | 'auto' | `${number}%`;
flexDirection?: 'row' | 'column' | 'row-reverse' | 'column-reverse';
rowGap?: number;
gap?: number;
columnGap?: number;
flexGrow?: number;
flexShrink?: number;
flexWrap?: 'wrap' | 'nowrap' | 'wrap-reverse';
height?: number | 'auto' | `${number}%`;
justifyContent?: JustifyContent;
left?: number | `${number}%`;
margin?: number | 'auto' | `${number}%`;
marginBottom?: number | 'auto' | `${number}%`;
marginEnd?: number | 'auto' | `${number}%`;
marginLeft?: number | 'auto' | `${number}%`;
marginRight?: number | 'auto' | `${number}%`;
marginStart?: number | 'auto' | `${number}%`;
marginTop?: number | 'auto' | `${number}%`;
marginInline?: number | 'auto' | `${number}%`;
marginBlock?: number | 'auto' | `${number}%`;
maxHeight?: number | `${number}%`;
maxWidth?: number | `${number}%`;
minHeight?: number | `${number}%`;
minWidth?: number | `${number}%`;
overflow?: 'visible' | 'hidden' | 'scroll';
padding?: number | `${number}%`;
paddingBottom?: number | `${number}%`;
paddingEnd?: number | `${number}%`;
paddingLeft?: number | `${number}%`;
paddingRight?: number | `${number}%`;
paddingStart?: number | `${number}%`;
paddingTop?: number | `${number}%`;
paddingInline?: number | `${number}%`;
paddingBlock?: number | `${number}%`;
position?: 'absolute' | 'relative' | 'static';
right?: number | `${number}%`;
start?: number | `${number}%`;
top?: number | `${number}%`;
insetInline?: number | `${number}%`;
insetBlock?: number | `${number}%`;
inset?: number | `${number}%`;
width?: number | 'auto' | `${number}%`;
};
export function applyStyle(node: YogaNode, style: FlexStyle = {}): void {
for (const key of Object.keys(style)) {
try {
switch (key) {
case 'alignContent':
node.setAlignContent(alignContent(style.alignContent));
break;
case 'alignItems':
node.setAlignItems(alignItems(style.alignItems));
break;
case 'alignSelf':
node.setAlignSelf(alignItems(style.alignSelf));
break;
case 'aspectRatio':
node.setAspectRatio(style.aspectRatio);
break;
case 'borderBottomWidth':
node.setBorder(Edge.Bottom, style.borderBottomWidth);
break;
case 'borderEndWidth':
node.setBorder(Edge.End, style.borderEndWidth);
break;
case 'borderLeftWidth':
node.setBorder(Edge.Left, style.borderLeftWidth);
break;
case 'borderRightWidth':
node.setBorder(Edge.Right, style.borderRightWidth);
break;
case 'borderStartWidth':
node.setBorder(Edge.Start, style.borderStartWidth);
break;
case 'borderTopWidth':
node.setBorder(Edge.Top, style.borderTopWidth);
break;
case 'borderWidth':
node.setBorder(Edge.All, style.borderWidth);
break;
case 'borderInlineWidth':
node.setBorder(Edge.Horizontal, style.borderInlineWidth);
break;
case 'borderBlockWidth':
node.setBorder(Edge.Vertical, style.borderBlockWidth);
break;
case 'bottom':
node.setPosition(Edge.Bottom, style.bottom);
break;
case 'display':
node.setDisplay(display(style.display));
break;
case 'end':
node.setPosition(Edge.End, style.end);
break;
case 'flex':
node.setFlex(style.flex);
break;
case 'flexBasis':
node.setFlexBasis(style.flexBasis);
break;
case 'flexDirection':
node.setFlexDirection(flexDirection(style.flexDirection));
break;
case 'rowGap':
node.setGap(Gutter.Row, style.rowGap);
break;
case 'gap':
node.setGap(Gutter.All, style.gap);
break;
case 'columnGap':
node.setGap(Gutter.Column, style.columnGap);
break;
case 'flexGrow':
node.setFlexGrow(style.flexGrow);
break;
case 'flexShrink':
node.setFlexShrink(style.flexShrink);
break;
case 'flexWrap':
node.setFlexWrap(flexWrap(style.flexWrap));
break;
case 'height':
node.setHeight(style.height);
break;
case 'justifyContent':
node.setJustifyContent(justifyContent(style.justifyContent));
break;
case 'left':
node.setPosition(Edge.Left, style.left);
break;
case 'margin':
node.setMargin(Edge.All, style.margin);
break;
case 'marginBottom':
node.setMargin(Edge.Bottom, style.marginBottom);
break;
case 'marginEnd':
node.setMargin(Edge.End, style.marginEnd);
break;
case 'marginLeft':
node.setMargin(Edge.Left, style.marginLeft);
break;
case 'marginRight':
node.setMargin(Edge.Right, style.marginRight);
break;
case 'marginStart':
node.setMargin(Edge.Start, style.marginStart);
break;
case 'marginTop':
node.setMargin(Edge.Top, style.marginTop);
break;
case 'marginInline':
node.setMargin(Edge.Horizontal, style.marginInline);
break;
case 'marginBlock':
node.setMargin(Edge.Vertical, style.marginBlock);
break;
case 'maxHeight':
node.setMaxHeight(style.maxHeight);
break;
case 'maxWidth':
node.setMaxWidth(style.maxWidth);
break;
case 'minHeight':
node.setMinHeight(style.minHeight);
break;
case 'minWidth':
node.setMinWidth(style.minWidth);
break;
case 'overflow':
node.setOverflow(overflow(style.overflow));
break;
case 'padding':
node.setPadding(Edge.All, style.padding);
break;
case 'paddingBottom':
node.setPadding(Edge.Bottom, style.paddingBottom);
break;
case 'paddingEnd':
node.setPadding(Edge.End, style.paddingEnd);
break;
case 'paddingLeft':
node.setPadding(Edge.Left, style.paddingLeft);
break;
case 'paddingRight':
node.setPadding(Edge.Right, style.paddingRight);
break;
case 'paddingStart':
node.setPadding(Edge.Start, style.paddingStart);
break;
case 'paddingTop':
node.setPadding(Edge.Top, style.paddingTop);
break;
case 'paddingInline':
node.setPadding(Edge.Horizontal, style.paddingInline);
break;
case 'paddingBlock':
node.setPadding(Edge.Vertical, style.paddingBlock);
break;
case 'position':
node.setPositionType(position(style.position));
break;
case 'right':
node.setPosition(Edge.Right, style.right);
break;
case 'start':
node.setPosition(Edge.Start, style.start);
break;
case 'top':
node.setPosition(Edge.Top, style.top);
break;
case 'insetInline':
node.setPosition(Edge.Horizontal, style.insetInline);
break;
case 'insetBlock':
node.setPosition(Edge.Vertical, style.insetBlock);
break;
case 'inset':
node.setPosition(Edge.All, style.inset);
break;
case 'width':
node.setWidth(style.width);
break;
}
} catch (e) {
// Fail gracefully
}
}
}
function alignContent(str?: AlignContent): Align {
switch (str) {
case 'flex-start':
return Align.FlexStart;
case 'flex-end':
return Align.FlexEnd;
case 'center':
return Align.Center;
case 'stretch':
return Align.Stretch;
case 'space-between':
return Align.SpaceBetween;
case 'space-around':
return Align.SpaceAround;
case 'space-evenly':
return Align.SpaceEvenly;
}
throw new Error(`"${str}" is not a valid value for alignContent`);
}
function alignItems(str?: AlignItems): Align {
switch (str) {
case 'flex-start':
return Align.FlexStart;
case 'flex-end':
return Align.FlexEnd;
case 'center':
return Align.Center;
case 'stretch':
return Align.Stretch;
case 'baseline':
return Align.Baseline;
}
throw new Error(`"${str}" is not a valid value for alignItems`);
}
function display(str?: 'none' | 'flex'): Display {
switch (str) {
case 'none':
return Display.None;
case 'flex':
return Display.Flex;
}
throw new Error(`"${str}" is not a valid value for display`);
}
function flexDirection(
str?: 'row' | 'column' | 'row-reverse' | 'column-reverse',
): FlexDirection {
switch (str) {
case 'row':
return FlexDirection.Row;
case 'column':
return FlexDirection.Column;
case 'row-reverse':
return FlexDirection.RowReverse;
case 'column-reverse':
return FlexDirection.ColumnReverse;
}
throw new Error(`"${str}" is not a valid value for flexDirection`);
}
function flexWrap(str?: 'wrap' | 'nowrap' | 'wrap-reverse'): Wrap {
switch (str) {
case 'wrap':
return Wrap.Wrap;
case 'nowrap':
return Wrap.NoWrap;
case 'wrap-reverse':
return Wrap.WrapReverse;
}
throw new Error(`"${str}" is not a valid value for flexWrap`);
}
function justifyContent(str?: JustifyContent): Justify {
switch (str) {
case 'flex-start':
return Justify.FlexStart;
case 'flex-end':
return Justify.FlexEnd;
case 'center':
return Justify.Center;
case 'space-between':
return Justify.SpaceBetween;
case 'space-around':
return Justify.SpaceAround;
case 'space-evenly':
return Justify.SpaceEvenly;
}
throw new Error(`"${str}" is not a valid value for justifyContent`);
}
function overflow(str?: 'visible' | 'hidden' | 'scroll'): Overflow {
switch (str) {
case 'visible':
return Overflow.Visible;
case 'hidden':
return Overflow.Hidden;
case 'scroll':
return Overflow.Scroll;
}
throw new Error(`"${str}" is not a valid value for overflow`);
}
function position(str?: 'absolute' | 'relative' | 'static'): PositionType {
switch (str) {
case 'absolute':
return PositionType.Absolute;
case 'relative':
return PositionType.Relative;
case 'static':
return PositionType.Static;
}
throw new Error(`"${str}" is not a valid value for position`);
}

View File

@@ -0,0 +1,68 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
html[data-theme='light'] {
--yg-color-node-depth-0: var(--ifm-color-gray-0);
--yg-color-node-depth-1: var(--ifm-color-gray-200);
--yg-color-node-depth-2: var(--ifm-color-gray-400);
--yg-color-node-depth-3: var(--ifm-color-gray-600);
--yg-color-node-depth-4: var(--ifm-color-gray-800);
--yg-border-node-depth-0: 1px solid var(--ifm-color-gray-200);
--yg-border-node-depth-1: 1px solid var(--ifm-color-gray-600);
--yg-border-node-depth-2: 1px solid var(--ifm-color-gray-700);
--yg-border-node-depth-3: 1px solid var(--ifm-color-gray-800);
--yg-border-node-depth-4: 1px solid var(--ifm-color-gray-900);
}
html[data-theme='dark'] {
--yg-color-node-depth-0: var(--ifm-color-gray-900);
--yg-color-node-depth-1: var(--ifm-color-gray-800);
--yg-color-node-depth-2: var(--ifm-color-gray-700);
--yg-color-node-depth-3: var(--ifm-color-gray-600);
--yg-color-node-depth-4: var(--ifm-color-gray-500);
--yg-border-node-depth-0: 1px solid var(--ifm-color-gray-800);
--yg-border-node-depth-1: 1px solid var(--ifm-color-gray-700);
--yg-border-node-depth-2: 1px solid var(--ifm-color-gray-600);
--yg-border-node-depth-3: 1px solid var(--ifm-color-gray-500);
--yg-border-node-depth-4: 1px solid var(--ifm-color-gray-400);
}
.layoutBox {
box-sizing: border-box;
border-radius: 2px;
}
.zeroDim {
border: 0 !important;
}
.depthZero {
background: var(--yg-color-node-depth-0);
border: var(--yg-border-node-depth-0);
}
.depthOne {
background-color: var(--yg-color-node-depth-1);
border: var(--yg-border-node-depth-1);
}
.depthTwo {
background-color: var(--yg-color-node-depth-2);
border: var(--yg-border-node-depth-2);
}
.depthThree {
background-color: var(--yg-color-node-depth-3);
border: var(--yg-border-node-depth-3);
}
.depthFour {
background-color: var(--yg-color-node-depth-4);
border: var(--yg-border-node-depth-4);
}

View File

@@ -0,0 +1,56 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import styles from './LayoutBox.module.css';
import clsx from 'clsx';
export type LayoutMetrics = {
top: number;
left: number;
width: number;
height: number;
overflow?: 'visible' | 'hidden' | 'scroll';
children?: LayoutMetrics[];
};
export type Props = Readonly<{
metrics: LayoutMetrics;
className?: string;
depth: number;
}>;
export default function LayoutBox({metrics, depth, className}: Props) {
const {children, ...style} = metrics;
return (
<div
className={clsx(
styles.layoutBox,
(metrics.height === 0 || metrics.width === 0) && styles.zeroDim,
depth % 5 == 0 && styles.depthZero,
depth % 5 == 1 && styles.depthOne,
depth % 5 == 2 && styles.depthTwo,
depth % 5 == 3 && styles.depthThree,
depth % 5 == 4 && styles.depthFour,
className,
)}
style={{
top: style.top,
left: style.left,
width: style.width,
height: style.height,
overflow: style.overflow,
position: depth === 0 ? 'relative' : 'absolute',
}}>
{children?.map((child, i) => (
<LayoutBox key={i} metrics={child} depth={depth + 1} />
))}
</div>
);
}

View File

@@ -0,0 +1,108 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
html[data-theme='light'] {
--yg-color-preview-background: var(--ifm-color-primary-lighter);
--yg-color-playground-background: var(--ifm-color-gray-200);
--yg-color-editor-border: var(--ifm-color-gray-400);
}
html[data-theme='dark'] {
--yg-color-preview-background: var(--ifm-color-primary-dark);
--yg-color-playground-background: var(--ifm-color-background);
--yg-color-editor-border: var(--ifm-color-gray-800);
}
.wrapper {
display: flex;
justify-content: center;
min-width: 900px;
width: 100%;
padding-block: 16px;
background-color: var(--yg-color-playground-background);
}
.editorColumn {
position: relative;
flex: 8;
min-width: 0;
}
.playgroundRow {
display: flex;
flex-direction: row;
column-gap: 16px;
}
.playgroundEditor {
font: var(--ifm-code-font-size) / var(--ifm-pre-line-height)
var(--ifm-font-family-monospace) !important;
direction: ltr;
height: calc(var(--yg-playground-height, 400px) - 32px);
overflow: scroll;
}
.playgroundEditor :global(.prism-code) {
height: 100%;
box-shadow: var(--ifm-global-shadow-lw);
border: 1px solid var(--yg-color-editor-border);
}
.previewColumn {
display: flex;
flex: 5;
height: calc(var(--yg-playground-height, 400px) - 32px);
align-items: center;
justify-content: center;
background-color: var(--yg-color-preview-background);
overflow: hidden;
border-radius: var(--ifm-pre-border-radius);
align-self: flex-start;
box-shadow: var(--ifm-global-shadow-lw);
}
.livePreviewWrapper {
box-shadow: var(--ifm-global-shadow-md);
}
.liveError {
align-self: flex-start;
font-size: 12px;
box-shadow: var(--ifm-global-shadow-lw);
background-color:var(--ifm-color-danger-darker);
color: white;
width: 100%;
text-align: center;
margin: 10px;
}
@media (max-width: 996px) {
.wrapper {
min-width: 100%;
}
.playgroundEditor {
height: max-content;
overflow: visible;
}
.playgroundRow {
flex-direction: column;
padding-inline: 5px;
}
.editorColumn {
padding: 0;
flex: 0 !important;
margin-bottom: 10px;
}
.previewColumn {
padding: 10px;
width: 100%;
}
}

View File

@@ -0,0 +1,165 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import React, {
CSSProperties,
Suspense,
lazy,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import {usePrismTheme} from '@docusaurus/theme-common';
import clsx from 'clsx';
import nullthrows from 'nullthrows';
import {LiveProvider, LiveEditor, LivePreview, LiveError} from 'react-live';
import EditorToolbar from './EditorToolbar';
import type {FlexStyle} from './FlexStyle';
import type {StyleNode} from './YogaViewer';
import styles from './Playground.module.css';
const defaultCode = `
<Layout config={{useWebDefaults: false}}>
<Node style={{width: 350, height: 350, padding: 20}}>
<Node style={{flex: 1}} />
</Node>
</Layout>
`.trim();
export type Props = Readonly<{
code?: string;
height?: CSSProperties['height'];
autoFocus?: boolean;
}>;
export default function Playground({code, height, autoFocus}: Props) {
const prismTheme = usePrismTheme();
const playgroundRef = useRef<HTMLDivElement>(null);
const [isLoaded, setIsLoaded] = useState(false);
const LivePreviewWrapper = useCallback(
(props: React.ComponentProps<'div'>) => {
useEffect(() => {
setIsLoaded(true);
}, []);
return <div {...props} className={styles.livePreviewWrapper} />;
},
[],
);
useEffect(() => {
// TODO: This is hacky and relies on being called after some operation
// "react-live" does which itself can manipulate global focus
if (isLoaded && autoFocus) {
const codeElem = playgroundRef?.current?.querySelector('.prism-code');
const sel = window.getSelection();
if (codeElem != null && sel != null) {
sel.selectAllChildren(codeElem);
sel.collapseToStart();
}
}
}, [isLoaded, autoFocus]);
const heightStyle = height
? ({'--yg-playground-height': height} as React.CSSProperties)
: undefined;
const resolvedCode = code ?? defaultCode;
return (
<LiveProvider code={resolvedCode} theme={prismTheme} scope={{Layout, Node}}>
<div className={styles.wrapper} ref={playgroundRef} style={heightStyle}>
<div className={clsx(styles.playgroundRow, 'container')}>
<div className={clsx(styles.editorColumn)}>
<EditorToolbar
getCode={useCallback(
() =>
nullthrows(
playgroundRef.current?.querySelector('.prism-code')
?.textContent,
),
[],
)}
/>
<LiveEditor className={clsx(styles.playgroundEditor)} />
</div>
<div className={clsx(styles.previewColumn)}>
<LivePreview
className={clsx(styles.livePreview)}
Component={LivePreviewWrapper}
/>
<LiveError className={clsx(styles.liveError)} />
</div>
</div>
</div>
</LiveProvider>
);
}
type LayoutProps = Readonly<{
children: React.ReactNode;
config?: {useWebDefaults?: boolean};
}>;
function Layout({children, config}: LayoutProps) {
if (React.Children.count(children) !== 1) {
return null;
}
const child = React.Children.only(children);
if (!React.isValidElement(child) || child.type !== Node) {
return null;
}
const styleNode = styleNodeFromYogaNode(child as unknown as Node);
return (
<Suspense fallback={null}>
<LazyYogaViewer
rootNode={styleNode}
useWebDefaults={config?.useWebDefaults}
/>
</Suspense>
);
}
type NodeProps = Readonly<{
children: React.ReactNode;
style: FlexStyle;
}>;
class Node extends React.PureComponent<NodeProps> {}
function styleNodeFromYogaNode(
yogaNode: React.ElementRef<typeof Node>,
): StyleNode {
const children: StyleNode[] = [];
React.Children.forEach(yogaNode.props.children, child => {
if (React.isValidElement(child) && child.type === Node) {
children.push(styleNodeFromYogaNode(child as unknown as Node));
}
});
return {
style: yogaNode.props.style,
children,
};
}
// Docusaurus SSR does not correctly support top-level await in the import
// chain
// 1. https://github.com/facebook/docusaurus/issues/7238
// 2. https://github.com/facebook/docusaurus/issues/9468
const LazyYogaViewer = lazy(() => import('./YogaViewer'));

View File

@@ -1,10 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
.input {
width: 100%;
}

View File

@@ -1,47 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import React from 'react';
import YogaEnumSelect from './YogaEnumSelect';
import YogaPositionEditor from './YogaPositionEditor';
import styles from './EditValue.module.css';
type Props<T> = {
property: string;
disabled?: boolean;
value?: T;
onChange: (property: string, value: T) => void;
placeholder?: string;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default (props: Props<any>) => {
if (YogaEnumSelect.availableProperties.indexOf(props.property) > -1) {
// @ts-ignore
return <YogaEnumSelect {...props} />;
} else if (
YogaPositionEditor.availableProperties.indexOf(props.property) > -1
) {
// @ts-ignore
return <YogaPositionEditor {...props} />;
} else {
return (
<input
className={styles.input}
type="text"
{...props}
onChange={e => props.onChange(props.property, e.target.value)}
placeholder={props.placeholder || 'undefined'}
onFocus={e => e.target.select()}
value={Number.isNaN(props.value) ? '' : props.value}
/>
);
}
};

View File

@@ -1,32 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
.editor {
display: flex;
flex-direction: column;
height: 100%;
}
.editor h2 {
margin-bottom: 8px;
margin-top: 20px;
font-size: 12px;
font-weight: 700;
color: var(--ifm-color-content-secondary);
text-transform: uppercase;
}
.tabItem {
overflow-y: auto;
max-height: 400px;
}
.editorButtons {
display: flex;
margin-top: auto;
gap: 5px;
}

View File

@@ -1,268 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import type {LayoutRecordType} from './LayoutRecord';
import type {Direction} from 'yoga-layout';
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import React from 'react';
import EditValue from './EditValue';
import styles from './Editor.module.css';
type Props = {
node: LayoutRecordType;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onChangeLayout: (key: string, value: any) => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onChangeSetting: (key: string, value: any) => void;
direction: Direction;
selectedNodeIsRoot: boolean;
onRemove?: () => void;
onAdd?: () => void;
};
export default function Editor(props: Props) {
const {node, selectedNodeIsRoot} = props;
const disabled = node == null;
return (
<div className={styles.editor}>
<Tabs block={true}>
<TabItem
value="flex"
label="Flex"
className={styles.tabItem}
default={true}>
<h2>Direction</h2>
<EditValue
property="direction"
value={props.direction}
onChange={props.onChangeSetting}
/>
<h2>Flex Direction</h2>
<EditValue
disabled={disabled}
property="flexDirection"
value={node ? node.flexDirection : undefined}
onChange={props.onChangeLayout}
/>
<div className="row margin--none">
<div className="col col--4">
<h2>Basis</h2>
<EditValue
// @ts-ignore
type="text"
property="flexBasis"
placeholder="auto"
disabled={disabled || selectedNodeIsRoot}
value={node ? node.flexBasis : undefined}
onChange={props.onChangeLayout}
/>
</div>
<div className="col col--4">
<h2>Grow</h2>
<EditValue
// @ts-ignore
type="text"
property="flexGrow margin--none"
placeholder="0"
disabled={disabled || selectedNodeIsRoot}
value={node ? node.flexGrow : undefined}
onChange={props.onChangeLayout}
/>
</div>
<div className="col col--4">
<h2>Shrink</h2>
<EditValue
// @ts-ignore
type="text"
property="flexShrink"
placeholder="1"
disabled={disabled || selectedNodeIsRoot}
value={node ? node.flexShrink : undefined}
onChange={props.onChangeLayout}
/>
</div>
</div>
<h2>Flex Wrap</h2>
<EditValue
disabled={disabled}
property="flexWrap"
value={node ? node.flexWrap : undefined}
onChange={props.onChangeLayout}
/>
</TabItem>
<TabItem value="alignment" label="Alignment" className={styles.tabItem}>
<h2>Justify Content</h2>
<EditValue
disabled={disabled}
property="justifyContent"
value={node ? node.justifyContent : undefined}
onChange={props.onChangeLayout}
/>
<h2>Align Items</h2>
<EditValue
disabled={disabled}
property="alignItems"
value={node ? node.alignItems : undefined}
onChange={props.onChangeLayout}
/>
<h2>Align Self</h2>
<EditValue
disabled={disabled || selectedNodeIsRoot}
property="alignSelf"
value={node ? node.alignSelf : undefined}
onChange={props.onChangeLayout}
/>
<h2>Align Content</h2>
<EditValue
disabled={disabled}
property="alignContent"
value={node ? node.alignContent : undefined}
onChange={props.onChangeLayout}
/>
</TabItem>
<TabItem value="layout" label="Layout" className={styles.tabItem}>
<h2>Width &times; Height</h2>
<div className="row margin--none">
<div className="col col--6">
<EditValue
// @ts-ignore
type="text"
placeholder="auto"
property="width"
disabled={disabled}
value={node ? node.width : undefined}
onChange={props.onChangeLayout}
/>
</div>
<div className="col col--6">
<EditValue
// @ts-ignore
type="text"
placeholder="auto"
property="height"
disabled={disabled}
value={node ? node.height : undefined}
onChange={props.onChangeLayout}
/>
</div>
</div>
<h2>Max-Width &times; Max-Height</h2>
<div className="row margin--none">
<div className="col col--6">
<EditValue
// @ts-ignore
type="text"
placeholder="none"
property="maxWidth"
disabled={disabled}
value={node ? node.maxWidth : undefined}
onChange={props.onChangeLayout}
/>
</div>
<div className="col col--6">
<EditValue
// @ts-ignore
type="text"
placeholder="none"
property="maxHeight"
disabled={disabled}
value={node ? node.maxHeight : undefined}
onChange={props.onChangeLayout}
/>
</div>
</div>
<h2>Min-Width &times; Min-Height</h2>
<div className="row margin--none">
<div className="col col--6">
<EditValue
// @ts-ignore
type="text"
placeholder="0"
property="minWidth"
disabled={disabled}
value={node ? node.minWidth : undefined}
onChange={props.onChangeLayout}
/>
</div>
<div className="col col--6">
<EditValue
// @ts-ignore
type="text"
placeholder="0"
property="minHeight"
disabled={disabled}
value={node ? node.minHeight : undefined}
onChange={props.onChangeLayout}
/>
</div>
</div>
<h2>Aspect Ratio</h2>
<EditValue
// @ts-ignore
type="text"
placeholder="auto"
property="aspectRatio"
disabled={disabled}
value={node ? node.aspectRatio : undefined}
onChange={props.onChangeLayout}
/>
{['padding', 'border', 'margin'].map(property => (
<EditValue
property={property}
key={property}
value={node ? node[property] : undefined}
onChange={props.onChangeLayout}
disabled={property === 'margin' && selectedNodeIsRoot}
/>
))}
<h2>Position Type</h2>
<EditValue
disabled={disabled || selectedNodeIsRoot}
property="positionType"
value={node ? node.positionType : undefined}
onChange={props.onChangeLayout}
/>
<EditValue
disabled={selectedNodeIsRoot}
property="position"
value={node ? node.position : undefined}
onChange={props.onChangeLayout}
/>
</TabItem>
</Tabs>
<div className={styles.editorButtons}>
<button
className="button button--block button--primary button--sm"
disabled={!props.onRemove}
onClick={props.onAdd}>
add child
</button>
<button
className="button button--block button--danger button--sm"
disabled={!props.onRemove}
onClick={props.onAdd}>
remove
</button>
</div>
</div>
);
}

View File

@@ -1,73 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {Record, List} from 'immutable';
import PositionRecord from './PositionRecord';
import type {PositionRecordType} from './PositionRecord';
import {Align, Justify, FlexDirection, Wrap, PositionType} from 'yoga-layout';
export type LayoutRecordType = ReturnType<LayoutRecordFactory>;
export type LayoutRecordFactory = Record.Factory<{
width?: number | 'auto';
height?: number | 'auto';
minWidth?: number;
minHeight?: number;
maxWidth?: number;
maxHeight?: number;
justifyContent?: Justify;
padding: PositionRecordType;
border: PositionRecordType;
margin: PositionRecordType;
position: PositionRecordType;
positionType: PositionType;
alignItems?: Align;
alignSelf?: Align;
alignContent?: Align;
flexDirection?: FlexDirection;
flexBasis?: number | 'auto';
flexGrow?: number;
flexShrink?: number;
flexWrap?: Wrap;
aspectRatio?: number | 'auto';
children?: List<LayoutRecordType>;
}>;
const r: LayoutRecordFactory = Record({
width: 'auto',
height: 'auto',
justifyContent: Justify.FlexStart,
alignItems: Align.Stretch,
alignSelf: Align.Auto,
alignContent: Align.Stretch,
flexDirection: FlexDirection.Row,
padding: PositionRecord(),
margin: PositionRecord(),
border: PositionRecord(),
position: PositionRecord({
left: NaN,
top: NaN,
right: NaN,
bottom: NaN,
}),
positionType: PositionType.Relative,
flexWrap: Wrap.NoWrap,
flexBasis: 'auto',
flexGrow: 0,
flexShrink: 1,
children: List(),
aspectRatio: 'auto',
minWidth: NaN,
maxWidth: NaN,
minHeight: NaN,
maxHeight: NaN,
});
export default r;

View File

@@ -1,300 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import React, {Component} from 'react';
import {Direction} 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 Sidebar from './Sidebar';
import type {LayoutRecordType} from './LayoutRecord';
import styles from './Playground.module.css';
import clsx from 'clsx';
type Props = {
layoutDefinition?: LayoutRecordType;
direction?: Direction;
maxDepth?: number;
maxChildren?: number;
minChildren?: number;
selectedNodePath?: Array<number>;
showGuides?: boolean;
className?: string;
height?: string | number;
persist?: boolean;
renderSidebar?: (
layoutDefinition: LayoutRecordType,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onChange: () => any,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
) => any;
};
type State = {
selectedNodePath?: Array<number>;
layoutDefinition: LayoutRecordType;
direction: Direction;
};
function getPath(path: Array<number>): Array<unknown> {
return path.reduce((acc, cv) => acc.concat('children', cv), []);
}
export default class Playground extends Component<Props, State> {
_containerRef?: HTMLElement;
static defaultProps = {
layoutDefinition: {
width: 500,
height: 500,
children: [
{width: 100, height: 100},
{width: 100, height: 100},
{width: 100, height: 100},
],
},
direction: Direction.LTR,
maxDepth: 3,
showGuides: true,
persist: false,
};
rehydrate = (node: LayoutRecordType): LayoutRecordType => {
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;
};
state = {
selectedNodePath: this.props.selectedNodePath,
layoutDefinition: this.rehydrate(this.props.layoutDefinition),
direction: this.props.direction,
};
componentDidMount() {
document.addEventListener('keydown', this.onKeyDown);
// rehydrate
if (window.location.search && window.location.search.length > 1) {
try {
const restoredState = JSON.parse(
atob(window.location.search.substr(1)),
);
this.setState({layoutDefinition: this.rehydrate(restoredState)});
} catch (e) {
window.history.replaceState(
{},
null,
window.location.origin + window.location.pathname,
);
}
}
}
componentWillUnmount() {
document.removeEventListener('keydown', this.onKeyDown);
}
onKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
this.hideSidePanes();
}
};
onMouseDown = (e: React.MouseEvent) => {
if (e.target === this._containerRef) {
this.hideSidePanes();
}
};
hideSidePanes() {
if (!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,
});
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
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');
// @ts-ignore
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)
// @ts-ignore
.push(LayoutRecord({width: 100, height: 100}));
this.modifyAtPath(path, updatedChildren);
}
};
modifyAtPath(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
path: Array<any>,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value: any,
selectedNodePath: Array<number> = this.state.selectedNodePath,
) {
const layoutDefinition = setIn(this.state.layoutDefinition, path, value);
this.setState({
layoutDefinition,
selectedNodePath,
});
if (this.props.persist) {
window.history.replaceState(
{},
null,
window.location.origin +
window.location.pathname +
'?' +
this.getHash(layoutDefinition),
);
}
}
getHash = (
layoutDefinition: LayoutRecordType = this.state.layoutDefinition,
): string =>
btoa(JSON.stringify(this.removeUnchangedProperties(layoutDefinition)));
removeUnchangedProperties = (
node: LayoutRecordType,
): {children?: unknown} => {
const untouchedLayout = LayoutRecord({});
const untouchedPosition = PositionRecord({});
const result: {children?: unknown} = {};
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: LayoutRecordType = (
this.state.selectedNodePath || []
).reduce(
(node: LayoutRecordType, cv) => node.children.get(cv),
this.state.layoutDefinition,
);
return selectedNode ? selectedNode.children.size : 0;
};
render() {
const {layoutDefinition, selectedNodePath, direction} = this.state;
const {height} = this.props;
// @ts-ignore
const selectedNode: LayoutRecordType | null = selectedNodePath
? layoutDefinition.getIn(getPath(selectedNodePath))
: null;
const playground = (
<div
className={styles.playground}
onMouseDown={this.onMouseDown}
style={{height, maxHeight: height}}
ref={ref => {
this._containerRef = ref;
}}>
<YogaNode
layoutDefinition={layoutDefinition}
selectedNodePath={selectedNodePath}
onClick={selectedNodePath => this.setState({selectedNodePath})}
onDoubleClick={this.onAdd}
direction={direction}
showGuides={this.props.showGuides}
/>
</div>
);
const sidebarContent = this.props.renderSidebar
? this.props.renderSidebar(
// @ts-ignore
layoutDefinition.getIn(getPath(selectedNodePath)),
this.onChangeLayout,
)
: this.state.selectedNodePath != null && (
<Editor
node={selectedNode}
selectedNodeIsRoot={
selectedNodePath ? selectedNodePath.length === 0 : false
}
onChangeLayout={this.onChangeLayout}
onChangeSetting={(key, value) =>
// @ts-ignore
this.setState({[key]: value})
}
direction={direction}
onRemove={
selectedNodePath && selectedNodePath.length > 0
? this.onRemove
: undefined
}
onAdd={
selectedNodePath && selectedNodePath.length < this.props.maxDepth
? this.onAdd
: undefined
}
/>
);
return (
<div className={clsx(styles.container, this.props.className)}>
{playground}
<Sidebar>{sidebarContent}</Sidebar>
</div>
);
}
}

View File

@@ -1,16 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
.PositionGuide {
position: absolute;
pointer-events: none;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
user-select: none;
}

View File

@@ -1,159 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import React, {Component} from 'react';
import PositionRecord from './PositionRecord';
import type {PositionRecordType} from './PositionRecord';
import './PositionGuide.css';
type Props = {
inset?: boolean;
reverse?: boolean;
position: PositionRecordType;
offset: PositionRecordType;
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 (
typeof top !== 'number' ||
typeof left !== 'number' ||
typeof right !== 'number' ||
typeof bottom !== 'number' ||
typeof oTop !== 'number' ||
typeof oLeft !== 'number' ||
typeof oRight !== 'number' ||
typeof oBottom !== 'number'
) {
return null;
}
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 (typeof top === 'number' && typeof bottom === 'number') {
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

@@ -1,28 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {Record} from 'immutable';
export type PositionRecordType = ReturnType<PositionRecordFactory>;
export type PositionRecordFactory = Record.Factory<{
top: string | number;
right: string | number;
bottom: string | number;
left: string | number;
}>;
const r: PositionRecordFactory = Record({
top: 0,
right: 0,
bottom: 0,
left: 0,
});
export default r;

View File

@@ -1,21 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
.placeholder {
color: var(--ifm-color-content-secondary);
margin: auto;
text-align: center;
}
.sidebar {
z-index: 3;
width: 320px;
background: var(--ifm-background-surface-color);
display: flex;
flex-direction: column;
padding: 20px;
}

View File

@@ -1,38 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import React, {Component} from 'react';
import styles from './Sidebar.module.css';
import clsx from 'clsx';
type Props = {
width?: number;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
children: any;
};
function PlaceholderContent() {
return (
<div className={styles.placeholder}>
<p>Select a node to edit its properties</p>
</div>
);
}
export default class Sidebar extends Component<Props> {
render() {
return (
<div
className={clsx('card', styles.sidebar)}
style={{width: this.props.width}}>
{this.props.children || <PlaceholderContent />}
</div>
);
}
}

View File

@@ -1,27 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
.buttonGroup {
display: flex;
}
.button {
flex: 1;
}
.select {
appearance: none;
width: 100%;
padding: calc(var(--ifm-button-padding-vertical) * 0.8) calc(var(--ifm-button-padding-horizontal) * 0.8);
background: transparent;
color: var(--ifm-font-color-base);
border: var(--ifm-button-border-width) solid var(--ifm-color-secondary);
border-radius: var(--ifm-button-border-radius);
font-size: calc(0.875rem * 0.8);
font-weight: var(--ifm-button-font-weight);
line-height: 1.5;
}

View File

@@ -1,93 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import React, {Component} from 'react';
import clsx from 'clsx';
import Yoga from 'yoga-layout';
import styles from './YogaEnumSelect.module.css';
const PROPERTY_LOOKUP = {
flexDirection: 'FLEX_DIRECTION',
direction: 'DIRECTION',
justifyContent: 'JUSTIFY',
alignSelf: 'ALIGN',
alignContent: 'ALIGN',
alignItems: 'ALIGN',
positionType: 'POSITION_TYPE',
flexWrap: 'WRAP',
};
type Property = keyof typeof PROPERTY_LOOKUP;
type Props = {
property: Property;
disabled?: boolean;
value: string | number;
onChange: (property: Property, value: number) => void;
};
export default class YogaEnumSelect extends Component<Props> {
static availableProperties = Object.keys(PROPERTY_LOOKUP);
values: Array<{key: string; value: number}>;
constructor(props: Props) {
super(props);
const property = PROPERTY_LOOKUP[props.property];
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(this.props.property, Yoga[key]);
};
getTitle = (property: string, key: string): string => {
const replacer = new RegExp(`^${property}_`);
return key.replace(replacer, '').replace('_', ' ').toLowerCase();
};
render() {
const property = PROPERTY_LOOKUP[this.props.property];
const selected = this.values.find(({value}) => value === this.props.value);
return this.values.length > 3 ? (
<select className={styles.select} name={this.props.property}>
{this.values.map(({key, value}) => (
<option key={key} value={value}>
{selected ? this.getTitle(property, key) : ''}
</option>
))}
</select>
) : (
<div className={clsx('button-group', styles.buttonGroup)}>
{this.values.map(({key, value}) => (
<button
className={clsx(
'button',
'button--sm',
'button--outline',
'button--secondary',
value === this.props.value && 'button--active',
styles.button,
)}
onClick={() => this.props.onChange(this.props.property, value)}>
{this.getTitle(property, key)}
</button>
))}
</div>
);
}
}

View File

@@ -1,65 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
.YogaNode {
box-sizing: border-box;
background: var(--ifm-background-surface-color);
position: absolute;
transform: scale(1);
box-shadow: var(--ifm-global-shadow-lw);
cursor: pointer;
animation: yoga-node-fadein 200ms ease;
}
@keyframes yoga-node-fadein {
0% {
transform: scale(1.05);
opacity: 0%;
}
100% {
transform: scale(1.0);
opacity: 100%;
}
}
.YogaNode.hover:not(.focused) {
background-color: var(--ifm-color-emphasis-100);
}
.YogaNode .YogaNode {
background: rgba(255, 255, 255, 0.7);
}
.YogaNode .YogaNode.hover{
background: rgba(240, 255, 249, 0.7);
}
.YogaNode.focused {
outline: 2px solid var(--ifm-color-primary);
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

@@ -1,307 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import React, {Component} from 'react';
import Yoga from 'yoga-layout';
import PositionGuide from './PositionGuide';
import PositionRecord from './PositionRecord';
import LayoutRecord from './LayoutRecord';
import type {LayoutRecordType} from './LayoutRecord';
import {Direction, Display, Edge, Node, Wrap} from 'yoga-layout';
import clsx from 'clsx';
import './YogaNode.css';
type ComputedLayout = {
left: number;
top: number;
width: number;
height: number;
children: Array<ComputedLayout>;
node: Node;
};
type Props = {
layoutDefinition: LayoutRecordType;
className?: string;
computedLayout?: ComputedLayout;
path: Array<number>;
selectedNodePath?: Array<number>;
direction?: Direction;
label?: string;
showGuides: boolean;
onClick?: (path: Array<number>) => void;
onDoubleClick?: (path: Array<number>) => void;
};
type State = {
visible?: boolean;
hovered: boolean;
};
export default class YogaNode extends Component<Props, State> {
node: Node;
_ref: HTMLDivElement;
static defaultProps = {
path: [],
label: 'root',
showGuides: true,
};
state = {
hovered: false,
visible: false,
};
computedLayout?: ComputedLayout;
rootNode?: Node;
constructor(props: Props) {
super(props);
if (!props.computedLayout) {
// is root node
this.calculateLayout(props);
this.state = {
hovered: false,
visible: !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();
}
}
onMouseMove = e => {
this.setState({hovered: e.target === this._ref});
};
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: LayoutRecordType): Node => {
const root = Yoga.Node.create();
const defaultLayout = LayoutRecord({});
[
'width',
'height',
'minWidth',
'maxWidth',
'minHeight',
'maxHeight',
'justifyContent',
'alignItems',
'alignSelf',
'alignContent',
'flexGrow',
'flexShrink',
'positionType',
'aspectRatio',
'flexWrap',
'flexDirection',
].forEach(key => {
try {
const value =
layoutDefinition[key] === ''
? defaultLayout[key]
: layoutDefinition[key];
root[`set${key[0].toUpperCase()}${key.substr(1)}`](value);
} catch (e) {
// Do nothing on failure
}
});
['padding', 'margin', 'position', 'border'].forEach(key => {
['top', 'right', 'bottom', 'left'].forEach(direction => {
try {
root[`set${key[0].toUpperCase()}${key.substr(1)}`](
Yoga[`EDGE_${direction.toUpperCase()}`],
layoutDefinition[key][direction],
);
} catch (e) {
// Do nothing on failure
}
});
});
root.setDisplay(Display.Flex);
(layoutDefinition.children || [])
.map(this.createYogaNodes)
.forEach((node, i) => {
root.insertChild(node, i);
});
return root;
};
getComputedLayout = (node: Node): ComputedLayout => {
return {
...node.getComputedLayout(),
node,
children: Array(node.getChildCount()).map((_, i) =>
this.getComputedLayout(node.getChild(i)),
),
};
};
onClick = (e: React.MouseEvent) => {
const {onClick} = this.props;
if (onClick) {
e.stopPropagation();
onClick(this.props.path);
}
};
onDoubleClick = (e: React.MouseEvent) => {
const {onDoubleClick} = this.props;
if (onDoubleClick) {
e.stopPropagation();
onDoubleClick(this.props.path);
}
};
onMouseLeave = (_e: React.MouseEvent) => this.setState({hovered: false});
showPositionGuides({node}: ComputedLayout) {
const padding = PositionRecord({
top: node.getComputedPadding(Edge.Top),
left: node.getComputedPadding(Edge.Left),
right: node.getComputedPadding(Edge.Right),
bottom: node.getComputedPadding(Edge.Bottom),
});
const border = PositionRecord({
top: node.getComputedBorder(Edge.Top),
left: node.getComputedBorder(Edge.Left),
right: node.getComputedBorder(Edge.Right),
bottom: node.getComputedBorder(Edge.Bottom),
});
const margin = PositionRecord({
top: node.getComputedMargin(Edge.Top),
left: node.getComputedMargin(Edge.Left),
right: node.getComputedMargin(Edge.Right),
bottom: node.getComputedMargin(Edge.Bottom),
});
const position = PositionRecord({
top: node.getPosition(Edge.Top).value,
left: node.getPosition(Edge.Left).value,
right: node.getPosition(Edge.Right).value,
bottom: node.getPosition(Edge.Bottom).value,
});
return [
<PositionGuide
key="border"
inset
position={border}
color="rgba(251, 170, 51, 0.15)"
reverse={node.getFlexWrap() === Wrap.WrapReverse}
/>,
<PositionGuide
key="padding"
inset
offset={border}
position={padding}
color="rgba(123, 179, 41, 0.1)"
reverse={node.getFlexWrap() === Wrap.WrapReverse}
/>,
<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;
const computedLayout: ComputedLayout =
this.props.computedLayout || this.computedLayout;
const {left, top, width, height, children} = computedLayout;
const isFocused = selectedNodePath && selectedNodePath.length === 0;
return (
<div
className={clsx(
'card',
'YogaNode',
className,
isFocused && 'focused',
this.state.hovered && 'hover',
this.state.visible === false && 'invisible',
)}
style={path.length == 0 ? {width, height} : {left, top, width, height}}
onDoubleClick={this.onDoubleClick}
onMouseMove={this.onMouseMove}
onMouseLeave={this.onMouseLeave}
ref={ref => {
this._ref = ref;
}}
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

@@ -1,30 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
.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: 12px;
font-weight: 700;
color: #444950;
}

View File

@@ -1,71 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import React, {Component} from 'react';
import PositionRecord from './PositionRecord';
import type {PositionRecordType} from './PositionRecord';
import './YogaPositionEditor.css';
type Property = 'position' | 'margin' | 'padding' | 'border';
type Props = {
value: PositionRecordType;
property: Property;
disabled?: boolean;
onChange: (property: Property, value: PositionRecordType) => void;
};
export default class YogaPositionEditor extends Component<Props> {
static availableProperties = ['position', 'margin', 'padding', 'border'];
static defaultProps = {
value: PositionRecord(),
};
render() {
const {onChange, value, property, disabled} = this.props;
return (
<div className="YogaPositionEditor">
<input
type="text"
value={Number.isNaN(value.top) ? '' : value.top}
disabled={disabled}
onChange={e => onChange(property, value.set('top', e.target.value))}
/>
<div className="YogaPositionEditorRow">
<input
type="text"
value={Number.isNaN(value.left) ? '' : value.left}
disabled={disabled}
onChange={e =>
onChange(property, value.set('left', e.target.value))
}
/>
{property.toUpperCase()}
<input
type="text"
value={Number.isNaN(value.right) ? '' : value.right}
disabled={disabled}
onChange={e =>
onChange(property, value.set('right', e.target.value))
}
/>
</div>
<input
type="text"
value={Number.isNaN(value.bottom) ? '' : value.bottom}
disabled={disabled}
onChange={e =>
onChange(property, value.set('bottom', e.target.value))
}
/>
</div>
);
}
}

View File

@@ -0,0 +1,114 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import React, {useMemo} from 'react';
import Yoga, {Direction, Overflow, Node as YogaNode} from 'yoga-layout';
import {FlexStyle, applyStyle} from './FlexStyle';
import LayoutBox from './LayoutBox';
import type {LayoutMetrics} from './LayoutBox';
export type Props = Readonly<{
rootNode: StyleNode;
width?: number;
height?: number;
className?: string;
useWebDefaults?: boolean;
}>;
export type StyleNode = {
style?: FlexStyle;
children?: StyleNode[];
};
export default function YogaViewer({
rootNode,
width,
height,
className,
useWebDefaults,
}: Props) {
const layout = useMemo(
() => layoutStyleTree(rootNode, width, height, {useWebDefaults}),
[rootNode, width, height],
);
return <LayoutBox metrics={layout} depth={0} className={className} />;
}
type LayoutConfig = Readonly<{
useWebDefaults?: boolean;
}>;
// This is not efficient and not a good real-world-example for the best way to use Yoga, but sufficient for a playground
function layoutStyleTree(
node: StyleNode,
rootWidth: number | undefined,
rootHeight: number | undefined,
layoutConfig: LayoutConfig,
): LayoutMetrics {
const root = yogaNodeFromStyleNode(node, layoutConfig);
root.calculateLayout(rootWidth, rootHeight, Direction.LTR);
const layoutMetrics = metricsFromYogaNode(root);
layoutMetrics.overflow = node.style?.overflow;
root.freeRecursive();
return layoutMetrics;
}
function yogaNodeFromStyleNode(
styleNode: StyleNode,
layoutConfig: LayoutConfig,
): YogaNode {
const node = Yoga.Node.create(
layoutConfig.useWebDefaults ? webDefaultsConfig : undefined,
);
applyStyle(node, styleNode.style);
for (const child of styleNode.children ?? []) {
node.insertChild(
yogaNodeFromStyleNode(child, layoutConfig),
node.getChildCount(),
);
}
return node;
}
const webDefaultsConfig = Yoga.Config.create();
webDefaultsConfig.setUseWebDefaults(true);
function metricsFromYogaNode(node: YogaNode): LayoutMetrics {
const children: LayoutMetrics[] = [];
for (let i = 0; i < node.getChildCount(); i++) {
children.push(metricsFromYogaNode(node.getChild(i)));
}
// Offset is relative to parent padding box, so we need to subtract the extra
// border we show as part of the box.
const parentBorderThickness = 1;
return {
top: node.getComputedTop() - parentBorderThickness,
left: node.getComputedLeft() - parentBorderThickness,
width: node.getComputedWidth(),
height: node.getComputedHeight(),
overflow: (() => {
switch (node.getOverflow()) {
case Overflow.Hidden:
return 'hidden';
case Overflow.Scroll:
return 'scroll';
case Overflow.Visible:
return 'visible';
}
})(),
children,
};
}

View File

@@ -5,156 +5,30 @@
* LICENSE file in the root directory of this source tree.
*/
/**
* CSS files with the .module.css suffix will be treated as CSS modules
* and scoped locally.
*/
html[data-theme='light'] {
--yg-color-playound-background: var(--ifm-color-gray-200);
}
html[data-theme='dark'] {
--yg-color-playound-background: var(--ifm-color-background);
}
.heroBanner {
flex: 1;
display: flex;
flex-direction: row;
justify-content: center;
}
}
.heroRow {
align-items: center;
}
.blueprintColumn {
display: flex;
align-items: center;
justify-content: center;
}
@media (max-width: 996px) {
.blueprintColumn {
display: none;
}
}
.blueprint {
--blueprint-gap: 5%;
--fadein-duration: 500ms;
box-shadow: var(--ifm-global-shadow-tl);
background-color: var(--ifm-background-surface-color);
border-radius: 0.5rem;
}
.blueprintContainer {
position: relative;
width: var(--ifm-col-width);
aspect-ratio: 1.0;
background-color: var(--ifm-color-primary-lighter);
box-shadow: var(--ifm-global-shadow-lw);
}
.blueprintAvatar {
position: absolute;
left: 0;
top: 0;
margin: var(--blueprint-gap) 0 0 var(--blueprint-gap);
width: calc(25% - (var(--blueprint-gap)));
height: calc(25% - (var(--blueprint-gap)));
animation: avatar-fadein var(--fadein-duration) ease;
}
@keyframes avatar-fadein {
0% {
transform: scale(1.1);
opacity: 0%;
}
50% {
transform: scale(1.0);
opacity: 100%;
}
}
.blueprintTitle {
position: absolute;
left: 25%;
top: 0;
right: 10%;
margin: var(--blueprint-gap) var(--blueprint-gap) 0 var(--blueprint-gap);
height: calc(10% - (var(--blueprint-gap)));
animation: title-fadein var(--fadein-duration) ease;
}
.blueprintSubtitle {
position: absolute;
left: 25%;
top: 10%;
right: 30%;
margin: var(--blueprint-gap);
height: calc(10% - (var(--blueprint-gap)));
animation: title-fadein var(--fadein-duration) ease;
}
@keyframes title-fadein {
0% {
transform: scale(1.1);
opacity: 0%;
}
25% {
transform: scale(1.1);
opacity: 0%;
}
75% {
transform: scale(1.0);
opacity: 100%;
}
}
.blueprintContent {
box-sizing: border-box;
position: absolute;
bottom: 0;
margin: var(--blueprint-gap);
width: calc(100% - (var(--blueprint-gap) * 2));
height: calc(75% - (var(--blueprint-gap) * 2));
animation: content-fadein var(--fadein-duration) ease;
}
@keyframes content-fadein {
0% {
transform: scale(1.1);
opacity: 0%;
}
50% {
transform: scale(1.1);
opacity: 0%;
}
100% {
transform: scale(1.0);
opacity: 100%;
}
}
.playgroundSection {
display: flex;
flex-direction: column;
justify-content: center;
height: 600px;
width: 100%;
background-color: var(--yg-color-playound-background);
}
.heroLogo {
width: 200px;
height: 200px;
}
@media (max-width: 996px) {
.playgroundSection {
.heroLogo {
display: none;
}
}
.playgroundSection :global(.prism-code) {
display: none;
}
}
.bg {
background-color: var(--yg-color-playground-background);
}

View File

@@ -5,83 +5,77 @@
* LICENSE file in the root directory of this source tree.
*/
import React, {Suspense} from 'react';
import React from 'react';
import clsx from 'clsx';
import Link from '@docusaurus/Link';
import Layout from '@theme/Layout';
import BrowserOnly from '@docusaurus/BrowserOnly';
import styles from './index.module.css';
import YogaLogo from '../../static/img/logo.svg';
import Playground from '../components/Playground';
function HeroSection() {
return (
<header className={clsx('hero', styles.heroBanner)}>
<div className={clsx('row', 'container', styles.heroRow)}>
<div className="col col--6">
<h1 className="hero__title">Yoga Layout</h1>
<h1 className="hero__title">Yoga</h1>
<p className="hero__subtitle">
A portable and perfomant layout engine targeting web standards
A portable layout engine targeting web standards
</p>
<Link className="button button--primary button--lg" to="/docs/intro">
Learn more
</Link>
</div>
<div className={clsx(['col col--6', styles.blueprintColumn])}>
<div className={clsx([styles.blueprint, styles.blueprintContainer])}>
<div className={styles.blueprintHeader}>
<div
className={clsx([styles.blueprint, styles.blueprintAvatar])}
/>
<div
className={clsx([styles.blueprint, styles.blueprintTitle])}
/>
<div
className={clsx([styles.blueprint, styles.blueprintSubtitle])}
/>
</div>
<div
className={clsx([styles.blueprint, styles.blueprintContent])}
/>
</div>
<div className="col col--2">
<YogaLogo className={styles.heroLogo} />
</div>
</div>
</header>
);
}
const LazyPlayground = React.lazy(
() => import('../components/Playground/Playground'),
);
// Docusaurus SSR does not correctly support top-level await
// 1. https://github.com/facebook/docusaurus/issues/7238
// 2. https://github.com/facebook/docusaurus/issues/9468
function BrowserOnlyPlayground() {
return (
<BrowserOnly fallback={null}>
{() => (
<Suspense fallback={null}>
<LazyPlayground className={styles.playground} />
</Suspense>
)}
</BrowserOnly>
);
}
const playgroundCode = `
<Layout config={{useWebDefaults: false}}>
<Node style={{ width: 250, height: 475, padding: 10 }}>
<Node style={{ flex: 1, rowGap: 10}}>
<Node style={{ height: 60 }} />
<Node style={{ flex: 1, marginInline: 10 }} />
<Node style={{ flex: 2, marginInline: 10 }} />
<Node
style={{
position: "absolute",
width: "100%",
bottom: 0,
height: 64,
flexDirection: "row",
alignItems: "center",
justifyContent: "space-around",
}}
>
<Node style={{ height: 40, width: 40 }} />
<Node style={{ height: 40, width: 40 }} />
<Node style={{ height: 40, width: 40 }} />
<Node style={{ height: 40, width: 40 }} />
</Node>
</Node>
</Node>
</Layout>
`.trim();
function PlaygroundSection() {
return (
<main className={styles.playgroundSection}>
<div className="container">
<BrowserOnlyPlayground />
</div>
<Playground height="600px" code={playgroundCode} autoFocus={true} />
</main>
);
}
export default function Home(): JSX.Element {
return (
<Layout title="Yoga Layout | A cross-platform layout engine">
<Layout>
<HeroSection />
<PlaygroundSection />
</Layout>

View File

@@ -5,27 +5,19 @@
* LICENSE file in the root directory of this source tree.
*/
html[data-theme='light'] {
html[data-theme='light'] {
--yg-color-playound-background: var(--ifm-color-gray-200);
}
}
html[data-theme='dark'] {
html[data-theme='dark'] {
--yg-color-playound-background: var(--ifm-color-background);
}
.container {
display: flex;
flex-direction: row;
background-color: var(--yg-color-playound-background);
min-height: 600px;
padding: 10px;
}
.playground {
.playgroundContainer {
display: flex;
flex: 1;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
.bg {
background-color: var(--yg-color-playound-background);
}

View File

@@ -0,0 +1,27 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import Layout from '@theme/Layout';
import {useLocation} from '@docusaurus/router';
import Playground from '../components/Playground';
import styles from './playground.module.css';
export default function PlaygroundPage(): JSX.Element {
const params = new URLSearchParams(useLocation().search);
const codeParam = params.get('code');
const code = codeParam ? atob(codeParam) : undefined;
return (
// @ts-ignore missing prop for `wrapperClassName`
<Layout wrapperClassName={styles.bg} title="Playground">
<Playground height="max(80vh, 600px)" code={code} autoFocus={true} />
</Layout>
);
}

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-copy" viewBox="-4 -4 24 24">
<path fill-rule="evenodd" d="M4 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2zm2-1a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1zM2 5a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-1h1v1a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h1v1z"/>
</svg>

After

Width:  |  Height:  |  Size: 409 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-link" viewBox="0 0 16 16">
<path d="M6.354 5.5H4a3 3 0 0 0 0 6h3a3 3 0 0 0 2.83-4H9c-.086 0-.17.01-.25.031A2 2 0 0 1 7 10.5H4a2 2 0 1 1 0-4h1.535c.218-.376.495-.714.82-1z"/>
<path d="M9 5.5a3 3 0 0 0-2.83 4h1.098A2 2 0 0 1 9 6.5h3a2 2 0 1 1 0 4h-1.535a4.02 4.02 0 0 1-.82 1H12a3 3 0 1 0 0-6z"/>
</svg>

After

Width:  |  Height:  |  Size: 401 B

View File

@@ -3,9 +3,8 @@
"extends": "@docusaurus/tsconfig",
"compilerOptions": {
"baseUrl": ".",
"target": "esnext",
"module": "esnext",
"moduleResolution": "bundler",
"allowImportingTsExtensions": true
"target": "es2022",
"allowImportingTsExtensions": true,
"strict": true
}
}

828
yarn.lock

File diff suppressed because it is too large Load Diff