Bug 1456828: wip (whole changes) draft
authorDaisuke Akatsuka <dakatsuka@mozilla.com>
Mon, 04 Jun 2018 21:14:41 +0900
changeset 803523 6497b01a1d3270aa57c00ddf65d1555ed9d45654
parent 803521 91703a787599030511d961fc83a4d5e2b6b13d43
push id112130
push userbmo:dakatsuka@mozilla.com
push dateMon, 04 Jun 2018 12:15:17 +0000
bugs1456828
milestone62.0a1
Bug 1456828: wip (whole changes) MozReview-Commit-ID: F7zrvQBqH3w
devtools/client/inspector/animation/animation.js
devtools/client/inspector/animation/components/AnimatedPropertyList.js
devtools/client/inspector/animation/components/AnimatedPropertyListContainer.js
devtools/client/inspector/animation/components/AnimatedPropertyListHeader.js
devtools/client/inspector/animation/components/AnimationDetailContainer.js
devtools/client/inspector/animation/components/AnimationList.js
devtools/client/inspector/animation/components/AnimationListContainer.js
devtools/client/inspector/animation/components/AnimationListHeader.js
devtools/client/inspector/animation/components/AnimationTimelineTickItem.js
devtools/client/inspector/animation/components/AnimationTimelineTickList.js
devtools/client/inspector/animation/components/App.js
devtools/client/inspector/animation/components/CurrentTimeScrubber.js
devtools/client/inspector/animation/components/CurrentTimeScrubberController.js
devtools/client/inspector/animation/components/IndicationBar.js
devtools/client/inspector/animation/components/InspectListBox.js
devtools/client/inspector/animation/components/KeyframesProgressBar.js
devtools/client/inspector/animation/components/KeyframesProgressTickItem.js
devtools/client/inspector/animation/components/KeyframesProgressTickList.js
devtools/client/inspector/animation/components/TickLabels.js
devtools/client/inspector/animation/components/TickLines.js
devtools/client/inspector/animation/components/moz.build
devtools/client/inspector/animation/test/browser_animation_animation-timeline-tick.js
devtools/client/inspector/animation/test/browser_animation_current-time-scrubber.js
devtools/client/inspector/animation/test/browser_animation_keyframes-progress-bar.js
devtools/client/inspector/animation/test/browser_animation_keyframes-progress-bar_after-resuming.js
devtools/client/inspector/animation/test/head.js
devtools/client/themes/animation.css
--- a/devtools/client/inspector/animation/animation.js
+++ b/devtools/client/inspector/animation/animation.js
@@ -577,16 +577,21 @@ class AnimationInspector {
    * https://drafts.csswg.org/web-animations/#the-animation-interface
    *
    * @param {Object} effectTiming
    *        e.g. { duration: 1000, fill: "both" }
    * @return {Animation}
    *         https://drafts.csswg.org/web-animations/#the-animation-interface
    */
   simulateAnimationForKeyframesProgressBar(effectTiming) {
+    // Don't simulate animation if the animation inspector is already destroyed.
+    if (!this.win) {
+      return null;
+    }
+
     if (!this.simulatedAnimationForKeyframesProgressBar) {
       this.simulatedAnimationForKeyframesProgressBar = new this.win.Animation();
     }
 
     this.simulatedAnimationForKeyframesProgressBar.effect =
       new this.win.KeyframeEffect(null, null, effectTiming);
 
     return this.simulatedAnimationForKeyframesProgressBar;
--- a/devtools/client/inspector/animation/components/AnimatedPropertyList.js
+++ b/devtools/client/inspector/animation/components/AnimatedPropertyList.js
@@ -1,28 +1,39 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-const { Component, createFactory } = require("devtools/client/shared/vendor/react");
+const { createFactory, PureComponent } = require("devtools/client/shared/vendor/react");
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 
 const AnimatedPropertyItem = createFactory(require("./AnimatedPropertyItem"));
+const InspectListBox = createFactory(require("./InspectListBox"));
+const KeyframesProgressBar = createFactory(require("./KeyframesProgressBar"));
+const TickLabels = createFactory(require("./TickLabels"));
+const TickLines = createFactory(require("./TickLines"));
 
-class AnimatedPropertyList extends Component {
+const { getFormatStr } = require("../utils/l10n");
+
+class AnimatedPropertyList extends PureComponent {
   static get propTypes() {
     return {
+      addAnimationsCurrentTimeListener: PropTypes.func.isRequired,
       animation: PropTypes.object.isRequired,
       emitEventForTest: PropTypes.func.isRequired,
       getAnimatedPropertyMap: PropTypes.func.isRequired,
+      getAnimationsCurrentTime: PropTypes.func.isRequired,
       getComputedStyle: PropTypes.func.isRequired,
+      removeAnimationsCurrentTimeListener: PropTypes.func.isRequired,
       simulateAnimation: PropTypes.func.isRequired,
+      simulateAnimationForKeyframesProgressBar: PropTypes.func.isRequired,
+      timeScale: PropTypes.object.isRequired,
     };
   }
 
   constructor(props) {
     super(props);
 
     this.state = {
       // Array of object which has the property name, the keyframes, its aniamtion type
@@ -104,42 +115,69 @@ class AnimatedPropertyList extends Compo
       }
     );
 
     emitEventForTest("animation-keyframes-rendered");
   }
 
   render() {
     const {
+      addAnimationsCurrentTimeListener,
+      animation,
+      getAnimationsCurrentTime,
       getComputedStyle,
+      removeAnimationsCurrentTimeListener,
       simulateAnimation,
+      simulateAnimationForKeyframesProgressBar,
+      timeScale,
     } = this.props;
-    const {
-      animatedProperties,
-    } = this.state;
+    const { animatedProperties } = this.state;
 
     if (!animatedProperties) {
       return null;
     }
 
-    return dom.ul(
+    const ticks = [0, 50, 100].map(position => {
+      return {
+        position,
+        label: getFormatStr("detail.propertiesHeader.percentage", position),
+      };
+    });
+
+    return dom.div(
       {
-        className: "animated-property-list"
+        className: `animated-property-list ${ animation.state.type }`
       },
-      animatedProperties.map(({ isUnchanged, keyframes, name, type }) => {
-        const state = this.getPropertyState(name);
-        return AnimatedPropertyItem(
-          {
-            getComputedStyle,
-            isUnchanged,
-            keyframes,
-            name,
-            simulateAnimation,
-            state,
-            type,
-          }
-        );
-      })
+      InspectListBox(
+        {
+          background: TickLines({ ticks }),
+          header: TickLabels({ ticks }),
+          items: animatedProperties.map(({ isUnchanged, keyframes, name, type }) => {
+            const state = this.getPropertyState(name);
+            return AnimatedPropertyItem(
+              {
+                getComputedStyle,
+                isUnchanged,
+                keyframes,
+                name,
+                simulateAnimation,
+                state,
+                type,
+              }
+            );
+          }),
+          indicator: KeyframesProgressBar(
+            {
+              addAnimationsCurrentTimeListener,
+              animation,
+              getAnimationsCurrentTime,
+              removeAnimationsCurrentTimeListener,
+              simulateAnimationForKeyframesProgressBar,
+              timeScale,
+            }
+          )
+        }
+      )
     );
   }
 }
 
 module.exports = AnimatedPropertyList;
deleted file mode 100644
--- a/devtools/client/inspector/animation/components/AnimatedPropertyListContainer.js
+++ /dev/null
@@ -1,77 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-const { createFactory, PureComponent } = require("devtools/client/shared/vendor/react");
-const dom = require("devtools/client/shared/vendor/react-dom-factories");
-const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
-
-const AnimatedPropertyList = createFactory(require("./AnimatedPropertyList"));
-const AnimatedPropertyListHeader = createFactory(require("./AnimatedPropertyListHeader"));
-
-class AnimatedPropertyListContainer extends PureComponent {
-  static get propTypes() {
-    return {
-      addAnimationsCurrentTimeListener: PropTypes.func.isRequired,
-      animation: PropTypes.object.isRequired,
-      emitEventForTest: PropTypes.func.isRequired,
-      getAnimatedPropertyMap: PropTypes.func.isRequired,
-      getAnimationsCurrentTime: PropTypes.func.isRequired,
-      getComputedStyle: PropTypes.func.isRequired,
-      removeAnimationsCurrentTimeListener: PropTypes.func.isRequired,
-      simulateAnimation: PropTypes.func.isRequired,
-      simulateAnimationForKeyframesProgressBar: PropTypes.func.isRequired,
-      timeScale: PropTypes.object.isRequired,
-    };
-  }
-
-  render() {
-    const {
-      addAnimationsCurrentTimeListener,
-      animation,
-      emitEventForTest,
-      getAnimatedPropertyMap,
-      getAnimationsCurrentTime,
-      getComputedStyle,
-      removeAnimationsCurrentTimeListener,
-      simulateAnimation,
-      simulateAnimationForKeyframesProgressBar,
-      timeScale,
-    } = this.props;
-
-    return dom.div(
-      {
-        className: `animated-property-list-container ${ animation.state.type }`
-      },
-      AnimatedPropertyListHeader(
-        {
-          addAnimationsCurrentTimeListener,
-          animation,
-          getAnimationsCurrentTime,
-          removeAnimationsCurrentTimeListener,
-          simulateAnimationForKeyframesProgressBar,
-          timeScale,
-        }
-      ),
-      dom.div(
-        {
-          className: "animated-property-list-background",
-        },
-        dom.span()
-      ),
-      AnimatedPropertyList(
-        {
-          animation,
-          emitEventForTest,
-          getAnimatedPropertyMap,
-          getComputedStyle,
-          simulateAnimation,
-        }
-      )
-    );
-  }
-}
-
-module.exports = AnimatedPropertyListContainer;
deleted file mode 100644
--- a/devtools/client/inspector/animation/components/AnimatedPropertyListHeader.js
+++ /dev/null
@@ -1,60 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-const { createFactory, PureComponent } = require("devtools/client/shared/vendor/react");
-const dom = require("devtools/client/shared/vendor/react-dom-factories");
-const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
-
-const KeyframesProgressBar = createFactory(require("./KeyframesProgressBar"));
-const KeyframesProgressTickList = createFactory(require("./KeyframesProgressTickList"));
-
-class AnimatedPropertyListHeader extends PureComponent {
-  static get propTypes() {
-    return {
-      addAnimationsCurrentTimeListener: PropTypes.func.isRequired,
-      animation: PropTypes.object.isRequired,
-      getAnimationsCurrentTime: PropTypes.func.isRequired,
-      removeAnimationsCurrentTimeListener: PropTypes.func.isRequired,
-      simulateAnimationForKeyframesProgressBar: PropTypes.func.isRequired,
-      timeScale: PropTypes.object.isRequired,
-    };
-  }
-
-  render() {
-    const {
-      addAnimationsCurrentTimeListener,
-      animation,
-      getAnimationsCurrentTime,
-      removeAnimationsCurrentTimeListener,
-      simulateAnimationForKeyframesProgressBar,
-      timeScale,
-    } = this.props;
-
-    return dom.div(
-      {
-        className: "animated-property-list-header"
-      },
-      dom.div(
-        {
-          className: "devtools-toolbar"
-        }
-      ),
-      KeyframesProgressTickList(),
-      KeyframesProgressBar(
-        {
-          addAnimationsCurrentTimeListener,
-          animation,
-          getAnimationsCurrentTime,
-          removeAnimationsCurrentTimeListener,
-          simulateAnimationForKeyframesProgressBar,
-          timeScale,
-        }
-      )
-    );
-  }
-}
-
-module.exports = AnimatedPropertyListHeader;
--- a/devtools/client/inspector/animation/components/AnimationDetailContainer.js
+++ b/devtools/client/inspector/animation/components/AnimationDetailContainer.js
@@ -5,18 +5,17 @@
 "use strict";
 
 const { connect } = require("devtools/client/shared/vendor/react-redux");
 const { createFactory, PureComponent } = require("devtools/client/shared/vendor/react");
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 
 const AnimationDetailHeader = createFactory(require("./AnimationDetailHeader"));
-const AnimatedPropertyListContainer =
-  createFactory(require("./AnimatedPropertyListContainer"));
+const AnimatedPropertyList = createFactory(require("./AnimatedPropertyList"));
 
 class AnimationDetailContainer extends PureComponent {
   static get propTypes() {
     return {
       addAnimationsCurrentTimeListener: PropTypes.func.isRequired,
       animation: PropTypes.object.isRequired,
       emitEventForTest: PropTypes.func.isRequired,
       getAnimatedPropertyMap: PropTypes.func.isRequired,
@@ -54,17 +53,17 @@ class AnimationDetailContainer extends P
           {
             animation,
             setDetailVisibility,
           }
         )
       :
         null,
       animation ?
-        AnimatedPropertyListContainer(
+        AnimatedPropertyList(
           {
             addAnimationsCurrentTimeListener,
             animation,
             emitEventForTest,
             getAnimatedPropertyMap,
             getAnimationsCurrentTime,
             getComputedStyle,
             removeAnimationsCurrentTimeListener,
--- a/devtools/client/inspector/animation/components/AnimationList.js
+++ b/devtools/client/inspector/animation/components/AnimationList.js
@@ -1,70 +1,147 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-const { createFactory, PureComponent } = require("devtools/client/shared/vendor/react");
+const { createFactory, PureComponent } =
+  require("devtools/client/shared/vendor/react");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
-const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+const ReactDOM = require("devtools/client/shared/vendor/react-dom");
 
 const AnimationItem = createFactory(require("./AnimationItem"));
+const CurrentTimeScrubber = createFactory(require("./CurrentTimeScrubber"));
+const InspectListBox = createFactory(require("./InspectListBox"));
+const TickLabels = createFactory(require("./TickLabels"));
+const TickLines = createFactory(require("./TickLines"));
+
+const { findOptimalTimeInterval } = require("../utils/utils");
+
+// The minimum spacing between 2 time graduation headers in the timeline (px).
+const TIME_GRADUATION_MIN_SPACING = 40;
 
 class AnimationList extends PureComponent {
   static get propTypes() {
     return {
+      addAnimationsCurrentTimeListener: PropTypes.func.isRequired,
       animations: PropTypes.arrayOf(PropTypes.object).isRequired,
       emitEventForTest: PropTypes.func.isRequired,
       getAnimatedPropertyMap: PropTypes.func.isRequired,
       getNodeFromActor: PropTypes.func.isRequired,
       onHideBoxModelHighlighter: PropTypes.func.isRequired,
       onShowBoxModelHighlighterForNode: PropTypes.func.isRequired,
+      removeAnimationsCurrentTimeListener: PropTypes.func.isRequired,
       selectAnimation: PropTypes.func.isRequired,
+      setAnimationsCurrentTime: PropTypes.func.isRequired,
       setHighlightedNode: PropTypes.func.isRequired,
       setSelectedNode: PropTypes.func.isRequired,
+      sidebarWidth: PropTypes.number.isRequired,
       simulateAnimation: PropTypes.func.isRequired,
       timeScale: PropTypes.object.isRequired,
     };
   }
 
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      ticks: [],
+    };
+  }
+
+  componentDidMount() {
+    this.updateState(this.props);
+  }
+
+  componentWillReceiveProps(nextProps) {
+    this.updateState(nextProps);
+  }
+
+  updateState(props) {
+    const { timeScale } = props;
+    const tickLinesEl = ReactDOM.findDOMNode(this).querySelector(".tick-lines");
+    const width = tickLinesEl.offsetWidth;
+    const animationDuration = timeScale.getDuration();
+    const minTimeInterval = TIME_GRADUATION_MIN_SPACING * animationDuration / width;
+    const intervalLength = findOptimalTimeInterval(minTimeInterval);
+    const intervalWidth = intervalLength * width / animationDuration;
+    const tickCount = Math.ceil(width / intervalWidth);
+
+    const ticks = [];
+
+    for (let i = 0; i <= tickCount; i++) {
+      const position = i * intervalWidth * 100 / width;
+      const label = timeScale.formatTime(timeScale.distanceToRelativeTime(position));
+      ticks.push({ position, label });
+    }
+
+    this.setState({ ticks });
+  }
+
   render() {
     const {
+      addAnimationsCurrentTimeListener,
       animations,
       emitEventForTest,
       getAnimatedPropertyMap,
       getNodeFromActor,
       onHideBoxModelHighlighter,
       onShowBoxModelHighlighterForNode,
+      removeAnimationsCurrentTimeListener,
       selectAnimation,
+      setAnimationsCurrentTime,
       setHighlightedNode,
       setSelectedNode,
       simulateAnimation,
       timeScale,
     } = this.props;
+    const { ticks } = this.state;
 
-    return dom.ul(
+    return dom.div(
       {
-        className: "animation-list"
+        className: "animation-list",
       },
-      animations.map(animation =>
-        AnimationItem(
-          {
-            animation,
-            emitEventForTest,
-            getAnimatedPropertyMap,
-            getNodeFromActor,
-            onHideBoxModelHighlighter,
-            onShowBoxModelHighlighterForNode,
-            selectAnimation,
-            setHighlightedNode,
-            setSelectedNode,
-            simulateAnimation,
-            timeScale,
-          }
-        )
+      InspectListBox(
+        {
+          background: TickLines({ ticks }),
+          header: TickLabels({ ticks }),
+          items: animations.map(animation =>
+            AnimationItem(
+              {
+                animation,
+                emitEventForTest,
+                getAnimatedPropertyMap,
+                getNodeFromActor,
+                onHideBoxModelHighlighter,
+                onShowBoxModelHighlighterForNode,
+                selectAnimation,
+                setHighlightedNode,
+                setSelectedNode,
+                simulateAnimation,
+                timeScale,
+              }
+            )
+          ),
+          indicator: CurrentTimeScrubber(
+            {
+              addAnimationsCurrentTimeListener,
+              removeAnimationsCurrentTimeListener,
+              setAnimationsCurrentTime,
+              timeScale,
+            }
+          )
+        }
       )
     );
   }
 }
 
-module.exports = AnimationList;
+const mapStateToProps = state => {
+  return {
+    sidebarWidth: state.animations.sidebarSize ? state.animations.sidebarSize.width : 0
+  };
+};
+
+module.exports = connect(mapStateToProps)(AnimationList);
deleted file mode 100644
--- a/devtools/client/inspector/animation/components/AnimationListContainer.js
+++ /dev/null
@@ -1,84 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-const { createFactory, PureComponent } =
-  require("devtools/client/shared/vendor/react");
-const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
-const dom = require("devtools/client/shared/vendor/react-dom-factories");
-
-const AnimationList = createFactory(require("./AnimationList"));
-const AnimationListHeader = createFactory(require("./AnimationListHeader"));
-
-class AnimationListContainer extends PureComponent {
-  static get propTypes() {
-    return {
-      addAnimationsCurrentTimeListener: PropTypes.func.isRequired,
-      animations: PropTypes.arrayOf(PropTypes.object).isRequired,
-      emitEventForTest: PropTypes.func.isRequired,
-      getAnimatedPropertyMap: PropTypes.func.isRequired,
-      getNodeFromActor: PropTypes.func.isRequired,
-      onHideBoxModelHighlighter: PropTypes.func.isRequired,
-      onShowBoxModelHighlighterForNode: PropTypes.func.isRequired,
-      removeAnimationsCurrentTimeListener: PropTypes.func.isRequired,
-      selectAnimation: PropTypes.func.isRequired,
-      setAnimationsCurrentTime: PropTypes.func.isRequired,
-      setHighlightedNode: PropTypes.func.isRequired,
-      setSelectedNode: PropTypes.func.isRequired,
-      simulateAnimation: PropTypes.func.isRequired,
-      timeScale: PropTypes.object.isRequired,
-    };
-  }
-
-  render() {
-    const {
-      addAnimationsCurrentTimeListener,
-      animations,
-      emitEventForTest,
-      getAnimatedPropertyMap,
-      getNodeFromActor,
-      onHideBoxModelHighlighter,
-      onShowBoxModelHighlighterForNode,
-      removeAnimationsCurrentTimeListener,
-      selectAnimation,
-      setAnimationsCurrentTime,
-      setHighlightedNode,
-      setSelectedNode,
-      simulateAnimation,
-      timeScale,
-    } = this.props;
-
-    return dom.div(
-      {
-        className: "animation-list-container"
-      },
-      AnimationListHeader(
-        {
-          addAnimationsCurrentTimeListener,
-          removeAnimationsCurrentTimeListener,
-          setAnimationsCurrentTime,
-          timeScale,
-        }
-      ),
-      AnimationList(
-        {
-          animations,
-          emitEventForTest,
-          getAnimatedPropertyMap,
-          getNodeFromActor,
-          onHideBoxModelHighlighter,
-          onShowBoxModelHighlighterForNode,
-          selectAnimation,
-          setHighlightedNode,
-          setSelectedNode,
-          simulateAnimation,
-          timeScale,
-        }
-      )
-    );
-  }
-}
-
-module.exports = AnimationListContainer;
deleted file mode 100644
--- a/devtools/client/inspector/animation/components/AnimationListHeader.js
+++ /dev/null
@@ -1,60 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-const { createFactory, PureComponent } =
-  require("devtools/client/shared/vendor/react");
-const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
-const dom = require("devtools/client/shared/vendor/react-dom-factories");
-
-const AnimationTimelineTickList = createFactory(require("./AnimationTimelineTickList"));
-const CurrentTimeScrubberController =
-  createFactory(require("./CurrentTimeScrubberController"));
-
-class AnimationListHeader extends PureComponent {
-  static get propTypes() {
-    return {
-      addAnimationsCurrentTimeListener: PropTypes.func.isRequired,
-      removeAnimationsCurrentTimeListener: PropTypes.func.isRequired,
-      setAnimationsCurrentTime: PropTypes.func.isRequired,
-      timeScale: PropTypes.object.isRequired,
-    };
-  }
-
-  render() {
-    const {
-      addAnimationsCurrentTimeListener,
-      removeAnimationsCurrentTimeListener,
-      setAnimationsCurrentTime,
-      timeScale,
-    } = this.props;
-
-    return dom.div(
-      {
-        className: "animation-list-header"
-      },
-      dom.div(
-        {
-          className: "devtools-toolbar"
-        }
-      ),
-      AnimationTimelineTickList(
-        {
-          timeScale
-        }
-      ),
-      CurrentTimeScrubberController(
-        {
-          addAnimationsCurrentTimeListener,
-          removeAnimationsCurrentTimeListener,
-          setAnimationsCurrentTime,
-          timeScale,
-        }
-      )
-    );
-  }
-}
-
-module.exports = AnimationListHeader;
deleted file mode 100644
--- a/devtools/client/inspector/animation/components/AnimationTimelineTickItem.js
+++ /dev/null
@@ -1,32 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-const { PureComponent } = require("devtools/client/shared/vendor/react");
-const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
-const dom = require("devtools/client/shared/vendor/react-dom-factories");
-
-class AnimationTimeTickItem extends PureComponent {
-  static get propTypes() {
-    return {
-      position: PropTypes.number.isRequired,
-      timeTickLabel: PropTypes.string.isRequired,
-    };
-  }
-
-  render() {
-    const { position, timeTickLabel } = this.props;
-
-    return dom.div(
-      {
-        className: "animation-timeline-tick-item",
-        style: { left: `${ position }%` }
-      },
-      timeTickLabel
-    );
-  }
-}
-
-module.exports = AnimationTimeTickItem;
deleted file mode 100644
--- a/devtools/client/inspector/animation/components/AnimationTimelineTickList.js
+++ /dev/null
@@ -1,101 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-const { Component, createFactory } = require("devtools/client/shared/vendor/react");
-const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
-const dom = require("devtools/client/shared/vendor/react-dom-factories");
-const { connect } = require("devtools/client/shared/vendor/react-redux");
-const ReactDOM = require("devtools/client/shared/vendor/react-dom");
-
-const AnimationTimelineTickItem = createFactory(require("./AnimationTimelineTickItem"));
-
-const { findOptimalTimeInterval } = require("../utils/utils");
-
-// The minimum spacing between 2 time graduation headers in the timeline (px).
-const TIME_GRADUATION_MIN_SPACING = 40;
-
-class AnimationTimelineTickList extends Component {
-  static get propTypes() {
-    return {
-      sidebarWidth: PropTypes.number.isRequired,
-      timeScale: PropTypes.object.isRequired,
-    };
-  }
-
-  constructor(props) {
-    super(props);
-
-    this.state = {
-      tickList: [],
-    };
-  }
-
-  componentDidMount() {
-    this.updateTickList(this.props);
-  }
-
-  componentWillReceiveProps(nextProps) {
-    this.updateTickList(nextProps);
-  }
-
-  shouldComponentUpdate(nextProps, nextState) {
-    if (this.state.tickList.length !== nextState.tickList.length) {
-      return true;
-    }
-
-    for (let i = 0; i < this.state.tickList.length; i++) {
-      const currentTickItem = this.state.tickList[i];
-      const nextTickItem = nextState.tickList[i];
-
-      if (currentTickItem.timeTickLabel !== nextTickItem.timeTickLabel) {
-        return true;
-      }
-    }
-
-    return false;
-  }
-
-  updateTickList(props) {
-    const { timeScale } = props;
-    const tickListEl = ReactDOM.findDOMNode(this);
-    const width = tickListEl.offsetWidth;
-    const animationDuration = timeScale.getDuration();
-    const minTimeInterval = TIME_GRADUATION_MIN_SPACING * animationDuration / width;
-    const intervalLength = findOptimalTimeInterval(minTimeInterval);
-    const intervalWidth = intervalLength * width / animationDuration;
-    const tickCount = width / intervalWidth;
-    const intervalPositionPercentage = 100 * intervalWidth / width;
-
-    const tickList = [];
-    for (let i = 0; i <= tickCount; i++) {
-      const position = i * intervalPositionPercentage;
-      const timeTickLabel =
-        timeScale.formatTime(timeScale.distanceToRelativeTime(position));
-      tickList.push({ position, timeTickLabel });
-    }
-
-    this.setState({ tickList });
-  }
-
-  render() {
-    const { tickList } = this.state;
-
-    return dom.div(
-      {
-        className: "animation-timeline-tick-list"
-      },
-      tickList.map(tickItem => AnimationTimelineTickItem(tickItem))
-    );
-  }
-}
-
-const mapStateToProps = state => {
-  return {
-    sidebarWidth: state.animations.sidebarSize ? state.animations.sidebarSize.width : 0
-  };
-};
-
-module.exports = connect(mapStateToProps)(AnimationTimelineTickList);
--- a/devtools/client/inspector/animation/components/App.js
+++ b/devtools/client/inspector/animation/components/App.js
@@ -5,17 +5,17 @@
 "use strict";
 
 const { Component, createFactory } = require("devtools/client/shared/vendor/react");
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 const { connect } = require("devtools/client/shared/vendor/react-redux");
 
 const AnimationDetailContainer = createFactory(require("./AnimationDetailContainer"));
-const AnimationListContainer = createFactory(require("./AnimationListContainer"));
+const AnimationList = createFactory(require("./AnimationList"));
 const AnimationToolbar = createFactory(require("./AnimationToolbar"));
 const NoAnimationPanel = createFactory(require("./NoAnimationPanel"));
 const SplitBox = createFactory(require("devtools/client/shared/components/splitter/SplitBox"));
 
 class App extends Component {
   static get propTypes() {
     return {
       addAnimationsCurrentTimeListener: PropTypes.func.isRequired,
@@ -108,17 +108,17 @@ class App extends Component {
               simulateAnimation,
               simulateAnimationForKeyframesProgressBar,
               timeScale,
             }
           ),
           endPanelControl: true,
           initialHeight: "50%",
           splitterSize: 1,
-          startPanel: AnimationListContainer(
+          startPanel: AnimationList(
             {
               addAnimationsCurrentTimeListener,
               animations,
               emitEventForTest,
               getAnimatedPropertyMap,
               getNodeFromActor,
               onHideBoxModelHighlighter,
               onShowBoxModelHighlighterForNode,
--- a/devtools/client/inspector/animation/components/CurrentTimeScrubber.js
+++ b/devtools/client/inspector/animation/components/CurrentTimeScrubber.js
@@ -1,32 +1,140 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-const { PureComponent } = require("devtools/client/shared/vendor/react");
+const { createFactory, PureComponent } = require("devtools/client/shared/vendor/react");
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+
+const IndicationBar = createFactory(require("./IndicationBar"));
 
 class CurrentTimeScrubber extends PureComponent {
   static get propTypes() {
     return {
-      offset: PropTypes.number.isRequired,
+      addAnimationsCurrentTimeListener: PropTypes.func.isRequired,
+      removeAnimationsCurrentTimeListener: PropTypes.func.isRequired,
+      setAnimationsCurrentTime: PropTypes.func.isRequired,
+      timeScale: PropTypes.object.isRequired,
+    };
+  }
+
+  constructor(props) {
+    super(props);
+
+    const { addAnimationsCurrentTimeListener } = props;
+    this.onCurrentTimeUpdated = this.onCurrentTimeUpdated.bind(this);
+    this.onMouseDown = this.onMouseDown.bind(this);
+    this.onMouseMove = this.onMouseMove.bind(this);
+    this.onMouseOut = this.onMouseOut.bind(this);
+    this.onMouseUp = this.onMouseUp.bind(this);
+
+    this.state = {
+      // position for the scrubber
+      position: 0,
     };
+
+    addAnimationsCurrentTimeListener(this.onCurrentTimeUpdated);
+  }
+
+  componentDidMount() {
+    const el = ReactDOM.findDOMNode(this);
+    el.addEventListener("mousedown", this.onMouseDown);
+  }
+
+  componentWillUnmount() {
+    const { removeAnimationsCurrentTimeListener } = this.props;
+    removeAnimationsCurrentTimeListener(this.onCurrentTimeUpdated);
+  }
+
+  onCurrentTimeUpdated(currentTime) {
+    const { timeScale } = this.props;
+
+    const position = currentTime / timeScale.getDuration();
+    this.setState({ position });
+  }
+
+  onMouseDown(event) {
+    event.stopPropagation();
+    const thisEl = ReactDOM.findDOMNode(this);
+    this.controllerArea = thisEl.getBoundingClientRect();
+    this.listenerTarget = thisEl.closest(".animation-list");
+    this.listenerTarget.addEventListener("mousemove", this.onMouseMove);
+    this.listenerTarget.addEventListener("mouseout", this.onMouseOut);
+    this.listenerTarget.addEventListener("mouseup", this.onMouseUp);
+    this.listenerTarget.classList.add("active-scrubber");
+
+    this.updateAnimationsCurrentTime(event.pageX, true);
+  }
+
+  onMouseMove(event) {
+    event.stopPropagation();
+    this.isMouseMoved = true;
+    this.updateAnimationsCurrentTime(event.pageX);
+  }
+
+  onMouseOut(event) {
+    event.stopPropagation();
+
+    if (!this.listenerTarget.contains(event.relatedTarget)) {
+      const endX = this.controllerArea.x + this.controllerArea.width;
+      const pageX = endX < event.pageX ? endX : event.pageX;
+      this.updateAnimationsCurrentTime(pageX, true);
+      this.uninstallListeners();
+    }
+  }
+
+  onMouseUp(event) {
+    event.stopPropagation();
+
+    if (this.isMouseMoved) {
+      this.updateAnimationsCurrentTime(event.pageX, true);
+      this.isMouseMoved = null;
+    }
+
+    this.uninstallListeners();
+  }
+
+  uninstallListeners() {
+    this.listenerTarget.removeEventListener("mousemove", this.onMouseMove);
+    this.listenerTarget.removeEventListener("mouseout", this.onMouseOut);
+    this.listenerTarget.removeEventListener("mouseup", this.onMouseUp);
+    this.listenerTarget.classList.remove("active-scrubber");
+    this.listenerTarget = null;
+    this.controllerArea = null;
+  }
+
+  updateAnimationsCurrentTime(pageX, needRefresh) {
+    const {
+      setAnimationsCurrentTime,
+      timeScale,
+    } = this.props;
+
+    const time = pageX - this.controllerArea.x < 0 ?
+                 0 :
+                 (pageX - this.controllerArea.x) /
+    this.controllerArea.width * timeScale.getDuration();
+
+    setAnimationsCurrentTime(time, needRefresh);
   }
 
   render() {
-    const { offset } = this.props;
+    const { position } = this.state;
 
     return dom.div(
       {
-        className: "current-time-scrubber",
-        style: {
-          transform: `translateX(${ offset }px)`,
-        },
-      }
+        className: "current-time-scrubber-area",
+      },
+      IndicationBar(
+        {
+          className: "current-time-scrubber",
+          position
+        }
+      )
     );
   }
 }
 
 module.exports = CurrentTimeScrubber;
deleted file mode 100644
--- a/devtools/client/inspector/animation/components/CurrentTimeScrubberController.js
+++ /dev/null
@@ -1,141 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-const { createFactory, PureComponent } = require("devtools/client/shared/vendor/react");
-const dom = require("devtools/client/shared/vendor/react-dom-factories");
-const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
-const ReactDOM = require("devtools/client/shared/vendor/react-dom");
-
-const CurrentTimeScrubber = createFactory(require("./CurrentTimeScrubber"));
-
-class CurrentTimeScrubberController extends PureComponent {
-  static get propTypes() {
-    return {
-      addAnimationsCurrentTimeListener: PropTypes.func.isRequired,
-      removeAnimationsCurrentTimeListener: PropTypes.func.isRequired,
-      setAnimationsCurrentTime: PropTypes.func.isRequired,
-      timeScale: PropTypes.object.isRequired,
-    };
-  }
-
-  constructor(props) {
-    super(props);
-
-    const { addAnimationsCurrentTimeListener } = props;
-    this.onCurrentTimeUpdated = this.onCurrentTimeUpdated.bind(this);
-    this.onMouseDown = this.onMouseDown.bind(this);
-    this.onMouseMove = this.onMouseMove.bind(this);
-    this.onMouseOut = this.onMouseOut.bind(this);
-    this.onMouseUp = this.onMouseUp.bind(this);
-
-    this.state = {
-      // offset of the position for the scrubber
-      offset: 0,
-    };
-
-    addAnimationsCurrentTimeListener(this.onCurrentTimeUpdated);
-  }
-
-  componentDidMount() {
-    const el = ReactDOM.findDOMNode(this);
-    el.addEventListener("mousedown", this.onMouseDown);
-  }
-
-  componentWillUnmount() {
-    const { removeAnimationsCurrentTimeListener } = this.props;
-    removeAnimationsCurrentTimeListener(this.onCurrentTimeUpdated);
-  }
-
-  onCurrentTimeUpdated(currentTime) {
-    const { timeScale } = this.props;
-
-    const thisEl = ReactDOM.findDOMNode(this);
-    const offset =
-      thisEl ? currentTime / timeScale.getDuration() * thisEl.clientWidth : 0;
-    this.setState({ offset });
-  }
-
-  onMouseDown(event) {
-    event.stopPropagation();
-    const thisEl = ReactDOM.findDOMNode(this);
-    this.controllerArea = thisEl.getBoundingClientRect();
-    this.listenerTarget = thisEl.closest(".animation-list-container");
-    this.listenerTarget.addEventListener("mousemove", this.onMouseMove);
-    this.listenerTarget.addEventListener("mouseout", this.onMouseOut);
-    this.listenerTarget.addEventListener("mouseup", this.onMouseUp);
-    this.listenerTarget.classList.add("active-scrubber");
-
-    this.updateAnimationsCurrentTime(event.pageX, true);
-  }
-
-  onMouseMove(event) {
-    event.stopPropagation();
-    this.isMouseMoved = true;
-    this.updateAnimationsCurrentTime(event.pageX);
-  }
-
-  onMouseOut(event) {
-    event.stopPropagation();
-
-    if (!this.listenerTarget.contains(event.relatedTarget)) {
-      const endX = this.controllerArea.x + this.controllerArea.width;
-      const pageX = endX < event.pageX ? endX : event.pageX;
-      this.updateAnimationsCurrentTime(pageX, true);
-      this.uninstallListeners();
-    }
-  }
-
-  onMouseUp(event) {
-    event.stopPropagation();
-
-    if (this.isMouseMoved) {
-      this.updateAnimationsCurrentTime(event.pageX, true);
-      this.isMouseMoved = null;
-    }
-
-    this.uninstallListeners();
-  }
-
-  uninstallListeners() {
-    this.listenerTarget.removeEventListener("mousemove", this.onMouseMove);
-    this.listenerTarget.removeEventListener("mouseout", this.onMouseOut);
-    this.listenerTarget.removeEventListener("mouseup", this.onMouseUp);
-    this.listenerTarget.classList.remove("active-scrubber");
-    this.listenerTarget = null;
-    this.controllerArea = null;
-  }
-
-  updateAnimationsCurrentTime(pageX, needRefresh) {
-    const {
-      setAnimationsCurrentTime,
-      timeScale,
-    } = this.props;
-
-    const time = pageX - this.controllerArea.x < 0 ?
-                   0 :
-                   (pageX - this.controllerArea.x) /
-                     this.controllerArea.width * timeScale.getDuration();
-
-    setAnimationsCurrentTime(time, needRefresh);
-  }
-
-  render() {
-    const { offset } = this.state;
-
-    return dom.div(
-      {
-        className: "current-time-scrubber-controller",
-      },
-      CurrentTimeScrubber(
-        {
-          offset,
-        }
-      )
-    );
-  }
-}
-
-module.exports = CurrentTimeScrubberController;
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/animation/components/IndicationBar.js
@@ -0,0 +1,69 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { PureComponent } = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+
+class IndicationBar extends PureComponent {
+  static get propTypes() {
+    return {
+      className: PropTypes.string.isRequired,
+      position: PropTypes.number.isRequired,
+      sidebarWidth: PropTypes.number.isRequired,
+    };
+  }
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      // offset of the position for this indicator
+      offset: 0,
+    };
+  }
+
+  componentDidMount() {
+    this.updateState(this.props);
+  }
+
+  componentWillReceiveProps(nextProps) {
+    this.updateState(nextProps);
+  }
+
+  updateState(props) {
+    const { position } = props;
+
+    const parentEl = ReactDOM.findDOMNode(this).parentNode;
+    const offset = parentEl.offsetWidth * position;
+
+    this.setState({ offset });
+  }
+
+  render() {
+    const { className } = this.props;
+    const { offset } = this.state;
+
+    return dom.div(
+      {
+        className: `indication-bar ${ className }`,
+        style: {
+          transform: `translateX(${ offset }px)`,
+        },
+      }
+    );
+  }
+}
+
+const mapStateToProps = state => {
+  return {
+    sidebarWidth: state.animations.sidebarSize ? state.animations.sidebarSize.width : 0
+  };
+};
+
+module.exports = connect(mapStateToProps)(IndicationBar);
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/animation/components/InspectListBox.js
@@ -0,0 +1,41 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { PureComponent } = require("devtools/client/shared/vendor/react");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+
+class InspectListBox extends PureComponent {
+  static get propTypes() {
+    return {
+      background: PropTypes.any.isRequired,
+      header: PropTypes.any.isRequired,
+      indicator: PropTypes.any.isRequired,
+      items: PropTypes.arrayOf(PropTypes.any).isRequired,
+    };
+  }
+
+  render() {
+    const {
+      background,
+      header,
+      indicator,
+      items
+    } = this.props;
+
+    return dom.div(
+      {
+        className: "inspect-list-box",
+      },
+      dom.div({ className: "background" }, background),
+      dom.div({ className: "indicator" }, indicator),
+      dom.div({ className: "header devtools-toolbar" }, header),
+      dom.ul({}, items)
+    );
+  }
+}
+
+module.exports = InspectListBox;
--- a/devtools/client/inspector/animation/components/KeyframesProgressBar.js
+++ b/devtools/client/inspector/animation/components/KeyframesProgressBar.js
@@ -1,18 +1,19 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-const { PureComponent } = require("devtools/client/shared/vendor/react");
+const { createFactory, PureComponent } = require("devtools/client/shared/vendor/react");
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
-const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+
+const IndicationBar = createFactory(require("./IndicationBar"));
 
 class KeyframesProgressBar extends PureComponent {
   static get propTypes() {
     return {
       addAnimationsCurrentTimeListener: PropTypes.func.isRequired,
       animation: PropTypes.object.isRequired,
       getAnimationsCurrentTime: PropTypes.func.isRequired,
       removeAnimationsCurrentTimeListener: PropTypes.func.isRequired,
@@ -22,50 +23,59 @@ class KeyframesProgressBar extends PureC
   }
 
   constructor(props) {
     super(props);
 
     this.onCurrentTimeUpdated = this.onCurrentTimeUpdated.bind(this);
 
     this.state = {
-      // offset of the position for the progress bar
-      offset: 0,
+      // position for the progress bar
+      position: 0,
     };
   }
 
-  componentDidMount() {
-    const { addAnimationsCurrentTimeListener } = this.props;
+  componentWillMount() {
+    const {
+      animation,
+      addAnimationsCurrentTimeListener,
+      getAnimationsCurrentTime,
+      timeScale,
+    } = this.props;
 
-    this.element = ReactDOM.findDOMNode(this);
+    addAnimationsCurrentTimeListener(this.onCurrentTimeUpdated);
     this.setupAnimation(this.props);
-    addAnimationsCurrentTimeListener(this.onCurrentTimeUpdated);
+    this.updateState(getAnimationsCurrentTime(), animation, timeScale);
   }
 
   componentWillReceiveProps(nextProps) {
     const { animation, getAnimationsCurrentTime, timeScale } = nextProps;
 
     this.setupAnimation(nextProps);
-    this.updateOffset(getAnimationsCurrentTime(), animation, timeScale);
+    this.updateState(getAnimationsCurrentTime(), animation, timeScale);
   }
 
   componentWillUnmount() {
     const { removeAnimationsCurrentTimeListener } = this.props;
 
     removeAnimationsCurrentTimeListener(this.onCurrentTimeUpdated);
-    this.element = null;
     this.simulatedAnimation = null;
   }
 
   onCurrentTimeUpdated(currentTime) {
     const { animation, timeScale } = this.props;
-    this.updateOffset(currentTime, animation, timeScale);
+    this.updateState(currentTime, animation, timeScale);
   }
 
-  updateOffset(currentTime, animation, timeScale) {
+  updateState(currentTime, animation, timeScale) {
+    if (!this.simulatedAnimation) {
+      // Animation inspector has been destroyed.
+      return;
+    }
+
     const {
       createdTime,
       playbackRate,
     } = animation.state;
 
     // If createdTime is not defined (which happens when connected to server older
     // than FF62), use previousStartTime instead. See bug 1454392
     const baseTime = typeof createdTime === "undefined"
@@ -75,20 +85,19 @@ class KeyframesProgressBar extends PureC
 
     if (isNaN(time)) {
       // Setting an invalid currentTime will throw so bail out if time is not a number for
       // any reason.
       return;
     }
 
     this.simulatedAnimation.currentTime = time;
-    const offset = this.element.offsetWidth *
-                   this.simulatedAnimation.effect.getComputedTiming().progress;
-
-    this.setState({ offset });
+    this.setState({
+      position: this.simulatedAnimation.effect.getComputedTiming().progress,
+    });
   }
 
   setupAnimation(props) {
     const {
       animation,
       simulateAnimationForKeyframesProgressBar,
     } = props;
 
@@ -99,27 +108,25 @@ class KeyframesProgressBar extends PureC
     const timing = Object.assign({}, animation.state, {
       iterations: animation.state.iterationCount || Infinity
     });
 
     this.simulatedAnimation = simulateAnimationForKeyframesProgressBar(timing);
   }
 
   render() {
-    const { offset } = this.state;
+    const { position } = this.state;
 
     return dom.div(
       {
-        className: "keyframes-progress-bar-area devtools-toolbar",
+        className: "keyframes-progress-bar-area",
       },
-      dom.div(
+      IndicationBar(
         {
           className: "keyframes-progress-bar",
-          style: {
-            transform: `translateX(${ offset }px)`,
-          },
+          position,
         }
       )
     );
   }
 }
 
 module.exports = KeyframesProgressBar;
deleted file mode 100644
--- a/devtools/client/inspector/animation/components/KeyframesProgressTickItem.js
+++ /dev/null
@@ -1,37 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-const { PureComponent } = require("devtools/client/shared/vendor/react");
-const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
-const dom = require("devtools/client/shared/vendor/react-dom-factories");
-
-class KeyframesProgressTickItem extends PureComponent {
-  static get propTypes() {
-    return {
-      direction: PropTypes.string.isRequired,
-      position: PropTypes.number.isRequired,
-      progressTickLabel: PropTypes.string.isRequired,
-    };
-  }
-
-  render() {
-    const {
-      direction,
-      position,
-      progressTickLabel,
-    } = this.props;
-
-    return dom.div(
-      {
-        className: `keyframes-progress-tick-item ${ direction }`,
-        style: { [direction]: `${ position }%` }
-      },
-      progressTickLabel
-    );
-  }
-}
-
-module.exports = KeyframesProgressTickItem;
deleted file mode 100644
--- a/devtools/client/inspector/animation/components/KeyframesProgressTickList.js
+++ /dev/null
@@ -1,37 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-const { createFactory, PureComponent } = require("devtools/client/shared/vendor/react");
-const dom = require("devtools/client/shared/vendor/react-dom-factories");
-
-const KeyframesProgressTickItem = createFactory(require("./KeyframesProgressTickItem"));
-const { getFormatStr } = require("../utils/l10n");
-
-class KeyframesProgressTickList extends PureComponent {
-  render() {
-    return dom.div(
-      {
-        className: "keyframes-progress-tick-list"
-      },
-      [0, 50, 100].map(progress => {
-        const direction = progress === 100 ? "right" : "left";
-        const position = progress === 100 ? 0 : progress;
-        const progressTickLabel =
-          getFormatStr("detail.propertiesHeader.percentage", progress);
-
-        return KeyframesProgressTickItem(
-          {
-            direction,
-            position,
-            progressTickLabel,
-          }
-        );
-      })
-    );
-  }
-}
-
-module.exports = KeyframesProgressTickList;
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/animation/components/TickLabels.js
@@ -0,0 +1,38 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { PureComponent } = require("devtools/client/shared/vendor/react");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+
+class TickLabels extends PureComponent {
+  static get propTypes() {
+    return {
+      ticks: PropTypes.array.isRequired,
+    };
+  }
+
+  render() {
+    const { ticks } = this.props;
+
+    return dom.div(
+      {
+        className: "tick-labels"
+      },
+      ticks.map(tick =>
+        dom.div(
+          {
+            className: "tick-label",
+            style: { left: `${ tick.position }%` },
+          },
+          tick.label
+        )
+      )
+    );
+  }
+}
+
+module.exports = TickLabels;
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/animation/components/TickLines.js
@@ -0,0 +1,37 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { PureComponent } = require("devtools/client/shared/vendor/react");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+
+class TickLines extends PureComponent {
+  static get propTypes() {
+    return {
+      ticks: PropTypes.array.isRequired,
+    };
+  }
+
+  render() {
+    const { ticks } = this.props;
+
+    return dom.div(
+      {
+        className: "tick-lines"
+      },
+      ticks.map(tick =>
+        dom.div(
+          {
+            className: "tick-line",
+            style: { left: `${ tick.position }%` }
+          }
+        )
+      )
+    );
+  }
+}
+
+module.exports = TickLines;
--- a/devtools/client/inspector/animation/components/moz.build
+++ b/devtools/client/inspector/animation/components/moz.build
@@ -5,33 +5,28 @@
 DIRS += [
     'graph',
     'keyframes-graph'
 ]
 
 DevToolsModules(
     'AnimatedPropertyItem.js',
     'AnimatedPropertyList.js',
-    'AnimatedPropertyListContainer.js',
-    'AnimatedPropertyListHeader.js',
     'AnimatedPropertyName.js',
     'AnimationDetailContainer.js',
     'AnimationDetailHeader.js',
     'AnimationItem.js',
     'AnimationList.js',
-    'AnimationListContainer.js',
-    'AnimationListHeader.js',
     'AnimationTarget.js',
-    'AnimationTimelineTickItem.js',
-    'AnimationTimelineTickList.js',
     'AnimationToolbar.js',
     'App.js',
     'CurrentTimeLabel.js',
     'CurrentTimeScrubber.js',
-    'CurrentTimeScrubberController.js',
+    'IndicationBar.js',
+    'InspectListBox.js',
     'KeyframesProgressBar.js',
-    'KeyframesProgressTickItem.js',
-    'KeyframesProgressTickList.js',
     'NoAnimationPanel.js',
     'PauseResumeButton.js',
     'PlaybackRateSelector.js',
     'RewindButton.js',
+    'TickLabels.js',
+    'TickLines.js',
 )
--- a/devtools/client/inspector/animation/test/browser_animation_animation-timeline-tick.js
+++ b/devtools/client/inspector/animation/test/browser_animation_animation-timeline-tick.js
@@ -19,53 +19,49 @@ const TIME_GRADUATION_MIN_SPACING = 40;
 add_task(async function() {
   await pushPref("devtools.inspector.three-pane-enabled", false);
   await addTab(URL_ROOT + "doc_simple_animation.html");
   await removeAnimatedElementsExcept([".end-delay", ".negative-delay"]);
   const { animationInspector, inspector, panel } = await openAnimationInspector();
   const timeScale = new TimeScale(animationInspector.state.animations);
 
   info("Checking animation list header element existence");
-  const listContainerEl = panel.querySelector(".animation-list-container");
+  const listContainerEl = panel.querySelector(".animation-list");
   const listHeaderEl = listContainerEl.querySelector(".devtools-toolbar");
   ok(listHeaderEl, "The header element should be in animation list container element");
 
   info("Checking time tick item elements existence");
   assertTimelineTickItems(timeScale, listContainerEl);
-  const timelineTickItemLength =
-    listContainerEl.querySelectorAll(".animation-timeline-tick-item").length;
+  const timelineTickItemLength = listContainerEl.querySelectorAll(".tick-label").length;
 
   info("Checking timeline tick item elements after enlarge sidebar width");
   await setSidebarWidth("100%", inspector);
   assertTimelineTickItems(timeScale, listContainerEl);
-  ok(timelineTickItemLength <
-     listContainerEl.querySelectorAll(".animation-timeline-tick-item").length,
+  ok(timelineTickItemLength < listContainerEl.querySelectorAll(".tick-label").length,
      "The timeline tick item elements should increase");
 });
 
 /**
  * Assert timeline tick item's position and label.
  *
  * @param {TimeScale} - timeScale
  * @param {Element} - listContainerEl
  */
 function assertTimelineTickItems(timeScale, listContainerEl) {
-  const timelineTickListEl =
-    listContainerEl.querySelector(".animation-timeline-tick-list");
+  const timelineTickListEl = listContainerEl.querySelector(".tick-labels");
   ok(timelineTickListEl,
     "The animation timeline tick list element should be in header");
 
   const width = timelineTickListEl.offsetWidth;
   const animationDuration = timeScale.getDuration();
   const minTimeInterval = TIME_GRADUATION_MIN_SPACING * animationDuration / width;
   const interval = findOptimalTimeInterval(minTimeInterval);
-  const expectedTickItem = Math.ceil(animationDuration / interval);
+  const expectedTickItem = Math.ceil(animationDuration / interval) + 1;
 
-  const timelineTickItemEls =
-    timelineTickListEl.querySelectorAll(".animation-timeline-tick-item");
+  const timelineTickItemEls = timelineTickListEl.querySelectorAll(".tick-label");
   is(timelineTickItemEls.length, expectedTickItem,
     "The expected number of timeline ticks were found");
 
   info("Make sure graduations are evenly distributed and show the right times");
   for (const [index, tickEl] of timelineTickItemEls.entries()) {
     const left = parseFloat(tickEl.style.left);
     const expectedPos = index * interval * 100 / animationDuration;
     is(Math.round(left), Math.round(expectedPos),
--- a/devtools/client/inspector/animation/test/browser_animation_current-time-scrubber.js
+++ b/devtools/client/inspector/animation/test/browser_animation_current-time-scrubber.js
@@ -10,17 +10,17 @@
 // * mouse drag on the scrubber
 
 add_task(async function() {
   await addTab(URL_ROOT + "doc_simple_animation.html");
   await removeAnimatedElementsExcept([".long"]);
   const { animationInspector, panel } = await openAnimationInspector();
 
   info("Checking scrubber controller existence");
-  const controllerEl = panel.querySelector(".current-time-scrubber-controller");
+  const controllerEl = panel.querySelector(".current-time-scrubber-area");
   ok(controllerEl, "scrubber controller should exist");
 
   info("Checking scrubber existence");
   const scrubberEl = controllerEl.querySelector(".current-time-scrubber");
   ok(scrubberEl, "scrubber should exist");
 
   info("Checking scrubber changes current time of animation and the position");
   const duration = animationInspector.state.timeScale.getDuration();
@@ -49,18 +49,18 @@ add_task(async function() {
   previousX = scrubberEl.getBoundingClientRect().x;
   await dragOnCurrentTimeScrubber(animationInspector, panel, 0.5, 2, 30);
   currentX = scrubberEl.getBoundingClientRect().x;
   isnot(previousX, currentX, "Scrubber should be draggable");
 
   info("Checking a behavior which mouse out from animation inspector area " +
        "during dragging from controller");
   await dragOnCurrentTimeScrubberController(animationInspector, panel, 0.5, 2);
-  ok(!panel.querySelector(".animation-list-container")
-           .classList.contains("active-scrubber"), "Click and DnD should be inactive");
+  ok(!panel.querySelector(".animation-list").classList.contains("active-scrubber"),
+     "Click and DnD should be inactive");
 });
 
 function assertPosition(scrubberEl, controllerEl, time, animationInspector) {
   const controllerBounds = controllerEl.getBoundingClientRect();
   const scrubberBounds = scrubberEl.getBoundingClientRect();
   const scrubberX = scrubberBounds.x + scrubberBounds.width / 2 - controllerBounds.x;
   const timeScale = animationInspector.state.timeScale;
   const expected = Math.round(time / timeScale.getDuration() * controllerBounds.width);
--- a/devtools/client/inspector/animation/test/browser_animation_keyframes-progress-bar.js
+++ b/devtools/client/inspector/animation/test/browser_animation_keyframes-progress-bar.js
@@ -84,13 +84,14 @@ add_task(async function() {
       assertPosition(barEl, areaEl, expectedPositions[i], animationInspector);
     }
   }
 });
 
 function assertPosition(barEl, areaEl, expectedRate, animationInspector) {
   const controllerBounds = areaEl.getBoundingClientRect();
   const barBounds = barEl.getBoundingClientRect();
-  const barX = barBounds.x + barBounds.width / 2 - controllerBounds.x;
+  // 6 means margin-left of .indication-bar
+  const barX = barBounds.x + barBounds.width / 2 - controllerBounds.x + 6;
   const expected = controllerBounds.width * expectedRate;
   ok(expected - 1 < barX && barX < expected + 1,
     `Position should apploximately be ${ expected } (x of bar is ${ barX })`);
 }
--- a/devtools/client/inspector/animation/test/browser_animation_keyframes-progress-bar_after-resuming.js
+++ b/devtools/client/inspector/animation/test/browser_animation_keyframes-progress-bar_after-resuming.js
@@ -29,14 +29,15 @@ async function assertPosition(panel, scr
   const barEl = areaEl.querySelector(".keyframes-progress-bar");
   const controllerBounds = areaEl.getBoundingClientRect();
 
   for (let i = 0; i < scrubberPositions.length; i++) {
     info(`Scrubber position is ${ scrubberPositions[i] }`);
     await clickOnCurrentTimeScrubberController(animationInspector,
                                                panel, scrubberPositions[i]);
     const barBounds = barEl.getBoundingClientRect();
-    const barX = barBounds.x + barBounds.width / 2 - controllerBounds.x;
+    // 6 means margin-left of .indication-bar
+    const barX = barBounds.x + barBounds.width / 2 - controllerBounds.x + 6;
     const expected = controllerBounds.width * expectedPositions[i];
     ok(expected - 1 < barX && barX < expected + 1,
        `Position should apploximately be ${ expected } (x of bar is ${ barX })`);
   }
 }
--- a/devtools/client/inspector/animation/test/head.js
+++ b/devtools/client/inspector/animation/test/head.js
@@ -185,17 +185,17 @@ const clickOnRewindButton = async functi
  *        This method calculates
  *        `mouseDownPosition * offsetWidth + offsetLeft of scrubber controller pane`
  *        as the clientX of MouseEvent.
  */
 const clickOnCurrentTimeScrubberController = async function(animationInspector,
                                                             panel,
                                                             mouseDownPosition,
                                                             mouseMovePosition) {
-  const controllerEl = panel.querySelector(".current-time-scrubber-controller");
+  const controllerEl = panel.querySelector(".current-time-scrubber-area");
   const bounds = controllerEl.getBoundingClientRect();
   const mousedonwX = bounds.width * mouseDownPosition;
 
   info(`Click ${ mousedonwX } on scrubber controller`);
   EventUtils.synthesizeMouse(controllerEl, mousedonwX, 0, {}, controllerEl.ownerGlobal);
   await waitForSummaryAndDetail(animationInspector);
 };
 
@@ -332,17 +332,17 @@ const dragOnCurrentTimeScrubber = async 
  * @param {Number} mouseMovePosition
  *        Dispatch mousemove event with mouseMovePosition after mousedown.
  *        Calculation for clinetX is same to above.
  */
 const dragOnCurrentTimeScrubberController = async function(animationInspector,
                                                             panel,
                                                             mouseDownPosition,
                                                             mouseMovePosition) {
-  const controllerEl = panel.querySelector(".current-time-scrubber-controller");
+  const controllerEl = panel.querySelector(".current-time-scrubber-area");
   const bounds = controllerEl.getBoundingClientRect();
   const mousedonwX = bounds.width * mouseDownPosition;
   const mousemoveX = bounds.width * mouseMovePosition;
 
   info(`Drag on scrubber controller from ${ mousedonwX } to ${ mousemoveX }`);
   EventUtils.synthesizeMouse(controllerEl, mousedonwX, 0,
                              { type: "mousedown" }, controllerEl.ownerGlobal);
   await waitForSummaryAndDetail(animationInspector);
@@ -363,17 +363,17 @@ const dragOnCurrentTimeScrubberControlle
  * @param {Number} pixels
  * @return {Object}
  *         {
  *           duration,
  *           rate,
  *         }
  */
 const getDurationAndRate = function(animationInspector, panel, pixels) {
-  const controllerEl = panel.querySelector(".current-time-scrubber-controller");
+  const controllerEl = panel.querySelector(".current-time-scrubber-area");
   const bounds = controllerEl.getBoundingClientRect();
   const duration =
     animationInspector.state.timeScale.getDuration() / bounds.width * pixels;
   const rate = 1 / bounds.width * pixels;
   return { duration, rate };
 };
 
 /**
--- a/devtools/client/themes/animation.css
+++ b/devtools/client/themes/animation.css
@@ -82,128 +82,60 @@ select.playback-rate-selector.devtools-b
   border-color: var(--toolbarbutton-hover-border-color);
 }
 
 .rewind-button::before {
   background-image: var(--rewind-image);
 }
 
 /* Animation List Container */
-.animation-list-container {
+.animation-list {
   height: 100%;
-  overflow-y: auto;
-  overflow-x: hidden;
-  position: relative;
+  overflow: hidden;
   width: 100%;
   -moz-user-select: none;
 }
 
-.animation-list-container.active-scrubber {
+.animation-list.active-scrubber {
   cursor: col-resize;
 }
 
-/* Animation List Header */
-.animation-list-header {
-  display: grid;
-  grid-template-columns: var(--sidebar-width) calc(100% - var(--sidebar-width) - var(--graph-right-offset)) var(--graph-right-offset);
-  line-height: var(--devtools-toolbar-height);
-  min-height: 100%;
-  padding: 0;
-  pointer-events: none;
-  position: sticky;
-  top: 0;
+/* Current Time Scrubber */
+.current-time-scrubber-area {
+  grid-column: 2 / 3;
   z-index: 2;
 }
 
-.animation-list-header .devtools-toolbar {
-  position: absolute;
-  width: 100%;
-}
-
-/* Animation Timeline Tick List */
-.animation-timeline-tick-list {
-  grid-column: 2/3;
-  height: 100%;
-  position: relative;
-}
-
-.animation-timeline-tick-item {
-  height: 100%;
-  position: absolute;
-}
-
-.animation-timeline-tick-item::before {
-  border-left: var(--tick-line-style);
-  content: "";
-  height: 100%;
-  position: absolute;
-}
-
-/* Current Time Scrubber */
-.current-time-scrubber-controller {
-  grid-column: 2 / 3;
-  height: 100%;
-  padding: 0;
-  position: absolute;
-  width: 100%;
-}
-
-.current-time-scrubber-controller::before {
+.current-time-scrubber-area::before {
   content: "";
   cursor: col-resize;
   height: var(--devtools-toolbar-height);
   pointer-events: auto;
   position: absolute;
   /* In order to click on edge of current-time-scrubber-controller element */
   width: calc(100% + 1px);
 }
 
-.current-time-scrubber {
+.current-time-scrubber-area .indication-bar {
   cursor: col-resize;
-  height: 100%;
-  margin-left: -6px;
   pointer-events: auto;
   position: absolute;
   width: 12px;
-  z-index: 1;
-}
-
-.current-time-scrubber::before {
-  border-left: 5px solid transparent;
-  border-right: 5px solid transparent;
-  border-top: 5px solid var(--scrubber-color);
-  content: "";
-  position: absolute;
-  top: 0;
-  width: 0;
 }
 
-.current-time-scrubber::after {
-  border-left: 1px solid var(--scrubber-color);
-  content: "";
-  height: 100%;
-  left: 5px;
-  position: absolute;
-  top: 0;
-  width: 0;
+.current-time-scrubber-area .indication-bar::before {
+  border-top-color: var(--scrubber-color);
 }
 
-/* Animation List */
-.animation-list {
-  list-style-type: none;
-  margin: 0;
-  padding: 0;
-  position: absolute;
-  top: var(--devtools-toolbar-height);
-  width: 100%;
+.current-time-scrubber-area .indication-bar::after {
+  border-left-color: var(--scrubber-color);
 }
 
 /* Animation Item */
 .animation-item {
-  display: flex;
   height: 30px;
 }
 
 .animation-item:nth-child(2n+1) {
   background-color: var(--animation-even-background-color);
 }
 
 .animation-item.cssanimation {
@@ -224,19 +156,19 @@ select.playback-rate-selector.devtools-b
 .animation-item.selected {
   background-color: var(--theme-selection-background-hover);
 }
 
 /* Animation Target */
 .animation-target {
   align-items: center;
   display: flex;
-  height: 100%;
+  grid-column: 1 / 2;
+  height: inherit;
   padding-left: 4px;
-  width: var(--sidebar-width);
 }
 
 /* Reps component */
 .animation-target .objectBox {
   display: flex;
   max-width: 100%;
 }
 
@@ -254,20 +186,20 @@ select.playback-rate-selector.devtools-b
 .animation-target .objectBox .open-inspector:hover,
 .animation-target.highlighting .objectBox .open-inspector {
   background-color: var(--theme-highlight-blue);
 }
 
 /* Summary Graph */
 .animation-summary-graph {
   cursor: pointer;
-  height: 100%;
+  grid-column: 2 / 3;
+  height: inherit;
   padding-top: 5px;
   position: relative;
-  width: calc(100% - var(--sidebar-width) - var(--graph-right-offset));
 }
 
 .animation-summary-graph.compositor::after {
   background-image: var(--fast-track-image);
   background-repeat: no-repeat;
   content: "";
   display: block;
   fill: var(--theme-content-color3);
@@ -377,17 +309,17 @@ select.playback-rate-selector.devtools-b
 /* Animation Detail */
 .animation-detail-container {
   background-color: var(--theme-body-background);
   display: flex;
   flex-direction: column;
   height: 100%;
   overflow: hidden;
   width: 100%;
-  z-index: 1;
+  z-index: 2;
 }
 
 .animation-detail-header {
   display: flex;
 }
 
 .animation-detail-title {
   flex: 1;
@@ -396,165 +328,73 @@ select.playback-rate-selector.devtools-b
   white-space: nowrap;
 }
 
 .animation-detail-close-button::before {
   background-image: url(chrome://devtools/skin/images/close.svg);
 }
 
 /* Animated Property List Container */
-.animated-property-list-container {
+.animated-property-list {
   display: flex;
   flex: 1;
   flex-direction: column;
   overflow-y: auto;
   position: relative;
 }
 
-/* Animated Property List Header */
-.animated-property-list-header {
-  display: grid;
-  grid-template-columns: var(--sidebar-width) calc(100% - var(--sidebar-width) - var(--graph-right-offset)) var(--graph-right-offset);
-  line-height: var(--devtools-toolbar-height);
-  min-height: 100%;
-  padding: 0;
-  pointer-events: none;
-  position: sticky;
-  top: 0;
-  z-index: 1;
-}
-
-.animated-property-list-header .devtools-toolbar {
-  position: absolute;
-  width: 100%;
-}
-
-/* Keyframes Progress Tick List */
-.keyframes-progress-tick-list {
-  grid-column: 2 / 3;
-  height: 100%;
-  position: absolute;
-  width: 100%;
-}
-
-.keyframes-progress-tick-item {
-  position: absolute;
-}
-
-.keyframes-progress-tick-item.left {
-  border-left: var(--tick-line-style);
-}
-
-.keyframes-progress-tick-item.right {
-  border-right: var(--tick-line-style);
-}
-
 /* Keyframes Progress Bar */
 .keyframes-progress-bar-area {
-  background: none;
   grid-column: 2 / 3;
-  padding: 0;
-  height: 100%;
-  position: absolute;
-  width: 100%;
 }
 
-.keyframes-progress-bar {
-  height: 100%;
-  position: absolute;
-  z-index: 1;
-}
-
-.keyframes-progress-bar::before {
-  border-left: 5px solid transparent;
-  border-right: 5px solid transparent;
-  border-top: 5px solid var(--progress-bar-color);
-  content: "";
-  left: -5px;
-  position: absolute;
-  top: 0;
-  width: 0;
-}
-
-.keyframes-progress-bar::after {
-  border-left: 1px solid var(--progress-bar-color);
-  content: "";
-  height: 100%;
-  position: absolute;
-  top: 0;
-  width: 0;
+.keyframes-progress-bar-area .indication-bar::before {
+  border-top-color: var(--progress-bar-color);
 }
 
-/* Animated Property List */
-.animated-property-list-background {
-  border-left: var(--tick-line-style);
-  border-right: var(--tick-line-style);
-  bottom: 0;
-	left: var(--sidebar-width);
-  min-height: 100%;
-	position: sticky;
-  top: 0;
-	width: calc(100% - var(--sidebar-width) - var(--graph-right-offset));
-}
-
-.animated-property-list-background span {
-  border-left: var(--tick-line-style);
-	height: 100%;
-	left: 50%;
-	position: absolute;
-}
-
-.animated-property-list {
-  flex: 1;
-  list-style-type: none;
-  margin: 0;
-  padding: 0;
-  position: absolute;
-  top: var(--devtools-toolbar-height);
-  width: 100%;
+.keyframes-progress-bar-area .indication-bar::after {
+  border-left-color: var(--progress-bar-color);
 }
 
 /* Animated Property Item */
 .animated-property-item {
-  display: flex;
   height: 30px;
 }
 
 .animated-property-item:nth-child(2n+1) {
   background-color: var(--animation-even-background-color);
 }
 
 .animated-property-item.unchanged {
   opacity: 0.6;
 }
 
 /* Animated Property Name */
 .animated-property-name {
   align-items: center;
   display: flex;
-  height: 100%;
+  height: inherit;
   justify-content: flex-end;
   padding-right: 10px;
-  width: var(--sidebar-width);
 }
 
 .animated-property-name.compositor span {
   padding-left: 15px;
   position: relative;
 }
 
-.animated-property-list-container.cssanimation .animated-property-name.compositor {
+.animated-property-list.cssanimation .animated-property-name.compositor {
   --fast-track-color: var(--stroke-color-cssanimation);
 }
 
-.animated-property-list-container.csstransition .animated-property-name.compositor {
+.animated-property-list.csstransition .animated-property-name.compositor {
   --fast-track-color: var(--stroke-color-csstransition);
 }
 
-.animated-property-list-container.scriptanimation .animated-property-name.compositor {
+.animated-property-list.scriptanimation .animated-property-name.compositor {
   --fast-track-color: var(--stroke-color-scriptanimation);
 }
 
 .animated-property-name.compositor span::before {
   background-image: var(--fast-track-image);
   background-repeat: no-repeat;
   background-size: contain;
   content: "";
@@ -568,19 +408,18 @@ select.playback-rate-selector.devtools-b
 
 .animated-property-name.warning span {
   text-decoration: underline dotted;
 }
 
 /* Keyframes Graph */
 .keyframes-graph {
   padding-top: 3px;
-  height: 100%;
+  height: inherit;
   position: relative;
-  width: calc(100% - var(--sidebar-width) - var(--graph-right-offset));
 }
 
 .keyframes-graph-path {
   height: 100%;
   width: 100%;
 }
 
 .keyframes-graph-path path {
@@ -644,32 +483,125 @@ select.playback-rate-selector.devtools-b
   pointer-events: auto;
   position: absolute;
   top: 50%;
   height: 10px;
   transform: translate(-5px, -3px);
   width: 10px;
 }
 
-.animated-property-list-container.cssanimation .keyframe-marker-item {
+.animated-property-list.cssanimation .keyframe-marker-item {
   background-color: var(--fill-color-cssanimation);
 }
 
-.animated-property-list-container.csstransition .keyframe-marker-item {
+.animated-property-list.csstransition .keyframe-marker-item {
   background-color: var(--fill-color-csstransition);
 }
 
-.animated-property-list-container.scriptanimation .keyframe-marker-item {
+.animated-property-list.scriptanimation .keyframe-marker-item {
   background-color: var(--fill-color-scriptanimation);
 }
 
 /* No Animation Panel */
 .animation-error-message {
   overflow: auto;
 }
 
 .animation-error-message > p {
   white-space: pre;
 }
 
 .animation-element-picker::before {
   background-image: var(--command-pick-image);
 }
+
+/* Inspect List Box */
+.inspect-list-box {
+  height: 100%;
+  overflow-y: auto;
+  overflow-x: hidden;
+}
+
+.inspect-list-box ul {
+  list-style-type: none;
+  margin: 0;
+  padding: 0;
+}
+
+.inspect-list-box .background,
+.inspect-list-box .indicator,
+.inspect-list-box .header,
+.inspect-list-box ul li {
+  display: grid;
+  grid-template-columns:
+    var(--sidebar-width) calc(100% - var(--sidebar-width) - var(--graph-right-offset)) var(--graph-right-offset);
+}
+
+.inspect-list-box .header {
+  padding: 0;
+  position: sticky;
+  top: 0;
+  z-index: 1;
+}
+
+/* Tick Lines */
+.tick-lines {
+  grid-column: 2/3;
+  position: relative;
+}
+
+.tick-line {
+  position: absolute;
+}
+
+.tick-line::before {
+  border-left: var(--tick-line-style);
+  content: "";
+  height: 100vh;
+  position: fixed;
+}
+
+/* Tick Labels */
+.tick-labels {
+  grid-column: 2/3;
+  height: 100%;
+  position: relative;
+}
+
+.tick-label {
+  border-left: var(--tick-line-style);
+  height: 100%;
+  position: absolute;
+}
+
+.animated-property-list .tick-label:last-child {
+  border-left: none;
+  border-right: var(--tick-line-style);
+  transform: translateX(calc(-100% + 0.5px));
+}
+
+/* Indication Bar */
+.indication-bar {
+  height: 100%;
+  margin-left: -6px;
+  position: absolute;
+  z-index: 2;
+}
+
+.indication-bar::before {
+  border-left: 5px solid transparent;
+  border-right: 5px solid transparent;
+  border-top: 5px solid;
+  content: "";
+  position: absolute;
+  top: 0;
+  width: 0;
+}
+
+.indication-bar::after {
+  border-left: 1px solid;
+  content: "";
+  height: 100%;
+  left: 5px;
+  position: absolute;
+  top: 0;
+  width: 0;
+}