<|start_filename|>src/devices/htcone.js<|end_filename|>
import React, { Component } from "react";
import Content from './lib/Content';
import Props from './lib/Props';

export default class Htcone extends Component {
  render() {
    return (
      <div className={`marvel-device htc-one ${this.props.orientation}`}>
        <div className="top-bar" />
        <div className="camera" />
        <div className="sensor" />
        <div className="speaker" />
        <div className="screen">
          <Content show={this.props.show} />
        </div>
      </div>
    );
  }
}

Htcone.propTypes = Props;


<|start_filename|>package.json<|end_filename|>
{
  "name": "react-device-frame",
  "version": "2.2.1",
  "description": "Preview website on a device frame",
  "main": "src/index.js",
  "scripts": {
    "build": "babel src -d build",
    "lint": "eslint src/** --fix ; exit 0",
    "lint:watch": "esw -w src/**"
  },
  "keywords": "react, device, frame, browser, preview, component",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/perfectcube/react-device-frame.git"
  },
  "author": "<NAME> <<EMAIL>> (https://kimolalekan.github.io/), <NAME> <<EMAIL>>",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/perfectcube/react-device-frame/issues"
  },
  "homepage": "https://github.com/perfectcube/react-device-frame#readme",
  "devDependencies": {
    "babel-cli": "^6.26.0",
    "babel-core": "^6.26.0",
    "babel-eslint": "^7.2.3",
    "babel-preset-es2015": "^6.24.1",
    "babel-preset-react": "^6.24.1",
    "eslint": "^4.7.1",
    "eslint-plugin-import": "^2.7.0",
    "eslint-plugin-jsx-a11y": "^6.0.2",
    "eslint-plugin-react": "^7.3.0",
    "eslint-watch": "^3.1.2",
    "polished": "^1.7.0",
    "prop-types": "^15.6.2",
    "react": "^16.0.0",
    "react-dom": "^16.0.0"
  },
  "dependencies": {
    "Devices.css": "https://github.com/perfectcube/devices.css",
    "lodash": "^4.17.15",
    "react-color": "^2.17.3",
    "react-drag-drawer": "^3.3.1",
    "react-tooltip": "^3.11.1",
    "semantic-ui-css": "^2.4.1",
    "semantic-ui-react": "^0.88.1"
  }
}


<|start_filename|>src/Device.js<|end_filename|>
import React, { Component } from "react";
import PropTypes from "prop-types";
import _ from 'lodash';

// import all supported components
import Iphonex from "./devices/iphonex";
import Iphone8 from "./devices/iphone8";
import Iphone8plus from "./devices/iphone8plus";
import Iphone5s from "./devices/iphone5s";
import Iphone5c from "./devices/iphone5c";
import Iphone4s from "./devices/iphone4s";
import Ipad from "./devices/ipad";
import Macbookpro from "./devices/macbookpro";
import Nexus5 from "./devices/nexus5";
import Galaxys5 from "./devices/galaxys5";
import Galaxynote8 from "./devices/galaxynote8";
import Htcone from "./devices/htcone";
import Lumia920 from "./devices/lumia920";

// grab all props for all devices
import Props from './devices/lib/Props';

// grab our local styles
import './scss/Device.scss';

// grab the styles from node_modules/Devices.css
import 'Devices.css/assets/devices.min.css';

/**
 * used by normalize to squash values in arrays and objects to lowercase
 * @param {object|array} set an array or single level deep hash
 * @returns your set with lowercase values in it
 */
const toLower = (set) => {
  let processed = null;
  const type = (_.isArray(set)) ? 'array' : 'object';
  // set all values in the map to lowercase so it wont matter what the values we check against are stored as
  switch (type) {
    case 'array':
      processed = [];
      _.each(set,(name)=>{
        const lower = _.toLower(name);
        processed.push(lower);
      });
    break;
  
    case 'object':
    default:
      processed = {};
      _.each(set,(value,name)=>{
        const lower = _.toLower(value);
        _.set(processed,name,lower);
      });
    break;
  }
  return processed;
}

/**
 * Post processes device data to normalize specific information within the set
 * @param {DEVICES} source our device dataset/constant
 */
const normalize = (source) => {
  // iterate over the source and alter whatever is necessary in each device
  _.each(source,(data,name) => {
    // pull the colors out of device metadata and pass them to the color value normalizer
    const color = toLower(data.meta.color);
    // set the normalized colors back into the device metadata
    _.set(source,`${name}.meta.color`,color);
    // set the altered device data into the destination object
    _.set(source,name,data);
  });
  return source;
}

// our "database" of supported devices; adds metadata for each device and attaches the 
// imported components to an object key that we can use later to pull in the device that we're 
// going to render
const DEVICES = normalize({
  'galaxy-note8':{
    meta: {
      size: 'large',
      type: 'phone',
      // no color options, source: https://marvelapp.github.io/devices.css/#note8
      color: {}
    },
    component: Galaxynote8
  },
  'galaxy-s5':{
    meta: {
      size: 'small',
      type: 'phone',
      // source: https://marvelapp.github.io/devices.css/#s5
      color: {
        'white':'#bcbcbc',
        'black':'#1e1e1e',
      }
    },
    component: Galaxys5
  },
  'htc-one':{
    meta: {
      size: 'small',
      type: 'phone',
      // no color options: source: https://marvelapp.github.io/devices.css/#htc-one
      color: {}
    },
    component: Htcone
  },
  'ipad':{
    meta: {
      size: 'medium',
      type: 'tablet',
      // source: https://marvelapp.github.io/devices.css/#ipad
      color: {
        'silver': '#bcbcbc',
        'black': '#686868',
      }
    },
    component: Ipad
  },
  'iphone-4s':{
    meta: {
      size: 'small',
      type: 'phone',
      // source: https://marvelapp.github.io/devices.css/#iphone4s
      // NOTE: color options on page say black/white; this is a bug on the page. 
      // TODO: submit PR for broken docs here: http://marvelapp.github.io/devices.css/#iphone4s
      color: {
        'silver': '#bcbcbc',
        'black': '#686868',
      }
    },
    component: Iphone4s
  },
  'iphone-5c':{
    meta: {
      size: 'small',
      type: 'phone',
      // source: https://marvelapp.github.io/devices.css/#iphone5c
      color: {
        'green': '#97E563',
        'white': '#ffFFff',
        'red': '#F96b6c',
        'yellow': '#f2Dc60',
        'blue': '#33A2db',
      },
    },
    component: Iphone5c
  },
  'iphone-5s':{
    meta: {
      size: 'small',
      type: 'phone',
      // source: https://marvelapp.github.io/devices.css/#iphone5s
      color: {
        'silver': '#d9dbdc',
        'gold': '#f9e7d3',
        'black': '#464646',
      },
    },
    component: Iphone5s
  },
  'iphone-8':{
    meta: {
      size: 'medium',
      type: 'phone',
      // source: https://marvelapp.github.io/devices.css/#iphone8
      color: {
        'gold': '#f9e7d3',
        'silver': '#d9dbdc',
        'black': '#464646',
      },
    },
    component: Iphone8
  },
  'iphone-8plus':{
    meta: {
      size: 'large',
      type: 'phone',
      // source: https://marvelapp.github.io/devices.css/#iphone8plus
      color: {
        'gold': '#f9e7d3',
        'silver': '#d9dbdc',
        'black': '#464646',
      },
    },
    component: Iphone8plus
  },
  'iphone-x':{
    meta: {
      size: 'medium',
      type: 'phone',
      // no color options. source: https://marvelapp.github.io/devices.css/#iphone-x
      color: {},
    },
    component: Iphonex
  },
  'lumia-920':{
    meta: {
      size: 'small',
      type: 'phone',
      // source: https://marvelapp.github.io/devices.css/#lumia920
      color: {
        'yellow': '#ffdd00',
        'black': '#000000',
        'white': '#ffffff',
        'red': '#cc3e32',
        'blue': '#00acdd',
      },
    },
    component: Lumia920
  },
  'macbook-pro':{
    meta: {
      size: 'medium',
      type: 'laptop',
      // no color options. source: https://marvelapp.github.io/devices.css/#macbook
      color: {},
    },
    component: Macbookpro
  },
  'nexus-5':{
    meta: {
      size: 'small',
      type: 'phone',
      // no color options. source: https://marvelapp.github.io/devices.css/#nexus5
      color: {},
    },
    component: Nexus5
  },
});

/**
 * Pulls our metadata from our device dataset DEVICES. Used to fill a convenience constant -> DEVICE_META
 * @param {DEVICES} from the device dataset 
 * @returns {object} a hash of {device-type:{metadata}} pairs
 */
const fillMeta = (from) => {
  const metadata = {};
  _.each(from,(device,name)=>{
    const data = _.pick(device,['meta']);
    _.set(metadata,name,data.meta);
  });
  return metadata;
}

/**
 * Creates human readable names from device type keys. see: DEVICES->object-keys
 * @param {string} type a valid device type
 * @returns {string} a human readable name for the device
 */
const deviceName = (type) => {
  let name = _.startCase(type);
  name = name.replace('Iphone','iPhone');
  name = name.replace('Ipad','iPad');
  name = name.replace('Macbook','MacBook');
  name = name.replace('Htc','HTC');
  name = name.replace('Note 8','Note8');
  name = name.replace('Galaxy S 5','Galaxy S5');
  //name = name.replace(/([45]) [SC]/g,"$1$2");
  name = name.replace('4 S','4s');
  name = name.replace('5 S','5s');
  name = name.replace('5 C','5c');
  return name;
}

/**
 * Pass in a device type-name to see if its supported by <Device/>.
 * @param {string} type a valid device type
 * @returns {bool} true if the device is supported
 */
const deviceSupported = (type) => {
  const supported = _.includes(SUPPORTED_DEVICES,type);
  return supported;
}

/**
 * Pass in a valid device type-name to get the supported colors for the device.
 * @param {string} type a valid device type
 * @param {string} format pass in 'array' or 'object'. object will return the raw color metadata, 
 *                        an object of color-name:'hex' entries, 'array' will send back only the color names
 * @returns {object} the supported colors for device 
 */
const deviceColors = (type,format='object') => {
  let colors = {};
  if (deviceSupported(type)) {
    // get the colors for this device
    colors = _.get(DEVICE_META,`${type}.color`,{});
  }
  if (format !== 'object') {
    colors = _.keys(colors);
  }
  return colors;
}

/**
 * Pass in a valid device type-name to get the default color for the device.
 * @param {string} type a valid device type
 * @returns {string} the default color for the device
 */
const deviceColor = (type,format='object') => {
  let color = null;
  const colors = deviceColors(type);
  // NOTE: we're not guarding for a valid device here because deviceColors() does that for us. 
  // * we do send back a null if the device name is invalid though...
  const have_colors = !_.isEmpty(colors);
  if (have_colors) {
    const names = _.keys(colors);
    // use the first entry in the array as the default color
    color = _.first(names);
    if (format === 'object') {
      color = {[color]:colors[color]}
    }
  }
  return color;
}

/**
 * From a given {value} lookup it's key, or from a given key lookup it's {value} in the device's available colors. For
 * example if a deice supports the color {'red':'#f96b6c'} and you pass in "red" you'll get back '#f96b6c' or if you 
 * pass in '#f96b6c' you'll get back 'red'.
 * @param {string} type a valid device type. use Device.getDevices() to see all devices and metadata for each device.
 * @param {string} value a string to lookup in the device's meta.color table. 
 * @returns {string|null} null if no mappable value found
 */
const colorMap = (type,value) => {
  // get all available colors for this device {type}
  const colors = deviceColors(type);
  // clone the available colors into an object we can alter
  let map = _.clone(colors);
  // invert the map so the keys are now the values
  const invert = _.invert(map);
  // merge the inverted values into the map
  map = _.merge(map,invert);
  // set the check value to lowercase so it wont matter what case the incoming value is set as
  const check = _.toLower(value);
  // lookup the value in th map pulling out the #HEX or 
  // the name based on what was passed in through value
  // send back the default if no value can be found
  const found = _.get(map,check,null);
  return found;
}

/**
 * Gets all supported color names for the device {type}
 * @param {string} type a valid device type. use Device.getDevices() to see all devices and metadata for each device.
 * @returns {array} an array of supported human readable color names for the device {type}
 */
const colorNames = (type) => {
  const available = deviceColors(type);
  const names = _.keys(available);
  return names;
}

/**
 * Gets all supported #hex color values for the device {type}
 * @param {string} type a valid device type. use Device.getDevices() to see all devices and metadata for each device.
 * @returns {array} an array of supported #hex color values for the device {type}
 */
const colorValues = (type) => {
  const available = deviceColors(type);
  const values = _.values(available);
  return values;
}

/**
 * Validates {using} against the device {type} and uses the default color for the device if {using} isn't a supported color
 * @param {string} type a valid device type. use Device.getDevices() to see all devices and metadata for each device.
 * @param {string} using a human readable color name, like "red". 
 * @returns {string|null} returns the color you passed in through {using} if valid, the default color for the device {type}, or null
 *                        if the device doesn't have a default color 
 */
const colorUse = (type,using) => {
  const available = deviceColors(type);
  const fallback = deviceColor(type,'name');
  // set the check value to lowercase so it wont matter what case the incoming value is set as
  using = _.toLower(using);
  // use the color in props or the fallback color
  const color = _.get(available,using,available[fallback]);
  return color;
}

/**
 * Pass in a valid device type-name and get back an array of info-* points mined from the device metadata.
 * @param {string} type a valid device type
 * @returns {array} an array of calculated information about the device
 */
const deviceStats = (type) => {
  const stats = [];
  if (deviceSupported(type)) {
    const meta = _.get(DEVICE_META,type,{});
    // add the device size to the stats
    stats.push(`info-size-${meta.size}`);
    // add the device type to the stats
    stats.push(`info-type-${meta.type}`);
    // add the number of colors the device supports to the stats
    const color_count = _.keys(meta.color).length;
    stats.push(`info-colors-${color_count}`);
  }
  return stats;
}

// a hash of {device-type:{metadata}} objects
export const DEVICE_META = fillMeta(DEVICES);
// an array of supported devices
export const SUPPORTED_DEVICES = _.keys(DEVICES);
// the default device. this is used if an invalid device is passed in through props.use
const DEFAULT_DEVICE = 'iphone-x';

export default class Device extends Component {

  static propTypes = {
    use: PropTypes.oneOf(SUPPORTED_DEVICES).isRequired,
    ...Props
  };

  static defaultProps = {
    title: '',
    use: DEFAULT_DEVICE,
    orientation: 'portrait',
  }
  
  // constructor(props){
  //   super(props)
  //   // TODO: figure out what colors are supported and enumerate that here in combination with props.use. 
  //   // + send a message to the console about using an invalid color for the props.use value when an invalid
  //   // + value is detected
  // }

  /**
   * @param {string} type a valid device type. use Device.getDevices() to see all devices and metadata for each device.
   * @param {object} cfg a configuration object with the following shape:
   *                     @as {string} can be "raw" or "names".  
   * @returns {array|object} if (cfg.as === "names") an array of color names. if (cfg.as === 'raw') an object with 
   *                          the all supported colors in it as a {name:'#HEX-COLOR',name2:'#HEX-COLOR'} pairs.
   */
  static getColors(type,cfg={}){
    cfg=_.merge({
      as:'raw' // this can be "raw" or "names"
    },cfg);
    const format = (cfg.as === 'names') ? 'array' : 'object';
    const colors = deviceColors(type,format);
    return colors;
  }

  /**
   * Get a list of all supported devices as keys and each devices metadata as values
   * @returns {object} a hash of {device-type:{metadata}} objects
   */
  static getDevices() {
    const devices = {};
    _.each(DEVICES,(device,name)=>{
      const data = _.pick(device,'meta');
      _.set(devices,name,data.meta);
    });
    return devices;
  }

  /**
   * Pass in a valid device type-name to get the default color for the device. 
   * @param {string} type a valid device type. use Device.getDevices() to see all devices and metadata for each device.
   * @param {object} cfg a configuration object with the following shape:
   *                     @as {string} can be "raw" or "name". if you're getting the default device color and don't want to pull all the 
   *                                  colors separately with Device.getColors() to get the value then use {as: 'raw'} 
   * @returns {string|object} if (cfg.as === name) a string with the default color in it. if (cfg.as === name) an object with 
   *                          the default color in it as a {name:'#HEX-COLOR'} pair.
   */
  static getColor(type,cfg={}){
    cfg=_.merge({
      as:'raw' // this can be "raw" or "name"
    },cfg);
    const format = (cfg.as === 'name') ? 'array' : 'object';
    const color = deviceColor(type,format);
    return color;
  }

  /**
   * Get an array of info-* strings that let you know about the device
   * @param {string} type a valid device type. use Device.getDevices() to see all devices and metadata for each device.
   * @returns {array} an array of strings, formatted device metadata
   */
  static getStats(type){
    const info = deviceStats(type);
    return info;
  }

  /**
   * Get the human readable formatted name of the device from it's device type (key).
   * @param {string} type a valid device type. use Device.getDevices() to see all devices and metadata for each device.
   * @returns {string}
   */
  static getName(type){
    const name = deviceName(type);
    return name;
  }

  /**
   * Check if a color {check} (name or value) is valid for a given device {type}
   * @param {string} type a valid device type. use Device.getDevices() to see all devices and metadata for each device.
   * @param {string} check a color value or name to check
   * @returns {bool}
   */
  static colorValid(type,check){
    const mapped = colorMap(type,check);
    const valid = (mapped !== null) ? true : false;
    return valid;
  }

  /**
   * Validates {using} against the device {type} and uses the default color for the device if {using} isn't a supported color
   * @param {string} type a valid device type. use Device.getDevices() to see all devices and metadata for each device.
   * @param {string} using a human readable color name, like "red". 
   * @returns {string|null} returns the color you passed in through {using} if valid, the default color for the device {type}, or null
   *                        if the device doesn't have a default color 
   */
  static colorUse(type,using){
    const choose = colorUse(type,using);
    return choose;
  }

  /**
   * Gets all supported color names for the device {type}
   * @param {string} type a valid device type. use Device.getDevices() to see all devices and metadata for each device.
   * @returns {array} an array of supported human readable color names for the device {type}
   */
  static colorNames(type){
    const colors = colorNames(type);
    return colors;
  }

  /**
   * Gets all supported #hex color values for the device {type}
   * @param {string} type a valid device type. use Device.getDevices() to see all devices and metadata for each device.
   * @returns {array} an array of supported #hex color values for the device {type}
   */
  static colorValues(type){
    const colors = colorValues(type);
    return colors;
  }

  /**
   * From a given {value} lookup it's key, or from a given key lookup it's {value} in the device's available colors. For
   * example if a deice supports the color {'red':'#f96b6c'} and you pass in "red" you'll get back '#f96b6c' or if you 
   * pass in '#f96b6c' you'll get back 'red'.
   * @param {string} type a valid device type. use Device.getDevices() to see all devices and metadata for each device.
   * @param {string} value a string to lookup in the device's meta.color table. 
   * @returns {string|null} null of no mappable color found
   */
  static colorMap(type,value){
    const color = colorMap(type,value);
    return color;
  }

  /**
   * Gets the component for a given device name. 
   * WARNING: this method does not guard invalid device names. check {name} with deviceSupported(name) before you call createDevice()
   * @param {string} name a supported device name that you've already checked with deviceSupported(name)
   */
  createDevice(name){

    const orientation = (this.props.orientation === 'landscape') ? "landscape" : "portrait";

    // try to use the requested color
    const try_color = this.props.color;
    // validate the requested color. use it if valid, fallback on the default.
    const color_hex = colorUse(name,try_color);
    // color use returns a hex value. map it to the color name for the device.
    const color_name = colorMap(name,color_hex);

    // render children if we have them, otherwise render whatever is in props.show 
    // NOTE: rendering of props.show is handled by ./devices/Content.js
    // * if props.show is a string we assume it's a valid url and slap that puppy into an <iframe> ... enjoy!
    const content = (this.props.children) ? this.props.children : this.props.show;

    const TheDevice = DEVICES[name].component;

    return (
      <TheDevice
        color={color_name}
        orientation={orientation}
        show={content}
        title={this.props.title}
      />
    );
  }

  getDevice(type) {
    let device = null;
    if(deviceSupported(type)){
      device = this.createDevice(type);
    }
    // an invalid device was requested. use the default device and show a warning message.
    else{
      const supported = SUPPORTED_DEVICES.join(', ');
      const default_name = deviceName(DEFAULT_DEVICE);
      const message = [
        `<Device /> can’t render name="${type}" because this device type isn't supported. Instead we’ve rendered a ${default_name} for you.`,"\n",
        `If you don’t like the default you can specify one of these: ${supported}.`
      ].join(' ');
      console.warn(message);
      device = this.createDevice(DEFAULT_DEVICE);
    }
    return device; 
  }

  render() {
    const device = this.getDevice(this.props.use);
    return (<div className="react-device-frame actual">{device}</div>);
  }
}


<|start_filename|>src/devices/lib/Props.js<|end_filename|>
import PropTypes from "prop-types";

// used in content display area. see <Content/>
const ContentProps = {
  title: PropTypes.string,
  show: PropTypes.oneOfType([ 
    PropTypes.string, 
    PropTypes.elementType,
    // This covers the case where we're passing in children instead of show=""
    PropTypes.any,
  ]),
}
// Additional props used by device types. see ../devices
const FrameProps = {
  color: PropTypes.string,
  orientation: PropTypes.oneOf(['portrait','landscape']),
};

// combined props for all device types and the base props for <Device/>
const Props = {
  ...ContentProps,
  ...FrameProps,
}

export default Props;
export {
  ContentProps,
  FrameProps,
}

<|start_filename|>src/lib/reSize.js<|end_filename|>
import React, { Component } from "react";
import _ from 'lodash';

export default function reSize(Wrapped) {
  return class extends Component {
    
    constructor(props){
      super(props);
      this.state = { 
        width: 0, 
        height: 0 
      };
    }

    componentDidMount() {
      this.update();
      window.addEventListener("resize", this.update);
    }

    componentWillUnmount() {
      window.removeEventListener("resize", this.update);
    }

    update = () => {
      this.setState({ 
        width: window.innerWidth, 
        height: window.innerHeight,
      });
    };

    report(){
      return _.cloneDeep(this.state);
    }

    render() {
      return (
        <Wrapped
          {...this.props}
          reSize={{
            width: this.state.width,
            height: this.state.height,
          }}
        />
      );
    }
  };
}

<|start_filename|>src/Dock.js<|end_filename|>
import React, { Component } from "react";
import PropTypes from 'prop-types';
import Drawer from 'react-drag-drawer';
import _ from 'lodash';
import {
  Button,
  Dropdown,
  Icon,
} from 'semantic-ui-react';
import 'semantic-ui-css/semantic.min.css';
import ReactTooltip from 'react-tooltip';
import { GithubPicker } from 'react-color';
import reSize from './lib/reSize';
import diff from './lib/diff'
// pull in Component and utils
import Device, {
  SUPPORTED_DEVICES,
} from './Device';
import './scss/Dock.scss';

import {CLASSES,DEFAULTS,ZOOM_MAP,ZOOM_LEVELS,ZOOMABLE} from './lib/constants';

class Dock extends Component {
  
  // holds refs for the drawer
  drawerRef = {
    container: null,
    overlay: null,
    content: null,
  }

  // Requirements for props
  static propTypes = {
    // the device that you want to render as the initial device. Note that its possible to 
    // pass in an invalid value if you're passing in a device that you've also set in 
    // props.blacklist
    device: PropTypes.oneOf(SUPPORTED_DEVICES).isRequired,
    // text for the close button
    close: PropTypes.string.isRequired,
    // text for the open button
    open: PropTypes.string.isRequired,
    // add a tooltip to the open drawer button
    tooltip: PropTypes.string,
    // text for the zoom menu
    zoom: PropTypes.shape({
      full:PropTypes.string,
      large:PropTypes.string,
      med:PropTypes.string,
      small:PropTypes.string,
    }).isRequired,
    view: PropTypes.oneOf(['full','large','med','small']).isRequired,
    // a function that is called whenever the zoom level or device type is changed
    // your function is passed an object with this shape as it's only argument: {
    //   zoom {string} one of: 'full','large','med','small'. this is the currently selected zoom level.
    //   device {string} one of <Device/>.SUPPORTED_DEVICES, like: "the-device-slug"
    //   orient {string} "landscape" or "portrait" 
    //   color {string} the currently selected color name. map this to the #hex color for the 
    //   <Device/> with Device.colorMap('device-type','color-name')
    //   float {string} "right" or "left"
    // }
    onData: PropTypes.func,
    // an array of device names that you want to remove from the list of SUPPORTED_DEVICES
    hide: PropTypes.array,
    // show or hide specific controls
    show: PropTypes.shape({
      zoom: PropTypes.bool,
      switcher: PropTypes.bool,
      color: PropTypes.bool,
      orientation: PropTypes.bool,
    }), 
    // add padding in px to the top of the drawer. handy if you have a header at the top of your page.
    padTop: PropTypes.number,
    // TODO: add an orientation selector
    // preselect an orientation for the orientation selector control
    orientation: PropTypes.oneOf(['portrait','landscape']),
  }
  
  // Defaults for props
  static defaultProps = {
    // default labels for the clone button
    close: 'Hide Preview',
    // default label for the open button
    open: 'Live Preview',
    tooltip: '',
    zoom: DEFAULTS.zoom,
    view: 'med',
    onData: null,
    show: DEFAULTS.show,
    hide: [],
    padTop: 0,
    orientation: 'portrait',
  }

  constructor(props){
    super(props);
    // merge any missing defaults from props.zoom from DEFAULTS.zoom 
    const zoom_text = _.merge(DEFAULTS.zoom,props.zoom);
    // get the zoom level for the human readable key in props.view
    const zoom_view = ZOOM_MAP[props.view];
    // remove devices from supported_devices if they appear in props.blacklist
    const supported_devices = _.xor(SUPPORTED_DEVICES,props.hide);
    // merge default into props.show
    const show_controls = _.merge(DEFAULTS.show,props.show);

    this.state = {
      // is the drawer open?
      open: false,

      // @DEVICE DISPLAY SIZE
      // the zoom menu text labels
      zoom: zoom_text,
      // the zoom level, an integer, one of: 70, 80, 90, or 100.
      view: zoom_view,

      // @PREVIEW LOCATION
      // is the preview at the right had side? false for left
      float: 'right', // this is "left" or "right"
      
      // @device display orientation
      // TODO: add orientation menu
      orient: props.orientation, // this is landscape or portrait

      // @DEVICE COLORS
      // the currently selected color
      color: 'black',
      // is the color chooser visible?
      chooser: false,

      // @DEVICES
      // which device are we viewing? see <Device/> -> SUPPORTED_DEVICES for supported labels
      device: props.device,
      // a list of supported devices
      supported: supported_devices,
      // show or hide specific controls
      show: show_controls,
    }

    this.measureDrawer = this.measureDrawer.bind(this);
    this.chooserToggle = this.chooserToggle.bind(this);

  }

  getDrawer(){
    const drawer = document.getElementById('react-device-frame-drawer');
    return drawer;
  }

  getOpenWrapper(){
    const wrapper = document.getElementById('react-device-frame-drawer__controls');
    return wrapper;
  }

  componentDidMount() {
    this.measureDrawer();
    window.addEventListener("resize", this.measureDrawer);
  }

  componentWillUnmount() {
    window.removeEventListener("resize", this.measureDrawer);
  }

  componentDidUpdate(prevProps, prevState){
    // figure out if state changed
    const state_changes = diff(this.state,prevState);
    const state_changed = !_.isEmpty(state_changes);
    // if state did change then pass the change back through props.onData
    if (state_changed) {
      // pull the items out of state that we need to send pack to the parent
      const {view,device,float,orient,color} = this.state;
      // if we have an onData callback defined then send it the items we've pulled out of state
      if (_.isFunction(this.props.onData)) {
        this.props.onData({
          zoom: ZOOM_MAP[view],
          device: device,
          float: float,
          orient: orient,
          color: color,
        });
      }
    }
    
  }

  setFloat(to){
    this.setState({ float: to },()=>{
      this.measureDrawer();
    });
  }

  cleanFloat(from){
    from.classList.remove(`at-left`);
    from.classList.remove(`at-right`);
  }

  setZoom(to){
    this.setState({ view: to },()=>{
      this.measureDrawer();
    });
  }

  cleanZoom(from){
    _.each(ZOOM_LEVELS,(level) => {
      from.classList.remove(`zoom-${level}`);
    });
  }

  setDevice(to){
    const wrapper = this.getOpenWrapper();
    this.cleanDevice(wrapper);
    this.setState({ device: to },()=>{
      this.toggle();
    });
  }

  cleanDevice(from){
    _.each(SUPPORTED_DEVICES,(name) => {
      from.classList.remove(`device-${name}`);
    });
  }

  viewportChanged(top){
    const element = this.getDrawer();
    if (top === true) {
      element.classList.remove('at-bottom');
      element.classList.add('at-top');
    }
    else {
      element.classList.remove('at-top');
      element.classList.add('at-bottom');
    }
  }

  // type selects a state key, one of 'device' or 'view'
  selected(type,select){
    const icon = (select === this.state[type]) ? (<Icon name="circle" size='mini' />) : null;        
    return icon;
  }

  toggle = () => {
    const { open } = this.state;
    const wrapper = this.getOpenWrapper();

    if (open === true) {
      wrapper.classList.add(CLASSES.drawer_open);
    }
    else {
      wrapper.classList.remove(CLASSES.drawer_open);
    }

    this.setState({ open: !open },()=>{
      this.measureDrawer();
    });
  }

  applyPadding(to){
    // pull the zoom level out of state
    const {view} = this.state;
    // get the un-zoomed device height
    const device_height = _.round(this.drawerRef.overlay.getBoundingClientRect().height);
    // get the height of the window form the reSize HOC
    const window_height = this.props.reSize.height;
    // output padding in px
    const units = 'px';
    // each close control is 30px (30.5px) tall but positioned relative so they collectively take up ≈60px
    const fixed_pad = 60; 
    // the amount of padding passed into the <Drawer/>
    const props_pad = this.props.padTop;
    // the total amount of additional padding when factoring in buttons and requested padding
    const extra = fixed_pad + props_pad;
    // the perceptual device height when zoomed
    const adjusted_device_height = _.round(device_height * (view/100));
    // the leftover height after removal of the device height from the window height
    const oversize =  (adjusted_device_height - window_height);
    // default to providing only the padding requested by props.padTop
    let provide_padding = props_pad;
    // if the perceptual size of the device is larger than the viewport then provide adjusted padding based on the
    // zoom settings
    if (oversize > 0) {
      // the padding left over after factoring in the perceived height when zoom resized by {view}
      const adjusted_padding = _.round(oversize * (view/100));
      // the needed padding after adding in the buttons and the requested padding
      const padding = adjusted_padding + extra;
      // only provide additional padding if we need it because the device is taller than the window,
      // otherwise, provide the requested padding from props.topPad + our buttons
      provide_padding = (adjusted_device_height >= window_height ) ? padding : props_pad;        
    }
    // apply padding to the drawer
    to.style.paddingTop = `${provide_padding}${units}`;
  }

  attachClasses(to){
    // pull the items out of state that we need to attach to the drawer
    const {view,device,float} = this.state;

    this.cleanZoom(to);
    this.cleanDevice(to);
    this.cleanFloat(to);
    to.classList.add(`zoom-${view}`);
    to.classList.add(`device-${device}`);
    to.classList.add(`at-${float}`);

  }

  measureDrawer(){
    // if the drawer is open then add classes and padding to it
    const drawer = this.getDrawer();
    if (drawer !== null) {
      this.applyPadding(drawer);
      this.attachClasses(drawer);
    }
  }

  chooserToggle(){
    // pull out the current value of chooser from state
    const {chooser:visible} = this.state;
    // swap the value of visible and reset that value into state
    this.setState({chooser: !visible});
  }

  // callback for react-color-picker. triggered while hovering over color swatches
  // NOTE: left in because we might add hover effects on the 
  // * device color switcher 
  // chooseColorHover(hovered){
  //   const {device} = this.state;
  //   const name = Device.colorMap(device,hovered.hex);
  //   console.table({
  //     '@':'chooseColorHover()',
  //     color: hovered.hex,
  //     name: name,
  //   });
  // }

  chooseColorComplete(chosen){
    const {device} = this.state;
    const name = Device.colorMap(device,chosen.hex);
    // set the selected color name into state
    this.setState({color: name});

  }

  controlsList(){
    const available = {
      color: (this.state.show.color),
      zoom: (this.state.show.zoom),
      float: (this.state.show.float),
      orient: (this.state.show.orient),
    }
    const classes = [];
    _.each(available,(value,name)=>{
      const readable = (value === true) ? 'has' : 'no';
      classes.push(`controls--${readable}-${name}`);
    });
    const list = classes.join(' ');
    return list;
  }

  render() {

    const { open, float, color } = this.state;

    // all colors available for the requested device
    const colors_available = Device.getColors(this.props.device);
    // the default color for the requested device
    const color_default = Device.getColor(this.props.device,{as:'name'});

    const selectable = {
      all: colors_available,
      default: color_default,
      use: color,
    };

    // TODO: add orientation menu for landscape/portrait

    const CloseButton = (props) => {
      return (
        <Button 
            labelPosition="left"
            icon
            // TODO: allow configurable button colors. see: https://react.semantic-ui.com/elements/button/#variations-colored
            color="blue" 
            size="small"
            circular
            className={`toggle toggle-close shadow ${props.classes}`} 
            onClick={()=>props.action()}
        >
          <Icon inverted name="close" />
          {props.text}
        </Button>
      );
    }

    const ZoomMenu = (props) => {
      
      const items = _.map(ZOOMABLE,(value,human) => {
        const item = (
          <Dropdown.Item
            key={value}
            onClick={(Event)=>{ 
                props.onChoice(value); 
            }}
          >
            {props.getSelected('view',value)}{props.text[human]}
          </Dropdown.Item>
        );
        return item;
      });

      const trigger = (
        <Button 
          // TODO: allow configurable button colors. see: https://react.semantic-ui.com/elements/button/#variations-colored
          // color="gray" 
          circular
          className="circle shadow"
          // onClick={()=>{ props.action(next); }}
        >
          <Icon name="setting" />
        </Button>
      );

      return (
        <Dropdown 
            compact
            pointing
            floating
            closeOnBlur={true}
            trigger={trigger}
            className="zoom-menu"
        >
          <Dropdown.Menu>{items}</Dropdown.Menu>
        </Dropdown>
      );
    }

    const FloatButton = (props) => {
      // get the next position for the float button as te opposite of it's 
      // current position
      const next = (props.showing === 'right') ? 'left' : 'right';
      return (
        <Button 
            // TODO: allow configurable button colors. see: https://react.semantic-ui.com/elements/button/#variations-colored
            // color="grey" 
            circular
            className="toggle toggle-float circle shadow"
            onClick={()=>{ props.action(next); }}
        >
          <Icon name={`arrow circle ${next}`} />
        </Button>
      );
    }

    const DeviceMenu = (props) => {
      
      return (
        <Dropdown 
            icon="setting" 
            text="Choose Device" 
            pointing="right"
            closeOnBlur={true}
            className="device-menu shadow"
        >
          <Dropdown.Menu>
          {props.items.map((item, index) =>{
            // cleanup each name to match the proper name for the device
            const name = Device.getName(item);
            return (
              <Dropdown.Item
                  key={index} 
                  className="" 
                  onClick={(Event)=>{ 
                    props.onChoice(item); 
                  }}
                  direction='right'
              >
                {props.getSelected('device',item)}{name}
              </Dropdown.Item>
            )}
          )}
          </Dropdown.Menu>
        </Dropdown>
      );
    }

    const ColorSwitch = (props) => {
      const colors = Device.colorValues(this.state.device);
      const using = Device.colorUse(this.state.device,props.selections.use);
      const have_colors = !_.isEmpty(colors);
      return (have_colors) ? (
        <React.Fragment>
          <Button 
            basic 
            className="shadow color-chooser"
            onClick={(Event)=>this.chooserToggle(Event)}
          >
            <i className="fa fa-circle fa-xs" style={{
              color: using,
              backgroundColor: using,
            }}></i>
          </Button>
          { (props.visible) ? (
          <GithubPicker 
              className="color-chooser-choices"
              // width="212px"
              colors={colors}
              color={using}
              onChange={(Color) => this.chooserToggle(Color)}
              onChangeComplete={(Color, Event) => {
                this.chooseColorComplete(Color);
              }} 
              // NOTE: left in because we might add hover effects on the 
              // * device color switcher 
              // onSwatchHover={ (color, event) => {
              //   // console.log({
              //   //   'swatch':'hover!',
              //   //   color:color,
              //   //   event:event
              //   // });
              //   this.chooseColorHover(color);
              // }} 
              triangle={(props.opens === 'left') ? "top-left" : "top-right"}
          />
          ) : null }
        </React.Fragment>
      ) : null;
    } 

    // TODO: add orientation controls
    // const Orientation = (props) => {
    //   return (
    //     <React.Fragment>

    //     </React.Fragment>
    //   );
    // } 

    const OpenButton = (props) => {
      const drawer_open_class = (props.open === true) ? CLASSES.drawer_open : '';
      const switcher_class = (props.switcher === true) ? 'with-device-menu' : 'no-device-menu';
      return (
        <div id="react-device-frame-drawer__controls" className={`control-wrapper ${drawer_open_class} ${switcher_class}`}>
          <div className="buttons">
            <Button 
                // TODO: allow configurable button colors. see: https://react.semantic-ui.com/elements/button/#variations-colored
                color="blue" 
                size="small"
                circular
                className="btn-pill toggle toggle-open shadow" 
                onClick={ ()=>{ props.toggle()} }
                data-tip
                data-for="react-device-frame-drawer__controls-hint"
                // INFO: Use these data-event attributes to style the tooltip and the content 
                // data-event='click' 
                // data-event-off='dblclick'
            >
              <Icon name="mobile alternate" />
              {props.text}
            </Button>
            { (props.switcher) ? (
            <DeviceMenu 
                items={props.menu.items}
                direction="down"
                getSelected={ (type,value)=>{ return props.menu.getSelected(type,value) }}
                onChoice={ (chosen) => { props.menu.onChoice(chosen); }}
            />
            ) : null }
          </div>
          { 
           // only show a tooltip if one was sent in through Dock.props.tooltip
           (props.tooltip) ? (
          <ReactTooltip 
              id="react-device-frame-drawer__controls-hint"
              place="left" 
              type="dark"
              effect="solid"
              aria-haspopup="true"
              // allow clicking links inside tooltip
              // clickable={true}
              // delayHide={500}
              getContent={(data) => ( props.tooltip )}
          />
          ) : null}
        </div>
      );
    }

    const controls_list = this.controlsList();

    return(
      <React.Fragment>
        <OpenButton 
            open={open}
            text={this.props.open}
            tooltip={this.props.tooltip}
            toggle={() => { this.toggle() }}
            menu={{
              items: this.state.supported,
              onChoice: (chosen) => { this.setDevice(chosen); },
              getSelected: (type,value) => { return this.selected(type,value); }
            }}
            switcher={this.state.show.switcher}
        />
        <Drawer
            id="react-device-frame-drawer"
            className="react-device-frame dock"
            open={open}
            allowClose={true}
            // direction
            onRequestClose={this.toggle}
            inViewportChange={(at_top)=>{
              this.viewportChanged(at_top);
            }}
            onOpen={()=>{
              this.measureDrawer();
            }}
            // TODO: bug report on Non-functional callback? better double check...
            // notifyWillClose={(a)=>{console.log({'@':'notifyWillClose()','a':a})}}
            modalElementClass="react-device-frame drawer-modal"
            containerElementClass="react-device-frame drawer-main"
            getContainerRef={(Ref)=>{
              const have_ref = !_.isEmpty(Ref);
              if(have_ref){
                this.drawerRef['container'] = Ref;
              }
            }}
            getModalRef={(Ref)=>{
              const have_ref = !_.isEmpty(Ref);
              if(have_ref){
                this.drawerRef['overlay'] = Ref;
              }
            }}
        >
          <div className={`close-control zoom-reset close-control-top ${controls_list}`}>
            { (this.state.show.float) ? (
            <FloatButton 
                showing={this.state.float}
                action={(to)=>this.setFloat(to)}
            />
            ) : null }
            <CloseButton 
                classes="toggle-top"
                text={this.props.close}
                action={()=>{this.toggle()}}
            />
            { (this.state.show.zoom) ? (
            <ZoomMenu 
                text={this.props.zoom}
                onChoice={(to)=>this.setZoom(to)}
                getSelected={(type,value)=>{ return this.selected(type,value) }}
            />
            ) : null }
            { (this.state.show.color) ? (
            <ColorSwitch 
                visible={this.state.chooser} 
                chosen={this.state.color} 
                opens={float}
                selections={selectable}
            />
            ) : null }
          </div>
          <div className="react-device-frame content-main" ref={(element)=>{ this.drawerRef['content'] = element}}>
            {this.props.children}
          </div>
          <div className="close-control zoom-reset close-control-bottom">
            { (this.state.show.float) ? (
            <FloatButton 
                showing={this.state.float}
                action={(to)=>this.setFloat(to)}
            />
            ) : null }
            <CloseButton 
                classes="toggle-bottom"
                text={this.props.close}
                action={()=>{ this.toggle() }}
            />
            { (this.state.show.zoom) ? (
            <ZoomMenu 
                text={this.props.zoom}
                onChoice={(to)=>this.setZoom(to)}
                getSelected={(type,value)=>{ return this.selected(type,value) }}
            />
            ) : null }
            { (this.state.show.color) ? (
            <ColorSwitch 
                visible={this.state.chooser} 
                chosen={this.state.color} 
                opens={float}
                selections={selectable}
            />
            ) : null }
          </div>
        </Drawer>
      </React.Fragment>
    )
  }
}

export default reSize(Dock);

<|start_filename|>src/index.js<|end_filename|>
import Device from './Device';
import Dock from './Dock';

export default Device;
export {
  Dock,
};

<|start_filename|>src/lib/constants.js<|end_filename|>
import _ from 'lodash';

// various classes that get tagged into the dom when state changes
const CLASSES = {
  drawer_open: 'drawer-open',
  at_top: 'at-top',
}

const DEFAULTS = {
  // the default labels for the zoom menu buttons
  zoom: {
    full: 'Huge',
    large: 'Large',
    med: 'Medium',
    small: 'Small',
  },
  show: {
    zoom: true,
    switcher: true,
    color: true, 
    orientation: true,
    float: true,
  }
}

// Map zoom text keys to zoom levels. Zoom text keys are used 
// to map props to viewable text, Zoom Levels provide the numeric
// levels that each text key sets into the dom when a zoom level changes
const ZOOMABLE = {
  'full': '100',
  'large': '90',
  'med': '80',
  'small': '70',
}

// flip the zoom_levels and merge the results back into the zoom_map
// so we can access a level from a label and a label from a level
const ZOOM_MAP = _.merge(_.clone(ZOOMABLE),_.invert(ZOOMABLE));

// the numeric levels that each text key sets into the dom when a zoom level changes
const ZOOM_LEVELS = [
  '100',
  '90',
  '80',
  '70'
];

export {
  CLASSES,
  DEFAULTS,
  ZOOM_MAP,
  ZOOM_LEVELS,
  ZOOMABLE,
}


<|start_filename|>src/lib/diff.js<|end_filename|>
import { transform, isEqual, isObject } from "lodash/fp";

const _transform = transform.convert({
	cap: false
});

const iteratee = baseObj => (result, value, key) => {
	if (!isEqual(value, baseObj[key])) {
		const valIsObj = isObject(value) && isObject(baseObj[key]);
		result[key] = valIsObj === true ? diff(value, baseObj[key]) : value;
	}
};

/**
 * 
 * @param {*} targetObj 
 * @param {*} baseObj 
 * @source https://gist.github.com/Yimiprod/7ee176597fef230d1451#gistcomment-2324809
 */
const diff = (targetObj, baseObj) => {
	return _transform(iteratee(baseObj), null, targetObj);
}

export default diff;

<|start_filename|>src/devices/lib/Content.js<|end_filename|>
import React, { Component } from "react";
import ContentProps from './Props';

class Content extends Component {

  static propTypes = ContentProps;

  render() {

    const probably_url = (typeof this.props.show === 'string');

    const rendered = ( 
      <React.Fragment>
        {(probably_url) ? (
          <iframe title={this.props.title} style={{
            width: '100%',
            height: '100%',
            border: 0,
          }} src={this.props.show} />
        ) : (
          <div>{this.props.show}</div>
        )} 
      </React.Fragment>
    );

    return rendered;
  }
}

export default Content;

<|start_filename|>src/devices/lumia920.js<|end_filename|>
import React, { Component } from "react";
import Content from './lib/Content';
import Props from './lib/Props';



export default class Lumia920 extends Component {
  render() {
    
    const color = (this.props.color) ? this.props.color : "yellow";

    return (
      <div
        className={`marvel-device lumia920 ${this.props.orientation} ${color}`}
      >
        <div className="top-bar" />
        <div className="volume" />
        <div className="camera" />
        <div className="speaker" />
        <div className="screen">
          <Content show={this.props.show} />
        </div>
      </div>
    );
  }
}

Lumia920.propTypes = Props;


<|start_filename|>src/devices/macbookpro.js<|end_filename|>
import React, { Component } from "react";
import Content from './lib/Content';
import Props from './lib/Props';

export default class Macbookpro extends Component {
  render() {
    return (
      <div className={`marvel-device macbook`}>
        <div className="top-bar" />
        <div className="camera" />
        <div className="screen">
          <Content show={this.props.show} />
        </div>
        <div className="bottom-bar" />
      </div>
    );
  }
}

Macbookpro.propTypes = Props;


<|start_filename|>src/devices/galaxynote8.js<|end_filename|>
import React, { Component } from "react";
import Content from './lib/Content';
import Props from './lib/Props';

export default class Galaxynote8 extends Component {
  render() {
    return (
      <div className={`marvel-device note8 ${this.props.orientation}`}>
        <div className="inner" />
        <div className="overflow">
          <div className="shadow" />
        </div>
        <div className="speaker" />
        <div className="sensors" />
        <div className="more-sensors" />
        <div className="sleep" />
        <div className="volume" />
        <div className="camera" />
        <div className="screen">
          <Content show={this.props.show} />
        </div>
      </div>
    );
  }
}

Galaxynote8.propTypes = Props;


<|start_filename|>src/devices/iphone8plus.js<|end_filename|>
import React, { Component } from "react";
import Content from './lib/Content';
import Props from './lib/Props';



export default class Iphone8plus extends Component {
  render() {
    
    const color = (this.props.color) ? this.props.color : "gold";

    return (
      <div
        className={`marvel-device iphone8plus ${
          this.props.orientation
        } ${color}`}
      >
        <div className="top-bar" />
        <div className="sleep" />
        <div className="volume" />
        <div className="camera" />
        <div className="sensor" />
        <div className="speaker" />
        <div className="screen">
          <Content show={this.props.show} />
        </div>
        <div className="home" />
        <div className="bottom-bar" />
      </div>
    );
  }
}

Iphone8plus.propTypes = Props;


<|start_filename|>src/devices/iphonex.js<|end_filename|>
import React, { Component } from "react";
import Content from './lib/Content';
import Props from './lib/Props';

export default class Iphonex extends Component {
  
  render(){
    return (
      <div className={`marvel-device iphone-x ${this.props.orientation}`}>
        <div className="notch">
          <div className="camera" />
          <div className="speaker" />
        </div>
        <div className="top-bar" />
        <div className="sleep" />
        <div className="bottom-bar" />
        <div className="volume" />
        <div className="overflow">
          <div className="shadow shadow--tr" />
          <div className="shadow shadow--tl" />
          <div className="shadow shadow--br" />
          <div className="shadow shadow--bl" />
        </div>
        <div className="inner-shadow" />
        <div className="screen">
          <Content show={this.props.show} />
        </div>
      </div>
    );  
  }

}

Iphonex.propTypes = Props;