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
view: NewComposeButton
name: 'NewComposeButton'
role: 'Global:Toolbar'
role: 'Root:Left:Toolbar'
_showInitialErrorDialog: (msg) ->
remote = require('remote')

View file

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

View file

@ -7,7 +7,7 @@
@import "buttons";
@compose-width: 800px;
@compose-min-height: 250px;
@compose-min-height: 150px;
.composer-inner-wrap {
position: relative;
@ -257,7 +257,9 @@ body.is-blurred .composer-inner-wrap .tokenizing-field .token {
#message-list {
.message-item-wrap.composer-outer-wrap {
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))
_delegateMouseEvents: (doc, method="addEventListener") ->
for type in ["mousemove", "mouseup", "mousedown", "mouseover", "mouseout"]
for type in ["mouseup", "mousedown"]
doc?[method]?(type, @_onMouseEvent)
_onMouseEvent: (event) ->

View file

@ -1,7 +1,28 @@
React = require "react"
MessageList = require "./message-list"
MessageToolbarItems = require "./message-toolbar-items.cjsx"
MessageToolbarItems = require "./message-toolbar-items"
MessageSubjectItem = require "./message-subject-item"
{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 =
item: null # The DOM item the main React component renders into
@ -32,6 +53,24 @@ module.exports =
mode: 'list'
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: ->
ComponentRegistry.unregister 'MessageToolbarItems'

View file

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

View file

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

View file

@ -3,26 +3,56 @@
@message-max-width: 800px;
// This class wraps the items that appear above the message list in the
// toolbar. We want the toolbar items to sit right above the centered
// content, so we need another 800px-wide container in the toolbar...
.message-toolbar-items {
order: -100;
width: 100%;
text-align: center;
position: absolute;
pointer-events: none;
.sheet-toolbar {
// This class wraps the items that appear above the message list in the
// toolbar. We want the toolbar items to sit right above the centered
// content, so we need another 800px-wide container in the toolbar...
.message-toolbar-items {
order: -10000;
width: 100%;
text-align: center;
position: absolute;
pointer-events: none;
.message-toolbar-items-inner {
margin: auto;
max-width: @message-max-width - @spacing-three-quarters*2;
text-align: left;
pointer-events: auto;
.message-toolbar-items-inner {
margin: auto;
max-width: @message-max-width - @spacing-three-quarters*2;
text-align: right;
pointer-events: none;
& > * {
pointer-events: auto;
}
}
}
}
.message-toolbar-items.hidden {
opacity: 0;
.message-toolbar-items.hidden {
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 {
@ -36,8 +66,6 @@
.message-list-headers {
margin: 0 auto;
padding: @spacing-double;
padding-bottom: @spacing-standard;
width: 100%;
max-width: @message-max-width;

View file

@ -1,9 +1,9 @@
{ComponentRegistry} = require 'inbox-exports'
ModeSwitch = require './mode-switch'
ModeToggle = require './mode-toggle'
module.exports =
activate: (state) ->
ComponentRegistry.register
name: 'ModeSwitch'
view: ModeSwitch
role: 'Root:Toolbar'
name: 'ModeToggle'
view: ModeToggle
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
view: SearchBar
name: 'SearchBar'
role: 'Global:Toolbar'
role: 'Root:Center:Toolbar'
deactivate: ->
ComponentRegistry.unregister 'SearchBar'

View file

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

View file

@ -8,8 +8,18 @@ Spinner = React.createClass
style: React.PropTypes.object
getInitialState: ->
hidden: false
paused: false
hidden: true
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) ->
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 = []
for objJSON in json
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?
objs.push(obj)
objs

View file

@ -1,8 +1,9 @@
React = require 'react/addons'
Sheet = require './sheet'
TitleBar = require './titlebar'
Flexbox = require './components/flexbox.cjsx'
ReactCSSTransitionGroup = React.addons.CSSTransitionGroup
RetinaImg = require './components/retina-img'
TimeoutTransitionGroup = require './components/timeout-transition-group'
_ = require 'underscore-plus'
{Actions,
ComponentRegistry,
@ -12,15 +13,39 @@ ToolbarSpacer = React.createClass
className: 'ToolbarSpacer'
propTypes:
order: React.PropTypes.number
render: ->
<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
className: 'Toolbar'
propTypes:
type: React.PropTypes.string
depth: React.PropTypes.number
getInitialState: ->
@_getStateFromStores()
@ -31,54 +56,60 @@ Toolbar = React.createClass
@setState(@_getStateFromStores())
@unlisteners.push ComponentRegistry.listen (event) =>
@setState(@_getStateFromStores())
window.addEventListener "resize", (event) =>
@recomputeLayout()
window.addEventListener("resize", @_onWindowResize)
window.requestAnimationFrame => @recomputeLayout()
componentWillUnmount: ->
@unlistener() if @unlistener
window.removeEventListener("resize", @_onWindowResize)
unlistener() for unlistener in @unlisteners
componentWillReceiveProps: (props) ->
@setState(@_getStateFromStores(props))
@replaceState(@_getStateFromStores(props))
componentDidUpdate: ->
# Wait for other components that are dirty (the actual columns in the sheet)
# 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: ->
# The main toolbar contains items with roles <sheet type>:Toolbar
# and Global:Toolbar
mainToolbar = @_flexboxForItems(@state.items)
style =
position:'absolute'
backgroundColor:'white'
width:'100%'
height:'100%'
zIndex: 1
# Column toolbars contain items with roles attaching them to 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}) =>
toolbars = @state.itemsForColumns.map ({column, items}) =>
<div style={position: 'absolute', top:0, display:'none'}
data-owner-name={name}
data-column={column}
key={column}>
{@_flexboxForItems(items)}
</div>
<ReactCSSTransitionGroup transitionName="sheet-toolbar">
{mainToolbar}
{columnToolbars}
</ReactCSSTransitionGroup>
<div style={style}>
{toolbars}
</div>
_flexboxForItems: (items) ->
components = items.map ({view, name}) =>
<view key={name} {...@props} />
<ReactCSSTransitionGroup
<TimeoutTransitionGroup
className="item-container"
component={Flexbox}
direction="row"
leaveTimeout={200}
enterTimeout={200}
transitionName="sheet-toolbar">
{components}
<ToolbarSpacer key="spacer-50" order={-50}/>
<ToolbarSpacer key="spacer+50" order={50}/>
</ReactCSSTransitionGroup>
</TimeoutTransitionGroup>
recomputeLayout: ->
return unless @isMounted()
@ -87,8 +118,9 @@ Toolbar = React.createClass
columnToolbarEls = @getDOMNode().querySelectorAll('[data-column]')
# 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
# as their respective columns in the top sheet
for columnToolbarEl in columnToolbarEls
@ -100,32 +132,42 @@ Toolbar = React.createClass
columnToolbarEl.style.left = "#{columnEl.offsetLeft}px"
columnToolbarEl.style.width = "#{columnEl.offsetWidth}px"
_onWindowResize: ->
@recomputeLayout()
_getStateFromStores: (props) ->
props ?= @props
state =
mode: WorkspaceStore.selectedLayoutMode()
items: []
itemsForColumns: []
for role in ["Global:Toolbar", "#{props.type}:Toolbar"]
for entry in ComponentRegistry.findAllByRole(role)
continue if entry.mode? and entry.mode != state.mode
state.items.push(entry)
items = {}
for column in ["Left", "Center", "Right"]
role = "#{props.type}:#{column}:Toolbar"
items = []
for entry in ComponentRegistry.findAllByRole(role)
continue if entry.mode? and entry.mode != state.mode
items.push(entry)
if items.length > 0
state.itemsForColumns.push({column, name, items})
items[column] = []
for role in ["Global:#{column}:Toolbar", "#{props.type}:#{column}:Toolbar"]
for entry in ComponentRegistry.findAllByRole(role)
continue if entry.mode? and entry.mode != state.mode
items[column].push(entry)
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
FlexboxForRoles = React.createClass
className: 'FlexboxForRoles'
propTypes:
roles: React.PropTypes.arrayOf(React.PropTypes.string)
@ -139,9 +181,17 @@ FlexboxForRoles = React.createClass
componentWillUnmount: ->
@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: ->
components = @state.items.map ({view, name}) =>
<view key={name} {...@props} />
<view key={name} />
<Flexbox direction="row">
{components}
@ -171,26 +221,43 @@ SheetContainer = React.createClass
render: ->
topSheetType = @state.stack[@state.stack.length - 1]
<Flexbox direction="column">
<TitleBar />
<div name="Toolbar" style={order:0} className="sheet-toolbar">
<Toolbar ref="toolbar" type={topSheetType}/>
</div>
<TimeoutTransitionGroup name="Toolbar"
style={order:0}
leaveTimeout={200}
enterTimeout={200}
className="sheet-toolbar"
transitionName="sheet-toolbar">
{@_toolbarComponents()}
</TimeoutTransitionGroup>
<div name="Top" style={order:1}>
<FlexboxForRoles roles={["Global:Top", "#{topSheetType}:Top"]}
type={topSheetType}/>
</div>
<div name="Center" style={order:2, flex: 1, position:'relative'}>
<ReactCSSTransitionGroup transitionName="sheet-stack">
{@_sheetComponents()}
</ReactCSSTransitionGroup>
</div>
<TimeoutTransitionGroup name="Center"
style={order:2, flex: 1, position:'relative'}
leaveTimeout={150}
enterTimeout={150}
transitionName="sheet-stack">
{@_sheetComponents()}
</TimeoutTransitionGroup>
<div name="Footer" style={order:3}>
<FlexboxForRoles roles={["Global:Footer", "#{topSheetType}:Footer"]}
type={topSheetType}/>
</div>
</Flexbox>
_toolbarComponents: ->
@state.stack.map (type, index) ->
<Toolbar type={type}
ref={"toolbar-#{index}"}
depth={index}
key={index} />
_sheetComponents: ->
@state.stack.map (type, index) =>
<Sheet type={type}
@ -198,11 +265,11 @@ SheetContainer = React.createClass
key={index}
onColumnSizeChanged={@_onColumnSizeChanged} />
_onColumnSizeChanged: ->
@refs.toolbar.recomputeLayout()
_onColumnSizeChanged: (sheet) ->
@refs["toolbar-#{sheet.props.depth}"]?.recomputeLayout()
_onStoreChange: ->
@setState @_getStateFromStores()
_.defer => @setState(@_getStateFromStores())
_getStateFromStores: ->
stack: WorkspaceStore.sheetStack()

View file

@ -28,6 +28,12 @@ Sheet = React.createClass
@unlisteners.push WorkspaceStore.listen (event) =>
@setState(@_getStateFromStores())
componentDidUpdate: ->
@props.onColumnSizeChanged(@) if @props.onColumnSizeChanged
shouldComponentUpdate: (nextProps, nextState) ->
not _.isEqual(nextProps, @props) or not _.isEqual(nextState, @state)
componentWillUnmount: ->
unlisten() for unlisten in @unlisteners
@ -49,24 +55,16 @@ Sheet = React.createClass
style={style}
data-type={@props.type}>
<Flexbox direction="row">
{@_backButtonComponent()}
{@_columnFlexboxComponents()}
</Flexbox>
</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: ->
@props.columns.map (column) =>
classes = @state[column] || []
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
minWidth = _.reduce classes, ((m,{view}) -> Math.max(view.minWidth ? 0, m)), 0
@ -78,7 +76,7 @@ Sheet = React.createClass
<ResizableRegion key={"#{@props.type}:#{column}"}
name={"#{@props.type}:#{column}"}
data-column={column}
onResize={@props.onColumnSizeChanged}
onResize={ => @props.onColumnSizeChanged(@) }
minWidth={minWidth}
maxWidth={maxWidth}
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;
-webkit-transition: opacity 0.2s linear 0.3s; //transition in
}
.spinner.hidden {
opacity: 0;
-webkit-transition: opacity 0.2s linear; //transition out

View file

@ -39,7 +39,7 @@
}
&:hover {
background-color: darken(@background-secondary, 5%);
cursor: -webkit-grab;
cursor: default;
}
&.selected,
&.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 {
left:100%;
transition: left .20s ease-out;
left:7%;
opacity: 0;
transition: all .15s ease-out;
}
.sheet-stack-enter.sheet-stack-enter-active {
left:0;
opacity: 1;
}
.sheet-stack-leave {
left:0;
transition: left .20s ease-in;
opacity: 1;
transition: all .15s ease-in;
}
.sheet-stack-leave.sheet-stack-leave-active {
left:100%;
left:7%;
opacity: 0;
}
.sheet-title-bar {
height:24px;
line-height: 24px;
cursor: default;
background: @toolbar-background-color;
-webkit-app-region: drag;
padding: 3px;
.toolbar-window-controls {
padding-top:14px;
padding-left:@spacing-half;
padding-right:60px;
text-align: center;
order: -1000;
min-width: 102px;
width: 102px;
button {
-webkit-app-region: no-drag;
@ -105,13 +105,13 @@ atom-workspace {
}
body.platform-win32, body.platform-linux {
.sheet-title-bar {
.toolbar-window-controls {
display:none;
}
}
body.is-blurred {
.sheet-title-bar {
.toolbar-window-controls {
button {
background-color: desaturate(fade(#FCB40A, 20%), 100%);
}
@ -121,11 +121,12 @@ body.is-blurred {
.sheet-toolbar {
position: relative;
-webkit-app-region: drag;
-webkit-user-select:none;
background: @toolbar-background-color;
border-bottom: 1px solid @border-color-divider;
width: 100%;
height: 50px;
// prevent flexbox from ever, ever resizing toolbars, no matter
// how much it thinks other content is being squished
min-height: 50px;
@ -141,6 +142,11 @@ body.is-blurred {
.item-spacer {
-webkit-app-region: drag;
}
.item-back {
order:-999;
padding-top: 5px;
padding-left: @spacing-three-quarters;
}
.btn-toolbar {
margin-top: @spacing-half;
@ -167,30 +173,6 @@ body.is-blurred {
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 {
width: 6px;
top: 0;