diff --git a/desktop/app/package.json b/desktop/app/package.json index 73404b36a..f65c0faa1 100644 --- a/desktop/app/package.json +++ b/desktop/app/package.json @@ -49,6 +49,7 @@ "react-color": "^2.18.1", "react-debounce-render": "^6.0.0", "react-dom": "^16.13.0", + "react-element-to-jsx-string": "^14.3.1", "react-markdown": "^4.2.2", "react-player": "^1.15.2", "react-redux": "^7.1.1", diff --git a/desktop/app/src/sandy-chrome/AppInspect.tsx b/desktop/app/src/sandy-chrome/AppInspect.tsx index 20a47d723..50e4d7a95 100644 --- a/desktop/app/src/sandy-chrome/AppInspect.tsx +++ b/desktop/app/src/sandy-chrome/AppInspect.tsx @@ -9,33 +9,23 @@ import React from 'react'; import {Button, Dropdown, Menu, Radio, Input} from 'antd'; -import {shell} from 'electron'; import {LeftSidebar, SidebarTitle, InfoIcon} from './LeftSidebar'; import { AppleOutlined, AndroidOutlined, SettingOutlined, } from '@ant-design/icons'; -import {Layout, styled} from '../ui'; +import {Layout, Link} from '../ui'; import {theme} from './theme'; const appTooltip = ( <> Inspect apps by selecting connected devices and emulators. Navigate and bookmark frequent destinations in the app. Refresh, screenshot and - screenrecord is also available. - { - - } + screenrecord is also available.{' '} + + Learn More + ); @@ -43,11 +33,13 @@ export function AppInspect() { return ( - <> + {appTooltip}}> App Inspect - + } defaultValue="mysite" /> @@ -55,20 +47,14 @@ export function AppInspect() { ; +const aBox =
A fixed child
; +const aFixedWidthBox = ( +
+ Fixed width box +
+); +const aFixedHeightBox = ( +
+ Fixed height box +
+); +const aDynamicBox = ( +
+ A dynamic child (flex: 1) +
+); +const someText = Some 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': ( + + ), + 'Basic container with fixed height': ( + + ), + 'bordered padded rounded': ( + +
child
+
+ ), + }, + }, + { + 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': ( + + {largeChild} + + ), + }, + }, + { + 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}': ( + + {aButton} + {someText} + {aBox} + {aDynamicBox} + + ), + 'Using flags: padded center gap={8} (great for toolbars and such)': ( + + {aButton} + {someText} + {aBox} + {aDynamicBox} + + ), + }, + }, + { + 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}': ( + + {aButton} + {someText} + {aBox} + {aDynamicBox} + + ), + 'Using flags: padded center gap={8} (great for toolbars and such)': ( + + {aButton} + {someText} + {aBox} + {aDynamicBox} + + ), + }, + }, + { + 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': ( + + {aFixedHeightBox} + {aDynamicBox} + + ), + 'Layout.Left': ( + + {aFixedWidthBox} + {aDynamicBox} + + ), + 'Layout.Right': ( + + {aDynamicBox} + {aFixedWidthBox} + + ), + 'Layout.Bottom': ( + + {aDynamicBox} + {aFixedHeightBox} + + ), + 'Layout.Top + scrollable': ( + + + {aFixedHeightBox} + {largeChild} + + + ), + 'Layout.Left + scrollable': ( + + + {aFixedWidthBox} + {largeChild} + + + ), + 'Layout.Right + scrollable': ( + + + {largeChild} + {aFixedWidthBox} + + + ), + 'Layout.Bottom + scrollable': ( + + + {largeChild} + {aFixedHeightBox} + + + ), + }, + }, +]; + +function ComponentPreview({title, demos, description, props}: PreviewProps) { + return ( + + + {description} + + + + {Object.entries(demos).map(([name, children]) => ( +
+ + +
+ {children} +
+
+ } key="2"> +
+
{reactElementToJSXString(children)}
+
+
+
+
+ ))} +
+
+ + + 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, + }, + ]} + /> + + + + + ); +} + +export const DesignComponentDemos = () => ( + + {demos.map((demo) => ( + + ))} + +); diff --git a/desktop/app/src/sandy-chrome/LeftRail.tsx b/desktop/app/src/sandy-chrome/LeftRail.tsx index 2df3e3146..b23838243 100644 --- a/desktop/app/src/sandy-chrome/LeftRail.tsx +++ b/desktop/app/src/sandy-chrome/LeftRail.tsx @@ -8,7 +8,7 @@ */ 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 { MobileFilled, @@ -31,24 +31,16 @@ import {errorCounterAtom} from '../chrome/ConsoleLogs'; import {ToplevelProps} from './SandyApp'; import {useValue} from 'flipper-plugin'; -const LeftRailContainer = styled(FlexColumn)({ - background: theme.backgroundDefault, +const LeftRailContainer = styled(Layout.Bottom)({ width: 48, - boxShadow: 'inset -1px 0px 0px rgba(0, 0, 0, 0.1)', - justifyContent: 'space-between', + borderRight: `1px solid ${theme.dividerColor}`, + padding: `${theme.paddingLarge}px ${theme.paddingSmall}px`, }); LeftRailContainer.displayName = 'LeftRailContainer'; -const LeftRailSection = styled(FlexColumn)({ - padding: '8px 0px', - alignItems: 'center', -}); -LeftRailSection.displayName = 'LeftRailSection'; - const LeftRailButtonElem = styled(Button)<{kind?: 'small'}>(({kind}) => ({ width: kind === 'small' ? 32 : 36, height: kind === 'small' ? 32 : 36, - margin: 6, padding: '5px 0', border: 'none', boxShadow: 'none', @@ -94,9 +86,9 @@ function LeftRailButton({ } const LeftRailDivider = styled(Divider)({ - margin: 10, - width: 36, - minWidth: 36, + margin: `10px 0`, + width: 32, + minWidth: 32, }); LeftRailDivider.displayName = 'LeftRailDividier'; @@ -106,7 +98,7 @@ export function LeftRail({ }: ToplevelProps) { return ( - + } title="App Inspect" @@ -116,18 +108,14 @@ export function LeftRail({ }} /> } title="Plugin Manager" /> - } - title="Notifications" - /> + } title="Notifications" /> - - + + } small @@ -147,7 +135,7 @@ export function LeftRail({ /> } title="Log In" /> - + ); } diff --git a/desktop/app/src/sandy-chrome/LeftSidebar.tsx b/desktop/app/src/sandy-chrome/LeftSidebar.tsx index 51adc925d..f8adfaf9f 100644 --- a/desktop/app/src/sandy-chrome/LeftSidebar.tsx +++ b/desktop/app/src/sandy-chrome/LeftSidebar.tsx @@ -10,15 +10,15 @@ import React from 'react'; import {theme} from './theme'; import styled from '@emotion/styled'; -import {Layout, FlexColumn} from '../ui'; -import {Button, Tooltip} from 'antd'; +import {Layout} from '../ui'; +import {Button, Tooltip, Typography} from 'antd'; import {InfoCircleOutlined} from '@ant-design/icons'; -export const LeftSidebar = styled(FlexColumn)({ - background: theme.backgroundDefault, - flex: 1, - padding: `10px 0`, -}); +export const LeftSidebar: React.FC = ({children}) => ( + + {children} + +); export function SidebarTitle({ children, @@ -28,22 +28,21 @@ export function SidebarTitle({ actions?: React.ReactNode; }) { return ( - - - {children} - <>{actions} - + + {children} + <>{actions} ); } -const LeftMenuTitle = styled.div({ - width: '100%', - fontFamily: 'SF Pro Text', - padding: `0px 12px`, - lineHeight: '16px', - fontSize: '12px', +const LeftMenuTitle = styled(Layout.Horizontal)({ + padding: `0px ${theme.paddingLarge}px`, + lineHeight: `${theme.space.large}px`, + fontSize: theme.fontSize.smallBody, textTransform: 'uppercase', + '> :first-child': { + flex: 1, + }, }); export const InfoIcon: React.FC<{}> = ({children}) => ( diff --git a/desktop/app/src/sandy-chrome/SandyApp.tsx b/desktop/app/src/sandy-chrome/SandyApp.tsx index a4b86bd4b..f27a9c174 100644 --- a/desktop/app/src/sandy-chrome/SandyApp.tsx +++ b/desktop/app/src/sandy-chrome/SandyApp.tsx @@ -9,14 +9,13 @@ import React, {useEffect, useState, useCallback} from 'react'; import {styled} from 'flipper'; -import {DatePicker, Space} from 'antd'; -import {Layout, FlexRow, Sidebar} from '../ui'; +import {Layout, Sidebar} from '../ui'; import {theme} from './theme'; import {Logger} from '../fb-interfaces/Logger'; import {LeftRail} from './LeftRail'; import {TemporarilyTitlebar} from './TemporarilyTitlebar'; -import TypographyExample from './TypographyExample'; +import SandyDesignSystem from './SandyDesignSystem'; import {registerStartupTime} from '../App'; import {useStore, useDispatch} from '../utils/useStore'; import {SandyContext} from './SandyContext'; @@ -86,43 +85,39 @@ export function SandyApp({logger}: {logger: Logger}) { - + - + {leftMenuContent && ( - {leftMenuContent} + + {leftMenuContent} + )} - + - - - - {staticView ? ( - React.createElement(staticView, { - logger: logger, - }) - ) : ( - - )} - - - - - - - - - - + + {staticView ? ( + React.createElement(staticView, { + logger: logger, + }) + ) : ( + + )} + + + + + + @@ -130,57 +125,19 @@ export function SandyApp({logger}: {logger: Logger}) { ); } -const LeftMenuContainer = styled.div({ - 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)({ +const MainContainer = styled(Layout.Right)({ background: theme.backgroundWash, - height: '100%', - width: '100%', }); -const MainContainer = styled('div')({ - display: 'flex', - width: '100%', - height: '100%', - background: theme.backgroundWash, - paddingRight: theme.space.middle, -}); - -export const ContentContainer = styled('div')({ - width: '100%', - margin: 0, - padding: 0, +export const ContentContainer = styled(Layout.Container)({ background: theme.backgroundDefault, border: `1px solid ${theme.dividerColor}`, borderRadius: theme.containerBorderRadius, boxShadow: `0px 0px 5px rgba(0, 0, 0, 0.05), 0px 0px 1px rgba(0, 0, 0, 0.05)`, -}); - -const MainContentWrapper = styled('div')({ - height: '100%', - width: '100%', - display: 'flex', - alignItems: 'stretch', - padding: `${theme.space.middle}px 0`, + marginTop: theme.space.large, + marginBottom: theme.space.large, }); function RightMenu() { return
RightMenu
; } - -function TemporarilyContent() { - return ( - - New UI for Flipper, Sandy Project! Nothing to see now. Go back to current - Flipper - - - - ); -} diff --git a/desktop/app/src/sandy-chrome/SandyDesignSystem.tsx b/desktop/app/src/sandy-chrome/SandyDesignSystem.tsx new file mode 100644 index 000000000..875f72cce --- /dev/null +++ b/desktop/app/src/sandy-chrome/SandyDesignSystem.tsx @@ -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 ( + + + +

+ Welcome to the Flipper Design System. The Flipper design system is + based on{' '} + + Ant Design + + . Any component found in the ANT documentation can be used. This + page demonstrates the usage of: +

+
    +
  • Colors
  • +
  • Typography
  • +
  • Theme Variables
  • +
  • Layout components
  • +
+

+ The following components from Ant should not be used: +

+
    +
  • + Layout: use Flipper's Layout.* instead. +
  • +
+

Sandy guidelines

+
    +
  • + Avoid using `margin` properties, use padding on the container + indeed, or gap in combination with{' '} + Layout.Horizontal|Vertical +
  • +
  • + Avoid using width / height: 100%, use{' '} + Layout.Container instead. +
  • +
+
+ + + + + + + + + + + + + + + + + + + Common Ant components, with modifiers applied. The{' '} + Title, Text and Link{' '} + components can be found by importing the{' '} + Typography namespace from Ant. + + } + type="info" + /> + Title + Title level=2 + Title level=3 + Title level=4 + Text + Text - type=secondary + Text - type=success + Text - type=warning + Text - danger + Text - disbled + Text - strong + Text - code + Link + + Link - type=secondary + + + + + + + + + The following theme veriables are exposed from the Flipper{' '} + theme object. See the colors section above for a + preview of the colors. + + } + /> +
{JSON.stringify(theme, null, 2)}
+
+ + + +
+
+ ); +} + +function ColorPreview({name}: {name: keyof typeof theme}) { + return ( + + + } + title={`theme.${name}`} + /> + + ); +} + +const resetLists = css` + ol, + ul { + list-style: revert; + margin-left: ${theme.space.huge}px; + } + .ant-alert { + margin-bottom: ${theme.space.huge}px; + } +`; diff --git a/desktop/app/src/sandy-chrome/TypographyExample.tsx b/desktop/app/src/sandy-chrome/TypographyExample.tsx deleted file mode 100644 index 06ad60b47..000000000 --- a/desktop/app/src/sandy-chrome/TypographyExample.tsx +++ /dev/null @@ -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 ( - - - h1. Headline - h2. Headline - h3. Headline - h4. Headline - Body - Regular - Body - Strong - - - Code - - - Primary - Secondary - - Disabled - Positive - Warning - Danger - - Link - - - Link Secondary - - - - ); -} diff --git a/desktop/app/src/sandy-chrome/theme.tsx b/desktop/app/src/sandy-chrome/theme.tsx index 8981897d4..bc209c6d4 100644 --- a/desktop/app/src/sandy-chrome/theme.tsx +++ b/desktop/app/src/sandy-chrome/theme.tsx @@ -26,17 +26,20 @@ export const theme = { dividerColor: 'var(--flipper-divider-color)', borderRadius: 'var(--flipper-border-radius)', containerBorderRadius: 8, + paddingSmall: 6, // vertical padding on inline elements like buttons + paddingLarge: 12, // horizontal ,,, space: { // from Space component in Ant tiny: 4, small: 8, - middle: 16, - large: 24, + medium: 12, + large: 16, + huge: 24, } as const, fontSize: { smallBody: '12px', - }, -}; + } as const, +} as const; /** * This hook returns whether dark mode is currently being used. diff --git a/desktop/app/src/ui/components/ContextMenuProvider.tsx b/desktop/app/src/ui/components/ContextMenuProvider.tsx index 20a6d00c0..f419fc824 100644 --- a/desktop/app/src/ui/components/ContextMenuProvider.tsx +++ b/desktop/app/src/ui/components/ContextMenuProvider.tsx @@ -17,7 +17,8 @@ interface ContextMenuManager { } const Container = styled.div({ - display: 'contents', + display: 'flex', + height: '100%', }); Container.displayName = 'ContextMenuProvider:Container'; diff --git a/desktop/app/src/ui/components/Layout.tsx b/desktop/app/src/ui/components/Layout.tsx index e434de754..3bfb80a2f 100644 --- a/desktop/app/src/ui/components/Layout.tsx +++ b/desktop/app/src/ui/components/Layout.tsx @@ -7,8 +7,98 @@ * @format */ -import React from 'react'; +import React, {CSSProperties} from 'react'; 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( + ({ + 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) => + ( + + {children} + + ) 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)(({gap, center}) => ({ + display: 'flex', + flexDirection: 'row', + gap, + alignItems: center ? 'center' : 'stretch', +})); + +const Vertical = styled(Container)(({gap, center}) => ({ + display: 'flex', + flexDirection: 'column', + gap, + alignItems: center ? 'center' : 'stretch', +})); 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. */ - center?: boolean; 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 = {fixedChild}; - - const dynamicElement = ( - - {reverse ? children[0] : children[1]} - - ); - return reverse ? ( - - {dynamicElement} - {fixedElement} - - ) : ( - - {fixedElement} - {dynamicElement} - - ); -} - -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: * 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. */ -const Layout: Record< - 'Left' | 'Right' | 'Top' | 'Bottom', - React.FC -> & - Record<'Horizontal' | 'Vertical', React.FC> = { - Top(props) { - return renderLayout(props, false, false); +const Layout = { + Top(props: SplitLayoutProps) { + const isSandy = useIsSandy(); + if (!isSandy) return renderLayout(props, false, false); + let [child1, child2] = props.children; + if (props.scrollable) child2 = {child2}; + return ( + + {child1} + {child2} + + ); }, - Bottom(props) { - return renderLayout(props, false, true); + Bottom(props: SplitLayoutProps) { + const isSandy = useIsSandy(); + if (!isSandy) return renderLayout(props, false, true); + let [child1, child2] = props.children; + if (props.scrollable) child1 = {child1}; + return ( + + {child1} + {child2} + + ); }, - Left(props) { - return renderLayout(props, true, false); + Left(props: SplitLayoutProps) { + const isSandy = useIsSandy(); + if (!isSandy) return renderLayout(props, true, false); + let [child1, child2] = props.children; + if (props.scrollable) child2 = {child2}; + return ( + + {child1} + {child2} + + ); }, - Right(props) { - return renderLayout(props, true, true); + Right(props: SplitLayoutProps) { + const isSandy = useIsSandy(); + if (!isSandy) return renderLayout(props, true, true); + let [child1, child2] = props.children; + if (props.scrollable) child1 = {child1}; + return ( + + {child1} + {child2} + + ); }, - Horizontal: styled.div(({gap, center, fillx, filly}) => ({ - display: 'flex', - flexDirection: 'row', - gap, - alignItems: center ? 'center' : 'stretch', - width: fillx ? '100%' : undefined, - height: filly ? '100%' : undefined, - })), - Vertical: styled.div(({gap, center, fillx, filly}) => ({ - display: 'flex', - flexDirection: 'column', - gap, - alignItems: center ? 'center' : 'stretch', - width: fillx ? '100%' : undefined, - height: filly ? '100%' : undefined, - })), + Container, + ScrollContainer, + Horizontal, + Vertical, }; Object.keys(Layout).forEach((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; diff --git a/desktop/app/src/ui/components/LegacyLayout.tsx b/desktop/app/src/ui/components/LegacyLayout.tsx new file mode 100644 index 000000000..4acf2a382 --- /dev/null +++ b/desktop/app/src/ui/components/LegacyLayout.tsx @@ -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 = {fixedChild}; + + const dynamicElement = ( + + {reverse ? children[0] : children[1]} + + ); + return reverse ? ( + + {dynamicElement} + {fixedElement} + + ) : ( + + {fixedElement} + {dynamicElement} + + ); +} diff --git a/desktop/app/src/ui/components/Link.tsx b/desktop/app/src/ui/components/Link.tsx index 944e754df..3bbbe8796 100644 --- a/desktop/app/src/ui/components/Link.tsx +++ b/desktop/app/src/ui/components/Link.tsx @@ -9,9 +9,11 @@ import styled from '@emotion/styled'; import {colors} from './colors'; -import {Component} from 'react'; +import {useCallback} from 'react'; import {shell} from 'electron'; import React from 'react'; +import {useIsSandy} from '../../sandy-chrome/SandyContext'; +import {Typography} from 'antd'; const StyledLink = styled.span({ color: colors.highlight, @@ -22,20 +24,33 @@ const StyledLink = styled.span({ }); StyledLink.displayName = 'Link:StyledLink'; -export default class Link extends Component<{ +const AntOriginalLink = Typography.Link; + +export default function Link(props: { href: string; children?: React.ReactNode; style?: React.CSSProperties; -}> { - onClick = () => { - shell.openExternal(this.props.href); - }; +}) { + const isSandy = useIsSandy(); + const onClick = useCallback( + (e: React.MouseEvent) => { + shell.openExternal(props.href); + e.preventDefault(); + e.stopPropagation(); + }, + [props.href], + ); - render() { - return ( - - {this.props.children || this.props.href} - - ); - } + return isSandy ? ( + + ) : ( + + {props.children || props.href} + + ); } + +// 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; diff --git a/desktop/app/src/ui/components/Sidebar.tsx b/desktop/app/src/ui/components/Sidebar.tsx index feae37f9a..dfbd13e40 100644 --- a/desktop/app/src/ui/components/Sidebar.tsx +++ b/desktop/app/src/ui/components/Sidebar.tsx @@ -175,7 +175,7 @@ export default class Sidebar extends Component { } const horizontal = position === 'left' || position === 'right'; - const gutterWidth = gutter ? theme.space.middle : 0; + const gutterWidth = gutter ? theme.space.large : 0; if (horizontal) { width = width == null ? 200 : width; @@ -202,10 +202,18 @@ export default class Sidebar extends Component { } minHeight={minHeight} 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} onResize={this.onResize} - gutterWidth={gutter ? theme.space.middle : undefined}> + gutterWidth={gutter ? theme.space.large : undefined}> ( ({enabled}) => ({ - width: theme.space.middle, - minWidth: theme.space.middle, + width: theme.space.large, + minWidth: theme.space.large, height: '100%', cursor: enabled ? undefined : 'default', // hide cursor from interactive container color: enabled ? theme.textColorPlaceholder : theme.backgroundWash, diff --git a/desktop/yarn.lock b/desktop/yarn.lock index 1c86f3232..af2dedcd0 100644 --- a/desktop/yarn.lock +++ b/desktop/yarn.lock @@ -1199,6 +1199,11 @@ lodash "^4.17.19" 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": version "0.2.3" 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" 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: version "2.0.4" 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" 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: version "2.2.1" 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" 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: version "3.0.2" resolved "https://registry.yarnpkg.com/react-inspector/-/react-inspector-3.0.2.tgz#c530a06101f562475537e47df428e1d7aff16ed8"