<|start_filename|>webpack.prod.js<|end_filename|>
const path = require('path');

const config = require('./webpack.config');

module.exports = {
  ...config,
  entry: config.entry.index,
  externals: [
    {
      react: {
        root: 'React',
        commonjs: 'react',
        commonjs2: 'react',
        amd: 'react',
      },
      'react-dom': {
        root: 'ReactDOM',
        commonjs: 'react-dom',
        commonjs2: 'react-dom',
        amd: 'react-dom',
      },
    },
  ],
  output: {
    filename: './dist/index.js',
    library: 'react-control-panel',
    libraryTarget: 'umd',
    path: path.resolve(__dirname),
  },
  mode: 'production',
  optimization: {
		minimize: false
	},
  devtool: 'source-map',
};


<|start_filename|>src/components/range.js<|end_filename|>
import React from 'react';
import { ClientStyle as Style } from 'react-css-component';
import isnumeric from 'is-numeric';
import uuid from 'uuid/v4';

import { withSettingState } from './context';
import Value from './value';
import getDynamicCss from './styles/range';
import { withErrorHandler, throwLogRangeError, validateStepParams } from '../error';
import {
  withScalerFunctions,
  numericOrDefault,
  numericOrDefaultElse,
  createNormalDisplayOptsGetter,
} from '../util';

const getLogDisplayOpts = withScalerFunctions(
  ({ min, max, step, value, scaleValue, scaleValueInverse }) => {
    // `value` is the logarithmic value that the user cares about.  We convert it into a value
    // from 1 to 100 in order to pass it to the slider.
    const sliderVal = scaleValueInverse(
      numericOrDefaultElse(value, () => scaleValue((min + max) / 2))
    );
    if (sliderVal * scaleValueInverse(max) < 0) {
      throwLogRangeError(sliderVal);
    }

    return { min: 0, max: 100, step: 1, logVal: value, sliderVal, scaleValue };
  }
);

const getNormalDisplayOpts = createNormalDisplayOptsGetter((min, max, value) =>
  numericOrDefault(value, (min + max) / 2)
);

export class InnerRange extends React.Component {
  state = { id: uuid() };

  render() {
    const { scale, steps, onChange, theme, ...props } = this.props;
    validateStepParams(props.step, steps);

    const { min, max, step, logVal, sliderVal, scaleValue } = (scale === 'log'
      ? getLogDisplayOpts
      : getNormalDisplayOpts)(props);
    // use `steps` if provided
    const processedStep = isnumeric(steps) ? (max - min) / steps : step;

    return (
      <React.Fragment>
        <Style css={getDynamicCss(theme, this.state.id)} />
        <input
          className={`control-panel-range-${this.state.id}`}
          type="range"
          value={sliderVal}
          min={min}
          max={max}
          step={processedStep}
          onChange={(e) => {
            // We take the value from the slider (range 1 to 100) and scale it into its logarithmic
            // representation before passing it into the state.
            onChange(scaleValue(parseFloat(e.target.value)));
          }}
        />
        <Value text={logVal} width="11%" />
      </React.Fragment>
    );
  }
}

export default withErrorHandler(withSettingState()(InnerRange));


<|start_filename|>src/components/context.js<|end_filename|>
import React from 'react';

const ControlPanelContext = React.createContext({});
export default ControlPanelContext;

export const withTheme = (Comp) => ({ ...props }) => (
  <ControlPanelContext.Consumer>
    {({ theme }) => <Comp theme={theme} {...props} />}
  </ControlPanelContext.Consumer>
);

const getLabelStyles = (theme) => ({
  body: {
    left: 0,
    width: '36%',
    display: 'inline-block',
    height: 20,
    paddingRight: '2%',
    verticalAlign: 'top',
  },
  text: {
    color: theme.text1,
    display: 'inline-block',
    verticalAlign: 'sub',
  },
});

export const Label = withTheme(({ label, theme }) => {
  const styles = getLabelStyles(theme);

  return (
    <div style={styles.body} className="draggable">
      <span title={label} className="draggable" style={styles.text}>
        {label}
      </span>
    </div>
  );
});

export const Container = ({ label, LabelComponent, children }) => (
  <div className="container draggable">
    <Label label={LabelComponent ? <LabelComponent label={label} /> : label || ''} />
    {children}
  </div>
);

const WithSettingStateWrapper = React.memo(({ renderContainer, label, LabelComponent, children }) =>
  renderContainer ? (
    <Container LabelComponent={LabelComponent} label={label}>
      {children}
    </Container>
  ) : (
    children
  )
);

export const withSettingState = (mapPropsToStyles) => (Comp) => ({
  label,
  LabelComponent,
  ...props
}) => (
  <ControlPanelContext.Consumer>
    {({ state, theme, indicateChange }) => {
      const compProps = {
        value: state[label],
        onChange: (newVal) => indicateChange(label, newVal),
        theme,
        ...props,
      };
      if (mapPropsToStyles) {
        compProps.styles = mapPropsToStyles(compProps);
      }

      return (
        <WithSettingStateWrapper
          label={label}
          LabelComponent={LabelComponent}
          renderContainer={props.renderContainer === false ? false : true}
        >
          <Comp {...compProps} />
        </WithSettingStateWrapper>
      );
    }}
  </ControlPanelContext.Consumer>
);


<|start_filename|>src/components/multibox.js<|end_filename|>
import React from 'react';
import PropTypes from 'prop-types';

import { withSettingState } from './context';

const UnwrappedMultibox = ({ theme, colors, names, count, value, onChange, styles }) => {
  const HydratedCheckbox = (checked, i) => (
    <React.Fragment key={i}>
      <span
        style={styles.getContentStyle(i, checked)}
        onClick={() => {
          // dirty mutation below
          value[i] = !checked;
          onChange(value);
        }}
      />
      <input type="checkbox" defaultChecked={checked} style={styles.checkbox} />
      {names[i] ? <span style={styles.label}>{names[i]}</span> : null}
    </React.Fragment>
  );

  const checkboxValues = value || new Array(count).map((_v, i) => value[i] || false);

  return (
    <div style={styles.main}>
      <span style={styles.innerWrapper}>{checkboxValues.map(HydratedCheckbox)}</span>
    </div>
  );
};

const mapPropsToStyles = ({ theme, colors }) => ({
  main: {
    position: 'relative',
    width: '60%',
    display: 'inline-block',
    paddingBottom: 7,
  },
  innerWrapper: { display: 'inline-block' },
  checkbox: {
    display: 'none',
    cursor: 'pointer',
  },
  label: {
    backgroundColor: theme.background2,
    paddingRight: 7,
    verticalAlign: 'middle',
    padding: 2,
    marginRight: 8,
    color: theme.text2,
  },
  getContentStyle: (i, checked) => {
    const checkedStyle = {
      width: 10,
      height: 10,
      border: `solid 4px ${theme.background2}`,
      cursor: 'pointer',
      backgroundColor: checked ? colors[i] : theme.foreground1,
    };

    return {
      display: 'inline-block',
      width: 18,
      height: 18,
      padding: 0,
      verticalAlign: 'middle',
      marginRight: 8,
      marginTop: 2,
      marginBottom: 1,
      backgroundColor: theme.background2,
      borderRadius: 0,
      cursor: 'pointer',
      ...(checked ? checkedStyle : {}),
    };
  },
});

const Multibox = withSettingState(mapPropsToStyles)(UnwrappedMultibox);
Multibox.propTypes = {
  colors: PropTypes.array,
  names: PropTypes.arrayOf(PropTypes.string),
  count: PropTypes.number,
};

Multibox.defaultProps = {
  colors: [],
  names: [],
  count: 2,
};

export default Multibox;


<|start_filename|>src/error.js<|end_filename|>
import React from 'react';
import isnumeric from 'is-numeric';

export class InvalidParamsError extends Error {}

export const ErrMsg = ({ msg }) => <span style={{ color: 'red' }}>{msg}</span>;

class ErrorHandlerWrapper extends React.Component {
  state = { error: null };

  componentDidCatch(err, info) {
    this.setState({ errMsg: err.message });
  }

  render() {
    if (this.state.errMsg) {
      return <ErrMsg msg={this.state.errMsg} />;
    } else {
      return <this.props.Comp {...this.props} />;
    }
  }
}

export const withErrorHandler = Comp => ({ ...props }) => (
  <ErrorHandlerWrapper Comp={Comp} {...props} />
);

const createValidator = (checkIfInvalid, createErrMsg) => (...args) => {
  if (!checkIfInvalid(...args)) {
    return;
  }

  throw new InvalidParamsError(createErrMsg(...args));
};

export const validateStepParams = createValidator(
  (step, steps) => !!step && !!steps,
  (step, steps) => `Cannot specify both step and steps. Got step = ${step}, steps = ${steps}`
);

export const validateLogStep = createValidator(
  isnumeric,
  step => `Log may only use steps (integer number of steps), not a step value. Got step = ${step}`
);

export const validateLogMinMax = createValidator(
  (min, max) => min * max <= 0,
  (min, max) =>
    `Log range min/max must have the same sign and not equal zero. Got min = ${min}, max = ${max}`
);

export const throwLogRangeError = createValidator(
  scaledVal => true,
  scaledVal =>
    `Log range initial value must have the same sign as min/max and must not equal zero. Got initial value = ${scaledVal}`
);


<|start_filename|>src/components/styles/range.js<|end_filename|>
export default (theme, id) => `
.control-panel-range-${id}::-webkit-slider-runnable-track {
  width: 100%;
  height: 20px;
  cursor: ew-resize;
  background: ${theme.background2};
}

.control-panel-range-${id}::-webkit-slider-thumb {
  height: 20px;
  width: 10px;
  background: ${theme.foreground1};
  cursor: ew-resize;
  -webkit-appearance: none;
  margin-top: 0px;
}

.control-panel-range-${id}:focus::-webkit-slider-runnable-track {
  background: ${theme.background2};
  outline: none;
}

.control-panel-range-${id}::-moz-range-track {
  width: 100%;
  height: 20px;
  cursor: ew-resize;
  background: ${theme.background2};
}

.control-panel-range-${id}::-moz-range-thumb {
  border: 0px solid rgba(0, 0, 0, 0);
  height: 20px;
  width: 10px;
  border-radius: 0px;
  background: ${theme.foreground1};
  cursor: ew-resize;
}

.control-panel-range-${id}::-ms-track {
  width: 100%;
  height: 20px;
  cursor: ew-resize;
  background: transparent;
  border-color: transparent;
  color: transparent;
}

.control-panel-range-${id}::-ms-fill-lower {
  background: ${theme.foreground1};
}

.control-panel-range-${id}::-ms-fill-upper {
  background: ${theme.foreground1};
}

.control-panel-range-${id}::-ms-thumb {
  width: 10px;
  border-radius: 0px;
  background: ${theme.foreground1};
  cursor: ew-resize;
  height: 20px;
}

.control-panel-range-${id}:focus::-ms-fill-lower {
  background: ${theme.foreground1};
  outline: none;
}

.control-panel-range-${id}:focus::-ms-fill-upper {
  background: ${theme.foreground1};
  outline: none;
}

.control-panel-range-${id} {
  -webkit-appearance: none;
  width: 47.5%;
  margin: 0px 0;
  margin-top: 2px;
}

.control-panel-range-${id}:focus {
  outline: none;
}
`;


<|start_filename|>src/components/interval.js<|end_filename|>
import React from 'react';
import isnumeric from 'is-numeric';

import { withSettingState } from './context';
import { withErrorHandler, throwLogRangeError, validateStepParams } from '../error';
import {
  numericOrDefault,
  numericOrDefaultElse,
  createNormalDisplayOptsGetter,
  withScalerFunctions,
  clamp,
} from '../util';
import Value from './value';
import './styles/interval.css';

const getLogDisplayOpts = withScalerFunctions(
  ({ min, max, step, value, scaleValue, scaleValueInverse }) => {
    // Scale the values down from their logarithmic represenations into the range used by the slider
    const sliderVals = [
      scaleValueInverse(numericOrDefaultElse(value[0], () => min + (max - min) * 0.25)),
      scaleValueInverse(numericOrDefaultElse(value[1], () => min + (max - min) * 0.75)),
    ];

    const scaledMax = scaleValue(max);
    if (scaleValue(sliderVals[0]) * scaledMax <= 0 || scaleValue(sliderVals[1]) * scaledMax <= 0) {
      throwLogRangeError(`[${sliderVals[0]},${sliderVals[1]}]`);
    }

    return {
      min: 0,
      max: 100,
      step: 1,
      sliderVal: sliderVals,
      logVal: value,
      scaleValue,
    };
  }
);

const getNormalDisplayOpts = createNormalDisplayOptsGetter((min, max, value) => [
  numericOrDefault(value[0], (min + max) * 0.25),
  numericOrDefault(value[1], (min + max) * 0.75),
]);

class Range extends React.Component {
  input = React.createRef();
  state = { activeIndex: -1, value: null, min: null, max: null, step: null };

  /**
   * Gets mouse position in page coords relative to the container
   */
  mouseX = ev => ev.pageX - this.input.current.getBoundingClientRect().left;

  calculateFraction = (evt, value, min, max) => ({
    fraction: clamp(this.mouseX(evt) / this.input.current.offsetWidth, 0, 1),
    lofrac: (value[0] - min) / (max - min),
    hifrac: (value[1] - min) / (max - min),
  });

  setActiveValue = evt => {
    if (this.state.activeIndex === -1) {
      return;
    }

    const { value, min, max, step, scaleValue } = this.computedScaleInfo;
    const { fraction, hifrac, lofrac } = this.calculateFraction(evt, value, min, max);

    // Clip against the other bound:
    const clippedFraction =
      this.state.activeIndex === 0 ? Math.min(hifrac, fraction) : Math.max(lofrac, fraction);

    // Compute and quantize the new value:
    var newValue = scaleValue(min + Math.round(((max - min) * clippedFraction) / step) * step);

    // Make sure it doesn't overrun the slider or pass the other side
    const clampTo =
      this.state.activeIndex === 0 ? [this.props.min, value[1]] : [value[0], this.props.max];
    value[this.state.activeIndex] = clamp(newValue, clampTo[0], clampTo[1]);
    this.props.onChange(value);
  };

  componentDidMount() {
    const mouseHandlerWrapper = fn => evt => {
      if (!this.state.dragging) {
        return;
      }
      this.setActiveValue(evt);
      fn && fn();
    };

    // Install event listeners on the document for mouse movement and release
    document.addEventListener('mousemove', mouseHandlerWrapper());
    document.addEventListener(
      'mouseup',
      mouseHandlerWrapper(() => this.setState({ dragging: false, activeIndex: -1 }))
    );
  }

  render() {
    const { scale, steps, theme, ...props } = this.props;

    const { min, max, step, sliderVal, logVal, scaleValue } = (scale === 'log'
      ? getLogDisplayOpts
      : getNormalDisplayOpts)(props);
    validateStepParams(props.step, steps);

    // These values need to be available for the global mouse event listeners, so we set them
    // as a class property.  This doesn't trigger re-renderings since we're not using `setState`.
    this.computedScaleInfo = {
      value: logVal,
      min,
      max,
      step: isnumeric(steps) ? (max - min) / steps : step,
      scaleValue,
    };

    return (
      <React.Fragment>
        <span
          className="control-panel-interval"
          style={{ backgroundColor: theme.background2 }}
          ref={this.input}
          onMouseDown={evt => {
            this.setState({ dragging: true });

            // Figure out which side is being clicked and adjust the active index accodingly
            const { fraction, hifrac, lofrac } = this.calculateFraction(evt, sliderVal, min, max);

            // This is just for making decisions, so perturb it ever so slightly just in case the
            // bounds are numerically equal.
            const lodiff = Math.abs(lofrac - Math.abs(max - min) * 1e-15 - fraction);
            const hidiff = Math.abs(hifrac + Math.abs(max - min) * 1e-15 - fraction);

            // Determine which one is closer
            this.setState({ activeIndex: lodiff < hidiff ? 0 : 1 });
          }}
        >
          <span
            className="control-panel-interval-handle"
            style={{
              left: ((sliderVal[0] - min) / (max - min)) * 100 + '%',
              right: 100 - ((sliderVal[1] - min) / (max - min)) * 100 + '%',
              backgroundColor: theme.foreground1,
            }}
          />
        </span>
        <Value text={logVal[0]} width="11%" left={true} />
        <Value text={logVal[1]} width="11%" />
      </React.Fragment>
    );
  }
}

export default withErrorHandler(withSettingState()(Range));


<|start_filename|>src/components/styles/interval.css<|end_filename|>
.control-panel-interval {
  -webkit-appearance: none;
  position: absolute;
  height: 20px;
  margin: 0px 0;
  width: 33%;
  left: 52.5%;
  cursor: ew-resize;

  -webkit-touch-callout: none;
  -webkit-user-select: none;
  -khtml-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
}

.control-panel-interval .control-panel-interval-handle {
  position: absolute;
  height: 20px;
  min-width: 1px;
}

.control-panel-interval-dragging * {
  -webkit-touch-callout: none !important;
  -webkit-user-select: none !important;
  -khtml-user-select: none !important;
  -moz-user-select: none !important;
  -ms-user-select: none !important;
  user-select: none !important;

  cursor: ew-resize !important;
}


<|start_filename|>src/components/custom.js<|end_filename|>
import React from 'react';

import { compose } from '../util';
import { withSettingState } from './context';
import { withErrorHandler } from '../error';

const CustomComponentInner = ({ Comp, renderContainer, ...props }) => <Comp {...props} />;

/**
 * Takes a prop, `Comp`, which is a component that will be rendered as a control panel input.
 *
 * If the `renderContainer` prop is `true` (default), a `Container` will be rendered as the parent
 * of this element.  That gives you the label on the left and assumes your styling is going to be
 * pretty close to that of the rest of control-panel's imputs.  If you have a highly custom
 * component, you may want to instead handle that all yourself in which you should pass
 * `renderContainer={false}` to `CustomComponent`.
 *
 * The `Comp` child will be passed the following props:
 * - `theme`
 * - `value`
 * - `onChange`
 */
const CustomComponent = compose(
  withSettingState(),
  withErrorHandler
)(CustomComponentInner);

export default CustomComponent;


<|start_filename|>src/components/title.js<|end_filename|>
import React from 'react';
import { withTheme } from './context';

export default withTheme(({ title, theme }) => {
  const style = {
    width: '100%',
    textAlign: 'center',
    color: theme.text2,
    textTransform: 'uppercase',
    height: 20,
    marginBottom: 4,
  };

  return (
    <div style={style} className="draggable">
      {title}
    </div>
  );
});


<|start_filename|>src/components/checkbox.js<|end_filename|>
import React from 'react';
import uuid from 'uuid/v4';

import { withSettingState } from './context';

const Checkbox = ({ theme, value: checked, onChange, styles }) => {
  const id = uuid();

  return (
    <React.Fragment>
      <input
        id={id}
        type="checkbox"
        checked={checked}
        style={styles.checkbox}
        onChange={() => onChange(!checked)}
      />
      <label htmlFor={id} style={styles.label} />
    </React.Fragment>
  );
};

const mapPropsToStyles = ({ theme, value: checked }) => {
  const labelCheckedStyle = {
    width: 10,
    height: 10,
    backgroundColor: theme.foreground1,
    border: `solid 4px ${theme.background2}`,
    cursor: 'pointer',
  };

  return {
    checkbox: {
      display: 'none',
      cursor: 'pointer',
    },
    label: {
      display: 'inline-block',
      width: 18,
      height: 18,
      padding: 0,
      verticalAlign: 'middle',
      marginRight: 8,
      marginTop: 2,
      backgroundColor: theme.background2,
      borderRadius: 0,
      cursor: 'pointer',
      ...(checked ? labelCheckedStyle : {}),
    },
  };
};

export default withSettingState(mapPropsToStyles)(Checkbox);


<|start_filename|>src/components/button.js<|end_filename|>
import React from 'react';
import PropTypes from 'prop-types';

import { withTheme, Container } from './context';

const getStyle = (theme, { focused, active, hover }) => {
  const hoverStyle = {
    color: theme.text2,
    backgroundColor: theme.background2hover,
  };

  const activeStyle = {
    color: theme.background2,
    backgroundColor: theme.text2,
  };

  return {
    position: 'absolute',
    textAlign: 'center',
    height: 20,
    width: '62%',
    border: 'none',
    cursor: 'pointer',
    right: 0,
    fontFamily: 'inherit',
    color: theme.text2,
    backgroundColor: theme.background2,
    outline: focused ? 'none' : undefined,
    ...(hover ? hoverStyle : {}),
    ...(active ? activeStyle : {}),
  };
};

class UnwrappedButton extends React.Component {
  state = { focus: false, hover: false, active: false };

  render = () => (
    <Container>
      <button
        style={getStyle(this.props.theme, this.state)}
        onClick={this.props.action}
        onFocus={() => this.setState({ focus: true })}
        onBlur={() => this.setState({ focus: false })}
        onMouseEnter={() => this.setState({ hover: true })}
        onMouseLeave={() => this.setState({ hover: false })}
        onMouseDown={() => {
          this.setState({ active: true });
          if (this.props.onmousedown) {
            this.props.onmousedown();
          }
        }}
        onMouseUp={() => {
          this.setState({ active: false });
          if (this.props.onmouseup) {
            this.props.onmouseup();
          }
        }}
      >
        {this.props.label}
      </button>
    </Container>
  );
}

const Button = withTheme(UnwrappedButton);
Button.propTypes = {
  action: PropTypes.func,
  onmousedown: PropTypes.func,
  onmouseup: PropTypes.func,
  label: PropTypes.string.isRequired,
};

export default Button;
