feat(ui): Updates from March 17th mockups

Summary:
There are two known issues:
- toolbar is not draggable in some areas when in three-pane mode.
- archive button appears over very long subjects. Propose moving this button elsewhere.

WIP

WIP

Test Plan: Run tests

Reviewers: evan

Reviewed By: evan

Differential Revision: https://review.inboxapp.com/D1311
This commit is contained in:
Ben Gotow 2015-03-18 18:21:04 -07:00
parent 015bd62937
commit aad21317e5
27 changed files with 507 additions and 159 deletions

View file

@ -80,7 +80,7 @@ module.exports =
ComponentRegistry.register ComponentRegistry.register
view: NewComposeButton view: NewComposeButton
name: 'NewComposeButton' name: 'NewComposeButton'
role: 'Global:Toolbar' role: 'Root:Left:Toolbar'
_showInitialErrorDialog: (msg) -> _showInitialErrorDialog: (msg) ->
remote = require('remote') remote = require('remote')

View file

@ -5,7 +5,7 @@ React = require 'react'
module.exports = module.exports =
NewComposeButton = React.createClass NewComposeButton = React.createClass
render: -> render: ->
<button style={order: -100} <button style={order: 101}
className="btn btn-toolbar" className="btn btn-toolbar"
data-tooltip="Compose new message" data-tooltip="Compose new message"
onClick={@_onNewCompose}> onClick={@_onNewCompose}>

View file

@ -7,7 +7,7 @@
@import "buttons"; @import "buttons";
@compose-width: 800px; @compose-width: 800px;
@compose-min-height: 250px; @compose-min-height: 150px;
.composer-inner-wrap { .composer-inner-wrap {
position: relative; position: relative;
@ -257,7 +257,9 @@ body.is-blurred .composer-inner-wrap .tokenizing-field .token {
#message-list { #message-list {
.message-item-wrap.composer-outer-wrap { .message-item-wrap.composer-outer-wrap {
padding-top: @spacing-standard; padding-top: @spacing-standard;
background: @background-off-primary; background-image: -webkit-gradient(linear, left top, left bottom, from(@background-off-primary), to(@background-primary));
background-repeat:no-repeat;
-webkit-background-size:100% 150px;
} }
} }

View file

@ -189,7 +189,7 @@ EmailFrame = React.createClass
@getDOMNode().dispatchEvent(new KeyboardEvent(event.type, event)) @getDOMNode().dispatchEvent(new KeyboardEvent(event.type, event))
_delegateMouseEvents: (doc, method="addEventListener") -> _delegateMouseEvents: (doc, method="addEventListener") ->
for type in ["mousemove", "mouseup", "mousedown", "mouseover", "mouseout"] for type in ["mouseup", "mousedown"]
doc?[method]?(type, @_onMouseEvent) doc?[method]?(type, @_onMouseEvent)
_onMouseEvent: (event) -> _onMouseEvent: (event) ->

View file

@ -1,7 +1,28 @@
React = require "react" React = require "react"
MessageList = require "./message-list" MessageList = require "./message-list"
MessageToolbarItems = require "./message-toolbar-items.cjsx" MessageToolbarItems = require "./message-toolbar-items"
MessageSubjectItem = require "./message-subject-item"
{ComponentRegistry} = require 'inbox-exports' {ComponentRegistry} = require 'inbox-exports'
{RetinaImg} = require 'ui-components'
DownButton = React.createClass
render: ->
<div className="message-toolbar-arrow down" onClick={@_onClick}>
<RetinaImg name="toolbar-down-arrow.png"/>
</div>
_onClick: ->
atom.commands.dispatch(document.body, 'application:next-item')
UpButton = React.createClass
render: ->
<div className="message-toolbar-arrow up" onClick={@_onClick}>
<RetinaImg name="toolbar-up-arrow.png"/>
</div>
_onClick: ->
atom.commands.dispatch(document.body, 'application:previous-item')
module.exports = module.exports =
item: null # The DOM item the main React component renders into item: null # The DOM item the main React component renders into
@ -32,6 +53,24 @@ module.exports =
mode: 'list' mode: 'list'
view: MessageToolbarItems view: MessageToolbarItems
ComponentRegistry.register
name: 'MessageSubjectItem'
role: 'Thread:Center:Toolbar'
mode: 'list'
view: MessageSubjectItem
ComponentRegistry.register
name: 'DownButton'
role: 'Thread:Right:Toolbar'
mode: 'list'
view: DownButton
ComponentRegistry.register
name: 'UpButton'
role: 'Thread:Right:Toolbar'
mode: 'list'
view: UpButton
deactivate: -> deactivate: ->
ComponentRegistry.unregister 'MessageToolbarItems' ComponentRegistry.unregister 'MessageToolbarItems'

View file

@ -36,6 +36,9 @@ MessageItem = React.createClass
componentWillUnmount: -> componentWillUnmount: ->
@_storeUnlisten() if @_storeUnlisten @_storeUnlisten() if @_storeUnlisten
shouldComponentUpdate: (nextProps, nextState) ->
not _.isEqual(nextProps, @props) or not _.isEqual(nextState, @state)
render: -> render: ->
messageIndicators = ComponentRegistry.findAllViewsByRole('MessageIndicator') messageIndicators = ComponentRegistry.findAllViewsByRole('MessageIndicator')
attachments = @_attachmentComponents() attachments = @_attachmentComponents()

View file

@ -23,6 +23,9 @@ MessageList = React.createClass
componentWillUnmount: -> componentWillUnmount: ->
unsubscribe() for unsubscribe in @_unsubscribers unsubscribe() for unsubscribe in @_unsubscribers
shouldComponentUpdate: (nextProps, nextState) ->
not _.isEqual(nextProps, @props) or not _.isEqual(nextState, @state)
componentDidUpdate: (prevProps, prevState) -> componentDidUpdate: (prevProps, prevState) ->
didLoad = prevState.messages.length is 0 and @state.messages.length > 0 didLoad = prevState.messages.length is 0 and @state.messages.length > 0
@ -48,6 +51,7 @@ MessageList = React.createClass
render: -> render: ->
return <div></div> if not @state.currentThread? return <div></div> if not @state.currentThread?
wrapClass = React.addons.classSet wrapClass = React.addons.classSet
"messages-wrap": true "messages-wrap": true
"ready": @state.ready "ready": @state.ready
@ -74,7 +78,7 @@ MessageList = React.createClass
lastHeight = -1 lastHeight = -1
stableCount = 0 stableCount = 0
scrollIfSettled = => scrollIfSettled = =>
return done() unless @isMounted() return unless @isMounted()
messageWrapHeight = messageWrap.getBoundingClientRect().height messageWrapHeight = messageWrap.getBoundingClientRect().height
if messageWrapHeight isnt lastHeight if messageWrapHeight isnt lastHeight
@ -101,8 +105,6 @@ MessageList = React.createClass
MessageListHeaders = ComponentRegistry.findAllViewsByRole('MessageListHeader') MessageListHeaders = ComponentRegistry.findAllViewsByRole('MessageListHeader')
<div className="message-list-headers"> <div className="message-list-headers">
<h2 className="message-subject">{@state.currentThread.subject}</h2>
{for MessageListHeader in MessageListHeaders {for MessageListHeader in MessageListHeaders
<MessageListHeader thread={@state.currentThread} /> <MessageListHeader thread={@state.currentThread} />
} }
@ -151,9 +153,12 @@ MessageList = React.createClass
ready: if MessageStore.itemsLoading() then false else @state?.ready ? false ready: if MessageStore.itemsLoading() then false else @state?.ready ? false
_prepareContentForDisplay: -> _prepareContentForDisplay: ->
_.delay =>
return unless @isMounted()
focusedMessage = @getDOMNode().querySelector(".initial-focus") focusedMessage = @getDOMNode().querySelector(".initial-focus")
@scrollToMessage focusedMessage, => @scrollToMessage focusedMessage, =>
@setState(ready: true) @setState(ready: true)
, 100
_threadParticipants: -> _threadParticipants: ->
# We calculate the list of participants instead of grabbing it from # We calculate the list of participants instead of grabbing it from

View file

@ -0,0 +1,25 @@
React = require 'react'
{ThreadStore} = require 'inbox-exports'
module.exports =
MessageSubjectItem = React.createClass
displayName: 'MessageSubjectItem'
getInitialState: ->
@_getStateFromStores()
componentDidMount: ->
@_unsubscriber = ThreadStore.listen @_onChange
componentWillUnmount: ->
@_unsubscriber() if @_unsubscriber
render: ->
<div className="message-toolbar-subject">{@state.thread?.subject}</div>
_onChange: ->
@setState(@_getStateFromStores())
_getStateFromStores: ->
thread: ThreadStore.selectedThread()

View file

@ -71,9 +71,6 @@ module.exports = React.createClass
<div className={classes}> <div className={classes}>
<div className="message-toolbar-items-inner"> <div className="message-toolbar-items-inner">
<ReplyButton />
<ReplyAllButton />
<ForwardButton />
<ArchiveButton /> <ArchiveButton />
</div> </div>
</div> </div>

View file

@ -3,11 +3,12 @@
@message-max-width: 800px; @message-max-width: 800px;
// This class wraps the items that appear above the message list in the .sheet-toolbar {
// toolbar. We want the toolbar items to sit right above the centered // This class wraps the items that appear above the message list in the
// content, so we need another 800px-wide container in the toolbar... // toolbar. We want the toolbar items to sit right above the centered
.message-toolbar-items { // content, so we need another 800px-wide container in the toolbar...
order: -100; .message-toolbar-items {
order: -10000;
width: 100%; width: 100%;
text-align: center; text-align: center;
position: absolute; position: absolute;
@ -16,13 +17,42 @@
.message-toolbar-items-inner { .message-toolbar-items-inner {
margin: auto; margin: auto;
max-width: @message-max-width - @spacing-three-quarters*2; max-width: @message-max-width - @spacing-three-quarters*2;
text-align: left; text-align: right;
pointer-events: none;
& > * {
pointer-events: auto; pointer-events: auto;
} }
} }
}
.message-toolbar-items.hidden { .message-toolbar-items.hidden {
opacity: 0; opacity: 0;
}
.message-toolbar-subject {
order:-99;
cursor: default;
color:@text-color-heading;
-webkit-app-region: drag;
margin:0;
margin-top:13px;
font-size: @font-size-h4;
font-weight: @font-weight-normal;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.message-toolbar-arrow.down {
order:101;
padding-top:6px;
}
.message-toolbar-arrow.up {
order:102;
padding-top:6px;
// <1 because of hit region padding on the button
margin-right: @spacing-standard * 0.75;
}
} }
#message-list { #message-list {
@ -36,8 +66,6 @@
.message-list-headers { .message-list-headers {
margin: 0 auto; margin: 0 auto;
padding: @spacing-double;
padding-bottom: @spacing-standard;
width: 100%; width: 100%;
max-width: @message-max-width; max-width: @message-max-width;

View file

@ -1,9 +1,9 @@
{ComponentRegistry} = require 'inbox-exports' {ComponentRegistry} = require 'inbox-exports'
ModeSwitch = require './mode-switch' ModeToggle = require './mode-toggle'
module.exports = module.exports =
activate: (state) -> activate: (state) ->
ComponentRegistry.register ComponentRegistry.register
name: 'ModeSwitch' name: 'ModeToggle'
view: ModeSwitch view: ModeToggle
role: 'Root:Toolbar' role: 'Root:Center:Toolbar'

View file

@ -0,0 +1,38 @@
{ComponentRegistry,
WorkspaceStore,
Actions} = require "inbox-exports"
{RetinaImg} = require 'ui-components'
React = require "react"
_ = require "underscore-plus"
module.exports =
ModeToggle = React.createClass
displayName: 'ModeToggle'
getInitialState: ->
mode: WorkspaceStore.selectedLayoutMode()
componentDidMount: ->
@unsubscribe = WorkspaceStore.listen(@_onStateChanged, @)
componentWillUnmount: ->
@unsubscribe?()
render: ->
<div className="mode-switch"
style={order:51, marginTop:10, marginRight:14}
onClick={@_onToggleMode}>
<RetinaImg
name="toolbar-icon-toggle-pane.png"
onClick={@_onToggleMode} />
</div>
_onStateChanged: ->
@setState
mode: WorkspaceStore.selectedLayoutMode()
_onToggleMode: ->
if @state.mode is 'list'
Actions.selectLayoutMode('split')
else
Actions.selectLayoutMode('list')

View file

@ -13,7 +13,7 @@ module.exports =
ComponentRegistry.register ComponentRegistry.register
view: SearchBar view: SearchBar
name: 'SearchBar' name: 'SearchBar'
role: 'Global:Toolbar' role: 'Root:Center:Toolbar'
deactivate: -> deactivate: ->
ComponentRegistry.unregister 'SearchBar' ComponentRegistry.unregister 'SearchBar'

View file

@ -3,11 +3,11 @@
.search-bar { .search-bar {
position: relative; position: relative;
order: 100; order: -100;
overflow: visible; overflow: visible;
z-index: 100; z-index: 100;
-webkit-user-select: none; -webkit-user-select: none;
width:270px; width:450px;
margin-top: (50px - 30px) / 2; margin-top: (50px - 30px) / 2;
margin-left: 12px; margin-left: 12px;
margin-right: 12px; margin-right: 12px;
@ -19,12 +19,12 @@
border:none; border:none;
input { input {
padding-left:25px; padding-left:30px;
width: 100%; width: 100%;
height: 30px; height: 30px;
} }
input.empty { input.empty {
text-align: center; text-align: left;
} }
input.empty:focus { input.empty:focus {
text-align: left; text-align: left;

View file

@ -8,8 +8,18 @@ Spinner = React.createClass
style: React.PropTypes.object style: React.PropTypes.object
getInitialState: -> getInitialState: ->
hidden: false hidden: true
paused: false paused: true
componentDidMount: ->
# The spinner always starts hidden. After it's mounted, it unhides itself
# if it's set to visible. This is a bit strange, but ensures that the CSS
# transition from .spinner.hidden => .spinner always happens, along with
# it's associated animation delay.
if @props.visible
_.defer =>
return unless @isMounted()
@setState({paused: false, hidden: false})
componentWillReceiveProps: (nextProps) -> componentWillReceiveProps: (nextProps) ->
hidden = if nextProps.visible? then !nextProps.visible else false hidden = if nextProps.visible? then !nextProps.visible else false

View file

@ -0,0 +1,162 @@
# WHY IS THIS FILE HERE? ReactCSSTransitionGroup is causing
# inconsitency exceptions when you hammer on the animations and don't let them
# finish. This is from http://khan.github.io/react-components/#timeout-transition-group
# and uses timeouts to clean up elements rather than listeners on CSS events, which
# don't always seem to fire.
# https://github.com/facebook/react/issues/1707
###*
# The CSSTransitionGroup component uses the 'transitionend' event, which
# browsers will not send for any number of reasons, including the
# transitioning node not being painted or in an unfocused tab.
#
# This TimeoutTransitionGroup instead uses a user-defined timeout to determine
# when it is a good time to remove the component. Currently there is only one
# timeout specified, but in the future it would be nice to be able to specify
# separate timeouts for enter and leave, in case the timeouts for those
# animations differ. Even nicer would be some sort of inspection of the CSS to
# automatically determine the duration of the animation or transition.
#
# This is adapted from Facebook's CSSTransitionGroup which is in the React
# addons and under the Apache 2.0 License.
###
React = require('react/addons')
ReactTransitionGroup = React.addons.TransitionGroup
TICK = 17
endEvents = ['webkitTransitionEnd', 'webkitAnimationEnd']
animationSupported = ->
endEvents.length != 0
###*
# Functions for element class management to replace dependency on jQuery
# addClass, removeClass and hasClass
###
addClass = (element, className) ->
if element.classList
element.classList.add className
else if !hasClass(element, className)
element.className = element.className + ' ' + className
element
removeClass = (element, className) ->
if hasClass(className)
if element.classList
element.classList.remove className
else
element.className = (' ' + element.className + ' ').replace(' ' + className + ' ', ' ').trim()
element
hasClass = (element, className) ->
if element.classList
element.classList.contains className
else
(' ' + element.className + ' ').indexOf(' ' + className + ' ') > -1
TimeoutTransitionGroupChild = React.createClass(
transition: (animationType, finishCallback) ->
node = @getDOMNode()
className = @props.name + '-' + animationType
activeClassName = className + '-active'
endListener = ->
removeClass node, className
removeClass node, activeClassName
# Usually this optional callback is used for informing an owner of
# a leave animation and telling it to remove the child.
finishCallback and finishCallback()
return
if !animationSupported()
endListener()
else
if animationType == 'enter'
@animationTimeout = setTimeout(endListener, @props.enterTimeout)
else if animationType == 'leave'
@animationTimeout = setTimeout(endListener, @props.leaveTimeout)
addClass node, className
# Need to do this to actually trigger a transition.
@queueClass activeClassName
return
queueClass: (className) ->
@classNameQueue.push className
if !@timeout
@timeout = setTimeout(@flushClassNameQueue, TICK)
return
flushClassNameQueue: ->
if @isMounted()
@classNameQueue.forEach ((name) ->
addClass @getDOMNode(), name
return
).bind(this)
@classNameQueue.length = 0
@timeout = null
return
componentWillMount: ->
@classNameQueue = []
return
componentWillUnmount: ->
if @timeout
clearTimeout @timeout
if @animationTimeout
clearTimeout @animationTimeout
return
componentWillEnter: (done) ->
if @props.enter
@transition 'enter', done
else
done()
return
componentWillLeave: (done) ->
if @props.leave
@transition 'leave', done
else
done()
return
render: ->
React.Children.only @props.children
)
TimeoutTransitionGroup = React.createClass(
propTypes:
enterTimeout: React.PropTypes.number.isRequired
leaveTimeout: React.PropTypes.number.isRequired
transitionName: React.PropTypes.string.isRequired
transitionEnter: React.PropTypes.bool
transitionLeave: React.PropTypes.bool
getDefaultProps: ->
transitionEnter: true
transitionLeave: true
_wrapChild: (child) ->
<TimeoutTransitionGroupChild
enterTimeout={@props.enterTimeout}
leaveTimeout={@props.leaveTimeout}
name={@props.transitionName}
enter={@props.transitionEnter}
leave={@props.transitionLeave}>
{child}
</TimeoutTransitionGroupChild>
render: ->
<ReactTransitionGroup
{...@props}
childFactory={@_wrapChild} />
)
module.exports = TimeoutTransitionGroup
# ---
# generated by js2coffee 2.0.1

View file

@ -147,6 +147,10 @@ class AttributeCollection extends Attribute
objs = [] objs = []
for objJSON in json for objJSON in json
obj = new @itemClass(objJSON) obj = new @itemClass(objJSON)
# Important: if no ids are in the JSON, don't make them up randomly.
# This causes an object to be "different" each time it's de-serialized
# even if it's actually the same, makes React components re-render!
obj.id = undefined
obj.fromJSON(objJSON) if obj.fromJSON? obj.fromJSON(objJSON) if obj.fromJSON?
objs.push(obj) objs.push(obj)
objs objs

View file

@ -1,8 +1,9 @@
React = require 'react/addons' React = require 'react/addons'
Sheet = require './sheet' Sheet = require './sheet'
TitleBar = require './titlebar'
Flexbox = require './components/flexbox.cjsx' Flexbox = require './components/flexbox.cjsx'
ReactCSSTransitionGroup = React.addons.CSSTransitionGroup RetinaImg = require './components/retina-img'
TimeoutTransitionGroup = require './components/timeout-transition-group'
_ = require 'underscore-plus'
{Actions, {Actions,
ComponentRegistry, ComponentRegistry,
@ -12,15 +13,39 @@ ToolbarSpacer = React.createClass
className: 'ToolbarSpacer' className: 'ToolbarSpacer'
propTypes: propTypes:
order: React.PropTypes.number order: React.PropTypes.number
render: -> render: ->
<div className="item-spacer" style={flex: 1, order:@props.order ? 0}></div> <div className="item-spacer" style={flex: 1, order:@props.order ? 0}></div>
ToolbarBack = React.createClass
className: 'ToolbarBack'
render: ->
<div className="item-back" onClick={@_onClick}>
<RetinaImg name="sheet-back.png" />
</div>
_onClick: ->
Actions.popSheet()
ToolbarWindowControls = React.createClass
displayName: 'ToolbarWindowControls'
render: ->
<div name="ToolbarWindowControls" className="toolbar-window-controls">
<button className="close" onClick={ -> atom.close()}></button>
<button className="minimize" onClick={ -> atom.minimize()}></button>
<button className="maximize" onClick={ -> atom.maximize()}></button>
</div>
ComponentRegistry.register
view: ToolbarWindowControls
name: 'ToolbarWindowControls'
role: 'Global:Left:Toolbar'
Toolbar = React.createClass Toolbar = React.createClass
className: 'Toolbar' className: 'Toolbar'
propTypes: propTypes:
type: React.PropTypes.string type: React.PropTypes.string
depth: React.PropTypes.number
getInitialState: -> getInitialState: ->
@_getStateFromStores() @_getStateFromStores()
@ -31,54 +56,60 @@ Toolbar = React.createClass
@setState(@_getStateFromStores()) @setState(@_getStateFromStores())
@unlisteners.push ComponentRegistry.listen (event) => @unlisteners.push ComponentRegistry.listen (event) =>
@setState(@_getStateFromStores()) @setState(@_getStateFromStores())
window.addEventListener "resize", (event) => window.addEventListener("resize", @_onWindowResize)
@recomputeLayout() window.requestAnimationFrame => @recomputeLayout()
componentWillUnmount: -> componentWillUnmount: ->
@unlistener() if @unlistener window.removeEventListener("resize", @_onWindowResize)
unlistener() for unlistener in @unlisteners
componentWillReceiveProps: (props) -> componentWillReceiveProps: (props) ->
@setState(@_getStateFromStores(props)) @replaceState(@_getStateFromStores(props))
componentDidUpdate: -> componentDidUpdate: ->
# Wait for other components that are dirty (the actual columns in the sheet) # Wait for other components that are dirty (the actual columns in the sheet)
# to update as well. # to update as well.
setTimeout(( => @recomputeLayout()), 1) window.requestAnimationFrame => @recomputeLayout()
shouldComponentUpdate: (nextProps, nextState) ->
# This is very important. Because toolbar uses ReactCSSTransitionGroup,
# repetitive unnecessary updates can break animations and cause performance issues.
not _.isEqual(nextProps, @props) or not _.isEqual(nextState, @state)
render: -> render: ->
# The main toolbar contains items with roles <sheet type>:Toolbar style =
# and Global:Toolbar position:'absolute'
mainToolbar = @_flexboxForItems(@state.items) backgroundColor:'white'
width:'100%'
height:'100%'
zIndex: 1
# Column toolbars contain items with roles attaching them to items toolbars = @state.itemsForColumns.map ({column, items}) =>
# in the sheet. Ex: MessageList:Toolbar items appear in the column
# toolbar for the column containing <MessageList/>.
columnToolbars = @state.itemsForColumns.map ({column, name, items}) =>
<div style={position: 'absolute', top:0, display:'none'} <div style={position: 'absolute', top:0, display:'none'}
data-owner-name={name}
data-column={column} data-column={column}
key={column}> key={column}>
{@_flexboxForItems(items)} {@_flexboxForItems(items)}
</div> </div>
<ReactCSSTransitionGroup transitionName="sheet-toolbar"> <div style={style}>
{mainToolbar} {toolbars}
{columnToolbars} </div>
</ReactCSSTransitionGroup>
_flexboxForItems: (items) -> _flexboxForItems: (items) ->
components = items.map ({view, name}) => components = items.map ({view, name}) =>
<view key={name} {...@props} /> <view key={name} {...@props} />
<ReactCSSTransitionGroup <TimeoutTransitionGroup
className="item-container" className="item-container"
component={Flexbox} component={Flexbox}
direction="row" direction="row"
leaveTimeout={200}
enterTimeout={200}
transitionName="sheet-toolbar"> transitionName="sheet-toolbar">
{components} {components}
<ToolbarSpacer key="spacer-50" order={-50}/> <ToolbarSpacer key="spacer-50" order={-50}/>
<ToolbarSpacer key="spacer+50" order={50}/> <ToolbarSpacer key="spacer+50" order={50}/>
</ReactCSSTransitionGroup> </TimeoutTransitionGroup>
recomputeLayout: -> recomputeLayout: ->
return unless @isMounted() return unless @isMounted()
@ -87,7 +118,8 @@ Toolbar = React.createClass
columnToolbarEls = @getDOMNode().querySelectorAll('[data-column]') columnToolbarEls = @getDOMNode().querySelectorAll('[data-column]')
# Find the top sheet in the stack # Find the top sheet in the stack
sheet = document.querySelector("[name='Sheet']:last-child") sheet = document.querySelector("[name='Sheet']:nth-child(#{@props.depth+1})")
return unless sheet
# Position item containers so they have the position and width # Position item containers so they have the position and width
# as their respective columns in the top sheet # as their respective columns in the top sheet
@ -100,32 +132,42 @@ Toolbar = React.createClass
columnToolbarEl.style.left = "#{columnEl.offsetLeft}px" columnToolbarEl.style.left = "#{columnEl.offsetLeft}px"
columnToolbarEl.style.width = "#{columnEl.offsetWidth}px" columnToolbarEl.style.width = "#{columnEl.offsetWidth}px"
_onWindowResize: ->
@recomputeLayout()
_getStateFromStores: (props) -> _getStateFromStores: (props) ->
props ?= @props props ?= @props
state = state =
mode: WorkspaceStore.selectedLayoutMode() mode: WorkspaceStore.selectedLayoutMode()
items: []
itemsForColumns: [] itemsForColumns: []
for role in ["Global:Toolbar", "#{props.type}:Toolbar"] items = {}
for entry in ComponentRegistry.findAllByRole(role)
continue if entry.mode? and entry.mode != state.mode
state.items.push(entry)
for column in ["Left", "Center", "Right"] for column in ["Left", "Center", "Right"]
role = "#{props.type}:#{column}:Toolbar" items[column] = []
items = [] for role in ["Global:#{column}:Toolbar", "#{props.type}:#{column}:Toolbar"]
for entry in ComponentRegistry.findAllByRole(role) for entry in ComponentRegistry.findAllByRole(role)
continue if entry.mode? and entry.mode != state.mode continue if entry.mode? and entry.mode != state.mode
items.push(entry) items[column].push(entry)
if items.length > 0
state.itemsForColumns.push({column, name, items})
if @props.depth > 0
items['Left'].push(view: ToolbarBack, name: 'ToolbarBack')
# If the left or right column does not contain any components, it won't
# be in the sheet. Go ahead and shift those toolbar items into the center
# region.
for column in ["Left", "Right"]
if ComponentRegistry.findAllByRole("#{props.type}:#{column}").length is 0
items['Center'].push(items[column]...)
delete items[column]
for key, val of items
state.itemsForColumns.push({column: key, items: val}) if val.length > 0
state state
FlexboxForRoles = React.createClass FlexboxForRoles = React.createClass
className: 'FlexboxForRoles' className: 'FlexboxForRoles'
propTypes: propTypes:
roles: React.PropTypes.arrayOf(React.PropTypes.string) roles: React.PropTypes.arrayOf(React.PropTypes.string)
@ -139,9 +181,17 @@ FlexboxForRoles = React.createClass
componentWillUnmount: -> componentWillUnmount: ->
@unlistener() if @unlistener @unlistener() if @unlistener
shouldComponentUpdate: (nextProps, nextState) ->
# Note: we actually ignore props.roles. If roles change, but we get
# the same items, we don't need to re-render. Our render function is
# a function of state only.
nextItemNames = nextState.items.map (i) -> i.name
itemNames = @state.items?.map (i) -> i.name
!_.isEqual(nextItemNames, itemNames)
render: -> render: ->
components = @state.items.map ({view, name}) => components = @state.items.map ({view, name}) =>
<view key={name} {...@props} /> <view key={name} />
<Flexbox direction="row"> <Flexbox direction="row">
{components} {components}
@ -171,26 +221,43 @@ SheetContainer = React.createClass
render: -> render: ->
topSheetType = @state.stack[@state.stack.length - 1] topSheetType = @state.stack[@state.stack.length - 1]
<Flexbox direction="column"> <Flexbox direction="column">
<TitleBar /> <TimeoutTransitionGroup name="Toolbar"
<div name="Toolbar" style={order:0} className="sheet-toolbar"> style={order:0}
<Toolbar ref="toolbar" type={topSheetType}/> leaveTimeout={200}
</div> enterTimeout={200}
className="sheet-toolbar"
transitionName="sheet-toolbar">
{@_toolbarComponents()}
</TimeoutTransitionGroup>
<div name="Top" style={order:1}> <div name="Top" style={order:1}>
<FlexboxForRoles roles={["Global:Top", "#{topSheetType}:Top"]} <FlexboxForRoles roles={["Global:Top", "#{topSheetType}:Top"]}
type={topSheetType}/> type={topSheetType}/>
</div> </div>
<div name="Center" style={order:2, flex: 1, position:'relative'}>
<ReactCSSTransitionGroup transitionName="sheet-stack"> <TimeoutTransitionGroup name="Center"
style={order:2, flex: 1, position:'relative'}
leaveTimeout={150}
enterTimeout={150}
transitionName="sheet-stack">
{@_sheetComponents()} {@_sheetComponents()}
</ReactCSSTransitionGroup> </TimeoutTransitionGroup>
</div>
<div name="Footer" style={order:3}> <div name="Footer" style={order:3}>
<FlexboxForRoles roles={["Global:Footer", "#{topSheetType}:Footer"]} <FlexboxForRoles roles={["Global:Footer", "#{topSheetType}:Footer"]}
type={topSheetType}/> type={topSheetType}/>
</div> </div>
</Flexbox> </Flexbox>
_toolbarComponents: ->
@state.stack.map (type, index) ->
<Toolbar type={type}
ref={"toolbar-#{index}"}
depth={index}
key={index} />
_sheetComponents: -> _sheetComponents: ->
@state.stack.map (type, index) => @state.stack.map (type, index) =>
<Sheet type={type} <Sheet type={type}
@ -198,11 +265,11 @@ SheetContainer = React.createClass
key={index} key={index}
onColumnSizeChanged={@_onColumnSizeChanged} /> onColumnSizeChanged={@_onColumnSizeChanged} />
_onColumnSizeChanged: -> _onColumnSizeChanged: (sheet) ->
@refs.toolbar.recomputeLayout() @refs["toolbar-#{sheet.props.depth}"]?.recomputeLayout()
_onStoreChange: -> _onStoreChange: ->
@setState @_getStateFromStores() _.defer => @setState(@_getStateFromStores())
_getStateFromStores: -> _getStateFromStores: ->
stack: WorkspaceStore.sheetStack() stack: WorkspaceStore.sheetStack()

View file

@ -28,6 +28,12 @@ Sheet = React.createClass
@unlisteners.push WorkspaceStore.listen (event) => @unlisteners.push WorkspaceStore.listen (event) =>
@setState(@_getStateFromStores()) @setState(@_getStateFromStores())
componentDidUpdate: ->
@props.onColumnSizeChanged(@) if @props.onColumnSizeChanged
shouldComponentUpdate: (nextProps, nextState) ->
not _.isEqual(nextProps, @props) or not _.isEqual(nextState, @state)
componentWillUnmount: -> componentWillUnmount: ->
unlisten() for unlisten in @unlisteners unlisten() for unlisten in @unlisteners
@ -49,24 +55,16 @@ Sheet = React.createClass
style={style} style={style}
data-type={@props.type}> data-type={@props.type}>
<Flexbox direction="row"> <Flexbox direction="row">
{@_backButtonComponent()}
{@_columnFlexboxComponents()} {@_columnFlexboxComponents()}
</Flexbox> </Flexbox>
</div> </div>
_backButtonComponent: ->
return [] if @props.depth is 0
<div className="sheet-edge" onClick={@_pop} key="back">
<div className="gradient"></div>
<div className="x"><RetinaImg name="sheet-back.png"/></div>
</div>
_columnFlexboxComponents: -> _columnFlexboxComponents: ->
@props.columns.map (column) => @props.columns.map (column) =>
classes = @state[column] || [] classes = @state[column] || []
return if classes.length is 0 return if classes.length is 0
components = classes.map ({name, view}) => <view key={name} {...@props} /> components = classes.map ({name, view}) -> <view key={name} />
maxWidth = _.reduce classes, ((m,{view}) -> Math.min(view.maxWidth ? 10000, m)), 10000 maxWidth = _.reduce classes, ((m,{view}) -> Math.min(view.maxWidth ? 10000, m)), 10000
minWidth = _.reduce classes, ((m,{view}) -> Math.max(view.minWidth ? 0, m)), 0 minWidth = _.reduce classes, ((m,{view}) -> Math.max(view.minWidth ? 0, m)), 0
@ -78,7 +76,7 @@ Sheet = React.createClass
<ResizableRegion key={"#{@props.type}:#{column}"} <ResizableRegion key={"#{@props.type}:#{column}"}
name={"#{@props.type}:#{column}"} name={"#{@props.type}:#{column}"}
data-column={column} data-column={column}
onResize={@props.onColumnSizeChanged} onResize={ => @props.onColumnSizeChanged(@) }
minWidth={minWidth} minWidth={minWidth}
maxWidth={maxWidth} maxWidth={maxWidth}
handle={handle}> handle={handle}>

View file

@ -1,13 +0,0 @@
React = require 'react'
module.exports =
TitleBar = React.createClass
displayName: 'TitleBar'
render: ->
<div name="TitleBar" className="sheet-title-bar">
{atom.getCurrentWindow().getTitle()}
<button className="close" onClick={ -> atom.close()}></button>
<button className="minimize" onClick={ -> atom.minimize()}></button>
<button className="maximize" onClick={ -> atom.maximize()}></button>
</div>

View file

@ -7,6 +7,7 @@
opacity: 1; opacity: 1;
-webkit-transition: opacity 0.2s linear 0.3s; //transition in -webkit-transition: opacity 0.2s linear 0.3s; //transition in
} }
.spinner.hidden { .spinner.hidden {
opacity: 0; opacity: 0;
-webkit-transition: opacity 0.2s linear; //transition out -webkit-transition: opacity 0.2s linear; //transition out

View file

@ -39,7 +39,7 @@
} }
&:hover { &:hover {
background-color: darken(@background-secondary, 5%); background-color: darken(@background-secondary, 5%);
cursor: -webkit-grab; cursor: default;
} }
&.selected, &.selected,
&.dragging { &.dragging {

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 230 KiB

After

Width:  |  Height:  |  Size: 263 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

View file

@ -36,33 +36,33 @@ atom-workspace {
} }
.sheet-stack-enter { .sheet-stack-enter {
left:100%; left:7%;
transition: left .20s ease-out; opacity: 0;
transition: all .15s ease-out;
} }
.sheet-stack-enter.sheet-stack-enter-active { .sheet-stack-enter.sheet-stack-enter-active {
left:0; left:0;
opacity: 1;
} }
.sheet-stack-leave { .sheet-stack-leave {
left:0; left:0;
transition: left .20s ease-in; opacity: 1;
transition: all .15s ease-in;
} }
.sheet-stack-leave.sheet-stack-leave-active { .sheet-stack-leave.sheet-stack-leave-active {
left:100%; left:7%;
opacity: 0;
} }
.sheet-title-bar { .toolbar-window-controls {
height:24px; padding-top:14px;
line-height: 24px;
cursor: default;
background: @toolbar-background-color;
-webkit-app-region: drag;
padding: 3px;
padding-left:@spacing-half; padding-left:@spacing-half;
padding-right:60px; order: -1000;
text-align: center; min-width: 102px;
width: 102px;
button { button {
-webkit-app-region: no-drag; -webkit-app-region: no-drag;
@ -105,13 +105,13 @@ atom-workspace {
} }
body.platform-win32, body.platform-linux { body.platform-win32, body.platform-linux {
.sheet-title-bar { .toolbar-window-controls {
display:none; display:none;
} }
} }
body.is-blurred { body.is-blurred {
.sheet-title-bar { .toolbar-window-controls {
button { button {
background-color: desaturate(fade(#FCB40A, 20%), 100%); background-color: desaturate(fade(#FCB40A, 20%), 100%);
} }
@ -121,6 +121,7 @@ body.is-blurred {
.sheet-toolbar { .sheet-toolbar {
position: relative; position: relative;
-webkit-app-region: drag; -webkit-app-region: drag;
-webkit-user-select:none;
background: @toolbar-background-color; background: @toolbar-background-color;
border-bottom: 1px solid @border-color-divider; border-bottom: 1px solid @border-color-divider;
width: 100%; width: 100%;
@ -141,6 +142,11 @@ body.is-blurred {
.item-spacer { .item-spacer {
-webkit-app-region: drag; -webkit-app-region: drag;
} }
.item-back {
order:-999;
padding-top: 5px;
padding-left: @spacing-three-quarters;
}
.btn-toolbar { .btn-toolbar {
margin-top: @spacing-half; margin-top: @spacing-half;
@ -167,30 +173,6 @@ body.is-blurred {
opacity:0; opacity:0;
} }
.sheet-edge {
height:100%;
z-index: @zindex-popover;
position: absolute;
.x {
position: absolute;
top: @spacing-standard * 1.5;
left: @spacing-standard * 1.5;
}
.gradient {
width:9px;
height:100%;
background-color: #f4f4f4;
background-image: -webkit-gradient(linear, left center, right center, from(rgb(244, 244, 244)), to(rgb(209, 209, 209)));
background-image: -webkit-linear-gradient(left, rgb(244, 244, 244), rgb(209, 209, 209));
background-image: -moz-linear-gradient(left, rgb(244, 244, 244), rgb(209, 209, 209));
background-image: -o-linear-gradient(left, rgb(244, 244, 244), rgb(209, 209, 209));
background-image: -ms-linear-gradient(left, rgb(244, 244, 244), rgb(209, 209, 209));
background-image: linear-gradient(left, rgb(244, 244, 244), rgb(209, 209, 209));
filter: progid:DXImageTransform.Microsoft.gradient(GradientType=1,StartColorStr='#f4f4f4', EndColorStr='#d1d1d1');
}
}
.flexbox-handle-horizontal { .flexbox-handle-horizontal {
width: 6px; width: 6px;
top: 0; top: 0;