feat(mode-switch): New layout designed for small form factors

Summary:
feat(mode-switch): almost working

Remove SheetStore in favor of bigger WorkspaceStore

Back button for mode switching

Test Plan: Tests WIP

Reviewers: evan

Reviewed By: evan

Differential Revision: https://review.inboxapp.com/D1292
This commit is contained in:
Ben Gotow 2015-03-13 13:11:24 -07:00
parent 7c9797c706
commit c301dcfbb1
32 changed files with 308 additions and 77 deletions

View file

@ -7,7 +7,6 @@
overflow: auto;
background-color: @source-list-bg;
box-shadow: inset -1px -2px 5px rgba(0, 0, 0, 0.22);
-webkit-user-select: none;
.item {
@ -27,6 +26,7 @@
.item-tag {
.unread {
float: right;
font-weight: @font-weight-medium;
color: @source-list-active-bg;
background: @source-list-bg;
}

View file

@ -9,17 +9,33 @@ module.exports =
activate: (@state={}) ->
# Register Message List Actions we provide globally
ComponentRegistry.register
name: 'MessageToolbarItems'
role: 'MessageList:Toolbar'
name: 'MessageListSplit'
role: 'Root:Right'
mode: 'split'
view: MessageList
ComponentRegistry.register
name: 'MessageToolbarItemsSplit'
role: 'Root:Right:Toolbar'
mode: 'split'
view: MessageToolbarItems
ComponentRegistry.register
name: 'MessageList'
role: 'Root:Right'
role: 'Thread:Center'
mode: 'list'
view: MessageList
ComponentRegistry.register
name: 'MessageToolbarItems'
role: 'Thread:Center:Toolbar'
mode: 'list'
view: MessageToolbarItems
deactivate: ->
ComponentRegistry.unregister 'MessageToolbarItems'
ComponentRegistry.unregister 'MessageListSplit'
ComponentRegistry.unregister 'MessageList'
serialize: -> @state

View file

@ -62,7 +62,7 @@ ArchiveButton = React.createClass
module.exports = React.createClass
getInitialState: ->
threadIsSelected: false
threadIsSelected: ThreadStore.selectedId()?
render: ->
classes = React.addons.classSet

View file

@ -12,7 +12,6 @@
text-align: center;
position: absolute;
pointer-events: none;
transition: opacity .25s ease-in-out;
.message-toolbar-items-inner {
margin: auto;
@ -22,7 +21,6 @@
}
}
// .message-toolbar-items also fades in and out when you select / deselect
.message-toolbar-items.hidden {
opacity: 0;
}

View file

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

View file

@ -0,0 +1,62 @@
{ComponentRegistry,
WorkspaceStore,
Actions} = require "inbox-exports"
{RetinaImg} = require 'ui-components'
React = require "react"
_ = require "underscore-plus"
module.exports =
ModeSwitch = React.createClass
displayName: 'ModeSwitch'
getInitialState: ->
mode: WorkspaceStore.selectedLayoutMode()
componentDidMount: ->
@unsubscribe = WorkspaceStore.listen(@_onStateChanged, @)
componentWillUnmount: ->
@unsubscribe?()
render: ->
knobX = if @state.mode is 'list' then 25 else 41
# Currently ModeSwitch is an opaque control that is not intended
# to be styled, hence the fixed margins and positions. If we
# turn this into a standard component one day, change!
<div className="mode-switch"
style={order:51, marginTop:14, marginRight:20}
onClick={@_onToggleMode}>
<RetinaImg
data-mode={'list'}
name="toolbar-icon-listmode.png"
active={@state.mode is 'list'}
onClick={@_onSetMode}
style={paddingRight:12} />
<RetinaImg
name="modeslider-bg.png"/>
<RetinaImg
name="modeslider-knob.png"
className="handle"
style={top:4, left: knobX}/>
<RetinaImg
data-mode={'split'}
name="toolbar-icon-splitpanes.png"
active={@state.mode is 'split'}
onClick={@_onSetMode}
style={paddingLeft:12} />
</div>
_onStateChanged: ->
@setState
mode: WorkspaceStore.selectedLayoutMode()
_onToggleMode: ->
if @state.mode is 'list'
Actions.selectLayoutMode('split')
else
Actions.selectLayoutMode('list')
_onSetMode: (event) ->
Actions.selectLayoutMode(event.target.dataset.mode)
event.stopPropagation()

View file

@ -0,0 +1,8 @@
{
"name": "mode-switch",
"version": "0.0.1",
"description": "Mode switch",
"main": "./lib/main",
"license": "Proprietary",
"private": true
}

View file

@ -0,0 +1,10 @@
.mode-switch {
z-index: 1000;
position: relative;
.handle {
position:absolute;
transition: left .2s ease-out;
}
}

View file

@ -20,7 +20,7 @@
}
.participants {
font-size: @font-size-large;
font-size: @font-size-base;
font-weight: @font-weight-semi-bold;
text-overflow: ellipsis;
overflow: hidden;
@ -43,7 +43,6 @@
height: 99%;
width: 5px;
top: 0;
left: 1px;
background: @unread-color;
}

View file

@ -22,11 +22,12 @@
'a' : 'application:reply-all' # Gmail
'f' : 'application:forward' # Gmail
# Default cross-platform core behaviors
'escape': 'application:pop-sheet'
# Default cross-platform core behaviors
'left': 'core:move-left'
'right': 'core:move-right'
'enter': 'core:confirm'
'escape': 'core:cancel'
'shift-up': 'core:select-up'
'shift-down': 'core:select-down'
'shift-left': 'core:select-left'

View file

@ -582,6 +582,9 @@ class Atom extends Model
'atom-workspace:logout': =>
@logout() if @isLoggedIn()
# Make sure we can't be made so small that the interface looks like crap
@getCurrentWindow().setMinimumSize(875, 500)
ipc.on 'onboarding-complete', =>
maximize = dimensions?.maximized and process.platform isnt 'darwin'
@displayWindow({maximize})

View file

@ -22,7 +22,10 @@ Mixin =
componentDidMount: ->
@_componentUnlistener = ComponentRegistry.listen =>
@setState(getViewsByName(@components))
if @isMounted() is false
console.log('WARNING: ComponentRegistry firing on unmounted component.')
return
@setState getViewsByName(@components)
componentWillUnmount: ->
@_componentUnlistener()
@ -33,7 +36,7 @@ class Component
# Don't shit the bed if the user forgets `new`
return new Component(attributes) unless @ instanceof Component
['name', 'model', 'view', 'role'].map (key) =>
['name', 'model', 'view', 'role', 'mode'].map (key) =>
@[key] = attributes[key] if attributes[key]
unless @name?

View file

@ -1,13 +1,28 @@
_ = require 'underscore-plus'
React = require 'react'
{Utils} = require "inbox-exports"
StylesImpactedByZoom = [
'top',
'left',
'right',
'bottom',
'paddingTop',
'paddingLeft',
'paddingRight',
'paddingBottom',
'marginTop',
'marginBottom',
'marginLeft',
'marginRight'
]
module.exports =
RetinaImg = React.createClass
displayName: 'RetinaImg'
propTypes:
name: React.PropTypes.string
style: React.PropTypes.object
className: React.PropTypes.string
# Optional additional properties which adjust the provided
# name. Makes it easy to write parent components when images
@ -23,7 +38,13 @@ RetinaImg = React.createClass
style = @props.style ? {}
style.zoom = if pathIsRetina then 0.5 else 1
<img className={@props.className ? ''} src={path} style={style} />
for key, val of style
val = "#{val}"
if key in StylesImpactedByZoom and val.indexOf('%') is -1
style[key] = val.replace('px','') / style.zoom
otherProps = _.omit(@props, _.keys(@constructor.propTypes))
<img src={path} style={style} {...otherProps} />
_pathFor: (name) ->
[basename, ext] = name.split('.')

View file

@ -59,6 +59,7 @@ windowActions = [
"selectThreadId",
"selectTagId",
"selectView",
"selectLayoutMode",
# Actions for composer
"composeReply",
@ -102,7 +103,8 @@ windowActions = [
"abortDownload",
"fileDownloaded",
"popSheet"
"popSheet",
"pushSheet"
]
allActions = [].concat(windowActions).concat(globalActions).concat(mainWindowActions)

View file

@ -5,10 +5,20 @@ Actions = require '../actions'
WorkspaceStore = Reflux.createStore
init: ->
@_resetInstanceVars()
@listenTo Actions.selectView, @_onSelectView
@listenTo Actions.selectLayoutMode, @_onSelectLayoutMode
@listenTo Actions.popSheet, @popSheet
@listenTo Actions.searchQueryCommitted, @popToRootSheet
@listenTo Actions.selectThreadId, @pushThreadSheet
atom.commands.add 'body',
'application:pop-sheet': => @popSheet()
_resetInstanceVars: ->
@_sheetStack = ["Root"]
@_view = 'threads'
@_layoutMode = 'list'
# Inbound Events
@ -16,9 +26,43 @@ WorkspaceStore = Reflux.createStore
@_view = view
@trigger(@)
_onSelectLayoutMode: (mode) ->
@_layoutMode = mode
@trigger(@)
# Accessing Data
selectedView: ->
@_view
selectedLayoutMode: ->
@_layoutMode
sheet: ->
@_sheetStack[@_sheetStack.length - 1]
sheetStack: ->
@_sheetStack
# Managing Sheets
pushSheet: (type) ->
@_sheetStack.push(type)
@trigger()
pushThreadSheet: (threadId) ->
if @selectedLayoutMode() is 'list' and threadId and @sheet() isnt "Thread"
@pushSheet("Thread")
popSheet: ->
if @_sheetStack.length > 1
@_sheetStack.pop()
@trigger()
popToRootSheet: ->
if @_sheetStack.length > 1
@_sheetStack = ["Root"]
@trigger()
module.exports = WorkspaceStore

View file

@ -1,10 +1,11 @@
React = require 'react'
SheetStore = require './sheet-store'
Sheet = require './sheet'
{Actions,ComponentRegistry} = require "inbox-exports"
Flexbox = require './components/flexbox.cjsx'
ReactCSSTransitionGroup = React.addons.CSSTransitionGroup
{Actions,
ComponentRegistry,
WorkspaceStore} = require "inbox-exports"
ToolbarSpacer = React.createClass
className: 'ToolbarSpacer'
@ -21,15 +22,23 @@ Toolbar = React.createClass
type: React.PropTypes.string
getInitialState: ->
@_getComponentRegistryState()
@_getStateFromStores()
componentDidMount: ->
@unlistener = ComponentRegistry.listen (event) =>
@setState(@_getComponentRegistryState())
@unlisteners = []
@unlisteners.push WorkspaceStore.listen (event) =>
@setState(@_getStateFromStores())
@unlisteners.push ComponentRegistry.listen (event) =>
@setState(@_getStateFromStores())
window.addEventListener "resize", (event) =>
@recomputeLayout()
componentWillUnmount: ->
@unlistener() if @unlistener
componentWillReceiveProps: (props) ->
@setState(@_getStateFromStores(props))
componentDidUpdate: ->
# Wait for other components that are dirty (the actual columns in the sheet)
# to update as well.
@ -43,34 +52,34 @@ Toolbar = React.createClass
# 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.itemsForViews.map ({column, name, items}) =>
<div style={position: 'absolute', top:0}
columnToolbars = @state.itemsForColumns.map ({column, name, items}) =>
<div style={position: 'absolute', top:0, display:'none'}
data-owner-name={name}
data-column={column}
key={column}>
{@_flexboxForItems(items)}
</div>
<div>
<ReactCSSTransitionGroup transitionName="sheet-toolbar">
{mainToolbar}
{columnToolbars}
</div>
</ReactCSSTransitionGroup>
_flexboxForItems: (items) ->
components = items.map ({view, name}) =>
<view key={name} {...@props} />
<Flexbox direction="row">
<ReactCSSTransitionGroup component={Flexbox} direction="row" transitionName="sheet-toolbar">
{components}
<ToolbarSpacer key="spacer-50" order={-50}/>
<ToolbarSpacer key="spacer+50" order={50} />
</Flexbox>
</ReactCSSTransitionGroup>
recomputeLayout: ->
return unless @isMounted()
# Find our item containers that are tied to specific columns
columnToolbarEls = this.getDOMNode().querySelectorAll('[data-column]')
columnToolbarEls = @getDOMNode().querySelectorAll('[data-column]')
# Find the top sheet in the stack
sheet = document.querySelector("[name='Sheet']:last-child")
@ -81,22 +90,33 @@ Toolbar = React.createClass
column = columnToolbarEl.dataset.column
columnEl = sheet.querySelector("[data-column='#{column}']")
continue unless columnEl
columnToolbarEl.style.display = 'inherit'
columnToolbarEl.style.left = "#{columnEl.offsetLeft}px"
columnToolbarEl.style.width = "#{columnEl.offsetWidth}px"
_getComponentRegistryState: ->
_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)
for column in ["Left", "Center", "Right"]
role = "#{props.type}:#{column}:Toolbar"
items = []
items.push(ComponentRegistry.findAllByRole("Global:Toolbar")...)
items.push(ComponentRegistry.findAllByRole("#{@props.type}:Toolbar")...)
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})
itemsForViews = []
for column in ['Left', 'Right', 'Center']
for {view, name} in ComponentRegistry.findAllByRole("#{@props.type}:#{column}")
itemsForView = ComponentRegistry.findAllByRole("#{name}:Toolbar")
if itemsForView.length > 0
itemsForViews.push({column, name, items: itemsForView})
{items, itemsForViews}
state
FlexboxForRoles = React.createClass
@ -128,6 +148,7 @@ FlexboxForRoles = React.createClass
items = items.concat(ComponentRegistry.findAllByRole(role))
{items}
module.exports =
SheetContainer = React.createClass
className: 'SheetContainer'
@ -136,7 +157,7 @@ SheetContainer = React.createClass
@_getStateFromStores()
componentDidMount: ->
@unsubscribe = SheetStore.listen @_onStoreChange
@unsubscribe = WorkspaceStore.listen @_onStoreChange
# It's important that every React class explicitly stops listening to
# atom events before it unmounts. Thank you event-kit
@ -146,7 +167,6 @@ SheetContainer = React.createClass
render: ->
topSheetType = @state.stack[@state.stack.length - 1]
<Flexbox direction="column">
<div name="Toolbar" style={order:0} className="sheet-toolbar">
<Toolbar ref="toolbar" type={topSheetType}/>
@ -180,5 +200,5 @@ SheetContainer = React.createClass
@setState @_getStateFromStores()
_getStateFromStores: ->
stack: SheetStore.stack()
stack: WorkspaceStore.sheetStack()

View file

@ -1,26 +0,0 @@
React = require "react"
Reflux = require 'reflux'
Actions = require './flux/actions'
SheetStore = Reflux.createStore
init: ->
@_stack = ["Root"]
@listenTo Actions.popSheet, @popSheet
# Exposed Data
pushSheet: (type) ->
@_stack.push(type)
@trigger()
popSheet: ->
@_stack.pop()
@trigger()
topSheetType: ->
@_stack[@_stack.length - 1]
stack: ->
@_stack
module.exports = SheetStore

View file

@ -1,6 +1,7 @@
React = require 'react'
_ = require 'underscore-plus'
{Actions,ComponentRegistry} = require "inbox-exports"
{Actions,ComponentRegistry, WorkspaceStore} = require "inbox-exports"
RetinaImg = require './components/retina-img.cjsx'
Flexbox = require './components/flexbox.cjsx'
ResizableRegion = require './components/resizable-region.cjsx'
@ -18,14 +19,17 @@ Sheet = React.createClass
columns: ['Left', 'Center', 'Right']
getInitialState: ->
@_getComponentRegistryState()
@_getStateFromStores()
componentDidMount: ->
@unlistener = ComponentRegistry.listen (event) =>
@setState(@_getComponentRegistryState())
@unlisteners ?= []
@unlisteners.push ComponentRegistry.listen (event) =>
@setState(@_getStateFromStores())
@unlisteners.push WorkspaceStore.listen (event) =>
@setState(@_getStateFromStores())
componentWillUnmount: ->
@unlistener() if @unlistener
unlisten() for unlisten in @unlisteners
render: ->
style =
@ -33,6 +37,13 @@ Sheet = React.createClass
backgroundColor:'white'
width:'100%'
height:'100%'
zIndex: 1
# Note - setting the z-index of the sheet is important, even though it's
# always 1. Assigning a z-index creates a "stacking context" in the browser,
# so z-indexes inside the sheet are relative to each other, but something in
# one sheet cannot be on top of something in another sheet.
# http://philipwalton.com/articles/what-no-one-told-you-about-z-index/
<div name={"Sheet"}
style={style}
@ -45,8 +56,9 @@ Sheet = React.createClass
_backButtonComponent: ->
return [] if @props.depth is 0
<div onClick={@_pop} key="back">
Back
<div className="sheet-edge" onClick={@_pop} key="back">
<div className="gradient"></div>
<div className="x"><RetinaImg name="sheet-back.png"/></div>
</div>
_columnFlexboxComponents: ->
@ -83,10 +95,17 @@ Sheet = React.createClass
{components}
</Flexbox>
_getComponentRegistryState: ->
_getStateFromStores: ->
state = {}
state.mode = WorkspaceStore.selectedLayoutMode()
for column in @props.columns
state["#{column}"] = ComponentRegistry.findAllByRole("#{@props.type}:#{column}")
views = []
for entry in ComponentRegistry.findAllByRole("#{@props.type}:#{column}")
continue if entry.mode? and entry.mode != state.mode
views.push(entry)
state["#{column}"] = views
state
_pop: ->

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 693 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 465 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 499 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 565 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 495 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 610 B

View file

@ -75,6 +75,48 @@ atom-workspace {
}
}
.sheet-toolbar-enter {
opacity:0;
transition: opacity .20s ease-out;
}
.sheet-toolbar-enter.sheet-toolbar-enter-active {
opacity:1;
}
.sheet-toolbar-leave {
opacity:1;
transition: opacity .20s ease-in;
}
.sheet-toolbar-leave.sheet-toolbar-leave-active {
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;