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 = {
|
export type Node = {
|
||||||
calculateLayout(
|
calculateLayout(
|
||||||
width?: number | 'auto',
|
width: number | 'auto' | undefined,
|
||||||
height?: number | 'auto',
|
height: number | 'auto' | undefined,
|
||||||
direction?: Direction,
|
direction?: Direction,
|
||||||
): void;
|
): void;
|
||||||
copyStyle(node: Node): void;
|
copyStyle(node: Node): void;
|
||||||
@@ -124,45 +124,48 @@ export type Node = {
|
|||||||
setAlignContent(alignContent: Align): void;
|
setAlignContent(alignContent: Align): void;
|
||||||
setAlignItems(alignItems: Align): void;
|
setAlignItems(alignItems: Align): void;
|
||||||
setAlignSelf(alignSelf: Align): void;
|
setAlignSelf(alignSelf: Align): void;
|
||||||
setAspectRatio(aspectRatio: number): void;
|
setAspectRatio(aspectRatio: number | undefined): void;
|
||||||
setBorder(edge: Edge, borderWidth: number): void;
|
setBorder(edge: Edge, borderWidth: number | undefined): void;
|
||||||
setDisplay(display: Display): void;
|
setDisplay(display: Display): void;
|
||||||
setFlex(flex: number): void;
|
setFlex(flex: number | undefined): void;
|
||||||
setFlexBasis(flexBasis: number | 'auto' | `${number}%`): void;
|
setFlexBasis(flexBasis: number | 'auto' | `${number}%` | undefined): void;
|
||||||
setFlexBasisPercent(flexBasis: number): void;
|
setFlexBasisPercent(flexBasis: number | undefined): void;
|
||||||
setFlexBasisAuto(): void;
|
setFlexBasisAuto(): void;
|
||||||
setFlexDirection(flexDirection: FlexDirection): void;
|
setFlexDirection(flexDirection: FlexDirection): void;
|
||||||
setFlexGrow(flexGrow: number): void;
|
setFlexGrow(flexGrow: number | undefined): void;
|
||||||
setFlexShrink(flexShrink: number): void;
|
setFlexShrink(flexShrink: number | undefined): void;
|
||||||
setFlexWrap(flexWrap: Wrap): void;
|
setFlexWrap(flexWrap: Wrap): void;
|
||||||
setHeight(height: number | 'auto' | `${number}%`): void;
|
setHeight(height: number | 'auto' | `${number}%` | undefined): void;
|
||||||
setIsReferenceBaseline(isReferenceBaseline: boolean): void;
|
setIsReferenceBaseline(isReferenceBaseline: boolean): void;
|
||||||
setHeightAuto(): void;
|
setHeightAuto(): void;
|
||||||
setHeightPercent(height: number): void;
|
setHeightPercent(height: number | undefined): void;
|
||||||
setJustifyContent(justifyContent: Justify): void;
|
setJustifyContent(justifyContent: Justify): void;
|
||||||
setGap(gutter: Gutter, gapLength: number): Value;
|
setGap(gutter: Gutter, gapLength: number | undefined): Value;
|
||||||
setMargin(edge: Edge, margin: number | 'auto' | `${number}%`): void;
|
setMargin(
|
||||||
|
edge: Edge,
|
||||||
|
margin: number | 'auto' | `${number}%` | undefined,
|
||||||
|
): void;
|
||||||
setMarginAuto(edge: Edge): void;
|
setMarginAuto(edge: Edge): void;
|
||||||
setMarginPercent(edge: Edge, margin: number): void;
|
setMarginPercent(edge: Edge, margin: number | undefined): void;
|
||||||
setMaxHeight(maxHeight: number | `${number}%`): void;
|
setMaxHeight(maxHeight: number | `${number}%` | undefined): void;
|
||||||
setMaxHeightPercent(maxHeight: number): void;
|
setMaxHeightPercent(maxHeight: number | undefined): void;
|
||||||
setMaxWidth(maxWidth: number | `${number}%`): void;
|
setMaxWidth(maxWidth: number | `${number}%` | undefined): void;
|
||||||
setMaxWidthPercent(maxWidth: number): void;
|
setMaxWidthPercent(maxWidth: number | undefined): void;
|
||||||
setDirtiedFunc(dirtiedFunc: DirtiedFunction | null): void;
|
setDirtiedFunc(dirtiedFunc: DirtiedFunction | null): void;
|
||||||
setMeasureFunc(measureFunc: MeasureFunction | null): void;
|
setMeasureFunc(measureFunc: MeasureFunction | null): void;
|
||||||
setMinHeight(minHeight: number | `${number}%`): void;
|
setMinHeight(minHeight: number | `${number}%` | undefined): void;
|
||||||
setMinHeightPercent(minHeight: number): void;
|
setMinHeightPercent(minHeight: number | undefined): void;
|
||||||
setMinWidth(minWidth: number | `${number}%`): void;
|
setMinWidth(minWidth: number | `${number}%` | undefined): void;
|
||||||
setMinWidthPercent(minWidth: number): void;
|
setMinWidthPercent(minWidth: number | undefined): void;
|
||||||
setOverflow(overflow: Overflow): void;
|
setOverflow(overflow: Overflow): void;
|
||||||
setPadding(edge: Edge, padding: number | `${number}%`): void;
|
setPadding(edge: Edge, padding: number | `${number}%` | undefined): void;
|
||||||
setPaddingPercent(edge: Edge, padding: number): void;
|
setPaddingPercent(edge: Edge, padding: number | undefined): void;
|
||||||
setPosition(edge: Edge, position: number | `${number}%`): void;
|
setPosition(edge: Edge, position: number | `${number}%` | undefined): void;
|
||||||
setPositionPercent(edge: Edge, position: number): void;
|
setPositionPercent(edge: Edge, position: number | undefined): void;
|
||||||
setPositionType(positionType: PositionType): void;
|
setPositionType(positionType: PositionType): void;
|
||||||
setWidth(width: number | 'auto' | `${number}%`): void;
|
setWidth(width: number | 'auto' | `${number}%` | undefined): void;
|
||||||
setWidthAuto(): void;
|
setWidthAuto(): void;
|
||||||
setWidthPercent(width: number): void;
|
setWidthPercent(width: number | undefined): void;
|
||||||
unsetDirtiedFunc(): void;
|
unsetDirtiedFunc(): void;
|
||||||
unsetMeasureFunc(): void;
|
unsetMeasureFunc(): void;
|
||||||
};
|
};
|
||||||
@@ -227,7 +230,11 @@ export default function wrapAssembly(lib: any): Yoga {
|
|||||||
? Unit.Percent
|
? Unit.Percent
|
||||||
: Unit.Point;
|
: Unit.Point;
|
||||||
asNumber = parseFloat(value);
|
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}`);
|
throw new Error(`Invalid value ${value} for ${fnName}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -66,6 +66,7 @@ export default {
|
|||||||
position: 'left',
|
position: 'left',
|
||||||
label: 'Documentation',
|
label: 'Documentation',
|
||||||
},
|
},
|
||||||
|
{to: '/playground', label: 'Playground', position: 'left'},
|
||||||
{to: '/blog', label: 'Blog', position: 'left'},
|
{to: '/blog', label: 'Blog', position: 'left'},
|
||||||
{
|
{
|
||||||
href: 'https://github.com/facebook/yoga',
|
href: 'https://github.com/facebook/yoga',
|
||||||
@@ -124,7 +125,11 @@ export default {
|
|||||||
},
|
},
|
||||||
prism: {
|
prism: {
|
||||||
theme: prismThemes.github,
|
theme: prismThemes.github,
|
||||||
darkTheme: prismThemes.dracula,
|
darkTheme: prismThemes.oneDark,
|
||||||
|
},
|
||||||
|
colorMode: {
|
||||||
|
defaultMode: 'dark',
|
||||||
|
respectPrefersColorScheme: true,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
@@ -17,14 +17,15 @@
|
|||||||
"lint:fix": "eslint . --fix"
|
"lint:fix": "eslint . --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@docusaurus/core": "3.0.0",
|
"@docusaurus/core": "3.0.1",
|
||||||
"@docusaurus/preset-classic": "3.0.0",
|
"@docusaurus/preset-classic": "3.0.1",
|
||||||
"@mdx-js/react": "^3.0.0",
|
"@mdx-js/react": "^3.0.0",
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^2.0.0",
|
||||||
"immutable": "^4.0.0",
|
"nullthrows": "^1.1.1",
|
||||||
"prism-react-renderer": "^2.1.0",
|
"prism-react-renderer": "^2.3.0",
|
||||||
"react": "^18.0.0",
|
"react": "^18.0.0",
|
||||||
"react-dom": "^18.0.0",
|
"react-dom": "^18.0.0",
|
||||||
|
"react-live": "^4.1.5",
|
||||||
"yoga-layout": "0.0.0"
|
"yoga-layout": "0.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@@ -22,19 +22,6 @@
|
|||||||
const sidebars = {
|
const sidebars = {
|
||||||
// By default, Docusaurus generates a sidebar from the docs folder structure
|
// By default, Docusaurus generates a sidebar from the docs folder structure
|
||||||
docsSidebar: [{type: 'autogenerated', dirName: '.'}],
|
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;
|
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.
|
* 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 {
|
.heroBanner {
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.heroRow {
|
.heroRow {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
|
||||||
|
|
||||||
.blueprintColumn {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 996px) {
|
.heroLogo {
|
||||||
.blueprintColumn {
|
width: 200px;
|
||||||
display: none;
|
height: 200px;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.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);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 996px) {
|
@media (max-width: 996px) {
|
||||||
.playgroundSection {
|
.heroLogo {
|
||||||
display: none;
|
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.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {Suspense} from 'react';
|
import React from 'react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import Link from '@docusaurus/Link';
|
import Link from '@docusaurus/Link';
|
||||||
import Layout from '@theme/Layout';
|
import Layout from '@theme/Layout';
|
||||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
|
||||||
|
|
||||||
import styles from './index.module.css';
|
import styles from './index.module.css';
|
||||||
|
|
||||||
|
import YogaLogo from '../../static/img/logo.svg';
|
||||||
|
import Playground from '../components/Playground';
|
||||||
|
|
||||||
function HeroSection() {
|
function HeroSection() {
|
||||||
return (
|
return (
|
||||||
<header className={clsx('hero', styles.heroBanner)}>
|
<header className={clsx('hero', styles.heroBanner)}>
|
||||||
<div className={clsx('row', 'container', styles.heroRow)}>
|
<div className={clsx('row', 'container', styles.heroRow)}>
|
||||||
<div className="col col--6">
|
<div className="col col--6">
|
||||||
<h1 className="hero__title">Yoga Layout</h1>
|
<h1 className="hero__title">Yoga</h1>
|
||||||
<p className="hero__subtitle">
|
<p className="hero__subtitle">
|
||||||
A portable and perfomant layout engine targeting web standards
|
A portable layout engine targeting web standards
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Link className="button button--primary button--lg" to="/docs/intro">
|
<Link className="button button--primary button--lg" to="/docs/intro">
|
||||||
Learn more
|
Learn more
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className={clsx(['col col--6', styles.blueprintColumn])}>
|
<div className="col col--2">
|
||||||
<div className={clsx([styles.blueprint, styles.blueprintContainer])}>
|
<YogaLogo className={styles.heroLogo} />
|
||||||
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const LazyPlayground = React.lazy(
|
const playgroundCode = `
|
||||||
() => import('../components/Playground/Playground'),
|
<Layout config={{useWebDefaults: false}}>
|
||||||
);
|
<Node style={{ width: 250, height: 475, padding: 10 }}>
|
||||||
|
<Node style={{ flex: 1, rowGap: 10}}>
|
||||||
// Docusaurus SSR does not correctly support top-level await
|
<Node style={{ height: 60 }} />
|
||||||
// 1. https://github.com/facebook/docusaurus/issues/7238
|
<Node style={{ flex: 1, marginInline: 10 }} />
|
||||||
// 2. https://github.com/facebook/docusaurus/issues/9468
|
<Node style={{ flex: 2, marginInline: 10 }} />
|
||||||
function BrowserOnlyPlayground() {
|
<Node
|
||||||
return (
|
style={{
|
||||||
<BrowserOnly fallback={null}>
|
position: "absolute",
|
||||||
{() => (
|
width: "100%",
|
||||||
<Suspense fallback={null}>
|
bottom: 0,
|
||||||
<LazyPlayground className={styles.playground} />
|
height: 64,
|
||||||
</Suspense>
|
flexDirection: "row",
|
||||||
)}
|
alignItems: "center",
|
||||||
</BrowserOnly>
|
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() {
|
function PlaygroundSection() {
|
||||||
return (
|
return (
|
||||||
<main className={styles.playgroundSection}>
|
<main className={styles.playgroundSection}>
|
||||||
<div className="container">
|
<Playground height="600px" code={playgroundCode} autoFocus={true} />
|
||||||
<BrowserOnlyPlayground />
|
|
||||||
</div>
|
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Home(): JSX.Element {
|
export default function Home(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<Layout title="Yoga Layout | A cross-platform layout engine">
|
<Layout>
|
||||||
<HeroSection />
|
<HeroSection />
|
||||||
<PlaygroundSection />
|
<PlaygroundSection />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
@@ -5,27 +5,19 @@
|
|||||||
* LICENSE file in the root directory of this source tree.
|
* 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);
|
--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);
|
--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;
|
flex: 1;
|
||||||
display: flex;
|
}
|
||||||
flex-direction: row;
|
|
||||||
justify-content: center;
|
.bg {
|
||||||
align-items: center;
|
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",
|
"extends": "@docusaurus/tsconfig",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"target": "esnext",
|
"target": "es2022",
|
||||||
"module": "esnext",
|
"allowImportingTsExtensions": true,
|
||||||
"moduleResolution": "bundler",
|
"strict": true
|
||||||
"allowImportingTsExtensions": true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user