Yoga Docs: Rename website-next to website (#1613)

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

So that GitHub links to edit a page point to the right place.

This will fail in OSS build until I switch the directory used by Vercel instance, but I am waiting to do that until ready to land, since that would cause other in progress changes to fail when exported.

Reviewed By: joevilches

Differential Revision: D54837857

fbshipit-source-id: 9bec90232dbe3ec8638568685671185d597fcf2d
This commit is contained in:
Nick Gerleman
2024-03-13 17:25:39 -07:00
committed by Facebook GitHub Bot
parent 108c2f30a2
commit 206b95aba5
88 changed files with 4 additions and 9 deletions

View File

@@ -0,0 +1,77 @@
/**
* 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;
}
.toolbar button {
display: flex;
align-items: center;
background: var(--prism-background-color);
color: var(--prism-color);
border: 1px solid var(--ifm-color-emphasis-300);
border-radius: var(--ifm-global-radius);
padding: 0.4rem;
line-height: 0;
transition: opacity var(--ifm-transition-fast) ease-in-out;
opacity: 0.4;
}
.toolbar button:focus-visible,
.toolbar button:hover {
opacity: 1 !important;
}
.icon {
width: 24px;
height: 24px;
}
.iconSwitcher {
position: relative;
width: 24px;
height: 24px;
}
.actionIcon,
.successIcon {
position: absolute;
top: 0;
left: 0;
fill: currentColor;
opacity: inherit;
width: inherit;
height: inherit;
transition: all var(--ifm-transition-fast) ease;
}
.successIcon {
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0.33);
opacity: 0;
color: #00d600;
}
.clicked .actionIcon {
transform: scale(0.33);
opacity: 0;
}
.clicked .successIcon {
transform: translate(-50%, -50%) scale(1);
opacity: 1;
transition-delay: 0.075s;
}
@media (max-width: 996px) {
.toolbar {
display: none;
}
}

View File

@@ -0,0 +1,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.
*
* @format
*/
import {useCallback, useEffect, useRef, useState} from 'react';
import clsx from 'clsx';
import lzString from 'lz-string';
import LinkIcon from '../../static/img/link.svg';
import SuccessIcon from '@theme/Icon/Success';
import styles from './EditorToolbar.module.css';
export type Props = Readonly<{
code: string;
className?: string;
style?: React.CSSProperties;
}>;
export default function EditorToolbar({
code,
className,
style,
}: Props): JSX.Element {
const handleShare = useCallback(() => {
navigator.clipboard.writeText(
window.location.origin +
`/playground?code=${lzString.compressToEncodedURIComponent(code)}`,
);
}, [code]);
return (
<div className={clsx(styles.toolbar, className)} style={style}>
<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);
}, [onClick]);
return (
<button
type="button"
className={clsx('clean-btn', isSuccess && styles.clicked)}
onClick={handleClick}
aria-label={label}>
<span className={styles.iconSwitcher} aria-hidden="true">
<Icon className={styles.actionIcon} />
<SuccessIcon className={styles.successIcon} />
</span>
</button>
);
}

View File

@@ -0,0 +1,426 @@
/**
* 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,
Direction,
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}%`;
direction?: 'ltr' | 'rtl';
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 'direction':
node.setDirection(direction(style.direction));
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 direction(str?: 'ltr' | 'rtl'): Direction {
switch (str) {
case 'ltr':
return Direction.LTR;
case 'rtl':
return Direction.RTL;
}
throw new Error(`"${str}" is not a valid value for direction`);
}
function display(str?: 'none' | 'flex'): Display {
switch (str) {
case 'none':
return Display.None;
case 'flex':
return Display.Flex;
}
throw new Error(`"${str}" is not a valid value for display`);
}
function flexDirection(
str?: 'row' | 'column' | 'row-reverse' | 'column-reverse',
): FlexDirection {
switch (str) {
case 'row':
return FlexDirection.Row;
case 'column':
return FlexDirection.Column;
case 'row-reverse':
return FlexDirection.RowReverse;
case 'column-reverse':
return FlexDirection.ColumnReverse;
}
throw new Error(`"${str}" is not a valid value for flexDirection`);
}
function flexWrap(str?: 'wrap' | 'nowrap' | 'wrap-reverse'): Wrap {
switch (str) {
case 'wrap':
return Wrap.Wrap;
case 'nowrap':
return Wrap.NoWrap;
case 'wrap-reverse':
return Wrap.WrapReverse;
}
throw new Error(`"${str}" is not a valid value for flexWrap`);
}
function justifyContent(str?: JustifyContent): Justify {
switch (str) {
case 'flex-start':
return Justify.FlexStart;
case 'flex-end':
return Justify.FlexEnd;
case 'center':
return Justify.Center;
case 'space-between':
return Justify.SpaceBetween;
case 'space-around':
return Justify.SpaceAround;
case 'space-evenly':
return Justify.SpaceEvenly;
}
throw new Error(`"${str}" is not a valid value for justifyContent`);
}
function overflow(str?: 'visible' | 'hidden' | 'scroll'): Overflow {
switch (str) {
case 'visible':
return Overflow.Visible;
case 'hidden':
return Overflow.Hidden;
case 'scroll':
return Overflow.Scroll;
}
throw new Error(`"${str}" is not a valid value for overflow`);
}
function position(str?: 'absolute' | 'relative' | 'static'): PositionType {
switch (str) {
case 'absolute':
return PositionType.Absolute;
case 'relative':
return PositionType.Relative;
case 'static':
return PositionType.Static;
}
throw new Error(`"${str}" is not a valid value for position`);
}

View File

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

View File

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

View File

@@ -0,0 +1,130 @@
/**
* 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-editor-border: var(--ifm-color-gray-400);
--yg-color-editor-fallback-bg: rgb(246, 248, 250);
--yg-color-editor-fallback-text: rgb(0, 0, 159);
}
html[data-theme='dark'] {
--yg-color-preview-background: var(--ifm-color-primary-dark);
--yg-color-editor-border: var(--ifm-color-gray-800);
--yg-color-editor-fallback-bg: rgb(40, 44, 52);
--yg-color-editor-fallback-text: rgb(209, 154, 102);
}
.wrapper {
min-width: 600px;
padding-block: 16px;
}
.playgroundRow {
display: flex;
flex-direction: row;
column-gap: 16px;
}
.editorColumn {
flex: 8;
min-width: 0;
overflow-y: auto;
border: 1px solid var(--yg-color-editor-border);
border-radius: var(--ifm-pre-border-radius);
position: relative;
}
.editorScroll {
overflow-y: auto;
}
.editorToolbar {
position: absolute;
top: var(--ifm-pre-padding);
right: var(--ifm-pre-padding);
}
.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) - 32px);
}
.playgroundEditor :global(.prism-code) {
box-shadow: var(--ifm-global-shadow-lw);
height: calc(var(--yg-playground-height) - 32px);
min-height: 300px;
border-radius: 0;
padding: var(--ifm-pre-padding) !important;
}
.liveEditorFallback {
background-color: var(--yg-color-editor-fallback-bg);
color: var(--yg-color-editor-fallback-text);
}
.previewColumn {
display: flex;
flex: 5;
height: calc(var(--yg-playground-height) - 32px);
min-height: 300px;
align-items: center;
justify-content: center;
background-color: var(--yg-color-preview-background);
overflow: hidden;
border-radius: var(--ifm-pre-border-radius);
align-self: stretch;
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: unset;
}
.playgroundEditor :global(.prism-code) {
height: unset;
min-height: 10em;
}
.playgroundRow {
flex-direction: column;
}
.editorColumn {
padding: 0;
margin-bottom: 10px;
flex: unset;
}
.previewColumn {
padding: 10px;
width: 100%;
flex: unset;
}
}

View File

@@ -0,0 +1,187 @@
/**
* 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,
useLayoutEffect,
useRef,
useState,
} from 'react';
import {usePrismTheme} from '@docusaurus/theme-common';
import clsx from 'clsx';
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';
import useIsBrowser from '@docusaurus/useIsBrowser';
export type Props = Readonly<{
code: string;
height?: CSSProperties['height'];
autoFocus?: boolean;
}>;
export default function Playground({code, height, autoFocus}: Props) {
const prismTheme = usePrismTheme();
const editorScrollRef = useRef<HTMLDivElement>(null);
const isBrowser = useIsBrowser();
const [liveCode, setLiveCode] = useState(code);
const [hasCodeChanged, setHasCodeChanged] = useState(false);
const [scrollbarWidth, setScrollbarWidth] = useState(0);
// Once react-live has hydrated the content-editable area, set focus to it
// if requested
useEffect(() => {
if (autoFocus && hasCodeChanged) {
const codeElem = editorScrollRef?.current?.querySelector('.prism-code');
const sel = window.getSelection();
if (codeElem?.clientHeight && sel != null) {
sel.selectAllChildren(codeElem);
sel.collapseToStart();
}
}
}, [autoFocus, hasCodeChanged]);
useLayoutEffect(() => {
// The toolbar is positioned relative to the outside of the scrolling
// container so it stays in the same place when scrolling, but this means
// it isn't automatically adjusted for scrollbar width. If code change
// causes overflow/scrollbar, adjust its position based on its width progrmatically.
if (editorScrollRef.current) {
setScrollbarWidth(
editorScrollRef.current.offsetWidth -
editorScrollRef.current.clientWidth,
);
}
}, [editorScrollRef, code]);
const heightStyle = height
? ({'--yg-playground-height': height} as React.CSSProperties)
: undefined;
const handleCodeChange = useCallback((code: string) => {
setHasCodeChanged(true);
setLiveCode(code);
}, []);
return (
<LiveProvider
code={liveCode}
theme={prismTheme}
scope={{Node: LiveNode, Layout: RootLiveNode}}>
<div className={styles.wrapper} style={heightStyle}>
<div className={clsx(styles.playgroundRow)}>
<div className={clsx(styles.editorColumn, 'playground-editor')}>
<div className={styles.editorScroll} ref={editorScrollRef}>
<EditorToolbar
code={liveCode}
className={styles.editorToolbar}
style={{paddingRight: scrollbarWidth + 'px'}}
/>
{isBrowser ? (
<LiveEditor
className={clsx(styles.playgroundEditor)}
onChange={handleCodeChange}
/>
) : (
<LiveEditorFallback code={liveCode} />
)}
</div>
</div>
<div className={clsx(styles.previewColumn)}>
<LivePreview className={clsx(styles.livePreview)} />
<LiveError className={clsx(styles.liveError)} />
</div>
</div>
</div>
</LiveProvider>
);
}
/**
* Provides a non-editable approximation of the LiveEditor result, without
* relying on prism rendering, for use during SSR.
* See https://github.com/facebook/docusaurus/issues/9629
*/
function LiveEditorFallback({code}: Readonly<{code: string}>) {
return (
<div className={clsx(styles.playgroundEditor)}>
<pre className={clsx('prism-code', styles.liveEditorFallback)}>
{code}
</pre>
</div>
);
}
type RootLiveNodeProps = Readonly<{
children: React.ReactNode;
config?: {useWebDefaults?: boolean};
}>;
function RootLiveNode({children, config}: RootLiveNodeProps) {
if (React.Children.count(children) !== 1) {
return null;
}
const child = React.Children.only(children);
if (!React.isValidElement(child) || child.type !== LiveNode) {
return null;
}
const styleNode = styleNodeFromLiveNode(child as unknown as LiveNode);
return (
<Suspense fallback={null}>
<LazyYogaViewer
rootNode={styleNode}
useWebDefaults={config?.useWebDefaults}
/>
</Suspense>
);
}
type LiveNodeProps = Readonly<{
children: React.ReactNode;
style: FlexStyle;
}>;
class LiveNode extends React.PureComponent<LiveNodeProps> {}
function styleNodeFromLiveNode(
liveNode: React.ElementRef<typeof LiveNode>,
): StyleNode {
const children: StyleNode[] = [];
React.Children.forEach(liveNode.props.children, child => {
if (React.isValidElement(child) && child.type === LiveNode) {
children.push(styleNodeFromLiveNode(child as unknown as LiveNode));
}
});
return {
style: liveNode.props.style,
children,
};
}
// Docusaurus SSR does not correctly support top-level await in the import
// chain
// 1. https://github.com/facebook/docusaurus/issues/7238
// 2. https://github.com/facebook/docusaurus/issues/9468
const LazyYogaViewer = lazy(() => import('./YogaViewer'));

View File

@@ -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 {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, useWebDefaults],
);
return <LayoutBox metrics={layout} depth={0} className={className} />;
}
type LayoutConfig = Readonly<{
useWebDefaults?: boolean;
}>;
// This is not efficient and not a good real-world-example for the best way to use Yoga, but sufficient for a playground
function layoutStyleTree(
node: StyleNode,
rootWidth: number | undefined,
rootHeight: number | undefined,
layoutConfig: LayoutConfig,
): LayoutMetrics {
const root = yogaNodeFromStyleNode(node, layoutConfig);
root.calculateLayout(rootWidth, rootHeight, Direction.LTR);
const layoutMetrics = metricsFromYogaNode(root);
layoutMetrics.overflow = node.style?.overflow;
root.freeRecursive();
return layoutMetrics;
}
function yogaNodeFromStyleNode(
styleNode: StyleNode,
layoutConfig: LayoutConfig,
): YogaNode {
const node = Yoga.Node.create(
layoutConfig.useWebDefaults ? webDefaultsConfig : undefined,
);
applyStyle(node, styleNode.style);
for (const child of styleNode.children ?? []) {
node.insertChild(
yogaNodeFromStyleNode(child, layoutConfig),
node.getChildCount(),
);
}
return node;
}
const webDefaultsConfig = Yoga.Config.create();
webDefaultsConfig.setUseWebDefaults(true);
function metricsFromYogaNode(node: YogaNode): LayoutMetrics {
const children: LayoutMetrics[] = [];
for (let i = 0; i < node.getChildCount(); i++) {
children.push(metricsFromYogaNode(node.getChild(i)));
}
// Offset is relative to parent padding box, so we need to subtract the extra
// border we show as part of the box.
const parentBorderThickness = 1;
return {
top: node.getComputedTop() - parentBorderThickness,
left: node.getComputedLeft() - parentBorderThickness,
width: node.getComputedWidth(),
height: node.getComputedHeight(),
overflow: (() => {
switch (node.getOverflow()) {
case Overflow.Hidden:
return 'hidden';
case Overflow.Scroll:
return 'scroll';
case Overflow.Visible:
return 'visible';
}
})(),
children,
};
}

View File

@@ -0,0 +1,37 @@
/**
* 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.
*/
/**
* Any CSS included here will be global. The classic template
* bundles Infima by default. Infima is a CSS framework designed to
* work well for content-centric websites.
*/
/* You can override the default Infima variables here. */
:root {
--ifm-color-primary-lightest: rgb(99, 183, 168);
--ifm-color-primary-lighter: rgb(70, 159, 143);
--ifm-color-primary-light: rgb(48, 135, 119);
--ifm-color-primary: rgb(33, 111, 97);
--ifm-color-primary-dark: rgb(22, 87, 75);
--ifm-color-primary-darker: rgb(14, 63, 54);
--ifm-color-primary-darkest: rgb(8, 39, 33);
--ifm-code-font-size: 95%;
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
}
/* For readability concerns, you should choose a lighter palette in dark mode. */
[data-theme='dark'] {
--ifm-color-primary-lightest: rgb(192, 231, 224);
--ifm-color-primary-lighter: rgb(146, 207, 196);
--ifm-color-primary-light: rgb(106, 183, 169);
--ifm-color-primary: rgb(74, 159, 144);
--ifm-color-primary-dark: rgb(51, 135, 120);
--ifm-color-primary-darker: rgb(34, 111, 97);
--ifm-color-primary-darkest: rgb(22, 87, 75);
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
}

View File

@@ -0,0 +1,34 @@
/**
* 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.
*/
.heroBanner {
justify-content: center;
}
.heroRow {
align-items: center;
justify-content: center;
}
.heroLogo {
width: 200px;
height: 200px;
}
@media (max-width: 996px) {
.heroLogo {
display: none;
}
.playgroundSection :global(.playground-editor) {
display: none;
}
}
.bg {
background-color: var(--yg-color-playground-background);
}

View File

@@ -0,0 +1,84 @@
/**
* 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 clsx from 'clsx';
import Link from '@docusaurus/Link';
import Layout from '@theme/Layout';
import styles from './index.module.css';
import YogaLogo from '../../static/img/logo.svg';
import Playground from '../components/Playground';
function HeroSection() {
return (
<header className={clsx('hero', styles.heroBanner)}>
<div className={clsx('row', 'container', styles.heroRow)}>
<div className="col col--6">
<h1 className="hero__title">Yoga</h1>
<p className="hero__subtitle">
A portable layout engine targeting web standards
</p>
<Link
className="button button--primary button--lg"
to="/docs/about-yoga">
Learn more
</Link>
</div>
<div className="col col--2">
<YogaLogo className={styles.heroLogo} />
</div>
</div>
</header>
);
}
const playgroundCode = `
<Layout config={{useWebDefaults: false}}>
<Node style={{width: 250, height: 475, padding: 10}}>
<Node style={{flex: 1, rowGap: 10}}>
<Node style={{height: 60}} />
<Node style={{flex: 1, marginInline: 10}} />
<Node style={{flex: 2, marginInline: 10}} />
<Node
style={{
position: "absolute",
width: "100%",
bottom: 0,
height: 64,
flexDirection: "row",
alignItems: "center",
justifyContent: "space-around",
}}
>
<Node style={{height: 40, width: 40}} />
<Node style={{height: 40, width: 40}} />
<Node style={{height: 40, width: 40}} />
<Node style={{height: 40, width: 40}} />
</Node>
</Node>
</Node>
</Layout>
`.trim();
function PlaygroundSection() {
return (
<main className={clsx('container', styles.playgroundSection)}>
<Playground height="600px" code={playgroundCode} autoFocus={true} />
</main>
);
}
export default function Home(): JSX.Element {
return (
<Layout>
<HeroSection />
<PlaygroundSection />
</Layout>
);
}

View File

@@ -0,0 +1,7 @@
---
title: Markdown page example
---
# Markdown page example
You don't need React to write simple standalone pages.

View File

@@ -0,0 +1,11 @@
/**
* 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.
*/
.playgroundContainer {
display: flex;
flex: 1;
}

View File

@@ -0,0 +1,56 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import Layout from '@theme/Layout';
import {useLocation} from '@docusaurus/router';
import lzString from 'lz-string';
import Playground from '../components/Playground';
import clsx from 'clsx';
import styles from './playground.module.css';
import useIsBrowser from '@docusaurus/useIsBrowser';
const defaultCode = `
<Layout config={{useWebDefaults: false}}>
<Node style={{width: 350, height: 350, padding: 20}}>
<Node style={{flex: 1}} />
</Node>
</Layout>
`.trim();
export default function PlaygroundPage(): JSX.Element {
const code = useCodeFromQueryParam();
return (
// @ts-ignore missing prop for `wrapperClassName`
<Layout wrapperClassName={clsx('container', styles.bg)} title="Playground">
<Playground
height="max(80vh, 600px)"
code={code}
autoFocus={true}
key={String(useIsBrowser())}
/>
</Layout>
);
}
function useCodeFromQueryParam(): string {
const location = useLocation();
// We don't know the query param ahead of time when doing SSR, so just render
// blank to avoid the appearance of code changing.
if (!useIsBrowser()) {
return '';
}
const params = new URLSearchParams(location.search);
const codeParam = params.get('code');
return codeParam
? lzString.decompressFromEncodedURIComponent(codeParam)
: defaultCode;
}