Introduce Design System Page / Design system improvements

Summary:
This diff introduces:

- ScrollContainer
- Make sure Ant Link components always open URLs externally, to avoid users needing to use electron api's
- Introduce a design systems page where people can find docs on how to organise sandy layout, and it also provides a convenient way to test layout mechanisms.
- Fixed several layout bugs found as a result of adding the design system page

In next diff:
- more convenience around organizing paddings
- making the design system accessible from the menu

Reviewed By: cekkaewnumchai

Differential Revision: D23930274

fbshipit-source-id: 4aab058d15b3391287e0e32513a5d83831448857
This commit is contained in:
Michel Weststrate
2020-10-01 05:32:07 -07:00
committed by Facebook GitHub Bot
parent 7358711e07
commit e8370e9fc1
15 changed files with 908 additions and 327 deletions

View File

@@ -49,6 +49,7 @@
"react-color": "^2.18.1", "react-color": "^2.18.1",
"react-debounce-render": "^6.0.0", "react-debounce-render": "^6.0.0",
"react-dom": "^16.13.0", "react-dom": "^16.13.0",
"react-element-to-jsx-string": "^14.3.1",
"react-markdown": "^4.2.2", "react-markdown": "^4.2.2",
"react-player": "^1.15.2", "react-player": "^1.15.2",
"react-redux": "^7.1.1", "react-redux": "^7.1.1",

View File

@@ -9,33 +9,23 @@
import React from 'react'; import React from 'react';
import {Button, Dropdown, Menu, Radio, Input} from 'antd'; import {Button, Dropdown, Menu, Radio, Input} from 'antd';
import {shell} from 'electron';
import {LeftSidebar, SidebarTitle, InfoIcon} from './LeftSidebar'; import {LeftSidebar, SidebarTitle, InfoIcon} from './LeftSidebar';
import { import {
AppleOutlined, AppleOutlined,
AndroidOutlined, AndroidOutlined,
SettingOutlined, SettingOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import {Layout, styled} from '../ui'; import {Layout, Link} from '../ui';
import {theme} from './theme'; import {theme} from './theme';
const appTooltip = ( const appTooltip = (
<> <>
Inspect apps by selecting connected devices and emulators. Navigate and Inspect apps by selecting connected devices and emulators. Navigate and
bookmark frequent destinations in the app. Refresh, screenshot and bookmark frequent destinations in the app. Refresh, screenshot and
screenrecord is also available. screenrecord is also available.{' '}
{ <Link href="https://fbflipper.com/docs/getting-started/index">
<Button Learn More
size="small" </Link>
type="link"
onClick={() => {
shell.openExternal(
'https://fbflipper.com/docs/getting-started/index',
);
}}>
Learn More
</Button>
}
</> </>
); );
@@ -43,11 +33,13 @@ export function AppInspect() {
return ( return (
<LeftSidebar> <LeftSidebar>
<Layout.Top scrollable> <Layout.Top scrollable>
<> <Layout.Container borderBottom>
<SidebarTitle actions={<InfoIcon>{appTooltip}</InfoIcon>}> <SidebarTitle actions={<InfoIcon>{appTooltip}</InfoIcon>}>
App Inspect App Inspect
</SidebarTitle> </SidebarTitle>
<TopSection fillx> <Layout.Vertical
padding={`${theme.space.small}px ${theme.space.medium}px`}
gap={theme.space.large}>
<DeviceDropdown /> <DeviceDropdown />
<Input addonAfter={<SettingOutlined />} defaultValue="mysite" /> <Input addonAfter={<SettingOutlined />} defaultValue="mysite" />
<Layout.Horizontal gap={theme.space.small}> <Layout.Horizontal gap={theme.space.small}>
@@ -55,20 +47,14 @@ export function AppInspect() {
<Button icon={<SettingOutlined />} type="link" /> <Button icon={<SettingOutlined />} type="link" />
<Button icon={<SettingOutlined />} type="link" /> <Button icon={<SettingOutlined />} type="link" />
</Layout.Horizontal> </Layout.Horizontal>
</TopSection> </Layout.Vertical>
</> </Layout.Container>
<div>Dynamic section</div> <Layout.Container>Dynamic section</Layout.Container>
</Layout.Top> </Layout.Top>
</LeftSidebar> </LeftSidebar>
); );
} }
const TopSection = styled(Layout.Vertical)({
boxShadow: `inset 0px -1px 0px ${theme.dividerColor}`,
padding: `8px 12px`,
gap: theme.space.middle,
});
function DeviceDropdown() { function DeviceDropdown() {
return ( return (
<Radio.Group value={1} size="small"> <Radio.Group value={1} size="small">

View File

@@ -0,0 +1,359 @@
/**
* Copyright (c) Facebook, Inc. and its 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 {Typography, Card, Table, Collapse, Button, Tabs} from 'antd';
import {Layout} from '../ui';
import {theme} from './theme';
import reactElementToJSXString from 'react-element-to-jsx-string';
import {CodeOutlined} from '@ant-design/icons';
const {Text} = Typography;
const demoStyle: Record<string, React.CSSProperties> = {
square: {
background: theme.successColor,
width: 50,
height: 50,
lineHeight: '50px',
textAlign: 'center',
},
border: {border: `1px dotted ${theme.primaryColor}`},
} as const;
type PreviewProps = {
title: string;
description?: string;
props: [string, string, string][];
demos: Record<string, React.ReactNode>;
};
const largeChild = (
<div style={{background: theme.warningColor}}>
<img src="https://fbflipper.com/img/mascot.png" height={500} />
</div>
);
const aButton = <Button>A button</Button>;
const aBox = <div style={{...demoStyle.square, width: 100}}>A fixed child</div>;
const aFixedWidthBox = (
<div style={{background: theme.primaryColor, width: 150, color: 'white'}}>
Fixed width box
</div>
);
const aFixedHeightBox = (
<div
style={{
background: theme.primaryColor,
height: 40,
lineHeight: '40px',
color: 'white',
}}>
Fixed height box
</div>
);
const aDynamicBox = (
<div style={{background: theme.warningColor, flex: 1}}>
A dynamic child (flex: 1)
</div>
);
const someText = <Text>Some text</Text>;
const demos: PreviewProps[] = [
{
title: 'Layout.Container',
description:
'Layout.Container can be used to organize the UI in regions. It takes care of paddings and borders. To arrange multiple children use one of the other Layout components. If you need a margin on this component, try to wrap it in other Layout component instead.',
props: [
['rounded', 'boolean (false)', 'Make the corners rounded'],
[
'padded',
'boolean (false)',
'Use a standard small padding for this container (use `padding` for non-default padding)',
],
[
'padding',
'CSS Padding',
'Short-hand to set the style.padding property',
],
[
'bordered',
'boolean (false)',
'This container will use a default border on all sides',
],
[
'borderTop',
'boolean (false)',
'Use a standard padding on the top side',
],
[
'borderRight',
'boolean (false)',
'Use a standard padding on the right side',
],
[
'borderBottom',
'boolean (false)',
'Use a standard padding on the bottom side',
],
[
'borderLeft',
'boolean (false)',
'Use a standard padding on the left side',
],
],
demos: {
'Basic container with fixed dimensions': (
<Layout.Container style={demoStyle.square}></Layout.Container>
),
'Basic container with fixed height': (
<Layout.Container
style={{
height: 50,
background: theme.successColor,
}}></Layout.Container>
),
'bordered padded rounded': (
<Layout.Container
bordered
padded
rounded
style={{background: theme.backgroundDefault, width: 200}}>
<div style={demoStyle.square}>child</div>
</Layout.Container>
),
},
},
{
title: 'Layout.ScrollContainer',
description:
'Use this component to create an area that can be scrolled. The scrollable area will automatically consume all available space. ScrollContainer accepts all properties that Container accepts as well. Padding will be applied to the child rather than the parent.',
props: [],
demos: {
'Basic usage': (
<Layout.ScrollContainer style={{height: 100}}>
{largeChild}
</Layout.ScrollContainer>
),
},
},
{
title: 'Layout.Horizontal',
description:
'Use this component to arrange multiple items horizontally. All vanilla Container props can be used as well.',
props: [
[
'gap',
'number (0)',
'Set the spacing between children. Typically theme.space.small should be used.',
],
[
'center',
'boolean (false)',
'If set, all children will use their own height, and they will be centered vertically in the layout. If not set, all children will be stretched to the height of the layout.',
],
],
demos: {
'Basic usage, gap={24}': (
<Layout.Horizontal gap={24}>
{aButton}
{someText}
{aBox}
{aDynamicBox}
</Layout.Horizontal>
),
'Using flags: padded center gap={8} (great for toolbars and such)': (
<Layout.Horizontal padded center gap={8}>
{aButton}
{someText}
{aBox}
{aDynamicBox}
</Layout.Horizontal>
),
},
},
{
title: 'Layout.Vertical',
description:
'Use this component to arrange multiple items vertically. All vanilla Container props can be used as well.',
props: [
[
'gap',
'number (0)',
'Set the spacing between children. Typically theme.space.small should be used.',
],
[
'center',
'boolean (false)',
'If set, all children will use their own height, and they will be centered vertically in the layout. If not set, all children will be stretched to the height of the layout.',
],
],
demos: {
'Basic usage, gap={24}': (
<Layout.Vertical gap={24}>
{aButton}
{someText}
{aBox}
{aDynamicBox}
</Layout.Vertical>
),
'Using flags: padded center gap={8} (great for toolbars and such)': (
<Layout.Vertical padded center gap={8}>
{aButton}
{someText}
{aBox}
{aDynamicBox}
</Layout.Vertical>
),
},
},
{
title: 'Layout.Top|Left|Right|Bottom',
description:
"Divides all available space over two children. The (top|left|right|bottom)-most first child will keep it's own dimensions, and positioned (top|left|right|bottom) of the other child. All remaining space will be assigned to the remaining child.",
props: [
[
'scrollable',
'boolean (false)',
'If set, the area of the second child will automatically be made scrollable.',
],
],
demos: {
'Layout.Top': (
<Layout.Top>
{aFixedHeightBox}
{aDynamicBox}
</Layout.Top>
),
'Layout.Left': (
<Layout.Left>
{aFixedWidthBox}
{aDynamicBox}
</Layout.Left>
),
'Layout.Right': (
<Layout.Right>
{aDynamicBox}
{aFixedWidthBox}
</Layout.Right>
),
'Layout.Bottom': (
<Layout.Bottom>
{aDynamicBox}
{aFixedHeightBox}
</Layout.Bottom>
),
'Layout.Top + scrollable': (
<Layout.Container style={{height: 150}}>
<Layout.Top scrollable>
{aFixedHeightBox}
{largeChild}
</Layout.Top>
</Layout.Container>
),
'Layout.Left + scrollable': (
<Layout.Container style={{height: 150}}>
<Layout.Left scrollable>
{aFixedWidthBox}
{largeChild}
</Layout.Left>
</Layout.Container>
),
'Layout.Right + scrollable': (
<Layout.Container style={{height: 150}}>
<Layout.Right scrollable>
{largeChild}
{aFixedWidthBox}
</Layout.Right>
</Layout.Container>
),
'Layout.Bottom + scrollable': (
<Layout.Container style={{height: 150}}>
<Layout.Bottom scrollable>
{largeChild}
{aFixedHeightBox}
</Layout.Bottom>
</Layout.Container>
),
},
},
];
function ComponentPreview({title, demos, description, props}: PreviewProps) {
return (
<Card title={title} size="small" type="inner">
<Layout.Vertical gap={theme.space.small}>
<Text type="secondary">{description}</Text>
<Collapse ghost>
<Collapse.Panel header="Examples" key="demos">
<Layout.Vertical gap={theme.space.large}>
{Object.entries(demos).map(([name, children]) => (
<div key={name}>
<Tabs type="line">
<Tabs.TabPane tab={name} key="1">
<div
style={{
background: theme.backgroundWash,
width: '100%',
}}>
{children}
</div>
</Tabs.TabPane>
<Tabs.TabPane tab={<CodeOutlined />} key="2">
<div
style={{
background: theme.backgroundWash,
width: '100%',
padding: theme.space.medium,
}}>
<pre>{reactElementToJSXString(children)}</pre>
</div>
</Tabs.TabPane>
</Tabs>
</div>
))}
</Layout.Vertical>
</Collapse.Panel>
<Collapse.Panel header="Props" key="props">
<Table
size="small"
pagination={false}
dataSource={props.map((prop) =>
Object.assign(prop, {key: prop[0]}),
)}
columns={[
{
title: 'Property',
dataIndex: 0,
width: 100,
},
{
title: 'Type and default',
dataIndex: 1,
width: 200,
},
{
title: 'Description',
dataIndex: 2,
},
]}
/>
</Collapse.Panel>
</Collapse>
</Layout.Vertical>
</Card>
);
}
export const DesignComponentDemos = () => (
<Layout.Vertical gap={theme.space.large}>
{demos.map((demo) => (
<ComponentPreview key={demo.title} {...demo} />
))}
</Layout.Vertical>
);

View File

@@ -8,7 +8,7 @@
*/ */
import React, {cloneElement, useState, useCallback} from 'react'; import React, {cloneElement, useState, useCallback} from 'react';
import {styled, FlexColumn} from 'flipper'; import {styled, Layout} from 'flipper';
import {Button, Divider, Badge, Tooltip} from 'antd'; import {Button, Divider, Badge, Tooltip} from 'antd';
import { import {
MobileFilled, MobileFilled,
@@ -31,24 +31,16 @@ import {errorCounterAtom} from '../chrome/ConsoleLogs';
import {ToplevelProps} from './SandyApp'; import {ToplevelProps} from './SandyApp';
import {useValue} from 'flipper-plugin'; import {useValue} from 'flipper-plugin';
const LeftRailContainer = styled(FlexColumn)({ const LeftRailContainer = styled(Layout.Bottom)({
background: theme.backgroundDefault,
width: 48, width: 48,
boxShadow: 'inset -1px 0px 0px rgba(0, 0, 0, 0.1)', borderRight: `1px solid ${theme.dividerColor}`,
justifyContent: 'space-between', padding: `${theme.paddingLarge}px ${theme.paddingSmall}px`,
}); });
LeftRailContainer.displayName = 'LeftRailContainer'; LeftRailContainer.displayName = 'LeftRailContainer';
const LeftRailSection = styled(FlexColumn)({
padding: '8px 0px',
alignItems: 'center',
});
LeftRailSection.displayName = 'LeftRailSection';
const LeftRailButtonElem = styled(Button)<{kind?: 'small'}>(({kind}) => ({ const LeftRailButtonElem = styled(Button)<{kind?: 'small'}>(({kind}) => ({
width: kind === 'small' ? 32 : 36, width: kind === 'small' ? 32 : 36,
height: kind === 'small' ? 32 : 36, height: kind === 'small' ? 32 : 36,
margin: 6,
padding: '5px 0', padding: '5px 0',
border: 'none', border: 'none',
boxShadow: 'none', boxShadow: 'none',
@@ -94,9 +86,9 @@ function LeftRailButton({
} }
const LeftRailDivider = styled(Divider)({ const LeftRailDivider = styled(Divider)({
margin: 10, margin: `10px 0`,
width: 36, width: 32,
minWidth: 36, minWidth: 32,
}); });
LeftRailDivider.displayName = 'LeftRailDividier'; LeftRailDivider.displayName = 'LeftRailDividier';
@@ -106,7 +98,7 @@ export function LeftRail({
}: ToplevelProps) { }: ToplevelProps) {
return ( return (
<LeftRailContainer> <LeftRailContainer>
<LeftRailSection> <Layout.Vertical center gap={10}>
<LeftRailButton <LeftRailButton
icon={<MobileFilled />} icon={<MobileFilled />}
title="App Inspect" title="App Inspect"
@@ -116,18 +108,14 @@ export function LeftRail({
}} }}
/> />
<LeftRailButton icon={<AppstoreOutlined />} title="Plugin Manager" /> <LeftRailButton icon={<AppstoreOutlined />} title="Plugin Manager" />
<LeftRailButton <LeftRailButton icon={<BellOutlined />} title="Notifications" />
count={8}
icon={<BellOutlined />}
title="Notifications"
/>
<LeftRailDivider /> <LeftRailDivider />
<DebugLogsButton <DebugLogsButton
toplevelSelection={toplevelSelection} toplevelSelection={toplevelSelection}
setToplevelSelection={setToplevelSelection} setToplevelSelection={setToplevelSelection}
/> />
</LeftRailSection> </Layout.Vertical>
<LeftRailSection> <Layout.Vertical center gap={10}>
<LeftRailButton <LeftRailButton
icon={<MedicineBoxOutlined />} icon={<MedicineBoxOutlined />}
small small
@@ -147,7 +135,7 @@ export function LeftRail({
/> />
<LeftSidebarToggleButton /> <LeftSidebarToggleButton />
<LeftRailButton icon={<LoginOutlined />} title="Log In" /> <LeftRailButton icon={<LoginOutlined />} title="Log In" />
</LeftRailSection> </Layout.Vertical>
</LeftRailContainer> </LeftRailContainer>
); );
} }

View File

@@ -10,15 +10,15 @@
import React from 'react'; import React from 'react';
import {theme} from './theme'; import {theme} from './theme';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import {Layout, FlexColumn} from '../ui'; import {Layout} from '../ui';
import {Button, Tooltip} from 'antd'; import {Button, Tooltip, Typography} from 'antd';
import {InfoCircleOutlined} from '@ant-design/icons'; import {InfoCircleOutlined} from '@ant-design/icons';
export const LeftSidebar = styled(FlexColumn)({ export const LeftSidebar: React.FC = ({children}) => (
background: theme.backgroundDefault, <Layout.Vertical borderRight padding={`${theme.space.small}px 0`}>
flex: 1, {children}
padding: `10px 0`, </Layout.Vertical>
}); );
export function SidebarTitle({ export function SidebarTitle({
children, children,
@@ -28,22 +28,21 @@ export function SidebarTitle({
actions?: React.ReactNode; actions?: React.ReactNode;
}) { }) {
return ( return (
<LeftMenuTitle> <LeftMenuTitle center>
<Layout.Right center> <Typography.Text>{children}</Typography.Text>
{children} <>{actions}</>
<>{actions}</>
</Layout.Right>
</LeftMenuTitle> </LeftMenuTitle>
); );
} }
const LeftMenuTitle = styled.div({ const LeftMenuTitle = styled(Layout.Horizontal)({
width: '100%', padding: `0px ${theme.paddingLarge}px`,
fontFamily: 'SF Pro Text', lineHeight: `${theme.space.large}px`,
padding: `0px 12px`, fontSize: theme.fontSize.smallBody,
lineHeight: '16px',
fontSize: '12px',
textTransform: 'uppercase', textTransform: 'uppercase',
'> :first-child': {
flex: 1,
},
}); });
export const InfoIcon: React.FC<{}> = ({children}) => ( export const InfoIcon: React.FC<{}> = ({children}) => (

View File

@@ -9,14 +9,13 @@
import React, {useEffect, useState, useCallback} from 'react'; import React, {useEffect, useState, useCallback} from 'react';
import {styled} from 'flipper'; import {styled} from 'flipper';
import {DatePicker, Space} from 'antd'; import {Layout, Sidebar} from '../ui';
import {Layout, FlexRow, Sidebar} from '../ui';
import {theme} from './theme'; import {theme} from './theme';
import {Logger} from '../fb-interfaces/Logger'; import {Logger} from '../fb-interfaces/Logger';
import {LeftRail} from './LeftRail'; import {LeftRail} from './LeftRail';
import {TemporarilyTitlebar} from './TemporarilyTitlebar'; import {TemporarilyTitlebar} from './TemporarilyTitlebar';
import TypographyExample from './TypographyExample'; import SandyDesignSystem from './SandyDesignSystem';
import {registerStartupTime} from '../App'; import {registerStartupTime} from '../App';
import {useStore, useDispatch} from '../utils/useStore'; import {useStore, useDispatch} from '../utils/useStore';
import {SandyContext} from './SandyContext'; import {SandyContext} from './SandyContext';
@@ -86,43 +85,39 @@ export function SandyApp({logger}: {logger: Logger}) {
<Layout.Top> <Layout.Top>
<TemporarilyTitlebar /> <TemporarilyTitlebar />
<Layout.Left> <Layout.Left>
<LeftSidebarContainer> <Layout.Horizontal>
<LeftRail <LeftRail
toplevelSelection={toplevelSelection} toplevelSelection={toplevelSelection}
setToplevelSelection={setToplevelSelection} setToplevelSelection={setToplevelSelection}
/> />
<Sidebar width={348} minWidth={220} maxWidth={800} gutter> <Sidebar width={250} minWidth={220} maxWidth={800} gutter>
{leftMenuContent && ( {leftMenuContent && (
<LeftMenuContainer>{leftMenuContent}</LeftMenuContainer> <Layout.Container borderRight>
{leftMenuContent}
</Layout.Container>
)} )}
</Sidebar> </Sidebar>
</LeftSidebarContainer> </Layout.Horizontal>
<MainContainer> <MainContainer>
<Layout.Right> <ContentContainer>
<MainContentWrapper> {staticView ? (
<ContentContainer> React.createElement(staticView, {
{staticView ? ( logger: logger,
React.createElement(staticView, { })
logger: logger, ) : (
}) <SandyDesignSystem />
) : ( )}
<TemporarilyContent /> </ContentContainer>
)} <Sidebar
</ContentContainer> width={300}
</MainContentWrapper> minWidth={220}
<Sidebar maxWidth={800}
width={300} gutter
minWidth={220} position="right">
maxWidth={800} <ContentContainer style={{marginRight: theme.space.large}}>
gutter <RightMenu />
position="right"> </ContentContainer>
<MainContentWrapper> </Sidebar>
<ContentContainer>
<RightMenu />
</ContentContainer>
</MainContentWrapper>
</Sidebar>
</Layout.Right>
</MainContainer> </MainContainer>
</Layout.Left> </Layout.Left>
</Layout.Top> </Layout.Top>
@@ -130,57 +125,19 @@ export function SandyApp({logger}: {logger: Logger}) {
); );
} }
const LeftMenuContainer = styled.div({ const MainContainer = styled(Layout.Right)({
background: theme.backgroundDefault,
paddingRight: 1, // to see the boxShadow
boxShadow: 'inset -1px 0px 0px rgba(0, 0, 0, 0.1)',
height: '100%',
width: '100%',
});
const LeftSidebarContainer = styled(FlexRow)({
background: theme.backgroundWash, background: theme.backgroundWash,
height: '100%',
width: '100%',
}); });
const MainContainer = styled('div')({ export const ContentContainer = styled(Layout.Container)({
display: 'flex',
width: '100%',
height: '100%',
background: theme.backgroundWash,
paddingRight: theme.space.middle,
});
export const ContentContainer = styled('div')({
width: '100%',
margin: 0,
padding: 0,
background: theme.backgroundDefault, background: theme.backgroundDefault,
border: `1px solid ${theme.dividerColor}`, border: `1px solid ${theme.dividerColor}`,
borderRadius: theme.containerBorderRadius, borderRadius: theme.containerBorderRadius,
boxShadow: `0px 0px 5px rgba(0, 0, 0, 0.05), 0px 0px 1px rgba(0, 0, 0, 0.05)`, boxShadow: `0px 0px 5px rgba(0, 0, 0, 0.05), 0px 0px 1px rgba(0, 0, 0, 0.05)`,
}); marginTop: theme.space.large,
marginBottom: theme.space.large,
const MainContentWrapper = styled('div')({
height: '100%',
width: '100%',
display: 'flex',
alignItems: 'stretch',
padding: `${theme.space.middle}px 0`,
}); });
function RightMenu() { function RightMenu() {
return <div>RightMenu</div>; return <div>RightMenu</div>;
} }
function TemporarilyContent() {
return (
<Space direction="vertical">
New UI for Flipper, Sandy Project! Nothing to see now. Go back to current
Flipper
<DatePicker />
<TypographyExample />
</Space>
);
}

View File

@@ -0,0 +1,158 @@
/**
* Copyright (c) Facebook, Inc. and its 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 {Typography, Button, Space, Input, Card, Alert, List} from 'antd';
import {Layout} from '../ui';
import {theme} from './theme';
import {css} from 'emotion';
import {DesignComponentDemos} from './DesignComponentDemos';
const {Title, Text, Link} = Typography;
export default function SandyDesignSystem() {
return (
<Layout.ScrollContainer className={resetLists}>
<Layout.Vertical gap={theme.space.large}>
<Card title="Flipper Design System">
<p>
Welcome to the Flipper Design System. The Flipper design system is
based on{' '}
<Link href="https://ant.design/components/overview/">
Ant Design
</Link>
. Any component found in the ANT documentation can be used. This
page demonstrates the usage of:
</p>
<ul>
<li>Colors</li>
<li>Typography</li>
<li>Theme Variables</li>
<li>Layout components</li>
</ul>
<p>
The following components from Ant should <em>not</em> be used:
</p>
<ul>
<li>
<code>Layout</code>: use Flipper's <code>Layout.*</code> instead.
</li>
</ul>
<p>Sandy guidelines</p>
<ul>
<li>
Avoid using `margin` properties, use padding on the container
indeed, or <code>gap</code> in combination with{' '}
<code>Layout.Horizontal|Vertical</code>
</li>
<li>
Avoid using <code>width / height: 100%</code>, use{' '}
<code>Layout.Container</code> instead.
</li>
</ul>
</Card>
<Card title="Colors">
<Alert message="The following colors are available on the <code>theme</code> object. Please stick to this color palette, as these colors will be translated to dark mode correctly." />
<ColorPreview name="primaryColor" />
<ColorPreview name="successColor" />
<ColorPreview name="errorColor" />
<ColorPreview name="warningColor" />
<ColorPreview name="textColorPrimary" />
<ColorPreview name="textColorSecondary" />
<ColorPreview name="textColorPlaceholder" />
<ColorPreview name="disabledColor" />
<ColorPreview name="backgroundDefault" />
<ColorPreview name="backgroundWash" />
<ColorPreview name="backgroundTransparentHover" />
<ColorPreview name="dividerColor" />
</Card>
<Card title="Typography">
<Space direction="vertical">
<Alert
message={
<>
Common Ant components, with modifiers applied. The{' '}
<code>Title</code>, <code>Text</code> and <code>Link</code>{' '}
components can be found by importing the{' '}
<code>Typography</code> namespace from Ant.
</>
}
type="info"
/>
<Title>Title</Title>
<Title level={2}>Title level=2</Title>
<Title level={3}>Title level=3</Title>
<Title level={4}>Title level=4</Title>
<Text>Text</Text>
<Text type="secondary">Text - type=secondary</Text>
<Text type="success">Text - type=success</Text>
<Text type="warning">Text - type=warning</Text>
<Text type="danger">Text - danger</Text>
<Text disabled>Text - disbled </Text>
<Text strong>Text - strong</Text>
<Text code>Text - code</Text>
<Link href="https://fbflipper.com/">Link</Link>
<Link type="secondary" href="https://fbflipper.com/">
Link - type=secondary
</Link>
<Button>Button</Button>
<Button size="small">Button - size=small</Button>
<Input placeholder="Input" />
</Space>
</Card>
<Card title="Theme variables">
<Alert
message={
<>
The following theme veriables are exposed from the Flipper{' '}
<code>theme</code> object. See the colors section above for a
preview of the colors.
</>
}
/>
<pre>{JSON.stringify(theme, null, 2)}</pre>
</Card>
<Card title="Layout components">
<DesignComponentDemos />
</Card>
</Layout.Vertical>
</Layout.ScrollContainer>
);
}
function ColorPreview({name}: {name: keyof typeof theme}) {
return (
<List.Item>
<List.Item.Meta
avatar={
<div
style={{
background: theme[name] as any,
width: 24,
height: 24,
borderRadius: theme.borderRadius,
}}
/>
}
title={`theme.${name}`}
/>
</List.Item>
);
}
const resetLists = css`
ol,
ul {
list-style: revert;
margin-left: ${theme.space.huge}px;
}
.ant-alert {
margin-bottom: ${theme.space.huge}px;
}
`;

View File

@@ -1,53 +0,0 @@
/**
* Copyright (c) Facebook, Inc. and its 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 {Typography, Button, Space, Input} from 'antd';
// import {styled, FlexColumn} from 'flipper';
const {Title, Text, Link} = Typography;
// const Container = styled(FlexColumn)({});
export default function TypographyExample() {
return (
<Space>
<Space direction="vertical">
<Title>h1. Headline</Title>
<Title level={2}>h2. Headline</Title>
<Title level={3}>h3. Headline</Title>
<Title level={4}>h4. Headline</Title>
<Text>Body - Regular</Text>
<Text strong>Body - Strong</Text>
<Button type="text" size="middle">
Button
</Button>
<Button type="text" size="small">
Button small
</Button>
<Text code>Code</Text>
</Space>
<Space direction="vertical">
<Text>Primary</Text>
<Text type="secondary">Secondary</Text>
<Input placeholder="Placeholder" />
<Text disabled>Disabled</Text>
<Text type="success">Positive</Text>
<Text type="warning">Warning</Text>
<Text type="danger">Danger</Text>
<Link href="https://fbflipper.com/" target="_blank">
Link
</Link>
<Link type="secondary" href="https://fbflipper.com/" target="_blank">
Link Secondary
</Link>
</Space>
</Space>
);
}

View File

@@ -26,17 +26,20 @@ export const theme = {
dividerColor: 'var(--flipper-divider-color)', dividerColor: 'var(--flipper-divider-color)',
borderRadius: 'var(--flipper-border-radius)', borderRadius: 'var(--flipper-border-radius)',
containerBorderRadius: 8, containerBorderRadius: 8,
paddingSmall: 6, // vertical padding on inline elements like buttons
paddingLarge: 12, // horizontal ,,,
space: { space: {
// from Space component in Ant // from Space component in Ant
tiny: 4, tiny: 4,
small: 8, small: 8,
middle: 16, medium: 12,
large: 24, large: 16,
huge: 24,
} as const, } as const,
fontSize: { fontSize: {
smallBody: '12px', smallBody: '12px',
}, } as const,
}; } as const;
/** /**
* This hook returns whether dark mode is currently being used. * This hook returns whether dark mode is currently being used.

View File

@@ -17,7 +17,8 @@ interface ContextMenuManager {
} }
const Container = styled.div({ const Container = styled.div({
display: 'contents', display: 'flex',
height: '100%',
}); });
Container.displayName = 'ContextMenuProvider:Container'; Container.displayName = 'ContextMenuProvider:Container';

View File

@@ -7,8 +7,98 @@
* @format * @format
*/ */
import React from 'react'; import React, {CSSProperties} from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import {theme} from '../../sandy-chrome/theme';
import {useIsSandy} from '../../sandy-chrome/SandyContext';
import {renderLayout} from './LegacyLayout';
type ContainerProps = {
children?: React.ReactNode;
className?: string;
style?: React.CSSProperties;
padding?: CSSProperties['padding'];
borderBottom?: boolean;
borderTop?: boolean;
borderRight?: boolean;
borderLeft?: boolean;
bordered?: boolean;
rounded?: boolean;
padded?: boolean;
};
const Container = styled.div<ContainerProps>(
({
bordered,
borderBottom,
borderLeft,
borderRight,
borderTop,
padding,
rounded,
padded,
}) => ({
display: 'flex',
flexDirection: 'column',
padding: padded ? theme.space.small : padding,
borderRadius: rounded ? theme.containerBorderRadius : undefined,
flex: 1,
borderStyle: 'solid',
borderColor: theme.dividerColor,
borderWidth: bordered
? 1
: `${borderTop ? 1 : 0}px ${borderRight ? 1 : 0}px ${
borderBottom ? 1 : 0
}px ${borderLeft ? 1 : 0}px`,
}),
);
const ScrollParent = styled.div({
flex: 1,
position: 'relative',
overflow: 'auto',
});
const ScrollChild = styled.div({
position: 'absolute',
minHeight: '100%',
minWidth: '100%',
});
const ScrollContainer = ({
children,
...rest
}: React.HTMLAttributes<HTMLDivElement>) =>
(
<ScrollParent {...rest}>
<ScrollChild>{children}</ScrollChild>
</ScrollParent>
) as any;
type DistributionProps = ContainerProps & {
/**
* Gab between individual items
*/
gap?: CSSProperties['gap'];
/**
* If set, items will be aligned in the center, if false (the default) items will be stretched.
*/
center?: boolean;
};
const Horizontal = styled(Container)<DistributionProps>(({gap, center}) => ({
display: 'flex',
flexDirection: 'row',
gap,
alignItems: center ? 'center' : 'stretch',
}));
const Vertical = styled(Container)<DistributionProps>(({gap, center}) => ({
display: 'flex',
flexDirection: 'column',
gap,
alignItems: center ? 'center' : 'stretch',
}));
type SplitLayoutProps = { type SplitLayoutProps = {
/** /**
@@ -18,88 +108,9 @@ type SplitLayoutProps = {
/** /**
* If set, items will be centered over the orthogonal direction, if false (the default) items will be stretched. * If set, items will be centered over the orthogonal direction, if false (the default) items will be stretched.
*/ */
center?: boolean;
children: [React.ReactNode, React.ReactNode]; children: [React.ReactNode, React.ReactNode];
}; };
const FixedContainer = styled('div')({
flex: 'none',
height: 'auto',
overflow: 'hidden',
});
FixedContainer.displayName = 'Layout:FixedContainer';
const ScrollContainer = styled('div')<{scrollable: boolean}>(
({scrollable}) => ({
overflow: scrollable ? 'auto' : 'hidden',
flex: 'auto',
display: 'flex',
}),
);
ScrollContainer.displayName = 'Layout:ScrollContainer';
const Container = styled('div')<{horizontal: boolean; center?: boolean}>(
({horizontal, center}) => ({
display: 'flex',
flex: 'auto',
flexDirection: horizontal ? 'row' : 'column',
height: '100%',
width: '100%',
overflow: 'hidden',
alignItems: center ? 'center' : undefined,
}),
);
Container.displayName = 'Layout:Container';
function renderLayout(
{children, scrollable, center}: SplitLayoutProps,
horizontal: boolean,
reverse: boolean,
) {
if (children.length !== 2) {
throw new Error('Layout expects exactly 2 children');
}
const fixedChild = reverse ? children[1] : children[0];
const fixedElement = <FixedContainer>{fixedChild}</FixedContainer>;
const dynamicElement = (
<ScrollContainer scrollable={!!scrollable}>
{reverse ? children[0] : children[1]}
</ScrollContainer>
);
return reverse ? (
<Container horizontal={horizontal} center={center}>
{dynamicElement}
{fixedElement}
</Container>
) : (
<Container horizontal={horizontal} center={center}>
{fixedElement}
{dynamicElement}
</Container>
);
}
type DistributionProps = {
/**
* Gab between individual items
*/
gap?: number;
/**
* If set, items will be aligned in the center, if false (the default) items will be stretched.
*/
center?: boolean;
/**
* If set, the layout will fill out to maximum width
*/
fillx?: boolean;
/**
* If set, the layout will fill out to maximum height
*/
filly?: boolean;
};
/** /**
* The Layout component divides all available screenspace over two components: * The Layout component divides all available screenspace over two components:
* A fixed top (or left) component, and all remaining space to a bottom component. * A fixed top (or left) component, and all remaining space to a bottom component.
@@ -111,43 +122,90 @@ type DistributionProps = {
* *
* Use Layout.Top / Right / Bottom / Left to indicate where the fixed element should live. * Use Layout.Top / Right / Bottom / Left to indicate where the fixed element should live.
*/ */
const Layout: Record< const Layout = {
'Left' | 'Right' | 'Top' | 'Bottom', Top(props: SplitLayoutProps) {
React.FC<SplitLayoutProps> const isSandy = useIsSandy();
> & if (!isSandy) return renderLayout(props, false, false);
Record<'Horizontal' | 'Vertical', React.FC<DistributionProps>> = { let [child1, child2] = props.children;
Top(props) { if (props.scrollable) child2 = <ScrollContainer>{child2}</ScrollContainer>;
return renderLayout(props, false, false); return (
<SandySplitContainer
{...props}
flexDirection="column"
flex1={0}
flex2={1}>
{child1}
{child2}
</SandySplitContainer>
);
}, },
Bottom(props) { Bottom(props: SplitLayoutProps) {
return renderLayout(props, false, true); const isSandy = useIsSandy();
if (!isSandy) return renderLayout(props, false, true);
let [child1, child2] = props.children;
if (props.scrollable) child1 = <ScrollContainer>{child1}</ScrollContainer>;
return (
<SandySplitContainer
{...props}
flexDirection="column"
flex1={1}
flex2={0}>
{child1}
{child2}
</SandySplitContainer>
);
}, },
Left(props) { Left(props: SplitLayoutProps) {
return renderLayout(props, true, false); const isSandy = useIsSandy();
if (!isSandy) return renderLayout(props, true, false);
let [child1, child2] = props.children;
if (props.scrollable) child2 = <ScrollContainer>{child2}</ScrollContainer>;
return (
<SandySplitContainer {...props} flexDirection="row" flex1={0} flex2={1}>
{child1}
{child2}
</SandySplitContainer>
);
}, },
Right(props) { Right(props: SplitLayoutProps) {
return renderLayout(props, true, true); const isSandy = useIsSandy();
if (!isSandy) return renderLayout(props, true, true);
let [child1, child2] = props.children;
if (props.scrollable) child1 = <ScrollContainer>{child1}</ScrollContainer>;
return (
<SandySplitContainer {...props} flexDirection="row" flex1={1} flex2={0}>
{child1}
{child2}
</SandySplitContainer>
);
}, },
Horizontal: styled.div<DistributionProps>(({gap, center, fillx, filly}) => ({ Container,
display: 'flex', ScrollContainer,
flexDirection: 'row', Horizontal,
gap, Vertical,
alignItems: center ? 'center' : 'stretch',
width: fillx ? '100%' : undefined,
height: filly ? '100%' : undefined,
})),
Vertical: styled.div<DistributionProps>(({gap, center, fillx, filly}) => ({
display: 'flex',
flexDirection: 'column',
gap,
alignItems: center ? 'center' : 'stretch',
width: fillx ? '100%' : undefined,
height: filly ? '100%' : undefined,
})),
}; };
Object.keys(Layout).forEach((key) => { Object.keys(Layout).forEach((key) => {
(Layout as any)[key].displayName = `Layout.${key}`; (Layout as any)[key].displayName = `Layout.${key}`;
}); });
const SandySplitContainer = styled.div<{
flex1: number;
flex2: number;
flexDirection: CSSProperties['flexDirection'];
}>((props) => ({
display: 'flex',
flex: 1,
flexDirection: props.flexDirection,
alignItems: 'stretch',
'> :first-child': {
flexGrow: props.flex1,
flexShrink: props.flex1,
},
'> :last-child': {
flexGrow: props.flex2,
flexShrink: props.flex2,
},
}));
export default Layout; export default Layout;

View File

@@ -0,0 +1,76 @@
/**
* Copyright (c) Facebook, Inc. and its 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 styled from '@emotion/styled';
const FixedContainer = styled.div({
flex: 'none',
height: 'auto',
overflow: 'hidden',
});
FixedContainer.displayName = 'Layout:FixedContainer';
const SplitScrollContainer = styled.div<{scrollable: boolean}>(
({scrollable}) => ({
overflow: scrollable ? 'auto' : 'hidden',
flex: 'auto',
display: 'flex',
}),
);
SplitScrollContainer.displayName = 'Layout:SplitScrollContainer';
const SplitContainer = styled.div<{horizontal: boolean; center?: boolean}>(
({horizontal, center}) => ({
display: 'flex',
flex: 'auto',
flexDirection: horizontal ? 'row' : 'column',
height: '100%',
width: '100%',
overflow: 'hidden',
alignItems: center ? 'center' : undefined,
}),
);
SplitContainer.displayName = 'Layout:SplitContainer';
/**
* @deprecated use Layout.Top|Left|Right|Bottom instead
*/
export function renderLayout(
{
children,
scrollable,
}: {scrollable?: boolean; children: [React.ReactNode, React.ReactNode]},
horizontal: boolean,
reverse: boolean,
) {
if (children.length !== 2) {
throw new Error('Layout expects exactly 2 children');
}
const fixedChild = reverse ? children[1] : children[0];
const fixedElement = <FixedContainer>{fixedChild}</FixedContainer>;
const dynamicElement = (
<SplitScrollContainer scrollable={!!scrollable}>
{reverse ? children[0] : children[1]}
</SplitScrollContainer>
);
return reverse ? (
<SplitContainer horizontal={horizontal}>
{dynamicElement}
{fixedElement}
</SplitContainer>
) : (
<SplitContainer horizontal={horizontal}>
{fixedElement}
{dynamicElement}
</SplitContainer>
);
}

View File

@@ -9,9 +9,11 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import {colors} from './colors'; import {colors} from './colors';
import {Component} from 'react'; import {useCallback} from 'react';
import {shell} from 'electron'; import {shell} from 'electron';
import React from 'react'; import React from 'react';
import {useIsSandy} from '../../sandy-chrome/SandyContext';
import {Typography} from 'antd';
const StyledLink = styled.span({ const StyledLink = styled.span({
color: colors.highlight, color: colors.highlight,
@@ -22,20 +24,33 @@ const StyledLink = styled.span({
}); });
StyledLink.displayName = 'Link:StyledLink'; StyledLink.displayName = 'Link:StyledLink';
export default class Link extends Component<{ const AntOriginalLink = Typography.Link;
export default function Link(props: {
href: string; href: string;
children?: React.ReactNode; children?: React.ReactNode;
style?: React.CSSProperties; style?: React.CSSProperties;
}> { }) {
onClick = () => { const isSandy = useIsSandy();
shell.openExternal(this.props.href); const onClick = useCallback(
}; (e: React.MouseEvent<any>) => {
shell.openExternal(props.href);
e.preventDefault();
e.stopPropagation();
},
[props.href],
);
render() { return isSandy ? (
return ( <AntOriginalLink {...props} onClick={onClick} />
<StyledLink onClick={this.onClick} style={this.props.style}> ) : (
{this.props.children || this.props.href} <StyledLink onClick={onClick} style={props.style}>
</StyledLink> {props.children || props.href}
); </StyledLink>
} );
} }
// XXX. For consistent usage, we monkey patch AntDesign's Link component,
// as we never want to open links internally, which gives a really bad experience
// @ts-ignore
Typography.Link = Link;

View File

@@ -175,7 +175,7 @@ export default class Sidebar extends Component<SidebarProps, SidebarState> {
} }
const horizontal = position === 'left' || position === 'right'; const horizontal = position === 'left' || position === 'right';
const gutterWidth = gutter ? theme.space.middle : 0; const gutterWidth = gutter ? theme.space.large : 0;
if (horizontal) { if (horizontal) {
width = width == null ? 200 : width; width = width == null ? 200 : width;
@@ -202,10 +202,18 @@ export default class Sidebar extends Component<SidebarProps, SidebarState> {
} }
minHeight={minHeight} minHeight={minHeight}
maxHeight={maxHeight} maxHeight={maxHeight}
height={!horizontal ? (onResize ? height : this.state.height) : '100%'} height={
!horizontal
? onResize
? height
: this.state.height
: gutter /*TODO: should use isSandy check*/
? undefined
: '100%'
}
resizable={resizable} resizable={resizable}
onResize={this.onResize} onResize={this.onResize}
gutterWidth={gutter ? theme.space.middle : undefined}> gutterWidth={gutter ? theme.space.large : undefined}>
<SidebarContainer <SidebarContainer
position={position} position={position}
backgroundColor={backgroundColor} backgroundColor={backgroundColor}
@@ -243,8 +251,8 @@ const GutterWrapper = ({
const VerticalGutterContainer = styled('div')<{enabled: boolean}>( const VerticalGutterContainer = styled('div')<{enabled: boolean}>(
({enabled}) => ({ ({enabled}) => ({
width: theme.space.middle, width: theme.space.large,
minWidth: theme.space.middle, minWidth: theme.space.large,
height: '100%', height: '100%',
cursor: enabled ? undefined : 'default', // hide cursor from interactive container cursor: enabled ? undefined : 'default', // hide cursor from interactive container
color: enabled ? theme.textColorPlaceholder : theme.backgroundWash, color: enabled ? theme.textColorPlaceholder : theme.backgroundWash,

View File

@@ -1199,6 +1199,11 @@
lodash "^4.17.19" lodash "^4.17.19"
to-fast-properties "^2.0.0" to-fast-properties "^2.0.0"
"@base2/pretty-print-object@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@base2/pretty-print-object/-/pretty-print-object-1.0.0.tgz#860ce718b0b73f4009e153541faff2cb6b85d047"
integrity sha512-4Th98KlMHr5+JkxfcoDT//6vY8vM+iSPrLNpHhRyLx2CFYi8e2RfqPLdpbnpo0Q5lQC5hNB79yes07zb02fvCw==
"@bcoe/v8-coverage@^0.2.3": "@bcoe/v8-coverage@^0.2.3":
version "0.2.3" version "0.2.3"
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
@@ -7147,6 +7152,13 @@ is-plain-obj@^2.0.0:
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287"
integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==
is-plain-object@3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-3.0.0.tgz#47bfc5da1b5d50d64110806c199359482e75a928"
integrity sha512-tZIpofR+P05k8Aocp7UI/2UTa9lTJSebCXpFFoR9aibpokDj/uXBsJ8luUu0tTVYKkMU6URDUuOfJZ7koewXvg==
dependencies:
isobject "^4.0.0"
is-plain-object@^2.0.3, is-plain-object@^2.0.4: is-plain-object@^2.0.3, is-plain-object@^2.0.4:
version "2.0.4" version "2.0.4"
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
@@ -7295,6 +7307,11 @@ isobject@^3.0.0, isobject@^3.0.1:
resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
isobject@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/isobject/-/isobject-4.0.0.tgz#3f1c9155e73b192022a80819bacd0343711697b0"
integrity sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA==
isomorphic-fetch@^2.1.1: isomorphic-fetch@^2.1.1:
version "2.2.1" version "2.2.1"
resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9" resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9"
@@ -10617,6 +10634,14 @@ react-dom@^16.13.0:
prop-types "^15.6.2" prop-types "^15.6.2"
scheduler "^0.19.1" scheduler "^0.19.1"
react-element-to-jsx-string@^14.3.1:
version "14.3.1"
resolved "https://registry.yarnpkg.com/react-element-to-jsx-string/-/react-element-to-jsx-string-14.3.1.tgz#a08fa6e46eb76061aca7eabc2e70f433583cb203"
integrity sha512-LRdQWRB+xcVPOL4PU4RYuTg6dUJ/FNmaQ8ls6w38YbzkbV6Yr5tFNESroub9GiSghtnMq8dQg2LcNN5aMIDzVg==
dependencies:
"@base2/pretty-print-object" "1.0.0"
is-plain-object "3.0.0"
react-inspector@^3.0.2: react-inspector@^3.0.2:
version "3.0.2" version "3.0.2"
resolved "https://registry.yarnpkg.com/react-inspector/-/react-inspector-3.0.2.tgz#c530a06101f562475537e47df428e1d7aff16ed8" resolved "https://registry.yarnpkg.com/react-inspector/-/react-inspector-3.0.2.tgz#c530a06101f562475537e47df428e1d7aff16ed8"