#phone.gray-border
  #phone-face.gray-border
    #sensor.gray-border
    #camera-and-speaker
      #camera.gray-border
      #speaker.gray-border
    #screen.gray-border
      #content
    #home-button.gray-border
  #mute-button.gray-border
  #volume-up-button.gray-border
  #volume-down-button.gray-border
  #power-button.gray-border
View Compiled
// Content
@import url('https://fonts.googleapis.com/css?family=Roboto:300');

$primary-color: #AA2331;
$secondary-color: #191919;

html {
  background-color: black;
}
@media only screen and (min-device-width : 376px)  { 
  html {
    background-color: #F4F5F7;
  }
}

#container {
  font-family: 'Roboto', sans-serif;
  color: $primary-color;
  background-color: black;
  height: 100%;
}
#list {
  position: relative;
  height: 100%;
}
#newItemPerspective {
  perspective: 500px;
  perspective-origin: center bottom;  
  position: relative;
}
#newItem {
  transform-origin: center bottom;
  transform-style: preserve-3d;
  position: absolute;
  width: 100%;
  bottom: 0;
}
.item {
  cursor: default;
  position: relative;
  transition: background-color 0.5s, opacity 0.5s;
}
.draggedItem {
  z-index: 9000;
  box-shadow: 0px 0px 20px -5px rgba(0,0,0,0.5);
  
  // transform: perspective(1px) scale(1.1); // This would be nice if it wouldn't cause blur with text
  width: 110%;
  height: 110%;
  margin-top: -3px;
  margin-left: -5%;
  
  & .itemTitle {
    font-size: 1.1em;
    padding: 17px*1.1;
  }
  
}
.itemTitle {
  padding: 17px;
  color: white;
  user-select: none;
}
#fakeInput {
  position: absolute;
  background-color: transparent;
  color: transparent;
  border: none;
  padding: 0;
  margin: 17px;
}
.itemInput {
  width: 100%;
  background-color: transparent;
  font-family: 'Roboto', sans-serif;
  color: white;
  border: none;
  padding: 0;
}
#fakeInput:focus, .itemInput:focus {
  outline: none;
}


// Frame
body {
  margin: 0;
}
/*
.html,
.body,
#phone,
#phone-face,
#screen,
#content {
  height: 100%;
}
*/

@media only screen and (min-device-width : 376px)  { 

  $scale:  1; //0.75;

  $phone-width: 412px*$scale;
  $phone-height: 852px*$scale;
  $phone-radius: 67px*$scale;
  $phone-face-width: 400px*$scale;
  $phone-face-height: 840px*$scale;
  $phone-face-radius: 61px*$scale;
  $screen-width: 375px*$scale;
  $screen-height: 667px*$scale;
  $border-size: 1px;
  $border-color: gray;

  #phone {
    position: relative;
    width: $phone-width;
    height: $phone-height;
    border-radius: $phone-radius;
    margin: 30px auto;
  }
  #phone-face {
    width: $phone-face-width;
    height: $phone-face-height;
    margin: (($phone-height - $phone-face-height)/2 - $border-size) auto;
    border-radius: $phone-face-radius;
  }
  #sensor {
    width: 11px*$scale;
    height: 11px*$scale;
    border-radius: 5px*$scale;
    margin: 15px*$scale auto 0 auto;
  }
  #camera-and-speaker {
    width: 110px*$scale;
    margin-top: 13px*$scale;
    margin-left: 122px*$scale;
    font-size: 0;
  }
  #camera {
    $size: 18px*$scale;
    display: inline-block;
    vertical-align: middle;
    width: $size;
    height: $size;
    border-radius: $size;
  }
  #speaker {
    display: inline-block;
    vertical-align: middle;
    width: 66px*$scale;
    height: 6px*$scale;
    border-radius: 3px*$scale;
    margin-left: 26px*$scale;
  }
  #screen {
    background-color: white;
    width: $screen-width;
    height: $screen-height;
    margin: 30px*$scale auto 0 auto;
    overflow: hidden;
  }
  #content {
    box-sizing: border-box;
    width: 100%/$scale;
    height: 100%/$scale;
    transform-origin: top left;
    transform: scale($scale, $scale);
    -webkit-font-smoothing: antialised;
    -moz-osx-font-smoothing: grayscale;
  }
  #home-button {
    $size: 59px*$scale;
    width: $size;
    height: $size;
    margin: 17px*$scale auto 0 auto;
    border-radius: $size;
  }
  #mute-button {
    position: absolute;
    left: -6px*$scale;
    top: 111px*$scale;
    height: 33px*$scale;
    width: 6px*$scale;
  }

  #volume-up-button {
    position: absolute;
    left: -5px*$scale;
    top: 181px*$scale;
    height: 65px*$scale;
    width: 5px*$scale;
    border-radius: 5px*$scale 0 0 5px*$scale;
  }

  #volume-down-button {
    position: absolute;
    left: -5px*$scale;
    top: 266px*$scale;
    height: 65px*$scale;
    width: 5px*$scale;
    border-radius: 5px*$scale 0 0 5px*$scale;
  }

  #power-button {
    position: absolute;
    right: -5px*$scale;
    top: 179px*$scale;
    height: 65px*$scale;
    width: 5px*$scale;
    border-radius: 0 5px*$scale 5px*$scale 0;
  }

  .gray-border {
    border: $border-size solid $border-color;
    box-sizing: border-box;
  }
  
}
View Compiled
/* Import { createStore, combineReducers } from 'redux' */
const { createStore, combineReducers } = Redux;

/* Import { connect, Provider } from 'react-redux' */
const { connect, Provider } = ReactRedux;

/*********
* RxJS utility
**********/
const util = (() => {
  /* Using RxJS for creating new aggregated events for complex user interactions,
   * like vertical / horizontal swipes, short clicks and hold than drag events 
   * on a single DOM item based on the basic mouse and touch events.
   */
  function getDragObservables(domItem) {
    const preventDefault = event => {
      if(event.target.tagName !== "INPUT" && event.target.tagName !== "TEXTAREA") event.preventDefault();
    }
    const mouseEventToCoordinate = mouseEvent => {
      preventDefault(mouseEvent);
      return {x: mouseEvent.clientX, y: mouseEvent.clientY};
    };
    const touchEventToCoordinate = touchEvent => {
      preventDefault(touchEvent);
      return {x: touchEvent.changedTouches[0].clientX, y: touchEvent.changedTouches[0].clientY};
    };

    let mouseDowns = Rx.Observable.fromEvent(domItem, "mousedown").map(mouseEventToCoordinate).do(() => console.log('mouse down'));
    let mouseMoves = Rx.Observable.fromEvent(window, "mousemove").map(mouseEventToCoordinate).do(() => console.log('mouse move'));
    let mouseUps = Rx.Observable.fromEvent(window, "mouseup").map(mouseEventToCoordinate).do(() => console.log('mouse up'));

    let touchStarts = Rx.Observable.fromEvent(domItem, "touchstart").map(touchEventToCoordinate).do(() => console.log('touch start'));
    let touchMoves = Rx.Observable.fromEvent(domItem, "touchmove").map(touchEventToCoordinate).do(() => console.log('touch move'));
    let touchEnds = Rx.Observable.fromEvent(window, "touchend").map(touchEventToCoordinate).do(() => console.log('touch end'));
    let touchCancels = Rx.Observable.fromEvent(window, "touchcancel").map(touchEventToCoordinate).do(() => console.log('touch cancel'));

    let _starts = mouseDowns.merge(touchStarts);
    let _moves = mouseMoves.merge(touchMoves);
    let _ends = mouseUps.merge(touchEnds).merge(touchCancels);
    
    const HOLDING_PERIOD = 600; // milliseconds

    // Clicks: Take the start-end pairs only if no more than 3 move events happen in between, and the end event is within the holding period
    let clicks = _starts.concatMap(dragStartEvent =>  
      _ends.first()                           
        .takeUntil(_moves.elementAt(3))
        .takeUntil(Rx.Observable.timer(HOLDING_PERIOD))
        .do(() => console.log('click'))
        .catch(err => Rx.Observable.empty())
    );
    
    // Holds: Take those starts where no end event and no more than 3 move event occurs during the holding period
    let holds = _starts.concatMap(dragStartEvent => 
      Rx.Observable.timer(HOLDING_PERIOD)
        .takeUntil(_moves.elementAt(3))                          
        .takeUntil(_ends)
        .map(() => ({x: dragStartEvent.x, y: dragStartEvent.y}))
        .do(() => console.log('hold'))
        .catch(err => Rx.Observable.empty())
    );

    // Move starts with direction: Pair the move start events with the 3rd subsequent move event,
    // but only if it happens during the holdign period and no end event happens in between
    let moveStartsWithDirection = _starts.concatMap(dragStartEvent => 
      _moves
        .takeUntil(_ends)
        .takeUntil(Rx.Observable.timer(HOLDING_PERIOD))        
        .elementAt(3)                                            
        .catch(err => Rx.Observable.empty())
        .map(dragEvent => {
          const intialDeltaX = dragEvent.x - dragStartEvent.x;
          const initialDeltaY = dragEvent.y - dragStartEvent.y;
          return {x: dragStartEvent.x, y: dragStartEvent.y, intialDeltaX, initialDeltaY};
        })
    );

    // Vertical move starts: Keep only those move start events where the 3rd subsequent move event is rather vertical than horizontal
    let verticalMoveStarts = moveStartsWithDirection.filter(dragStartEvent => 
      Math.abs(dragStartEvent.intialDeltaX) < Math.abs(dragStartEvent.initialDeltaY)
    ).do(() => console.log('vertical move starts'));

    // Horizontal move starts: Keep only those move start events where the 3rd subsequent move event is rather horizontal than vertical
    let horizontalMoveStarts = moveStartsWithDirection.filter(dragStartEvent => 
      Math.abs(dragStartEvent.intialDeltaX) >= Math.abs(dragStartEvent.initialDeltaY)
    ).do(() => console.log('horizontal move starts'));

    // Take the moves until an end occurs
    const movesUntilEnds = dragStartEvent => 
      _moves.takeUntil(_ends).map(dragEvent => {
        const x = dragEvent.x - dragStartEvent.x;
        const y = dragEvent.y - dragStartEvent.y;
        return {x, y};
      });

    let verticalMoves = verticalMoveStarts.concatMap(movesUntilEnds).do(() => console.log('vertical move'));
    let horizontalMoves = horizontalMoveStarts.concatMap(movesUntilEnds).do(() => console.log('horizontal move'));
    let dragMoves = holds.concatMap(movesUntilEnds).do(() => console.log('dragging'));

    const lastMovesAtEnds = dragStartEvent => 
      _ends.first().map(dragEndEvent => {
        console.log(dragStartEvent, dragEndEvent);
        const x = dragEndEvent.x - dragStartEvent.x;
        const y = dragEndEvent.y - dragStartEvent.y;
        return {x, y};
      });

    let ends = _starts.concatMap(lastMovesAtEnds);
    let verticalMoveEnds = verticalMoveStarts.concatMap(lastMovesAtEnds).do(() => console.log('vertical move end'));
    let horizontalMoveEnds = horizontalMoveStarts.concatMap(lastMovesAtEnds).do(() => console.log('horizontal move end'));
    let dragMoveEnds = holds.concatMap(lastMovesAtEnds).do(() => console.log('dragging end'));

    return {
      clicks, holds,
      verticalMoveStarts, horizontalMoveStarts, 
      verticalMoves, horizontalMoves, 
      verticalMoveEnds, horizontalMoveEnds,
      dragMoves, dragMoveEnds
    };
  }
  
  return {getDragObservables};
})();

/*********
* REACT: List
**********/
const List = (() => {
  class UnconnectedList extends React.Component {
    constructor({id, title, color, edited}) {
      super({id, title, color, edited});

      this.state = {
        y: 0,
        newItemTitle: 'Pull to Create Item',
        fakeInputVisibile: false
      };

      this.newItemId = 0;
      this.itemHeight = 52;

      this.slideBack = this.slideBack.bind(this);
      this.slideToEdit = this.slideToEdit.bind(this);
      this.onFakeInputBlur = this.onFakeInputBlur.bind(this);
    }

    slideBack() {
      let lastTime = null;

      const slideBackAnimation = (time => {
        let y = null; 
        if(lastTime !== null) {
          const delta = (time - lastTime) * 0.3;
          if(this.state.y > 0) {
            y = Math.max(0, this.state.y - delta);
          }else{
            y = Math.min(0, this.state.y + delta);
          }
          this.setState({y});
        }
        lastTime = time;
        if(y !== 0) requestAnimationFrame(slideBackAnimation);
      }).bind(this);

      requestAnimationFrame(slideBackAnimation)
    }

    slideToEdit() {
      let lastTime = null;

      const slideToEditAnimation = ((resolve, reject, time) => {
        let y = this.state.y; 
        if(lastTime !== null) {
          const delta = (time - lastTime) * 0.7;
          y = Math.max(this.itemHeight, this.state.y - delta);
          this.setState({y});
        }
        console.log(y);
        lastTime = time;
        if(y > this.itemHeight) {
          requestAnimationFrame((time) => {slideToEditAnimation(resolve, reject, time)});
        }else{
          this.setState({y: 0});
          resolve();
        }
      }).bind(this);

      return new Promise((resolve, reject) => {
        requestAnimationFrame((time) => {
          slideToEditAnimation(resolve, reject, time)
        });  
      });
    }
    
    onFakeInputBlur() {
      this.setState({fakeInputVisibile: false});
    }

    render() {
      const newItemRotate = Math.max(0, Math.min(90, Math.asin(-Math.min(this.itemHeight, this.state.y)/this.itemHeight)/Math.PI*180+90));
      const fakeInput = this.state.fakeInputVisibile ? (
        <input 
          /* The fake input's role is to force the keyboard to show up on mobile devices. 
          * On mobile devices the focus can only be set to an input field from code if:
          * - the focus method is called as a direct result of a user interaction or
          * - the keyboard is already shown.
          * In this case the user interaction happens to be in this component therefore,
          * the focus can be set here, but the item desired focus is two components deep,
          * so the focus is set for the fake input to force the keyboard to show up, 
          * then reset by the real input field once the keyboard is already there.
          */
          id='fakeInput' type='text'
          ref={(fakeInput) => { this.fakeInput = fakeInput; }} 
          onBlur={this.onFakeInputBlur}/>
      ) : '';
      return (
        <div id='container'>
          <div 
            id='list'
            style={{top: Math.min(0, this.state.y)}}
            ref={(draggable) => { this.draggable = draggable; }} >
            
            <div id='newItemPerspective' style={{height: Math.max(0, this.state.y)}}>
              <div id='newItem' style={{ transform: `rotateX(${newItemRotate}deg)`}}>
                {fakeInput}
                <Item 
                  key={this.newItemId} id={this.newItemId} 
                  title={this.state.newItemTitle} color={this.props.colors[0]}/>
              </div>
            </div>            
            {this.props.items.map((item, index) => 
              <Item 
                key={item.id} id={item.id} 
                title={item.title} color={this.props.colors[index]}
                edit_mode={this.props.edit_mode} 
                edited={this.props.edited_item === item.id || this.props.edited_item === 'FIRST' && index === 0}/>
            )}
          </div>
        </div>
      );
    }

    componentDidMount() {
      const observables = util.getDragObservables(this.draggable);

      observables.verticalMoves.forEach(coordinate => {
        if(this.props.edit_mode === false) {
          if(coordinate.y > this.itemHeight) {
            this.setState({
              newItemTitle: 'Release to Create Item',
              y: coordinate.y
            });
          }else{
            this.setState({
              newItemTitle: 'Pull to Create Item',
              y: coordinate.y
            });
          }

        }
      });

      observables.verticalMoveEnds.forEach(coordinate => {
        if(this.props.edit_mode === false) {
          if(coordinate.y > this.itemHeight) {
            this.setState({
              newItemTitle: '',
              fakeInputVisibile: true
            });
            this.fakeInput.focus();
            this.slideToEdit()
              .then(this.props.appendTop);
          }else {
            this.slideBack();
          }
        }
      });
    }
  }

  const listMapStateToProps = (state, ownProps) => {
    return {
      items: state.items,
      colors: state.colors,
      edit_mode: state.edit.edit_mode,
      edited_item: state.edit.edited_item
    }
  }

  const listMapDispatchToProps = (dispatch, ownProps) => {
    return {
      appendTop: () => {
        dispatch({ type: 'APPEND_TOP' })
      },
    }
  }
  return connect(listMapStateToProps, listMapDispatchToProps)(UnconnectedList);
})();

/*********
* REACT: Item
**********/
const Item = (() => {
  class UnconnectedItem extends React.Component {
    constructor({id, title, color, edit_mode, edited}) {
      super({id, title, color, edit_mode, edited});

      this.state = {
        x: 0,
        height: 'auto',
        dragging: false,
        shrinking: false
      };

      this.itemHeight = 52;
      this.relativePosition = 0; // Relative position of the item during rearrange
      this.terminate = false;

      this.slideBack = this.slideBack.bind(this);
      this.slideDone = this.slideDone.bind(this);
      this.slideDelete = this.slideDelete.bind(this);
      this.shrink = this.shrink.bind(this);
      this.done = this.done.bind(this);
      this.delete = this.delete.bind(this);
    }

    slideBack() {
      let lastTime = null;

      const slideBackAnimation = (time => {
        let x = null; 
        if(lastTime !== null) {
          const delta = (time - lastTime) * 0.3;
          if(this.state.x > 0) {
            x = Math.max(0, this.state.x - delta);
          }else{
            x = Math.min(0, this.state.x + delta);
          }
          this.setState({x});
        }
        lastTime = time;
        if(x !== 0) requestAnimationFrame(slideBackAnimation);
      }).bind(this);

      requestAnimationFrame(slideBackAnimation)
    }

    slideDone() {
      let lastTime = null;

      const slideDoneAnimation = ((resolve, reject, time) => {
        let x = null; 
        if(lastTime !== null) {
          const delta = (time - lastTime) * 1;
          x = this.state.x + delta;
          this.setState({x});
        }
        lastTime = time;
        if(x < 375) {
          requestAnimationFrame((time) => { 
            slideDoneAnimation(resolve, reject, time); 
          });
        }else{
          resolve();
        }
      }).bind(this);

      return new Promise((resolve, reject) => {
        requestAnimationFrame((time) => {
          slideDoneAnimation(resolve, reject, time)
        });
      });
    }

    slideDelete() {
      let lastTime = null;

      const slideDeleteAnimation = ((resolve, reject, time) => {
        let x = null; 
        if(lastTime !== null) {
          const delta = (time - lastTime) * 1;
          x = this.state.x - delta;
          this.setState({x});
        }
        lastTime = time;
        if(x > -375) {
          requestAnimationFrame((time) => {
            slideDeleteAnimation(resolve, reject, time);
          });
        }else{
          resolve();
        }
      }).bind(this);

      return new Promise((resolve, reject) => {
        requestAnimationFrame((time) => {
          slideDeleteAnimation(resolve, reject, time);
        });  
      });
    }

    shrink() {
      this.setState({
        shrinking: true, 
        height: parseInt(getComputedStyle(this.draggable).getPropertyValue('height'))
      });

      let lastTime = null;

      const shrinkAnimation = ((resolve, reject, time) => {
        let height = this.state.height;
        if(lastTime !== null) {
          const delta = (time - lastTime) * 0.5;
          height = Math.max(0, this.state.height - delta);
          this.setState({height});
        }
        lastTime = time;
        if(height > 0) {
          requestAnimationFrame((time) => {
            shrinkAnimation(resolve, reject, time)
          });
        }else{
          resolve();
        }
      }).bind(this);

      return new Promise((resolve, reject) => {
        requestAnimationFrame((time) => {
          shrinkAnimation(resolve, reject, time);  
        });
      });
    }

    done() {
      this.props.done(this.props.id);
    }

    delete() {
      this.props.delete(this.props.id);
    }

    render() {
      const itemContent = this.props.edited !== true ? 
            this.props.title : 
            <ItemInput id={this.props.id} title={this.props.title} />
      return (
        <div className={"item" + (this.state.dragging === true ? " draggedItem" : "")}
          style={{
            backgroundColor: this.props.color,
            left: this.state.x,
            top: this.state.y,
            height: this.state.height,
            minHeight: (this.state.shrinking ? 0 : this.itemHeight),
            opacity: (this.props.edit_mode === true && this.props.edited === false ? 0.3 : 1)
          }}
          ref={(draggable) => { this.draggable = draggable; }} >
          <div className="itemTitle">
            {itemContent}
          </div>
        </div>
      );
    }

    componentDidMount() {
      const observables = util.getDragObservables(this.draggable);

      observables.holds.forEach(() => {
        this.relativePosition = 0;
        this.setState({dragging: true});
      });
      
      observables.dragMoves.forEach(coordinate => {
        if(this.props.edit_mode === false) {
          
          let y = coordinate.y - this.relativePosition * this.itemHeight;
          if(y > this.itemHeight) {
            this.relativePosition++;            
            this.props.moveDown(this.props.id);
          }else if(y < - this.itemHeight) {
            this.relativePosition--;
            this.props.moveUp(this.props.id);
          }
          y = coordinate.y - this.relativePosition * this.itemHeight;
          this.setState({x: coordinate.x/2, y});
        }
      });
      
      observables.dragMoveEnds.forEach(coordinate => {
        this.setState({
          x: 0,
          y: 0,
          dragging: false
        });
      });

      observables.clicks.forEach(() => {
        if(this.props.edit_mode === false) {
          this.props.editModeOn(this.props.id);
        }else if(this.props.edited === false) {
          this.props.editModeOff();
        }
      });

      observables.horizontalMoves.forEach(coordinate => {
        if(this.props.edit_mode === false) {
          this.setState({x: coordinate.x});
        }
      });

      observables.horizontalMoveEnds.forEach(coordinate => {
        if(this.props.edit_mode === false) {
          if(coordinate.x > 40) {
            this.slideDone()
              .then(this.shrink)
              .then(this.done);
          }else if(coordinate.x < - 40) {
            this.slideDelete()
              .then(this.shrink)
              .then(this.delete);
          }else {
            this.slideBack();
          }
        }
      });
    }

    componentDidUpdate() {
      if(this.terminate === false && this.props.edit_mode === false && this.props.title === '') {
        this.terminate = true;
        this.slideDelete()
          .then(this.shrink)
          .then(this.delete);
      }
    }
  }

  const itemMapDispatchToProps = (dispatch, ownProps) => {
    return {
      editModeOn: (id) => {
        dispatch({ type: 'EDIT_MODE_ON', id });
      },
      editModeOff: () => {
        dispatch({ type: 'EDIT_MODE_OFF' });
      },
      done: (id) => {
        dispatch({ type: 'DONE', id });
      },
      delete: (id) => {
        dispatch({ type: 'DELETE', id });
      },  
      moveUp: (id) => {
        dispatch({ type: 'MOVE_UP', id });
      },
      moveDown: (id) => {
        dispatch({ type: 'MOVE_DOWN', id });
      },  
    }
  }
  return connect(null, itemMapDispatchToProps)(UnconnectedItem);
})();

/*********
* REACT: Item Input
**********/
const ItemInput = (() => {
  class UnconnectedItemInput extends React.Component {
    constructor({id, title}) {
      super({id, title});

      this.keyPressed = this.keyPressed.bind(this);
      this.update = this.update.bind(this);
      this.blur = this.blur.bind(this);
    }
    
    update(event) {
      this.props.update(this.props.id, event.target.value);
    }

    keyPressed(event) {
      if (event.keyCode == 13) {  // Enter / Return key
        this.props.editModeOff();
      }
    }

    blur(event) {
      this.props.editModeOff();
    }

    render() {
      return (
        <input type="text" className="itemInput"
          value={this.props.title} 
          onChange={this.update} onKeyDown={this.keyPressed} onBlur={this.blur}
          ref={(input) => { this.input = input; }} />
      );
    }

    componentDidMount() {
      this.input.focus();
      // Unrelated curiosity: How to trigger a touch event from code: https://w3c.github.io/touch-events/#touchevent-interface
    }
  }

  const itemInputMapDispatchToProps = (dispatch, ownProps) => {
    return {
      editModeOff: () => {
        dispatch({ type: 'EDIT_MODE_OFF' });
      },
      update: (id, title) => {
        dispatch({ type: 'UPDATE', id, title });
      },
    }
  }
  return connect(null, itemInputMapDispatchToProps)(UnconnectedItemInput);
})();

/*********
* REDUX: Store
**********/
const store = (() => {
  const initialState = {
    items: [
      {id: 1, title: 'Swipe to the right to complete task'},
      {id: 2, title: 'Swipe to the left to delete item'},
      {id: 3, title: 'Pull down to create item'},
      {id: 4, title: 'Tap to edit description'},
      {id: 5, title: 'Hold and drag to rearrange'},
      {id: 6, title: 'Go to the gym'},
      {id: 7, title: 'Buy groceries'},
    ],
    colors: ['#D90015', '#DC1C17', '#DE3A17', '#E25819', '#E4751B', '#E7921B', '#E9AF1D'],
    edit: {
      edit_mode: false,
      edited_item: null
    }
  };

  const itemReducer = (state = initialState.items, action) => {
    switch (action.type) {
      case 'APPEND_TOP': {
        console.log('APPEND TOP');
        const newId = new Date().getTime(); // No, the ID does not have to be the current time, but the current time is sort of unique
        return [{id: newId, title: ''}, ...state];
      }
      case 'DONE': {
        return state.filter(item => item.id !== action.id);
      }
      case 'DELETE': {
        return state.filter(item => item.id !== action.id);
      }
      case 'MOVE_DOWN': {
        console.log('MOVE DOWN', action.id);
        const index = state.findIndex(item => item.id === action.id);
        if(index === state.length - 1) return state;
        return [
          ...state.slice(0, index),
          state[index+1],
          state[index],
          ...state.slice(index+2)
        ];
      }
      case 'MOVE_UP': {
        console.log('MOVE UP', action.id);
        const index = state.findIndex(item => item.id === action.id);
        if(index === 0) return state;
        return [
          ...state.slice(0, index-1),
          state[index],
          state[index-1],
          ...state.slice(index+1)
        ];
      }
      case 'UPDATE': {
        return state.map(item => {
          if(item.id !== action.id) {
            return item; // This isn't the item we care about - keep it as-is
          }
          return { // Otherwise, this is the one we want - return an updated value
            id: action.id,
            title: action.title
          };
        });
      }
      default:
        return state;
      }
  };

  const colorReducer = (state = initialState.colors, action) => {
    //https://gist.github.com/rosszurowski/67f04465c424a9bc0dae
    const lerpColor = (a, b, amount) => { 
      var ah = parseInt(a.replace(/#/g, ''), 16),
          ar = ah >> 16, ag = ah >> 8 & 0xff, ab = ah & 0xff,
          bh = parseInt(b.replace(/#/g, ''), 16),
          br = bh >> 16, bg = bh >> 8 & 0xff, bb = bh & 0xff,
          rr = ar + amount * (br - ar),
          rg = ag + amount * (bg - ag),
          rb = ab + amount * (bb - ab);
      return '#' + ((1 << 24) + (rr << 16) + (rg << 8) + rb | 0).toString(16).slice(1);
    }

    // Generates a range of interpolated colors from color a to b
    const colorRange = (length, a = '#D90015', b = '#E9AF1D') =>
      Array.from({length}, (value, key) => lerpColor(a, b, 1/length*key));

    switch (action.type) {
      case 'APPEND_TOP': {
        return colorRange(state.length + 1);
      }
      case 'DONE':
      case 'DELETE': {
        return colorRange(state.length - 1);
      }
      default:
        return state;
    }
  };

  const editReducer = (state = initialState.edit, action) => {
    switch (action.type) {
      case 'APPEND_TOP': {
        return {
          edit_mode: true,
          edited_item: 'FIRST'
        }
      }
      case 'EDIT_MODE_ON': {
        console.log('EDIT MODE ON', action.id);
        return {
          edit_mode: true,
          edited_item: action.id
        };
      }
      case 'EDIT_MODE_OFF': {
        console.log('EDIT MODE OFF');
        return {
          edit_mode: false,
          edited_item: null
        };
      }
      default:
        return state;
    }
  };

  const mainReducer = combineReducers({
    items: itemReducer,
    colors: colorReducer,
    edit: editReducer
  });

  return createStore(mainReducer);
})();

/*********
* REACT DOM
**********/
ReactDOM.render(
  <Provider store={store}>
    <List />
  </Provider>,
  document.getElementById('content')
);
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/react/15.4.2/react.min.js
  2. https://cdnjs.cloudflare.com/ajax/libs/react/15.4.2/react-dom.min.js
  3. https://cdnjs.cloudflare.com/ajax/libs/rxjs/2.3.22/rx.all.js
  4. https://cdnjs.cloudflare.com/ajax/libs/redux/3.6.0/redux.min.js
  5. https://cdnjs.cloudflare.com/ajax/libs/react-redux/5.0.2/react-redux.min.js