From 7b7622bf70463a553e7233e36b63edd587652105 Mon Sep 17 00:00:00 2001 From: Tilman Frick Date: Fri, 27 Apr 2018 23:03:17 +0200 Subject: [PATCH] feat: update styling of layout and element pane (#393) * feat(component): update styling of layout * feat(component): update styling of element pane * feat: unselect items when sidebar root receives click * refactor: simplify app view by moving out splashscreen * style: use tabs for indentation * feat(component): add drag image styling * fixup! increase element drag size * fixup! add check for null on drag element * fix: avoid side effect froms drop areas on click interaction * fix: remove obsolete handler * fix: select right drag element target * fix: adapt to new interface * fix: make ts happy * fix: restore icon click functionality * feat: show either properties or patterns pane * fix: restore pattern drag * fix: set backgroundColor during startup * fix: handle draggable states via unified state * feat: add button to show pattern pane * feat: add button styling * fixup! change requests * fix: pass hex color to BrowserWindow * refactor: use 100vh to move element outside of viewport * fix: clean up dragimg nodes after dragging * fix: get color from styleguide * fix: read drag image colors from styleguide * fix: update add button position and side bar borders * fix: set hover and active styling to preview resizer * fix: add element overflow gradient * fix: make entire tree element draggable * fix: reenable placeholder targets for off-list payloads * fix: do not write bogus dom attributes --- package-lock.json | 3 +- src/component/container/app.tsx | 137 +++++------ src/component/container/element-list.tsx | 217 +++++++++++++---- src/component/container/element-wrapper.tsx | 108 +++++---- src/component/container/property-list.tsx | 7 +- src/component/container/splash-screen.tsx | 40 ++++ src/electron/main.ts | 2 + src/lsg/patterns/add-button/index.tsx | 86 +++++++ src/lsg/patterns/add-button/pattern.json | 6 + src/lsg/patterns/chrome/index.tsx | 19 +- src/lsg/patterns/colors/index.tsx | 8 + src/lsg/patterns/element/demo.tsx | 20 +- src/lsg/patterns/element/index.tsx | 224 +++++++++++------- src/lsg/patterns/global-styles/index.tsx | 5 +- src/lsg/patterns/icons/index.tsx | 2 + src/lsg/patterns/layout/demo.tsx | 2 +- src/lsg/patterns/layout/index.tsx | 39 ++- src/lsg/patterns/list/index.tsx | 14 +- src/lsg/patterns/panes/element-pane/index.tsx | 25 +- .../patterns/panes/patterns-pane/index.tsx | 12 +- src/lsg/patterns/panes/preview-pane/index.tsx | 25 +- .../patterns/panes/property-pane/index.tsx | 7 +- src/lsg/patterns/tag.tsx | 24 ++ src/store/store.ts | 35 ++- 24 files changed, 741 insertions(+), 326 deletions(-) create mode 100644 src/component/container/splash-screen.tsx create mode 100644 src/lsg/patterns/add-button/index.tsx create mode 100644 src/lsg/patterns/add-button/pattern.json create mode 100644 src/lsg/patterns/tag.tsx diff --git a/package-lock.json b/package-lock.json index 9ea077b8e..cca61b495 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6775,7 +6775,8 @@ }, "jsbn": { "version": "0.1.1", - "bundled": true + "bundled": true, + "optional": true }, "json-schema": { "version": "0.2.3", diff --git a/src/component/container/app.tsx b/src/component/container/app.tsx index 252bac6bd..f508df978 100644 --- a/src/component/container/app.tsx +++ b/src/component/container/app.tsx @@ -1,30 +1,25 @@ -import Button, { Order } from '../../lsg/patterns/button'; +import AddButton from '../../lsg/patterns/add-button'; import { ChromeContainer } from '../chrome/chrome-container'; -import { colors } from '../../lsg/patterns/colors'; -import Copy, { Size as CopySize } from '../../lsg/patterns/copy'; import { remote } from 'electron'; import { ElementList } from '../../component/container/element-list'; import ElementPane from '../../lsg/patterns/panes/element-pane'; import * as FileExtraUtils from 'fs-extra'; import globalStyles from '../../lsg/patterns/global-styles'; -import { Headline } from '../../lsg/patterns/headline'; import { IconName, IconRegistry } from '../../lsg/patterns/icons'; import Layout, { MainArea, SideBar } from '../../lsg/patterns/layout'; -import Link from '../../lsg/patterns/link'; import { createMenu } from '../../electron/menu'; import * as MobX from 'mobx'; import { observer } from 'mobx-react'; import * as PathUtils from 'path'; -import { PatternListContainer } from '../../component/container/pattern-list'; +import { PatternListContainer } from './pattern-list'; import PatternsPane from '../../lsg/patterns/panes/patterns-pane'; import { PreviewPaneWrapper } from '../../component/container/preview-pane-wrapper'; import * as ProcessUtils from 'process'; import { PropertyList } from './property-list'; import PropertyPane from '../../lsg/patterns/panes/property-pane'; import * as React from 'react'; -import Space, { Size as SpaceSize } from '../../lsg/patterns/space'; -import SplashScreen from '../../lsg/patterns/splash-screen'; -import { Store } from '../../store/store'; +import { SplashScreen } from './splash-screen'; +import { RightPane, Store } from '../../store/store'; globalStyles(); @@ -36,15 +31,13 @@ export class App extends React.Component { private static PROPERTIES_LIST_ID = 'propertieslist'; @MobX.observable protected activeTab: string = App.PATTERN_LIST_ID; + private ctrlDown: boolean = false; private shiftDown: boolean = false; public constructor(props: {}) { super(props); this.handleTabNaviagtionClick = this.handleTabNaviagtionClick.bind(this); - this.handleMainWindowClick = this.handleMainWindowClick.bind(this); - this.handleCreateNewSpaceClick = this.handleCreateNewSpaceClick.bind(this); - this.handleOpenSpaceClick = this.handleOpenSpaceClick.bind(this); } public componentDidMount(): void { @@ -52,16 +45,8 @@ export class App extends React.Component { this.redirectUndoRedo(); } - private getDevTools(): React.StatelessComponent | null { - try { - const DevToolsExports = require('mobx-react-devtools'); - return DevToolsExports ? DevToolsExports.default : undefined; - } catch (error) { - return null; - } - } - - protected handleCreateNewSpaceClick(): void { + // TODO: Should move to store + protected createNewSpace(): void { let appPath: string = remote.app.getAppPath().replace('.asar', '.asar.unpacked'); if (appPath.indexOf('node_modules') >= 0) { appPath = ProcessUtils.cwd(); @@ -82,18 +67,20 @@ export class App extends React.Component { ); } + private getDevTools(): React.StatelessComponent | null { + try { + const DevToolsExports = require('mobx-react-devtools'); + return DevToolsExports ? DevToolsExports.default : undefined; + } catch (error) { + return null; + } + } + private handleMainWindowClick(): void { Store.getInstance().setElementFocussed(false); createMenu(); } - protected handleOpenSpaceClick(): void { - remote.dialog.showOpenDialog({ properties: ['openDirectory'] }, filePaths => { - store.openStyleguide(filePaths[0]); - store.openFirstPage(); - }); - } - protected handleTabNaviagtionClick(evt: React.MouseEvent, id: string): void { this.activeTab = id; } @@ -106,6 +93,14 @@ export class App extends React.Component { return this.activeTab === App.PROPERTIES_LIST_ID; } + // TODO: Should move to store + protected openSpace(): void { + remote.dialog.showOpenDialog({ properties: ['openDirectory'] }, filePaths => { + store.openStyleguide(filePaths[0]); + store.openFirstPage(); + }); + } + private redirectUndoRedo(): void { document.body.onkeydown = event => { if (event.keyCode === 16) { @@ -145,54 +140,52 @@ export class App extends React.Component { const DevTools = this.getDevTools(); return ( - + - - {project && [ - - - - - - - - , - , - - - - - - ]} - - {!project && ( - - - - Getting started with Alva - - - - - You can open an existing Alva space or create a new one based on our - designkit including some basic components to kickstart your project. - - - - - - - or open existing Alva space - - + {project ? ( + + store.setSelectedElement()} + hasBorder + > + + + + { + e.stopPropagation(); + store.setRightPane(RightPane.Patterns); + store.setSelectedElement(); + }} + /> + + + + {store.getRightPane() === RightPane.Properties && ( + + + + )} + {store.getRightPane() === RightPane.Patterns && ( + + + + )} + + + ) : ( + this.createNewSpace()} + onSecondaryButtonClick={() => this.openSpace()} + /> )} - - {DevTools ? : null} ); diff --git a/src/component/container/element-list.tsx b/src/component/container/element-list.tsx index 641cf8270..edd8f9ae3 100644 --- a/src/component/container/element-list.tsx +++ b/src/component/container/element-list.tsx @@ -1,3 +1,4 @@ +import { colors } from '../../lsg/patterns/colors'; import { elementMenu } from '../../electron/context-menus'; import { ElementLocationCommand } from '../../store/command/element-location-command'; import { ElementWrapper } from './element-wrapper'; @@ -10,9 +11,31 @@ import { Pattern } from '../../store/styleguide/pattern'; import * as React from 'react'; import { Slot } from '../../store/styleguide/slot'; import { Store } from '../../store/store'; +import * as uuid from 'uuid'; + +export interface ElementListState { + dragging: boolean; +} + +const DRAG_IMG_STYLE = ` + position: fixed; + top: 100vh; + background-color: ${colors.white.toString()}; + color: ${colors.black.toString()}; + padding: 6px 18px; + border-radius: 3px; + font-size: 12px; + opacity: 1; +`; @observer -export class ElementList extends React.Component { +export class ElementList extends React.Component<{}, ElementListState> { + private dragImg?: HTMLElement; + + public state = { + dragging: true + }; + public componentDidMount(): void { createMenu(); } @@ -25,20 +48,22 @@ export class ElementList extends React.Component { key: string, element: PageElement, selectedElement?: PageElement - ): ListItemProps { + ): ElementNodeProps { const store = Store.getInstance(); const pattern: Pattern | undefined = element.getPattern(); if (!pattern) { return { label: key, - value: '(invalid)', - children: [] + title: '(invalid)', + id: uuid.v4(), + children: [], + dragging: this.state.dragging }; } - let defaultSlotItems: ListItemProps[] | undefined = []; - const slots: ListItemProps[] = []; + let defaultSlotItems: ElementNodeProps[] | undefined = []; + const slots: ElementNodeProps[] = []; pattern.getSlots().forEach(slot => { const listItem = this.createItemFromSlot(slot, element, selectedElement); @@ -50,21 +75,13 @@ export class ElementList extends React.Component { } }); - const updatePageElement: React.MouseEventHandler = event => { - event.stopPropagation(); - store.setSelectedElement(element); - store.setElementFocussed(true); - }; - return { label: key, - value: element.getName(), - onClick: updatePageElement, - onContextMenu: () => elementMenu(element), - handleDragStart: (e: React.DragEvent) => { - Store.getInstance().setDraggedElement(element); - }, - handleDragDropForChild: (e: React.DragEvent) => { + title: element.getName(), + dragging: this.state.dragging, + id: element.getId(), + onDragDropForChild: (e: React.DragEvent) => { + this.handleDragEnd(e); const patternId = e.dataTransfer.getData('patternId'); const newParent = element.getParent(); @@ -109,7 +126,8 @@ export class ElementList extends React.Component { ); store.setSelectedElement(draggedElement); }, - handleDragDrop: (e: React.DragEvent) => { + onDragDrop: (e: React.DragEvent) => { + this.handleDragEnd(e); const patternId = e.dataTransfer.getData('patternId'); let draggedElement: PageElement | undefined; @@ -145,11 +163,11 @@ export class ElementList extends React.Component { slot: Slot, element: PageElement, selectedElement?: PageElement - ): ListItemProps { + ): ElementNodeProps { const store = Store.getInstance(); const slotId = slot.getId(); const slotContents: PageElement[] = element.getSlotContents(slotId); - const childItems: ListItemProps[] = []; + const childItems: ElementNodeProps[] = []; const selectedSlot = store.getSelectedSlotId(); slotContents.forEach((value: PageElement, index: number) => { @@ -169,13 +187,15 @@ export class ElementList extends React.Component { store.setElementFocussed(false); }; - const slotListItem: ListItemProps = { - value: `\uD83D\uDD18 ${slot.getName()}`, + const slotListItem: ElementNodeProps = { + id: slot.getId(), + title: `\uD83D\uDD18 ${slot.getName()}`, draggable: false, + dragging: this.state.dragging, children: childItems, label: slotId, onClick: updateSelectedSlot, - handleDragDrop: (e: React.DragEvent) => { + onDragDrop: (e: React.DragEvent) => { const patternId = e.dataTransfer.getData('patternId'); let draggedElement: PageElement | undefined; @@ -208,40 +228,141 @@ export class ElementList extends React.Component { return slotListItem; } + private handleClick(e: React.MouseEvent): void { + const element = elementFromTarget(e.target); + e.stopPropagation(); + Store.getInstance().setSelectedElement(element); + Store.getInstance().setElementFocussed(true); + } + + private handleContextMenu(e: React.MouseEvent): void { + const element = elementFromTarget(e.target); + if (element) { + elementMenu(element); + } + } + + private handleDragEnd(e: React.DragEvent): void { + this.setState({ dragging: false }); + + if (this.dragImg && this.dragImg.parentNode) { + this.dragImg.parentNode.removeChild(this.dragImg); + } + } + + private handleDragStart(e: React.DragEvent): void { + this.setState({ dragging: true }); + const element = elementFromTarget(e.target); + + if (element) { + Store.getInstance().setDraggedElement(element); + const dragImg = document.createElement('div'); + dragImg.textContent = element.getName(); + dragImg.setAttribute('style', DRAG_IMG_STYLE); + document.body.appendChild(dragImg); + e.dataTransfer.setDragImage(dragImg, 75, 15); + this.dragImg = dragImg; + } + } + + private handleMouseLeave(e: React.MouseEvent): void { + this.setState({ dragging: true }); + } + + private handleMouseOver(e: React.MouseEvent): void { + this.setState({ dragging: false }); + } + public render(): JSX.Element | null { const store = Store.getInstance(); const page: Page | undefined = store.getCurrentPage(); - if (page) { - const rootElement = page.getRoot(); - if (!rootElement) { - return null; - } + if (!page) { + return null; + } - const selectedElement = store.getSelectedElement(); + const rootElement = page.getRoot(); - return this.renderList(this.createItemFromElement('Root', rootElement, selectedElement)); - } else { + if (!rootElement) { return null; } - } - public renderList(item: ListItemProps, key?: number): JSX.Element { + const selectedElement = store.getSelectedElement(); + const item = this.createItemFromElement('Root', rootElement, selectedElement); + return ( - this.handleClick(e)} + onContextMenu={e => this.handleContextMenu(e)} + onDragStart={e => this.handleDragStart(e)} + onDragEnd={e => this.handleDragEnd(e)} + onMouseOver={e => this.handleMouseOver(e)} + onMouseLeave={e => this.handleMouseLeave(e)} > - {item.children && - item.children.length > 0 && - item.children.map((child, index) => this.renderList(child, index))} - + + ); } } + +export interface ElementNodeProps extends ListItemProps { + children?: ElementNodeProps[]; + dragging: boolean; + id: string; +} + +function ElementTree(props: ElementNodeProps): JSX.Element { + const children = Array.isArray(props.children) ? props.children : []; + + return ( + + {children.map(child => ( + + ))} + + ); +} + +function above(node: EventTarget, selector: string): HTMLElement | null { + let el = node as HTMLElement; + let ended = false; + + while (el && !ended) { + if (el.matches(selector)) { + break; + } + + if (el.parentElement !== null) { + el = el.parentElement; + } else { + ended = true; + break; + } + } + + return ended ? null : el; +} + +function elementFromTarget(target: EventTarget): PageElement | undefined { + const el = above(target, '[data-id]'); + + if (!el) { + return; + } + + const id = el.getAttribute('data-id'); + + if (typeof id !== 'string') { + return; + } + + const store = Store.getInstance(); + const page = store.getCurrentPage(); + + if (!page) { + return; + } + + return page.getElementById(id); +} diff --git a/src/component/container/element-wrapper.tsx b/src/component/container/element-wrapper.tsx index eb0a6096b..8aca40fc1 100644 --- a/src/component/container/element-wrapper.tsx +++ b/src/component/container/element-wrapper.tsx @@ -9,40 +9,42 @@ export interface ElementWrapperState { export interface ElementWrapperProps { active?: boolean; - handleClick?: React.MouseEventHandler; - handleContextMenu?: React.MouseEventHandler; - handleDragDrop?: React.DragEventHandler; - handleDragDropForChild?: React.DragEventHandler; - handleDragStart?: React.DragEventHandler; + dragging: boolean; + id: string; + onClick?: React.MouseEventHandler; + onContextMenu?: React.MouseEventHandler; + onDragDrop?: React.DragEventHandler; + onDragDropForChild?: React.DragEventHandler; + onDragStart?: React.DragEventHandler; open?: boolean; title: string; } export class ElementWrapper extends React.Component { - public constructor(props: ElementWrapperProps) { - super(props); + public state = { + open: this.props.open, + highlightPlaceholder: false, + highlight: false + }; - this.state = { - open: this.props.open, - highlight: false - }; - - this.handleIconClick = this.handleIconClick.bind(this); - this.handleDragStart = this.handleDragStart.bind(this); - this.handleDragEnter = this.handleDragEnter.bind(this); - this.handleDragLeave = this.handleDragLeave.bind(this); - this.handleDragDrop = this.handleDragDrop.bind(this); - this.handleDragEnterForChild = this.handleDragEnterForChild.bind(this); - this.handleDragLeaveForChild = this.handleDragLeaveForChild.bind(this); - this.handleDragDropForChild = this.handleDragDropForChild.bind(this); + private handleClick(e: React.MouseEvent): void { + const target = e.target as HTMLElement; + const icon = above(target, 'svg[data-icon]'); + + if (icon) { + e.stopPropagation(); + this.setState({ + open: !this.state.open + }); + } } private handleDragDrop(e: React.DragEvent): void { this.setState({ highlight: false }); - if (typeof this.props.handleDragDrop === 'function') { - this.props.handleDragDrop(e); + if (typeof this.props.onDragDrop === 'function') { + this.props.onDragDrop(e); } } @@ -50,8 +52,8 @@ export class ElementWrapper extends React.Component this.handleClick(e)} + onDragDrop={e => this.handleDragDrop(e)} + onDragDropForChild={e => this.handleDragDropForChild(e)} + onDragEnter={e => this.handleDragEnter(e)} + onDragEnterForChild={e => this.handleDragEnterForChild(e)} + onDragLeave={e => this.handleDragLeave(e)} + onDragLeaveForChild={e => this.handleDragLeaveForChild(e)} + onDragStart={e => this.handleDragStart(e)} highlight={this.state.highlight} highlightPlaceholder={this.state.highlightPlaceholder} - handleClick={handleClick} - handleContextMenu={handleContextMenu} - draggable - handleIconClick={this.handleIconClick} - handleDragStart={this.handleDragStart} - handleDragEnter={this.handleDragEnter} - handleDragLeave={this.handleDragLeave} - handleDragDrop={this.handleDragDrop} - handleDragEnterForChild={this.handleDragEnterForChild} - handleDragLeaveForChild={this.handleDragLeaveForChild} - handleDragDropForChild={this.handleDragDropForChild} + id={this.props.id} + open={!this.state.open} + title={title} > {children} ); } } + +function above(node: EventTarget, selector: string): HTMLElement | null { + let el = node as HTMLElement; + let ended = false; + + while (el && !ended) { + if (el.matches(selector)) { + break; + } + + if (el.parentElement !== null) { + el = el.parentElement; + } else { + ended = true; + break; + } + } + + return ended ? null : el; +} diff --git a/src/component/container/property-list.tsx b/src/component/container/property-list.tsx index ecf839036..61fa50786 100644 --- a/src/component/container/property-list.tsx +++ b/src/component/container/property-list.tsx @@ -98,7 +98,12 @@ class PropertyTree extends React.Component { const { property } = context; return ( - + {this.isOpen ? this.renderItems() : 'hidden'} ); diff --git a/src/component/container/splash-screen.tsx b/src/component/container/splash-screen.tsx new file mode 100644 index 000000000..805b4fe24 --- /dev/null +++ b/src/component/container/splash-screen.tsx @@ -0,0 +1,40 @@ +import Button, { Order } from '../../lsg/patterns/button'; +import { colors } from '../../lsg/patterns/colors'; +import Copy, { Size as CopySize } from '../../lsg/patterns/copy'; +import { Headline } from '../../lsg/patterns/headline'; +import Link from '../../lsg/patterns/link'; +import * as React from 'react'; +import Space, { Size as SpaceSize } from '../../lsg/patterns/space'; +// tslint:disable-next-line +import SplashScreenContainer from '../../lsg/patterns/splash-screen'; + +export interface SplashScreenProps { + onPrimaryButtonClick?: React.MouseEventHandler; + onSecondaryButtonClick?: React.MouseEventHandler; +} + +export function SplashScreen(props: SplashScreenProps): JSX.Element { + return ( + + + + Getting started with Alva + + + + + You can open an existing Alva space or create a new one based on our designkit + including some basic components to kickstart your project. + + + + + + + or open existing Alva space + + + ); +} diff --git a/src/electron/main.ts b/src/electron/main.ts index 39d9d97b5..6e4cdf40b 100644 --- a/src/electron/main.ts +++ b/src/electron/main.ts @@ -1,4 +1,5 @@ import { checkForUpdates } from './auto-updater'; +import { colors } from '../lsg/patterns/colors'; import { app, BrowserWindow, ipcMain, screen } from 'electron'; import * as PathUtils from 'path'; import * as url from 'url'; @@ -17,6 +18,7 @@ function createWindow(): void { minWidth: 780, minHeight: 380, titleBarStyle: 'hiddenInset', + backgroundColor: colors.grey97.toString('hex'), title: 'Alva' }); diff --git a/src/lsg/patterns/add-button/index.tsx b/src/lsg/patterns/add-button/index.tsx new file mode 100644 index 000000000..43d66600c --- /dev/null +++ b/src/lsg/patterns/add-button/index.tsx @@ -0,0 +1,86 @@ +import { colors } from '../colors'; +import { Icon, IconName, Size as IconSize } from '../icons'; +import * as React from 'react'; +import { getSpace, Size as SpaceSize } from '../space'; +import styled from 'styled-components'; + +export interface AddButtonProps { + onClick?: React.MouseEventHandler; + label?: string; + active?: boolean; +} + +interface StyledAddButtonProps { + active?: boolean; +} + +interface StyledIconProps { + active?: boolean; +} + +const StyledAddButton = styled.div` + width: 100%; + border-top: 1px solid ${colors.black.toString('rgb', { alpha: 0.1 })}; + @media screen and (-webkit-min-device-pixel-ratio: 2) { + border-top-width: 0.5px; + } + cursor: default; + user-select: none; + display: flex; + flex: none; + height: 40px; + box-sizing: border-box; + padding: ${getSpace(SpaceSize.XXS)}px ${getSpace(SpaceSize.XS)}px ${getSpace(SpaceSize.XXS)}px + ${getSpace(SpaceSize.L)}px; + justify-content: space-between; + color: ${colors.grey36.toString()}; + + &:hover { + background: ${colors.grey90.toString()}; + } + + ${(props: StyledAddButtonProps) => + props.active + ? ` + border-top: 1px solid ${colors.blue.toString('rgb', { alpha: 0.1 })}; + @media screen and (-webkit-min-device-pixel-ratio: 2) { + border-top-width: 0.5px; + } + background: ${colors.blue80.toString()}; + color: ${colors.blue.toString()}; + + &:hover { + background: ${colors.blue80.toString()}; + } + ` + : ''}; +`; + +const StyledLabelWrapper = styled.div` + font-size: 15px; + padding-top: ${getSpace(SpaceSize.XS)}px; +`; + +const StyledIconWrapper = styled.div` + margin: ${getSpace(SpaceSize.XS)}px; + padding: ${getSpace(SpaceSize.XXS)}px; + border-radius: ${getSpace(SpaceSize.XXS)}px; +`; + +const StyledIcon = styled(Icon)` + fill: ${colors.grey60.toString()}; + + ${(props: StyledIconProps) => (props.active ? `fill: ${colors.blue.toString()};` : '')}; +`; + +const AddButton: React.StatelessComponent = props => ( + + {props.label} + + + + + +); + +export default AddButton; diff --git a/src/lsg/patterns/add-button/pattern.json b/src/lsg/patterns/add-button/pattern.json new file mode 100644 index 000000000..86d08b971 --- /dev/null +++ b/src/lsg/patterns/add-button/pattern.json @@ -0,0 +1,6 @@ +{ + "name": "add-button", + "displayName": "Add Button", + "version": "1.0.0", + "flag": "alpha" +} diff --git a/src/lsg/patterns/chrome/index.tsx b/src/lsg/patterns/chrome/index.tsx index 379c67105..09815f1eb 100644 --- a/src/lsg/patterns/chrome/index.tsx +++ b/src/lsg/patterns/chrome/index.tsx @@ -20,12 +20,18 @@ const StyledChrome = styled.div` display: flex; align-items: center; width: 100%; - height: 54px; + height: 40px; padding: ${getSpace(SpaceSize.XS)}px ${getSpace(SpaceSize.XXL) * 3}px; + border-bottom: 1px solid ${colors.grey90.toString()}; + @media screen and (-webkit-min-device-pixel-ratio: 2) { + border-bottom-width: 0.5px; + } + background: ${colors.white.toString()}; font-family: ${fonts().NORMAL_FONT}; -webkit-app-region: drag; -webkit-user-select: none; user-select: none; + -webkit-font-smoothing: antialiased; `; const StyledChromeTitle = styled.div` @@ -38,13 +44,12 @@ const StyledChromeTitle = styled.div` const StyledTitleWrapper = styled.div` position: relative; - margin: 0 ${getSpace(SpaceSize.XS)}px; + margin: -${getSpace(SpaceSize.XXS)}px ${getSpace(SpaceSize.XS)}px 0; white-space: nowrap; - text-overflow: ellipsis; + text-overflow: ellipsis; overflow: hidden; text-align: center; width: 130px; - } `; interface StyledIconWrapperProps { @@ -53,7 +58,7 @@ interface StyledIconWrapperProps { const StyledIconWrapper = styled.div` margin: ${getSpace(SpaceSize.XS)}px; - padding: ${getSpace(SpaceSize.XS)}px; + padding: ${getSpace(SpaceSize.XXS)}px; border-radius: ${getSpace(SpaceSize.XXS)}px; &:hover { @@ -77,13 +82,13 @@ const Chrome: React.StatelessComponent = props => ( - + {props.title} - + {props.children} diff --git a/src/lsg/patterns/colors/index.tsx b/src/lsg/patterns/colors/index.tsx index df1a1576a..3130251df 100644 --- a/src/lsg/patterns/colors/index.tsx +++ b/src/lsg/patterns/colors/index.tsx @@ -71,6 +71,10 @@ export const colors = { displayName: 'Blue 40', rgb: [102, 169, 230] }), + blue80: new Color({ + displayName: 'Blue 80', + rgb: [212, 226, 242] + }), grey20: new Color({ displayName: 'Grey 20', rgb: [52, 61, 69] @@ -95,6 +99,10 @@ export const colors = { displayName: 'Grey 90', rgb: [229, 230, 231] }), + grey97: new Color({ + displayName: 'Grey 97', + rgb: [247, 247, 247] + }), white: new Color({ displayName: 'White', rgb: [255, 255, 255] diff --git a/src/lsg/patterns/element/demo.tsx b/src/lsg/patterns/element/demo.tsx index ad92988ab..237c46197 100644 --- a/src/lsg/patterns/element/demo.tsx +++ b/src/lsg/patterns/element/demo.tsx @@ -4,8 +4,6 @@ import Element from './index'; import * as React from 'react'; import styled from 'styled-components'; -const NOOP = () => {}; // tslint:disable-line no-empty - const StyledTestDiv = styled.div` flex-grow: 1; max-width: 200px; @@ -16,40 +14,34 @@ const ElementDemo: React.StatelessComponent = (): JSX.Element => ( Default - + Active - + With Child and handleIconClick - { - e.stopPropagation(); - }} - handleIconClick={NOOP} - title="Element" - > + e.stopPropagation()} title="Element" dragging> Child With Child and open - + Child With child and active - + Child With child, active and open - + Child diff --git a/src/lsg/patterns/element/index.tsx b/src/lsg/patterns/element/index.tsx index bce1a9377..35d02c042 100644 --- a/src/lsg/patterns/element/index.tsx +++ b/src/lsg/patterns/element/index.tsx @@ -3,22 +3,24 @@ import { Icon, IconName, Size as IconSize } from '../icons'; import * as React from 'react'; import { getSpace, Size } from '../space'; import styled from 'styled-components'; +import { tag } from '../tag'; export interface ElementProps { active?: boolean; draggable?: boolean; - handleClick?: React.MouseEventHandler; - handleContextMenu?: React.MouseEventHandler; - handleDragDrop?: React.DragEventHandler; - handleDragDropForChild?: React.DragEventHandler; - handleDragEnter?: React.DragEventHandler; - handleDragEnterForChild?: React.DragEventHandler; - handleDragLeave?: React.DragEventHandler; - handleDragLeaveForChild?: React.DragEventHandler; - handleDragStart?: React.DragEventHandler; - handleIconClick?: React.MouseEventHandler; + dragging: boolean; highlight?: boolean; highlightPlaceholder?: boolean; + id?: string; + onClick?: React.MouseEventHandler; + onContextMenu?: React.MouseEventHandler; + onDragDrop?: React.DragEventHandler; + onDragDropForChild?: React.DragEventHandler; + onDragEnter?: React.DragEventHandler; + onDragEnterForChild?: React.DragEventHandler; + onDragLeave?: React.DragEventHandler; + onDragLeaveForChild?: React.DragEventHandler; + onDragStart?: React.DragEventHandler; open?: boolean; title: string; } @@ -30,6 +32,7 @@ interface StyledElementLabelProps { interface StyledIconProps { active?: boolean; + id?: string; open?: boolean; } @@ -45,76 +48,129 @@ export interface StyledPlaceholder { } const StyledElement = styled.div` - cursor: default; position: relative; + z-index: 1; `; -const StyledElementLabel = styled.div` +const div = tag('div').omit(['active', 'highlight']); + +const StyledElementLabel = styled(div)` position: relative; display: flex; - padding: 9px ${getSpace(Size.L)}px 9px ${getSpace(Size.XL)}px; - border-radius: 3px; - cursor: pointer; + padding: ${getSpace(Size.XS)}px ${getSpace(Size.L)}px ${getSpace(Size.XS)}px ${getSpace( + Size.XXL +)}px; align-items: center; - color: ${colors.black.toString()}; + color: ${colors.grey20.toString()}; position: relative; + font-size: 15px; + line-height: 21px; + z-index: 1; + + &::before { + content: ''; + display: block; + position: absolute; + height: 100%; + width: 240px; + left: 0; + top: 0; + margin-left: -240px; + } &:hover { - background: ${colors.grey90.toString()}; + background ${colors.black.toString('rgb', { alpha: 0.05 })}; + + &::before { + background: ${colors.black.toString('rgb', { alpha: 0.05 })}; + } } ${(props: StyledElementLabelProps) => props.active ? ` - color: ${colors.white.toString()}; - background: ${colors.blue40.toString()}; + color: ${colors.blue.toString()}; + background: ${colors.blue80.toString()}; + + &::before { + background: ${colors.blue80.toString()}; + } - &:hover { - background: ${colors.blue40.toString()}; - } + &:hover { + background: ${colors.blue80.toString()}; + + &::before { + background: ${colors.blue80.toString()}; + } + } ` : ''}; ${(props: StyledElementLabelProps) => props.highlight ? ` background: ${colors.grey90.toString()}; + + &::before { + background: ${colors.grey90.toString()}; + } ` : ''}; `; -const StyledPlaceholder = styled.div` +const placeholderDiv = tag('div').omit(['highlightPlaceholder']); +const StyledPlaceholder = styled(placeholderDiv)` position: relative; - height: 10px; + height: ${getSpace(Size.S)}; width: 100%; - margin-top: -5px; - margin-bottom: -5px; - border-radius: 3px; + margin-top: -${getSpace(Size.XS)}; + margin-bottom: -${getSpace(Size.XS)}; + z-index: 10; + + &::before { + content: ''; + display: block; + position: absolute; + height: 6px; + width: 6px; + left: 6px; + top: 3px; + border-radius: 3px; + background: ${colors.blue40.toString()}; + transform: scale(0); + transition: transform 0.2s; + z-index: 20; + } &::after { content: ''; display: block; position: absolute; - height: 100%; - width: 100%; - left: 0; - top: 0; - background: ${colors.grey90.toString()}; + height: 2px; + width: calc(100% - 6px); + left: ${getSpace(Size.XS)}; + top: 5px; + background: ${colors.blue40.toString()}; transform: scaleY(0); transition: transform 0.2s; - z-index: 50; + z-index: 20; } ${(props: StyledPlaceholder) => props.highlightPlaceholder ? ` - &:after { + &::before { + transform: scale(1); + } + + &::after { transform: scaleY(1); } ` : ''}; `; -const StyledElementChild = styled.div` +const elementDiv = tag('div').omit(['open']); +const StyledElementChild = styled(elementDiv)` flex-basis: 100%; padding-left: ${getSpace(Size.L)}px; ${(props: StyledElementChildProps) => (props.open ? 'display: block;' : 'display: none;')}; @@ -122,78 +178,66 @@ const StyledElementChild = styled.div` const StyledIcon = styled(Icon)` position: absolute; - left: 0; + left: ${getSpace(Size.XS) + getSpace(Size.XXS)}px; fill: ${colors.grey60.toString()}; - width: 12px; - height: 12px; + width: ${getSpace(Size.S)}px; + height: ${getSpace(Size.S)}px; padding: ${getSpace(Size.XS)}px; transition: transform 0.2s; ${(props: StyledIconProps) => (props.open ? 'transform: rotate(90deg)' : '')}; - ${(props: StyledIconProps) => (props.active ? 'fill: white' : '')}; + ${(props: StyledIconProps) => (props.active ? `fill: ${colors.blue20.toString()}` : '')}; `; -const Element: React.StatelessComponent = props => { - const { - children, - title, - active, - open, - highlight, - draggable, - handleClick, - handleContextMenu, - handleIconClick, - handleDragStart, - handleDragEnter, - handleDragLeave, - handleDragDrop, - handleDragEnterForChild, - handleDragLeaveForChild, - handleDragDropForChild, - highlightPlaceholder - } = props; - - return ( - +const Element: React.StatelessComponent = props => ( + e.stopPropagation()} + onMouseLeave={e => e.stopPropagation()} + > + {props.dragging && ( ) => { e.preventDefault(); }} - onDragEnter={handleDragEnterForChild} - onDragLeave={handleDragLeaveForChild} - onDrop={handleDragDropForChild} + onDragEnter={props.onDragEnterForChild} + onDragLeave={props.onDragLeaveForChild} + onDrop={props.onDragDropForChild} /> - ) => { - e.preventDefault(); - }} - draggable={draggable} - onDragStart={handleDragStart} - onDragEnter={handleDragEnter} - onDragLeave={handleDragLeave} - onDrop={handleDragDrop} - active={active} - highlight={highlight} - onClick={handleClick} - onContextMenu={handleContextMenu} - > - {children && ( + )} + ) => { + e.preventDefault(); + }} + onDragEnter={props.onDragEnter} + onDragLeave={props.onDragLeave} + onDrop={props.onDragDrop} + > + {Array.isArray(props.children) && + props.children.length > 0 && ( )} - {title} - - {children && {children}} - - ); -}; +
{props.title}
+ + {props.children && ( + {props.children} + )} +
+); export default Element; diff --git a/src/lsg/patterns/global-styles/index.tsx b/src/lsg/patterns/global-styles/index.tsx index f95fd92d6..d19dd3973 100644 --- a/src/lsg/patterns/global-styles/index.tsx +++ b/src/lsg/patterns/global-styles/index.tsx @@ -1,12 +1,13 @@ +import { colors } from '../colors'; import { fonts } from '../fonts'; -import {injectGlobal} from 'styled-components'; +import { injectGlobal } from 'styled-components'; export default function globalStyles(): void { // tslint:disable-next-line return injectGlobal` body { margin: 0; - background-color: #f7f7f7; + background-color: ${colors.grey97.toString()};; font-family: ${fonts().NORMAL_FONT}; font-size: 12px; } diff --git a/src/lsg/patterns/icons/index.tsx b/src/lsg/patterns/icons/index.tsx index 5a093e685..55eb84373 100644 --- a/src/lsg/patterns/icons/index.tsx +++ b/src/lsg/patterns/icons/index.tsx @@ -15,6 +15,7 @@ export interface IconRegistryProps { export interface IconProps { className?: string; color?: Color; + dataIcon?: string; handleClick?: React.MouseEventHandler; name: IconName | null; size?: Size; @@ -104,6 +105,7 @@ export const Icon: React.StatelessComponent = (props): JSX.Element => className={props.className} iconColor={props.color} size={props.size} + data-icon={props.dataIcon} > {icon !== null && } diff --git a/src/lsg/patterns/layout/demo.tsx b/src/lsg/patterns/layout/demo.tsx index b3cd97df7..3fb3da39b 100644 --- a/src/lsg/patterns/layout/demo.tsx +++ b/src/lsg/patterns/layout/demo.tsx @@ -26,7 +26,7 @@ const LayoutDemo: React.StatelessComponent = (): JSX.Element => ( Horizontal Horizontal
- + Vertical with margins Vertical with margins Vertical with margins diff --git a/src/lsg/patterns/layout/index.tsx b/src/lsg/patterns/layout/index.tsx index ca7d3938a..213cbaf0c 100644 --- a/src/lsg/patterns/layout/index.tsx +++ b/src/lsg/patterns/layout/index.tsx @@ -1,24 +1,43 @@ +import { colors } from '../colors'; import * as React from 'react'; -import { getSpace, Size } from '../space'; import styled from 'styled-components'; export interface LayoutProps { className?: string; directionVertical?: boolean; - handleClick?: React.MouseEventHandler; - hasPaddings?: boolean; + hasBorder?: boolean; + onClick?: React.MouseEventHandler; + side?: string; } const StyledLayout = styled.div` display: flex; ${(props: LayoutProps) => (props.directionVertical ? 'flex-direction: column;' : '')}; - ${(props: LayoutProps) => (props.hasPaddings ? `padding: 0 ${getSpace(Size.L)}px` : '')}; + ${(props: LayoutProps) => + props.hasBorder && props.side == 'left' + ? ` + border-right: 1px solid ${colors.black.toString('rgb', { alpha: 0.1 })}; + @media screen and (-webkit-min-device-pixel-ratio: 2) { + border-right-width: 0.5px; + } + ` + : ''}; + ${(props: LayoutProps) => + props.hasBorder && props.side == 'right' + ? ` + border-left: 1px solid ${colors.black.toString('rgb', { alpha: 0.1 })}; + @media screen and (-webkit-min-device-pixel-ratio: 2) { + border-left-width: 0.5px; + } + ` + : ''}; `; const StyledMainArea = styled(StyledLayout)` box-sizing: border-box; height: 100vh; - padding-top: 54px; + padding-top: 40px; + -webkit-font-smoothing: antialiased; `; const StyledSideBar = styled(StyledLayout)` @@ -30,7 +49,7 @@ export const MainArea: React.StatelessComponent = props => ( {props.children} @@ -40,7 +59,9 @@ export const SideBar: React.StatelessComponent = props => ( {props.children} @@ -50,8 +71,8 @@ const Layout: React.StatelessComponent = props => ( {props.children} diff --git a/src/lsg/patterns/list/index.tsx b/src/lsg/patterns/list/index.tsx index 5ca97b348..205e0bf8f 100644 --- a/src/lsg/patterns/list/index.tsx +++ b/src/lsg/patterns/list/index.tsx @@ -11,13 +11,13 @@ export interface ListItemProps { active?: boolean; children?: ListItemProps[]; draggable?: boolean; - handleDragDrop?: React.DragEventHandler; - handleDragDropForChild?: React.DragEventHandler; - handleDragStart?: React.DragEventHandler; label?: string; onClick?: React.MouseEventHandler; onContextMenu?: React.MouseEventHandler; - value: string; + onDragDrop?: React.DragEventHandler; + onDragDropForChild?: React.DragEventHandler; + onDragStart?: React.DragEventHandler; + title: string; } interface StyledListItemProps { @@ -44,9 +44,9 @@ const StyledUl = styled.ul` const StyledLi = styled.li` line-height: 25px; list-style: none; - ${(props: StyledListItemProps) => (props.onClick ? 'cursor: pointer;' : '')} ${( - props: StyledListItemProps - ) => (props.active ? 'background: #def' : '')}; + ${(props: StyledListItemProps) => (props.onClick ? 'cursor: pointer;' : '')}; + ${(props: StyledListItemProps) => + props.active ? `background: ${colors.blue80.toString()}` : ''}; `; const StyledLabel = styled.span` diff --git a/src/lsg/patterns/panes/element-pane/index.tsx b/src/lsg/patterns/panes/element-pane/index.tsx index 68839c19a..578f14e89 100644 --- a/src/lsg/patterns/panes/element-pane/index.tsx +++ b/src/lsg/patterns/panes/element-pane/index.tsx @@ -1,28 +1,33 @@ +import { colors } from '../../colors'; import * as React from 'react'; +import { getSpace, Size } from '../../space'; import styled from 'styled-components'; const StyledElementPane = styled.div` position: relative; - flex-grow: 3; - flex-shrink: 0; - flex-basis: 60%; + flex: 1; overflow: scroll; + padding-top: ${getSpace(Size.M)}px; + padding-bottom: ${getSpace(Size.XL)}px; - /*FadeOut*/ &::after { content: ''; position: sticky; bottom: 0; display: block; width: 100%; - height: 40px; - background: linear-gradient(to bottom, - rgba(247, 247, 247, 0) 0%, - rgba(247, 247, 247, 0.5) 15%, - rgba(247, 247, 247, 1) 100%); + height: ${getSpace(Size.XXXL)}px; + background: linear-gradient( + to bottom, + ${colors.grey97.toString('rgb', { alpha: 0 })}, + ${colors.grey97.toString('rgb', { alpha: 1 })} + ); + z-index: 15; } `; -const ElementPane: React.StatelessComponent = props => {props.children}; +const ElementPane: React.StatelessComponent = props => ( + {props.children} +); export default ElementPane; diff --git a/src/lsg/patterns/panes/patterns-pane/index.tsx b/src/lsg/patterns/panes/patterns-pane/index.tsx index 44d9ed1f1..0245213bc 100644 --- a/src/lsg/patterns/panes/patterns-pane/index.tsx +++ b/src/lsg/patterns/panes/patterns-pane/index.tsx @@ -1,20 +1,18 @@ -import { colors } from '../../colors'; import * as React from 'react'; import { getSpace, Size } from '../../space'; import styled from 'styled-components'; const StyledPatternsPane = styled.div` box-sizing: border-box; - flex-grow: 2; - flex-shrink: 0; - flex-basis: 40%; - padding: ${getSpace(Size.M)}px 0; - border-top: 1px solid ${colors.grey90.toString()}; + flex: 1; + padding: ${getSpace(Size.M)}px; overflow: scroll; margin-left: -${getSpace(Size.L)}px; margin-right: -${getSpace(Size.L)}px; `; -const PatternsPane: React.StatelessComponent = props => {props.children}; +const PatternsPane: React.StatelessComponent = props => ( + {props.children} +); export default PatternsPane; diff --git a/src/lsg/patterns/panes/preview-pane/index.tsx b/src/lsg/patterns/panes/preview-pane/index.tsx index b104675f7..a96ab814a 100644 --- a/src/lsg/patterns/panes/preview-pane/index.tsx +++ b/src/lsg/patterns/panes/preview-pane/index.tsx @@ -21,7 +21,7 @@ const StyledPreviewWrapper = styled.div` `; const StyledPreviewResizer = styled.div` - width: 12px; + width: 9px; height: 100%; cursor: ew-resize; &::after { @@ -30,10 +30,21 @@ const StyledPreviewResizer = styled.div` top: 50%; transform: translateY(-50%); height: 36px; - width: 6px; + width: 3px; margin: 3px; - border-radius: 5px; - background: grey; + border-radius: 2px; + background: ${colors.grey80.toString()}; + } + + &:hover { + &::after { + background: ${colors.grey60.toString()}; + } + } + &:active { + &::after { + background: ${colors.blue40.toString()}; + } } `; @@ -41,15 +52,13 @@ const BaseStyledPreviewPane = styled.div` flex-grow: 1; overflow: hidden; background: ${colors.white.toString()}; - border-radius: 6px 6px 0 0; - box-shadow: 0 3px 9px 0 ${colors.black.toRGBString(0.15)}; `; const StyledPreviewPane = BaseStyledPreviewPane.extend.attrs({ style: (props: PreviewPaneProps) => ({ maxWidth: `${props.width}px` || 'none' }) -}) `${(props: PreviewPaneProps) => ({})}`; +})`${(props: PreviewPaneProps) => ({})}`; export default class PreviewPane extends React.Component { private previewPane: HTMLElement; @@ -84,7 +93,7 @@ export default class PreviewPane extends React.Component { ` }} /> diff --git a/src/lsg/patterns/panes/property-pane/index.tsx b/src/lsg/patterns/panes/property-pane/index.tsx index ecebd737b..8a285c7a3 100644 --- a/src/lsg/patterns/panes/property-pane/index.tsx +++ b/src/lsg/patterns/panes/property-pane/index.tsx @@ -1,12 +1,17 @@ import * as React from 'react'; +import { getSpace, Size } from '../../space'; import styled from 'styled-components'; const StyledPropertyPane = styled.div` flex-grow: 1; flex-shrink: 0; flex-basis: 40%; + padding: ${getSpace(Size.M)}px; + overflow: scroll; `; -const PropertyPane: React.StatelessComponent = props => {props.children}; +const PropertyPane: React.StatelessComponent = props => ( + {props.children} +); export default PropertyPane; diff --git a/src/lsg/patterns/tag.tsx b/src/lsg/patterns/tag.tsx new file mode 100644 index 000000000..979bfb469 --- /dev/null +++ b/src/lsg/patterns/tag.tsx @@ -0,0 +1,24 @@ +import { omit, pick } from 'lodash'; +import * as React from 'react'; + +export interface Tag { + omit(whitelist: string[]): React.SFC>; + pick(blacklist: string[]): React.SFC>; +} + +export function tag(TagName: string): Tag { + return { + omit(blacklist: string[]): React.SFC<{}> { + return props => { + const p = omit(props, blacklist); + return {p.children}; + }; + }, + pick(whitelist: string[]): React.SFC<{}> { + return props => { + const p = pick(props, whitelist); + return {p.children}; + }; + } + }; +} diff --git a/src/store/store.ts b/src/store/store.ts index 96a01c744..832c1b2e1 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -14,6 +14,11 @@ import { Preferences } from './preferences'; import { Project } from './project'; import { Styleguide } from './styleguide/styleguide'; +export enum RightPane { + Patterns = 'Patterns', + Properties = 'Properties' +} + /** * The central entry-point for all application state, managed by MobX. * Use this object and its properties in your React components, @@ -93,6 +98,12 @@ export class Store { */ @MobX.observable private relativePatternsPath: string; + /** + * The well-known enum name of content that should be visible in + * the right-hand sidebar/pane. + */ + @MobX.observable private rightPane: RightPane | null = null; + /** * The currently selected element in the element list. * The properties pane shows the properties of this element, @@ -416,6 +427,17 @@ export class Store { return this.projects; } + /** + * @return The content id to show in the right-hand sidebar + * @see isElementFocussed + */ + public getRightPane(): RightPane { + if (this.rightPane === null) { + return this.selectedElement ? RightPane.Properties : RightPane.Patterns; + } + return this.rightPane; + } + /** * Returns the currently selected element in the element list. * The properties pane shows the properties of this element, @@ -781,7 +803,7 @@ export class Store { * Sets the element that is currently being dragged, or undefined if there is none. * @param draggedElement The dragged element or undefined. */ - public setDraggedElement(draggedElement: PageElement): void { + public setDraggedElement(draggedElement?: PageElement): void { this.draggedElement = draggedElement; } @@ -818,6 +840,14 @@ export class Store { this.patternSearchTerm = patternSearchTerm; } + /** + * @return The content id to show in the right-hand sidebar + * @see rightPane + */ + public setRightPane(pane: RightPane | null): void { + this.rightPane = pane; + } + /** * Sets the currently selected element in the element list. * The properties pane shows the properties of this element, @@ -826,7 +856,8 @@ export class Store { * @param selectedElement The selected element or undefined. * @see setElementFocussed */ - public setSelectedElement(selectedElement: PageElement | undefined): void { + public setSelectedElement(selectedElement?: PageElement): void { + this.rightPane = null; this.selectedElement = selectedElement; this.selectedSlotId = undefined; }