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:
committed by
Facebook GitHub Bot
parent
0d03d8a06d
commit
77742af676
@@ -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}`);
|
||||
}
|
||||
}
|
||||
|
@@ -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,
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
@@ -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": {
|
||||
|
@@ -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;
|
||||
|
80
website-next/src/components/EditorToolbar.module.css
Normal file
80
website-next/src/components/EditorToolbar.module.css
Normal 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;
|
||||
}
|
||||
}
|
82
website-next/src/components/EditorToolbar.tsx
Normal file
82
website-next/src/components/EditorToolbar.tsx
Normal 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>
|
||||
);
|
||||
}
|
411
website-next/src/components/FlexStyle.ts
Normal file
411
website-next/src/components/FlexStyle.ts
Normal 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`);
|
||||
}
|
68
website-next/src/components/LayoutBox.module.css
Normal file
68
website-next/src/components/LayoutBox.module.css
Normal 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);
|
||||
}
|
56
website-next/src/components/LayoutBox.tsx
Normal file
56
website-next/src/components/LayoutBox.tsx
Normal 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>
|
||||
);
|
||||
}
|
108
website-next/src/components/Playground.module.css
Normal file
108
website-next/src/components/Playground.module.css
Normal 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%;
|
||||
}
|
||||
}
|
165
website-next/src/components/Playground.tsx
Normal file
165
website-next/src/components/Playground.tsx
Normal 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'));
|
@@ -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%;
|
||||
}
|
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
@@ -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;
|
||||
}
|
@@ -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 × 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 × 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 × 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>
|
||||
);
|
||||
}
|
@@ -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;
|
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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,
|
||||
];
|
||||
}
|
||||
}
|
@@ -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;
|
@@ -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;
|
||||
}
|
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
114
website-next/src/components/YogaViewer.tsx
Normal file
114
website-next/src/components/YogaViewer.tsx
Normal 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,
|
||||
};
|
||||
}
|
@@ -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);
|
||||
}
|
||||
|
@@ -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>
|
||||
|
@@ -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);
|
||||
}
|
27
website-next/src/pages/playground.tsx
Normal file
27
website-next/src/pages/playground.tsx
Normal 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>
|
||||
);
|
||||
}
|
3
website-next/static/img/copy.svg
Normal file
3
website-next/static/img/copy.svg
Normal 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 |
4
website-next/static/img/link.svg
Normal file
4
website-next/static/img/link.svg
Normal 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 |
@@ -3,9 +3,8 @@
|
||||
"extends": "@docusaurus/tsconfig",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true
|
||||
"target": "es2022",
|
||||
"allowImportingTsExtensions": true,
|
||||
"strict": true
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user