$color-neutral-light: #b0b5b8
$color-neutral-medium: #545e61
html
box-sizing: border-box
background-color: #2d3437
font-size: 62.5%
font-family: Roboto, Helvetica, sans-serif
overflow: hidden
-webkit-font-smoothing: antialiased
-moz-osx-font-smoothing: grayscale
*, *::before, *::after
box-sizing: inherit
html, body
height: 100%
.sr-only
position: absolute
width: 1px
height: 1px
padding: 0
margin: -1px
overflow: hidden
clip: rect(0, 0, 0, 0)
border: 0
#app
display: flex
flex: 1 1 auto
flex-direction: row
flex-wrap: wrap
justify-content: space-between
align-items: center
.morph-button-to-modal-wrapper
position: relative
overflow: visible
top: 80vh
left: 10vw
width: 3.2rem
height: 3.2rem
.morph-button-to-modal
position: relative
top: 0
bottom: 0
overflow: hidden
min-width: 3.2rem
min-height: 3.2rem
width: 3.2rem
//max-width: 3.2rem
max-height: 3.2rem
border: 0.2rem solid #fff
border-radius: 50%
background: transparent
color: #fff
will-change: content, width, max-width, max-height, transform
transition-property: width, max-width, max-height, border-color, border-radius, background-color
transition-duration: 350ms, 350ms, 350ms, 200ms, 200ms, 400ms
transition-timing-function: cubic-bezier(0.46, 0.03, 0.52, 0.96)
transition-delay: 100ms, 100ms, 200ms, 400ms, 400ms, 200ms
//transition-delay: 200ms
&.open
z-index: 1000
width: 90rem
max-width: calc(100vw - 2rem)
max-height: calc(100vh - 2rem)
border-color: transparent
border-radius: 1rem
background-color: #394144
transition-delay: 200ms, 200ms, 200ms, 200ms, 200ms, 200ms
button
appearence: none
background: none
border: none
padding: 0
.morph-button-enter
opacity: 0.01
&.morph-button-enter-active
opacity: 1
transition-property: opacity
transition-duration: 300ms
transition-timing-function: ease-out
transition-delay: 400ms
.morph-button-leave
opacity: 1
&.morph-button-leave-active
opacity: 0
transition: opacity 200ms ease-out
.morph-modal-enter
opacity: 0.01
&.morph-modal-enter-active
opacity: 1
transition-property: opacity 320ms ease-out 500ms
.morph-modal-leave
opacity: 1
&.morph-modal-leave-active
opacity: 0
transition: opacity 200ms ease-out
.morph-button
position: absolute
width: 100%
height: 100%
.morph-modal-container
display: flex
flex-flow: column nowrap
align-items: stretch
position: relative
padding: 2rem
.morph-modal-title
flex: 0 0 auto
overflow: hidden
padding-bottom: 2rem
border-bottom: 0.1rem solid $color-neutral-medium
font-size: 2rem
font-weight: 500
white-space: nowrap
text-overflow: ellipsis
.btn-close
position: absolute
top: 1.4rem
right: 2rem
color: $color-neutral-light
font-size: 2.4rem
.morph-modal-body
flex: 0 1 auto
overflow-y: auto
max-height: calc(100vh - 14rem)
font-size: 1.5rem
.device-entry-info-modal
.morph-modal-enter
.device-details-info
> *
border-color: transparent
> *
opacity: 0.01
&.morph-modal-enter-active
.device-details-info
> *
border-left-color: $color-neutral-medium
transition: border-color 250ms ease-in 500ms
> *
opacity: 1
transition: opacity 300ms ease-in 420ms
@for $i from 1 through 24
&:nth-child(#{$i})
transition-delay: ($i * 32) + 420ms
//.morph-modal-leave
// &.morph-leave-active
.modal-button
font-size: 3.2rem
.info-key-value
display: flex
justify-content: space-between
white-space: nowrap
line-height: 2.5
&, .info-key, .info-value
margin: 0
.info-key
flex: 0 1 auto
overflow: hidden
padding-right: 1em
color: #fff
font-weight: 600
text-overflow: ellipsis
.info-value
flex: 0 0 auto
text-align: right
color: $color-neutral-light
.device-details-info
display: flex
flex-flow: row nowrap
justify-content: space-between
width: 100%
max-width: 100%
margin: 3rem 0
> *
flex: 0 1 33.33333%
max-width: 33.33333%
padding: 0 3rem
border-left: 0.1rem solid $color-neutral-medium
&:first-of-type
border: none
padding-left: 0
&:last-of-type
padding-right: 0
.sub.info-key::before
content: "\21B3"
margin-right: 0.5em
font-family: sans-serif
color: $color-neutral-light
.device-details-info-header
display: flex
flex-flow: row nowrap
align-content: center
margin-bottom: 4rem
padding: 2.4rem 0 1.8rem
border-bottom: 0.1rem solid $color-neutral-medium
.device-image
margin-right: 2.8rem
color: #36d3b4
font-size: 6.4rem
.device-name, .device-online-status
display: block
.device-name
font-size: 2.4rem
font-weight: 500
.device-online-status
font-style: italic
&::before
content: "("
&::after
content: ")"
.device-volume
display: inline-block
width: 100%
break-inside: avoid
// to save time color these
.device-os-and-connection
.fa-check
color: #24E873
.fa-times
color: #FF005A
View Compiled
{PropTypes} = React
{button, dd, div, dt, dl, i, li, span, ul} = React.DOM
{CSSTransitionGroup} = React.addons
# Loosely Definining Types:
#
# type alias Volume =
# { name : String
# , subdirs : Maybe (List String) // but not actually wrapped in Maybe
# }
#
# type alias Coord =
# { x : Number
# , y : Number
# }
# availableVolums : List Volume
availableVolumes =
[
{name: "iPhoto"}
{name: "LightRoom"}
{name: "Macintosh HD", subdirs: ["Users/lucius"]}
{name: "Lucius's Biig Hard Drive Name"}
{name: "MOVIES"}
{name: "Music"}
{name: "OnePlus Two"}
{name: "NSFW", subdirs: ["latina", "teen", "twins", "snakes"]}
{name: "Lucius Thumb"}
{name: "Snakes"}
{name: "Animu"}
]
# Button & Title are passed in as a prop, children ar the modal
#
# MorphButtonToModal : ReactClass
MorphButtonToModal = React.createClass {
name: "MorphButtonToModal"
propTypes: {
buttonDom: PropTypes.node
titleDom: PropTypes.node
}
getInitialState: :getInitialState ->
{
isOpen: false
mainTranslateCoord: {x: 0, y: 0}
}
#componentWillMount: :componentWillMount ->
# return
componentWillUnmount: :componentWillUnmount ->
window.cancelAnimationFrame @mainTranslateAnim
window.removeEventListener "resize", @boundResizeHandler, false
document.removeEventListener "click", @clickOutClose, false
return
#mainTranslateDuration : Int
mainTranslateDuration: 600
# coordToTranslate : Coord -> String
coordToTranslate: :coordToTranslate({x, y}) ->
"translate(" + x + "px, " + y + "px)"
# quadInOut : Number -> Number -> Number -> Number -> Number
quadInOut: :quadInOut(time, begin, end, duration) ->
if (time = time / (duration / 2)) < 1
(end - begin) / 2 * time * time + begin
else
(end - begin) * -1 / 2 * ((time -= 1) * (time - 2) - 1) + begin
mainTranslateBack: :mainTranslateBack ->
startTime = window.performance.now!
startX = @state.mainTranslateCoord.x
startY = @state.mainTranslateCoord.y
duration = @mainTranslateDuration
translate = (t) ~>
deltaTime = window.performance.now! - startTime
newCoord = {
x: @quadInOut deltaTime, startX, 0, duration
y: @quadInOut deltaTime, startY, 0, duration
}
@setState {mainTranslateCoord: newCoord}
if deltaTime < duration
@mainTranslateAnim = window.requestAnimationFrame translate
translate!
return
mainTranslateToCenter: :mainTranslateToCenter ->
{morphMain} = @refs
startTime = window.performance.now!
rect = morphMain.getBoundingClientRect!
startX = rect.left
startY = rect.top
duration = @mainTranslateDuration
translate = (t) ~>
deltaTime = window.performance.now! - startTime
{width, height} = morphMain.getBoundingClientRect!
newCoord = {
x: @quadInOut deltaTime, 0, ((window.innerWidth - width) * 0.5) - startX, duration
y: @quadInOut deltaTime, 0, ((window.innerHeight - height) * 0.5) - startY, duration
}
@setState {mainTranslateCoord: newCoord}
if deltaTime < duration
@mainTranslateAnim = window.requestAnimationFrame translate
translate!
return
resizeHandler: :resizeHandler(event) ->
{top, left} = @refs.morphWrapper.getBoundingClientRect!
{width, height} = @refs.morphMain.getBoundingClientRect!
newCoord = {
x: ((window.innerWidth - width) * 0.5) - left
y: ((window.innerHeight - height) * 0.5) - top
}
@setState {mainTranslateCoord: newCoord}
return
toggleClickOutClose: :toggleClickOutClose ->
if @state.isOpen
@boundResizeHandler = @resizeHandler.bind @
@clickOutClose = (event) ~>
if not event.target.closest ".morph-modal-container"
@closeModal!
window.addEventListener "resize", @boundResizeHandler, false
document.addEventListener "click", @clickOutClose, false
else
window.removeEventListener "resize", @boundResizeHandler, false
document.removeEventListener "click", @clickOutClose, false
return
openModal: :openModal(event) ->
@mainTranslateToCenter!
@setState {isOpen: true}, @toggleClickOutClose
return
closeModal: :closeModal(event) ->
@mainTranslateBack!
@setState {isOpen: false}, @toggleClickOutClose
return
# renderButton : () -> React.Element
renderButton: :renderButton ->
div {ref: "morphButtonContainer", className: "morph-button-container"},
button {ref: "morphButton", className: "morph-button", onClick: @openModal}, @props.buttonDom
# renderModal : () -> React.Element
renderModal: :renderModal ->
div {ref: "morphModalContainer", className: "morph-modal-container"},
div {ref: "morphModalTitle", className: "morph-modal-title"},
@props.titleDom
button {ref: "morphCloseButton", className: "btn-close", onClick: @closeModal},
i {ref: "morphCloseButtonIcon", className: "fa fa-times"}
span {ref: "morphCloseButtonLabel", className: "sr-only"}, "Close"
div {ref: "morphModalBody", className: "morph-modal-body"},
@props.children
# render : () -> React.Element
render: :render ->
classNames = ["morph-button-to-modal", (if @state.isOpen then "open" else "closed"), @props.className].join " " .trim!
wrapperClassNames = classNames.split " " .map ((s) -> s + "-wrapper") .join " "
div {ref: "morphWrapper", className: wrapperClassNames},
div {ref: "morphMain", key: "morphButtonToModal", className: classNames, style: {transform: @coordToTranslate @state.mainTranslateCoord}},
React.createElement CSSTransitionGroup,
{ key : "morphButtonTrans"
, transitionName : "morph-button"
, transitionEnterTimeout : 700
, transitionLeaveTimeout : 200
},
if @state.isOpen then null else @renderButton!,
React.createElement CSSTransitionGroup,
{ key : "morphModalTrans"
, transitionName : "morph-modal"
, transitionEnterTimeout : 1200
, transitionLeaveTimeout : 200
},
if @state.isOpen then @renderModal! else null
}
DeviceInfoButtonModal = React.createClass {
name: "DeviceInfoButtonModal"
propTypes: {
volumes: PropTypes.arrayOf PropTypes.shape {
name: PropTypes.string.isRequired
subdirs: PropTypes.arrayOf PropTypes.string
}
}
# renderVolume : Int -> Volume -> Int -> React.Element
renderVolume: :renderVolume(colIdx, vol, idx=0) ->
div {key: "vol" + colIdx + "." + idx, className: "device-volume"},
[ dl {key: colIdx + "." + idx, className: "info-key-value"},
dt {key: "key", className: "info-key"}, vol.name
dd {key: "value", className: "info-value"}, "Available"
].concat if not vol.subdirs or not vol.subdirs.length then [] else vol.subdirs.map (sub, idx) ~>
dl {key: idx, className: "info-key-value"}, [
dt {key: "subkey" + idx, className: "sub info-key"}, sub
dd {key: "subvalue" + idx, className: "sub info-value"}, "Available"
]
# renderVolumes : List Volume -> Int -> React.Element
renderVolumes: :renderVolumes(volumes, count) ->
count = count or @this.props.volumes.reduce (acc, vol) ->
acc + 1 + (if vol.subdirs then vol.subdirs.length else 0)
, 0
if count < 7
div {key: "vol-list-0", className: "device-volume-list", style: {flexBasis: "50%", maxWidth: "50%", paddingLeft: "9rem"}},
volumes.map @renderVolume.bind @, 0
else
splitAt = Math.ceil count * 0.5
splitVolumes = ([head, ...tail], count_=0, acc=[[], []]) ->
acc[if count_ < splitAt then 0 else 1].push head
if tail.length === 0 then acc else splitVolumes tail, count_ + 1 + (if head.subdirs then head.subdirs.length else 0), acc
splitVolumes(volumes).map (volColumn, idx) ~>
div {key: "vol-list-" + idx, className: "device-volume-list"},
volColumn.map @renderVolume.bind @, idx
# renderDeviceDetailsInfo : () -> React.Element
renderDeviceDetailsInfo: :renderDeviceDetailsInfo ->
volCount = @props.volumes.reduce (acc, vol) ->
acc + 1 + (if vol.subdirs then vol.subdirs.length else 0)
, 0
div {className: "device-details-info"}, [
div {key: "os-conn-0", className: "device-os-and-connection", style: if volCount < 7 then {flexBasis: "50%", maxWidth: "50%", paddingRight: "9rem"} else {}},
dl {className: "info-key-value"},
dt {className: "info-key"}, "Operating System"
dd {className: "info-value"}, "OS X"
dl {className: "info-key-value"},
dt {className: "info-key"},
i {className: "fa fa-check"}, null
span null, " LAN"
dd {className: "info-value"}, "192.168.1.1:8111"
dl {className: "info-key-value"},
dt {className: "info-key"},
i {className: "fa fa-times"}, null
span null, " P2P"
dd {className: "info-value"}, "none"
dl {className: "info-key-value"},
dt {className: "info-key"},
i {className: "fa fa-check"}, null
span null, " Relay"
dd {className: "info-value"}, "toast.al"
].concat @renderVolumes availableVolumes, volCount
# renderButtonDom : React.Element
renderButtonDom: [
i {key: "buttonIcon", className: "info-icon fa fa-info"}, null
span {key: "buttonLabel", className: "info-label sr-only"}, "Device Info"
]
# renderTitleDom : React.Element
renderTitleDom: [
i {key: "titleIcon", className: "title-icon fa fa-desktop", style: {paddingRight: "0.5em"}}, null
span {key: "titleLabel", className: "title-label"}, "Devices Info"
]
# deviceInfoButtonModal : () -> React.Element
render: :render ->
React.createElement MorphButtonToModal, {
key: "deviceEntryInfoModal"
className: "device-entry-info-modal"
buttonDom: @renderButtonDom
titleDom: @renderTitleDom
}, div {key: "deviceDetailsInfoContainer", className: "device-details-info-container"},
div {className: "device-details-info-header"},
i {className: "device-image fa fa-desktop"}, null
div {className: "device-label"},
span {className: "device-name"}, "Lucius Desktop"
span {className: "device-online-status"}, "Online"
@renderDeviceDetailsInfo!
}
# render the guy
ReactDOM.render (React.createElement DeviceInfoButtonModal, {volumes: availableVolumes}), document.getElementById "app"
View Compiled