(http://github.com/gaearon)",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/gaearon/redux-devtools-log-monitor/issues"
+ },
+ "homepage": "https://github.com/gaearon/redux-devtools-log-monitor",
+ "devDependencies": {
+ "babel-cli": "^6.3.15",
+ "babel-core": "^6.1.20",
+ "babel-eslint": "^5.0.0-beta4",
+ "babel-loader": "^6.2.0",
+ "babel-preset-es2015-loose": "^6.1.3",
+ "babel-preset-react": "^6.3.13",
+ "babel-preset-stage-0": "^6.3.13",
+ "eslint": "^0.23",
+ "eslint-config-airbnb": "0.0.6",
+ "eslint-plugin-react": "^3.6.3",
+ "expect": "^1.6.0",
+ "mocha": "^2.2.5",
+ "mocha-jsdom": "^1.0.0",
+ "rimraf": "^2.3.4",
+ "webpack": "^1.11.0"
+ },
+ "peerDependencies": {
+ "react": "^15.0.0 || ^16.0.0",
+ "redux-devtools": "^3.4.0"
+ },
+ "dependencies": {
+ "lodash.debounce": "^4.0.4",
+ "prop-types": "^15.0.0",
+ "react-json-tree": "^0.11.0",
+ "react-pure-render": "^1.0.2",
+ "redux-devtools-themes": "^1.0.0"
+ }
+}
diff --git a/packages/redux-devtools-log-monitor/src/LogMonitor.js b/packages/redux-devtools-log-monitor/src/LogMonitor.js
new file mode 100644
index 00000000..a2130458
--- /dev/null
+++ b/packages/redux-devtools-log-monitor/src/LogMonitor.js
@@ -0,0 +1,229 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import shouldPureComponentUpdate from 'react-pure-render/function';
+import * as themes from 'redux-devtools-themes';
+import { ActionCreators } from 'redux-devtools';
+import { updateScrollTop, startConsecutiveToggle } from './actions';
+import reducer from './reducers';
+import LogMonitorButtonBar from './LogMonitorButtonBar';
+import LogMonitorEntryList from './LogMonitorEntryList';
+import debounce from 'lodash.debounce';
+
+const { toggleAction, setActionsActive } = ActionCreators;
+
+const styles = {
+ container: {
+ fontFamily: 'monaco, Consolas, Lucida Console, monospace',
+ position: 'relative',
+ overflowY: 'hidden',
+ width: '100%',
+ height: '100%',
+ minWidth: 300,
+ direction: 'ltr'
+ },
+ elements: {
+ position: 'absolute',
+ left: 0,
+ right: 0,
+ top: 0,
+ bottom: 0,
+ overflowX: 'hidden',
+ overflowY: 'auto'
+ }
+};
+
+export default class LogMonitor extends Component {
+ static update = reducer;
+
+ static propTypes = {
+ dispatch: PropTypes.func,
+ computedStates: PropTypes.array,
+ actionsById: PropTypes.object,
+ stagedActionIds: PropTypes.array,
+ skippedActionIds: PropTypes.array,
+ monitorState: PropTypes.shape({
+ initialScrollTop: PropTypes.number,
+ consecutiveToggleStartId: PropTypes.number
+ }),
+
+ preserveScrollTop: PropTypes.bool,
+ select: PropTypes.func,
+ theme: PropTypes.oneOfType([
+ PropTypes.object,
+ PropTypes.string
+ ]),
+ expandActionRoot: PropTypes.bool,
+ expandStateRoot: PropTypes.bool,
+ markStateDiff: PropTypes.bool,
+ hideMainButtons: PropTypes.bool
+ };
+
+ static defaultProps = {
+ select: (state) => state,
+ theme: 'nicinabox',
+ preserveScrollTop: true,
+ expandActionRoot: true,
+ expandStateRoot: true,
+ markStateDiff: false
+ };
+
+ shouldComponentUpdate = shouldPureComponentUpdate;
+
+ updateScrollTop = debounce(() => {
+ const node = this.node;
+ this.props.dispatch(updateScrollTop(node ? node.scrollTop : 0));
+ }, 500);
+
+ constructor(props) {
+ super(props);
+ this.handleToggleAction = this.handleToggleAction.bind(this);
+ this.handleToggleConsecutiveAction = this.handleToggleConsecutiveAction.bind(this);
+ this.getRef = this.getRef.bind(this);
+ }
+
+ scroll() {
+ const node = this.node;
+ if (!node) {
+ return;
+ }
+ if (this.scrollDown) {
+ const { offsetHeight, scrollHeight } = node;
+ node.scrollTop = scrollHeight - offsetHeight;
+ this.scrollDown = false;
+ }
+ }
+
+ componentDidMount() {
+ const node = this.node;
+ if (!node || !this.props.monitorState) {
+ return;
+ }
+
+ if (this.props.preserveScrollTop) {
+ node.scrollTop = this.props.monitorState.initialScrollTop;
+ node.addEventListener('scroll', this.updateScrollTop);
+ } else {
+ this.scrollDown = true;
+ this.scroll();
+ }
+ }
+
+ componentWillUnmount() {
+ const node = this.node;
+ if (node && this.props.preserveScrollTop) {
+ node.removeEventListener('scroll', this.updateScrollTop);
+ }
+ }
+
+ componentWillReceiveProps(nextProps) {
+ const node = this.node;
+ if (!node) {
+ this.scrollDown = true;
+ } else if (
+ this.props.stagedActionIds.length <
+ nextProps.stagedActionIds.length
+ ) {
+ const { scrollTop, offsetHeight, scrollHeight } = node;
+
+ this.scrollDown = Math.abs(
+ scrollHeight - (scrollTop + offsetHeight)
+ ) < 20;
+ } else {
+ this.scrollDown = false;
+ }
+ }
+
+ componentDidUpdate() {
+ this.scroll();
+ }
+
+ handleToggleAction(id) {
+ this.props.dispatch(toggleAction(id));
+ }
+
+ handleToggleConsecutiveAction(id) {
+ const { monitorState, actionsById } = this.props;
+ const { consecutiveToggleStartId } = monitorState;
+ if (consecutiveToggleStartId && actionsById[consecutiveToggleStartId]) {
+ const { skippedActionIds } = this.props;
+ const start = Math.min(consecutiveToggleStartId, id);
+ const end = Math.max(consecutiveToggleStartId, id);
+ const active = skippedActionIds.indexOf(consecutiveToggleStartId) > -1;
+ this.props.dispatch(setActionsActive(start, end + 1, active));
+ this.props.dispatch(startConsecutiveToggle(null));
+ } else if (id > 0) {
+ this.props.dispatch(startConsecutiveToggle(id));
+ }
+ }
+
+ getTheme() {
+ let { theme } = this.props;
+ if (typeof theme !== 'string') {
+ return theme;
+ }
+
+ if (typeof themes[theme] !== 'undefined') {
+ return themes[theme];
+ }
+
+ console.warn('DevTools theme ' + theme + ' not found, defaulting to nicinabox');
+ return themes.nicinabox;
+ }
+
+ getRef(node) {
+ this.node = node;
+ }
+
+ render() {
+ const theme = this.getTheme();
+ const { consecutiveToggleStartId } = this.props.monitorState;
+
+ const {
+ dispatch,
+ actionsById,
+ skippedActionIds,
+ stagedActionIds,
+ computedStates,
+ currentStateIndex,
+ select,
+ expandActionRoot,
+ expandStateRoot,
+ markStateDiff
+ } = this.props;
+
+ const entryListProps = {
+ theme,
+ actionsById,
+ skippedActionIds,
+ stagedActionIds,
+ computedStates,
+ currentStateIndex,
+ consecutiveToggleStartId,
+ select,
+ expandActionRoot,
+ expandStateRoot,
+ markStateDiff,
+ onActionClick: this.handleToggleAction,
+ onActionShiftClick: this.handleToggleConsecutiveAction
+ };
+
+ return (
+
+ {!this.props.hideMainButtons &&
+
1}
+ hasSkippedActions={skippedActionIds.length > 0}
+ />
+ }
+
+
+
+
+ );
+ }
+}
diff --git a/packages/redux-devtools-log-monitor/src/LogMonitorButton.js b/packages/redux-devtools-log-monitor/src/LogMonitorButton.js
new file mode 100644
index 00000000..eab3a439
--- /dev/null
+++ b/packages/redux-devtools-log-monitor/src/LogMonitorButton.js
@@ -0,0 +1,96 @@
+import React from 'react';
+import brighten from './brighten';
+import shouldPureComponentUpdate from 'react-pure-render/function';
+
+const styles = {
+ base: {
+ cursor: 'pointer',
+ fontWeight: 'bold',
+ borderRadius: 3,
+ padding: 4,
+ marginLeft: 3,
+ marginRight: 3,
+ marginTop: 5,
+ marginBottom: 5,
+ flexGrow: 1,
+ display: 'inline-block',
+ fontSize: '0.8em',
+ color: 'white',
+ textDecoration: 'none'
+ }
+};
+
+export default class LogMonitorButton extends React.Component {
+ shouldComponentUpdate = shouldPureComponentUpdate;
+
+ constructor(props) {
+ super(props);
+
+ this.handleMouseEnter = this.handleMouseEnter.bind(this);
+ this.handleMouseLeave = this.handleMouseLeave.bind(this);
+ this.handleMouseDown = this.handleMouseDown.bind(this);
+ this.handleMouseUp = this.handleMouseUp.bind(this);
+ this.onClick = this.onClick.bind(this);
+
+ this.state = {
+ hovered: false,
+ active: false
+ };
+ }
+
+ handleMouseEnter() {
+ this.setState({ hovered: true });
+ }
+
+ handleMouseLeave() {
+ this.setState({ hovered: false });
+ }
+
+ handleMouseDown() {
+ this.setState({ active: true });
+ }
+
+ handleMouseUp() {
+ this.setState({ active: false });
+ }
+
+ onClick() {
+ if (!this.props.enabled) {
+ return;
+ }
+ if (this.props.onClick) {
+ this.props.onClick();
+ }
+ }
+
+ render() {
+ let style = {
+ ...styles.base,
+ backgroundColor: this.props.theme.base02
+ };
+ if (this.props.enabled && this.state.hovered) {
+ style = {
+ ...style,
+ backgroundColor: brighten(this.props.theme.base02, 0.2)
+ };
+ }
+ if (!this.props.enabled) {
+ style = {
+ ...style,
+ opacity: 0.2,
+ cursor: 'text',
+ backgroundColor: 'transparent'
+ };
+ }
+ return (
+
+ {this.props.children}
+
+ );
+ }
+}
diff --git a/packages/redux-devtools-log-monitor/src/LogMonitorButtonBar.js b/packages/redux-devtools-log-monitor/src/LogMonitorButtonBar.js
new file mode 100644
index 00000000..48b50b04
--- /dev/null
+++ b/packages/redux-devtools-log-monitor/src/LogMonitorButtonBar.js
@@ -0,0 +1,82 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import shouldPureComponentUpdate from 'react-pure-render/function';
+import { ActionCreators } from 'redux-devtools';
+import LogMonitorButton from './LogMonitorButton';
+
+const { reset, rollback, commit, sweep } = ActionCreators;
+
+const style = {
+ textAlign: 'center',
+ borderBottomWidth: 1,
+ borderBottomStyle: 'solid',
+ borderColor: 'transparent',
+ zIndex: 1,
+ display: 'flex',
+ flexDirection: 'row'
+};
+
+export default class LogMonitorButtonBar extends Component {
+ static propTypes = {
+ dispatch: PropTypes.func,
+ theme: PropTypes.object
+ };
+
+ shouldComponentUpdate = shouldPureComponentUpdate;
+
+ constructor(props) {
+ super(props);
+ this.handleReset = this.handleReset.bind(this);
+ this.handleRollback = this.handleRollback.bind(this);
+ this.handleSweep = this.handleSweep.bind(this);
+ this.handleCommit = this.handleCommit.bind(this);
+ }
+
+ handleRollback() {
+ this.props.dispatch(rollback());
+ }
+
+ handleSweep() {
+ this.props.dispatch(sweep());
+ }
+
+ handleCommit() {
+ this.props.dispatch(commit());
+ }
+
+ handleReset() {
+ this.props.dispatch(reset());
+ }
+
+ render() {
+ const { theme, hasStates, hasSkippedActions } = this.props;
+ return (
+
+
+ Reset
+
+
+ Revert
+
+
+ Sweep
+
+
+ Commit
+
+
+ );
+ }
+}
diff --git a/packages/redux-devtools-log-monitor/src/LogMonitorEntry.js b/packages/redux-devtools-log-monitor/src/LogMonitorEntry.js
new file mode 100644
index 00000000..971afb81
--- /dev/null
+++ b/packages/redux-devtools-log-monitor/src/LogMonitorEntry.js
@@ -0,0 +1,153 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import JSONTree from 'react-json-tree';
+import LogMonitorEntryAction from './LogMonitorEntryAction';
+import shouldPureComponentUpdate from 'react-pure-render/function';
+
+const styles = {
+ entry: {
+ display: 'block',
+ WebkitUserSelect: 'none'
+ },
+
+ root: {
+ marginLeft: 0
+ }
+};
+
+const getDeepItem = (data, path) => path.reduce((obj, key) => obj && obj[key], data);
+const dataIsEqual = (data, previousData, keyPath) => {
+ const path = [...keyPath].reverse().slice(1);
+
+ return getDeepItem(data, path) === getDeepItem(previousData, path);
+};
+
+export default class LogMonitorEntry extends Component {
+ static propTypes = {
+ state: PropTypes.object.isRequired,
+ action: PropTypes.object.isRequired,
+ actionId: PropTypes.number.isRequired,
+ select: PropTypes.func.isRequired,
+ inFuture: PropTypes.bool,
+ error: PropTypes.string,
+ onActionClick: PropTypes.func.isRequired,
+ onActionShiftClick: PropTypes.func.isRequired,
+ collapsed: PropTypes.bool,
+ selected: PropTypes.bool,
+ expandActionRoot: PropTypes.bool,
+ expandStateRoot: PropTypes.bool
+ };
+
+ shouldComponentUpdate = shouldPureComponentUpdate;
+
+ constructor(props) {
+ super(props);
+ this.handleActionClick = this.handleActionClick.bind(this);
+ this.shouldExpandNode = this.shouldExpandNode.bind(this);
+ }
+
+ printState(state, error) {
+ let errorText = error;
+ if (!errorText) {
+ try {
+ const data = this.props.select(state);
+ let theme;
+
+ if (this.props.markStateDiff) {
+ const previousData = typeof this.props.previousState !== 'undefined' ?
+ this.props.select(this.props.previousState) :
+ undefined;
+ const getValueStyle = ({ style }, nodeType, keyPath) => ({
+ style: {
+ ...style,
+ backgroundColor: dataIsEqual(data, previousData, keyPath) ?
+ 'transparent' :
+ this.props.theme.base01
+ }
+ });
+ const getNestedNodeStyle = ({ style }, keyPath) => ({
+ style: {
+ ...style,
+ ...(keyPath.length > 1 ? {} : styles.root)
+ }
+ });
+ theme = {
+ extend: this.props.theme,
+ tree: styles.tree,
+ value: getValueStyle,
+ nestedNode: getNestedNodeStyle
+ };
+ } else {
+ theme = this.props.theme;
+ }
+
+ return (
+
+ );
+ } catch (err) {
+ errorText = 'Error selecting state.';
+ }
+ }
+
+ return (
+
+ {errorText}
+
+ );
+ }
+
+ handleActionClick(e) {
+ const { actionId, onActionClick, onActionShiftClick } = this.props;
+ if (actionId > 0) {
+ if (e.shiftKey) {
+ onActionShiftClick(actionId);
+ } else {
+ onActionClick(actionId);
+ }
+ }
+ }
+
+ shouldExpandNode(keyName, data, level) {
+ return this.props.expandStateRoot && level === 0;
+ }
+
+ render() {
+ const { actionId, error, action, state, collapsed, selected, inFuture } = this.props;
+ const styleEntry = {
+ opacity: collapsed ? 0.5 : 1,
+ cursor: (actionId > 0) ? 'pointer' : 'default'
+ };
+
+ return (
+
+
+ {!collapsed &&
+
+ {this.printState(state, error)}
+
+ }
+
+ );
+ }
+}
diff --git a/packages/redux-devtools-log-monitor/src/LogMonitorEntryAction.js b/packages/redux-devtools-log-monitor/src/LogMonitorEntryAction.js
new file mode 100644
index 00000000..dfebc8a4
--- /dev/null
+++ b/packages/redux-devtools-log-monitor/src/LogMonitorEntryAction.js
@@ -0,0 +1,59 @@
+import React, { Component } from 'react';
+import JSONTree from 'react-json-tree';
+
+const styles = {
+ actionBar: {
+ paddingTop: 8,
+ paddingBottom: 7,
+ paddingLeft: 16
+ },
+ payload: {
+ margin: 0,
+ paddingLeft: 16,
+ overflow: 'auto'
+ }
+};
+
+export default class LogMonitorAction extends Component {
+ constructor(props) {
+ super(props);
+ this.shouldExpandNode = this.shouldExpandNode.bind(this);
+ }
+
+ renderPayload(payload) {
+ return (
+
+ { Object.keys(payload).length > 0 ?
+ : '' }
+
+ );
+ }
+
+ shouldExpandNode(keyName, data, level) {
+ return this.props.expandActionRoot && level === 0;
+ }
+
+ render() {
+ const { type, ...payload } = this.props.action;
+ return (
+
+
+ {type !== null && type.toString()}
+
+ {!this.props.collapsed ? this.renderPayload(payload) : ''}
+
+ );
+ }
+}
diff --git a/packages/redux-devtools-log-monitor/src/LogMonitorEntryList.js b/packages/redux-devtools-log-monitor/src/LogMonitorEntryList.js
new file mode 100644
index 00000000..2a74044a
--- /dev/null
+++ b/packages/redux-devtools-log-monitor/src/LogMonitorEntryList.js
@@ -0,0 +1,79 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import LogMonitorEntry from './LogMonitorEntry';
+import shouldPureComponentUpdate from 'react-pure-render/function';
+
+export default class LogMonitorEntryList extends Component {
+ static propTypes = {
+ actionsById: PropTypes.object,
+ computedStates: PropTypes.array,
+ stagedActionIds: PropTypes.array,
+ skippedActionIds: PropTypes.array,
+ currentStateIndex: PropTypes.number,
+ consecutiveToggleStartId: PropTypes.number,
+
+ select: PropTypes.func.isRequired,
+ onActionClick: PropTypes.func.isRequired,
+ theme: PropTypes.oneOfType([
+ PropTypes.object,
+ PropTypes.string
+ ]),
+ expandActionRoot: PropTypes.bool,
+ expandStateRoot: PropTypes.bool
+ };
+
+ shouldComponentUpdate = shouldPureComponentUpdate;
+
+ render() {
+ const elements = [];
+ const {
+ theme,
+ actionsById,
+ computedStates,
+ currentStateIndex,
+ consecutiveToggleStartId,
+ select,
+ skippedActionIds,
+ stagedActionIds,
+ expandActionRoot,
+ expandStateRoot,
+ markStateDiff,
+ onActionClick,
+ onActionShiftClick
+ } = this.props;
+
+ for (let i = 0; i < stagedActionIds.length; i++) {
+ const actionId = stagedActionIds[i];
+ const action = actionsById[actionId].action;
+ const { state, error } = computedStates[i];
+ let previousState;
+ if (i > 0) {
+ previousState = computedStates[i - 1].state;
+ }
+ elements.push(
+ -1}
+ inFuture={i > currentStateIndex}
+ selected={consecutiveToggleStartId === i}
+ error={error}
+ expandActionRoot={expandActionRoot}
+ expandStateRoot={expandStateRoot}
+ markStateDiff={markStateDiff}
+ onActionClick={onActionClick}
+ onActionShiftClick={onActionShiftClick} />
+ );
+ }
+
+ return (
+
+ {elements}
+
+ );
+ }
+}
diff --git a/packages/redux-devtools-log-monitor/src/actions.js b/packages/redux-devtools-log-monitor/src/actions.js
new file mode 100644
index 00000000..b46bd7be
--- /dev/null
+++ b/packages/redux-devtools-log-monitor/src/actions.js
@@ -0,0 +1,9 @@
+export const UPDATE_SCROLL_TOP = '@@redux-devtools-log-monitor/UPDATE_SCROLL_TOP';
+export function updateScrollTop(scrollTop) {
+ return { type: UPDATE_SCROLL_TOP, scrollTop };
+}
+
+export const START_CONSECUTIVE_TOGGLE = '@@redux-devtools-log-monitor/START_CONSECUTIVE_TOGGLE';
+export function startConsecutiveToggle(id) {
+ return { type: START_CONSECUTIVE_TOGGLE, id };
+}
diff --git a/packages/redux-devtools-log-monitor/src/brighten.js b/packages/redux-devtools-log-monitor/src/brighten.js
new file mode 100644
index 00000000..8599ec1d
--- /dev/null
+++ b/packages/redux-devtools-log-monitor/src/brighten.js
@@ -0,0 +1,16 @@
+export default function(hexColor, lightness) {
+ let hex = String(hexColor).replace(/[^0-9a-f]/gi, '');
+ if (hex.length < 6) {
+ hex = hex.replace(/(.)/g, '$1$1');
+ }
+ let lum = lightness || 0;
+
+ let rgb = '#';
+ let c;
+ for (let i = 0; i < 3; ++i) {
+ c = parseInt(hex.substr(i * 2, 2), 16);
+ c = Math.round(Math.min(Math.max(0, c + (c * lum)), 255)).toString(16);
+ rgb += ('00' + c).substr(c.length);
+ }
+ return rgb;
+}
diff --git a/packages/redux-devtools-log-monitor/src/index.js b/packages/redux-devtools-log-monitor/src/index.js
new file mode 100644
index 00000000..0a3a289b
--- /dev/null
+++ b/packages/redux-devtools-log-monitor/src/index.js
@@ -0,0 +1 @@
+export default from './LogMonitor';
diff --git a/packages/redux-devtools-log-monitor/src/reducers.js b/packages/redux-devtools-log-monitor/src/reducers.js
new file mode 100644
index 00000000..60198378
--- /dev/null
+++ b/packages/redux-devtools-log-monitor/src/reducers.js
@@ -0,0 +1,24 @@
+import { UPDATE_SCROLL_TOP, START_CONSECUTIVE_TOGGLE } from './actions';
+
+function initialScrollTop(props, state = 0, action) {
+ if (!props.preserveScrollTop) {
+ return 0;
+ }
+
+ return action.type === UPDATE_SCROLL_TOP ?
+ action.scrollTop :
+ state;
+}
+
+function startConsecutiveToggle(props, state, action) {
+ return action.type === START_CONSECUTIVE_TOGGLE ?
+ action.id :
+ state;
+}
+
+export default function reducer(props, state = {}, action) {
+ return {
+ initialScrollTop: initialScrollTop(props, state.initialScrollTop, action),
+ consecutiveToggleStartId: startConsecutiveToggle(props, state.consecutiveToggleStartId, action)
+ };
+}
diff --git a/packages/redux-devtools-log-monitor/test/index.spec.js b/packages/redux-devtools-log-monitor/test/index.spec.js
new file mode 100644
index 00000000..e69de29b
diff --git a/yarn.lock b/yarn.lock
index aed1cad2..a0da2164 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7840,7 +7840,7 @@ lodash.debounce@^3.1.1:
dependencies:
lodash._getnative "^3.0.0"
-lodash.debounce@^4.0.3:
+lodash.debounce@^4.0.3, lodash.debounce@^4.0.4:
version "4.0.8"
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168=
@@ -9618,7 +9618,7 @@ promzard@^0.3.0:
dependencies:
read "1"
-prop-types@^15.5.10, prop-types@^15.5.6, prop-types@^15.5.7, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.2:
+prop-types@^15.0.0, prop-types@^15.5.10, prop-types@^15.5.6, prop-types@^15.5.7, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.2:
version "15.6.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102"
integrity sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==