Bug 1210796 - Part 6: Fixed animation detail panel. r=pbro

MozReview-Commit-ID: CYIka7UkTPx

--HG--
extra : rebase_source : 3ff32fe380eebd9a3f0a2b5ea7e9ded046ecb98e
This commit is contained in:
Daisuke Akatsuka 2017-04-18 12:15:55 +09:00
Родитель dbba9bba01
Коммит 819485ef9b
12 изменённых файлов: 287 добавлений и 138 удалений

Просмотреть файл

@ -7,6 +7,7 @@
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<link rel="stylesheet" href="chrome://devtools/skin/animationinspector.css" type="text/css"/>
<link rel="stylesheet" href="resource://devtools/client/shared/components/splitter/split-box.css"/>
<script type="application/javascript" src="chrome://devtools/content/shared/theme-switching.js"/>
</head>
<body class="theme-sidebar devtools-monospace" role="application" empty="true">
@ -26,6 +27,16 @@
<p id="error-hint"></p>
<button id="element-picker" data-standalone="true" class="devtools-button"></button>
</div>
<script type="text/javascript">
/* eslint-disable */
var isInChrome = window.location.href.includes("chrome:");
if (isInChrome) {
var exports = {};
var Cu = Components.utils;
var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
var { BrowserLoader } = Cu.import("resource://devtools/client/shared/browser-loader.js", {});
}
</script>
<script type="application/javascript" src="animation-controller.js"></script>
<script type="application/javascript" src="animation-panel.js"></script>
</body>

Просмотреть файл

@ -205,12 +205,12 @@ AnimationDetails.prototype = {
// Add animated property header.
const headerEl = createNode({
parent: this.containerEl,
attributes: { "class": "animated-properties-header property" }
attributes: { "class": "animated-properties-header" }
});
// Add progress tick container.
const progressTickContainerEl = createNode({
parent: headerEl,
parent: this.containerEl,
attributes: { "class": "progress-tick-container track-container" }
});

Просмотреть файл

@ -7,7 +7,8 @@
"use strict";
const EventEmitter = require("devtools/shared/event-emitter");
const {createNode, TimeScale} = require("devtools/client/animationinspector/utils");
const {createNode, TimeScale, getFormattedAnimationTitle} =
require("devtools/client/animationinspector/utils");
const { LocalizationHelper } = require("devtools/shared/l10n");
const L10N =
@ -354,30 +355,6 @@ AnimationTimeBlock.prototype = {
}
};
/**
* Get a formatted title for this animation. This will be either:
* "some-name", "some-name : CSS Transition", "some-name : CSS Animation",
* "some-name : Script Animation", or "Script Animation", depending
* if the server provides the type, what type it is and if the animation
* has a name
* @param {AnimationPlayerFront} animation
*/
function getFormattedAnimationTitle({state}) {
// Older servers don't send a type, and only know about
// CSSAnimations and CSSTransitions, so it's safe to use
// just the name.
if (!state.type) {
return state.name;
}
// Script-generated animations may not have a name.
if (state.type === "scriptanimation" && !state.name) {
return L10N.getStr("timeline.scriptanimation.unnamedLabel");
}
return L10N.getFormatStr(`timeline.${state.type}.nameLabel`, state.name);
}
/**
* Render delay section.
* @param {Element} parentEl - Parent element of this appended path element.

Просмотреть файл

@ -10,12 +10,17 @@ const EventEmitter = require("devtools/shared/event-emitter");
const {
createNode,
findOptimalTimeInterval,
getFormattedAnimationTitle,
TimeScale
} = require("devtools/client/animationinspector/utils");
const {AnimationDetails} = require("devtools/client/animationinspector/components/animation-details");
const {AnimationTargetNode} = require("devtools/client/animationinspector/components/animation-target-node");
const {AnimationTimeBlock} = require("devtools/client/animationinspector/components/animation-time-block");
const { LocalizationHelper } = require("devtools/shared/l10n");
const L10N =
new LocalizationHelper("devtools/client/locales/animationinspector.properties");
// The minimum spacing between 2 time graduation headers in the timeline (px).
const TIME_GRADUATION_MIN_SPACING = 40;
// When the container window is resized, the timeline background gets refreshed,
@ -42,7 +47,6 @@ function AnimationsTimeline(inspector, serverTraits) {
this.animations = [];
this.targetNodes = [];
this.timeBlocks = [];
this.details = [];
this.inspector = inspector;
this.serverTraits = serverTraits;
@ -63,16 +67,51 @@ exports.AnimationsTimeline = AnimationsTimeline;
AnimationsTimeline.prototype = {
init: function (containerEl) {
this.win = containerEl.ownerDocument.defaultView;
this.rootWrapperEl = containerEl;
this.rootWrapperEl = createNode({
parent: containerEl,
attributes: {
"class": "animation-timeline"
}
this.setupSplitBox();
this.setupAnimationTimeline();
this.setupAnimationDetail();
this.win.addEventListener("resize",
this.onWindowResize);
},
setupSplitBox: function () {
const browserRequire = this.win.BrowserLoader({
window: this.win,
useOnlyShared: true
}).require;
const React = browserRequire("devtools/client/shared/vendor/react");
const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
const SplitBox = React.createFactory(
browserRequire("devtools/client/shared/components/splitter/split-box"));
const splitter = SplitBox({
className: "animation-root",
initialSize: "0 0",
maxSize: "calc(100% - (var(--timeline-animation-height) * 2))",
splitterSize: 1,
endPanelControl: true,
startPanel: React.DOM.div({
className: "animation-timeline"
}),
endPanel: React.DOM.div({
className: "animation-detail"
}),
vert: false
});
ReactDOM.render(splitter, this.rootWrapperEl);
},
setupAnimationTimeline: function () {
const animationTimelineEl = this.rootWrapperEl.querySelector(".animation-timeline");
let scrubberContainer = createNode({
parent: this.rootWrapperEl,
parent: animationTimelineEl,
attributes: {"class": "scrubber-wrapper"}
});
@ -89,11 +128,17 @@ AnimationsTimeline.prototype = {
"class": "scrubber-handle"
}
});
createNode({
parent: this.scrubberHandleEl,
attributes: {
"class": "scrubber-line"
}
});
this.scrubberHandleEl.addEventListener("mousedown",
this.onScrubberMouseDown);
this.headerWrapper = createNode({
parent: this.rootWrapperEl,
parent: animationTimelineEl,
attributes: {
"class": "header-wrapper"
}
@ -110,27 +155,73 @@ AnimationsTimeline.prototype = {
this.onScrubberMouseDown);
this.timeTickEl = createNode({
parent: this.rootWrapperEl,
parent: animationTimelineEl,
attributes: {
"class": "time-body track-container"
}
});
this.animationsEl = createNode({
parent: this.rootWrapperEl,
parent: animationTimelineEl,
nodeType: "ul",
attributes: {
"class": "animations"
}
});
},
this.win.addEventListener("resize",
this.onWindowResize);
setupAnimationDetail: function () {
this.animationDetailEl = this.rootWrapperEl.querySelector(".animation-detail");
this.animationDetailEl.dataset.defaultDisplayStyle =
this.win.getComputedStyle(this.animationDetailEl).display;
this.animationDetailEl.style.display = "none";
const animationDetailHeaderEl = createNode({
parent: this.animationDetailEl,
attributes: {
"class": "animation-detail-header"
}
});
const headerTitleEl = createNode({
parent: animationDetailHeaderEl,
attributes: {
"class": "devtools-toolbar"
}
});
createNode({
parent: headerTitleEl,
textContent: L10N.getStr("detail.headerTitle")
});
this.animationAnimationNameEl = createNode({
parent: headerTitleEl
});
const animationDetailBodyEl = createNode({
parent: this.animationDetailEl,
attributes: {
"class": "animation-detail-body"
}
});
this.animatedPropertiesEl = createNode({
parent: animationDetailBodyEl,
attributes: {
"class": "animated-properties"
}
});
this.details = new AnimationDetails(this.serverTraits);
this.details.init(this.animatedPropertiesEl);
},
destroy: function () {
this.stopAnimatingScrubber();
this.unrender();
this.details.destroy();
this.win.removeEventListener("resize",
this.onWindowResize);
@ -141,15 +232,18 @@ AnimationsTimeline.prototype = {
this.rootWrapperEl.remove();
this.animations = [];
this.rootWrapperEl = null;
this.timeHeaderEl = null;
this.animationsEl = null;
this.animatedPropertiesEl = null;
this.scrubberEl = null;
this.scrubberHandleEl = null;
this.win = null;
this.inspector = null;
this.serverTraits = null;
this.animationDetailEl = null;
this.animationAnimationNameEl = null;
this.animatedPropertiesEl = null;
},
/**
@ -176,10 +270,8 @@ AnimationsTimeline.prototype = {
TimeScale.reset();
this.destroySubComponents("targetNodes");
this.destroySubComponents("timeBlocks");
this.destroySubComponents("details", [{
event: "frame-selected",
fn: this.onFrameSelected
}]);
this.details.off("frame-selected", this.onFrameSelected);
this.details.unrender();
this.animationsEl.innerHTML = "";
},
@ -206,18 +298,27 @@ AnimationsTimeline.prototype = {
let el = this.rootWrapperEl;
let animationEl = el.querySelectorAll(".animation")[index];
let propsEl = el.querySelectorAll(".animated-properties")[index];
// Toggle the selected state on this animation.
animationEl.classList.toggle("selected");
propsEl.classList.toggle("selected");
// Render the details component for this animation if it was shown.
if (animationEl.classList.contains("selected")) {
this.details[index].render(animation);
// Add class of animation type.
if (!this.animatedPropertiesEl.classList.contains(animation.state.type)) {
this.animatedPropertiesEl.className =
`animated-properties ${ animation.state.type }`;
}
this.animationDetailEl.style.display =
this.animationDetailEl.dataset.defaultDisplayStyle;
this.details.render(animation);
this.emit("animation-selected", animation);
this.animationAnimationNameEl.textContent =
getFormattedAnimationTitle(animation);
} else {
this.emit("animation-unselected", animation);
this.animationDetailEl.style.display = "none";
}
},
@ -331,21 +432,6 @@ AnimationsTimeline.prototype = {
}
});
// Right below the line is a hidden-by-default line for displaying the
// inline keyframes.
let detailsEl = createNode({
parent: this.animationsEl,
nodeType: "li",
attributes: {
"class": "animated-properties " + animation.state.type
}
});
let details = new AnimationDetails(this.serverTraits);
details.init(detailsEl);
details.on("frame-selected", this.onFrameSelected);
this.details.push(details);
// Left sidebar for the animated node.
let animatedNodeEl = createNode({
parent: animationEl,
@ -376,6 +462,7 @@ AnimationsTimeline.prototype = {
timeBlock.on("selected", this.onAnimationSelected);
}
this.details.on("frame-selected", this.onFrameSelected);
// Use the document's current time to position the scrubber (if the server
// doesn't provide it, hide the scrubber entirely).

Просмотреть файл

@ -52,12 +52,6 @@ add_task(function* () {
ok(hasExpectedWarnings(propertiesList),
"The list of properties panel contains the right warnings");
info("Click to unselect the animation");
yield clickOnAnimation(panel, 0, true);
ok(!isNodeVisible(propertiesList),
"The list of properties panel is hidden again");
});
function hasExpectedProperties(containerEl) {

Просмотреть файл

@ -37,8 +37,5 @@ add_task(function* () {
function isTimeBlockSelected(timeline, index) {
let animation = timeline.rootWrapperEl.querySelectorAll(".animation")[index];
let animatedProperties = timeline.rootWrapperEl.querySelectorAll(
".animated-properties")[index];
return animation.classList.contains("selected") &&
animatedProperties.classList.contains("selected");
return animation.classList.contains("selected");
}

Просмотреть файл

@ -22,22 +22,22 @@ add_task(function* () {
yield clickOnAnimation(panel, 0);
info("Click on the first keyframe of the first animated property");
yield clickKeyframe(panel, 0, "background-color", 0);
yield clickKeyframe(panel, "background-color", 0);
info("Make sure the scrubber stopped moving and is at the right position");
yield assertScrubberMoving(panel, false);
checkScrubberPos(scrubberEl, 0);
info("Click on a keyframe in the middle");
yield clickKeyframe(panel, 0, "transform", 2);
yield clickKeyframe(panel, "transform", 2);
info("Make sure the scrubber is at the right position");
checkScrubberPos(scrubberEl, 50);
});
function* clickKeyframe(panel, animIndex, property, index) {
let keyframeComponent = getKeyframeComponent(panel, animIndex, property);
let keyframeEl = getKeyframeEl(panel, animIndex, property, index);
function* clickKeyframe(panel, property, index) {
let keyframeComponent = getKeyframeComponent(panel, property);
let keyframeEl = getKeyframeEl(panel, property, index);
let onSelect = keyframeComponent.once("frame-selected");
EventUtils.sendMouseEvent({type: "click"}, keyframeEl,

Просмотреть файл

@ -11,7 +11,7 @@ add_task(function* () {
let {panel} = yield openAnimationInspector();
let timelineComponent = panel.animationsTimelineComponent;
let timeBlockComponents = timelineComponent.timeBlocks;
let detailsComponents = timelineComponent.details;
let detailsComponent = timelineComponent.details;
for (let i = 0; i < timeBlockComponents.length; i++) {
info(`Expand time block ${i} so its keyframes are visible`);
@ -26,7 +26,7 @@ add_task(function* () {
// Get the first set of keyframes (there's only one animated property
// anyway), and the first frame element from there, we're only interested in
// its offset.
let keyframeComponent = detailsComponents[i].keyframeComponents[0];
let keyframeComponent = detailsComponent.keyframeComponents[0];
let frameEl = keyframeComponent.keyframesEl.querySelector(".frame");
checkKeyframeOffset(containerEl, frameEl, state);
}

Просмотреть файл

@ -385,7 +385,7 @@ function* clickOnAnimation(panel, index, shouldClose) {
// the animation-detail-rendering-completed event.
let onReady = shouldClose
? Promise.resolve()
: timeline.details[index].once("animation-detail-rendering-completed");
: timeline.details.once("animation-detail-rendering-completed");
info("Click on animation " + index + " in the timeline");
let timeBlock = timeline.rootWrapperEl.querySelectorAll(".time-block")[index];
@ -399,13 +399,12 @@ function* clickOnAnimation(panel, index, shouldClose) {
/**
* Get an instance of the Keyframes component from the timeline.
* @param {AnimationsPanel} panel The panel instance.
* @param {Number} animationIndex The index of the animation in the timeline.
* @param {String} propertyName The name of the animated property.
* @return {Keyframes} The Keyframes component instance.
*/
function getKeyframeComponent(panel, animationIndex, propertyName) {
function getKeyframeComponent(panel, propertyName) {
let timeline = panel.animationsTimelineComponent;
let detailsComponent = timeline.details[animationIndex];
let detailsComponent = timeline.details;
return detailsComponent.keyframeComponents
.find(c => c.propertyName === propertyName);
}
@ -413,14 +412,12 @@ function getKeyframeComponent(panel, animationIndex, propertyName) {
/**
* Get a keyframe element from the timeline.
* @param {AnimationsPanel} panel The panel instance.
* @param {Number} animationIndex The index of the animation in the timeline.
* @param {String} propertyName The name of the animated property.
* @param {Index} keyframeIndex The index of the keyframe.
* @return {DOMNode} The keyframe element.
*/
function getKeyframeEl(panel, animationIndex, propertyName, keyframeIndex) {
let keyframeComponent = getKeyframeComponent(panel, animationIndex,
propertyName);
function getKeyframeEl(panel, propertyName, keyframeIndex) {
let keyframeComponent = getKeyframeComponent(panel, propertyName);
return keyframeComponent.keyframesEl
.querySelectorAll(".frame")[keyframeIndex];
}

Просмотреть файл

@ -308,3 +308,28 @@ function getJsPropertyName(cssPropertyName) {
});
}
exports.getJsPropertyName = getJsPropertyName;
/**
* Get a formatted title for this animation. This will be either:
* "some-name", "some-name : CSS Transition", "some-name : CSS Animation",
* "some-name : Script Animation", or "Script Animation", depending
* if the server provides the type, what type it is and if the animation
* has a name
* @param {AnimationPlayerFront} animation
*/
function getFormattedAnimationTitle({state}) {
// Older servers don't send a type, and only know about
// CSSAnimations and CSSTransitions, so it's safe to use
// just the name.
if (!state.type) {
return state.name;
}
// Script-generated animations may not have a name.
if (state.type === "scriptanimation" && !state.name) {
return L10N.getStr("timeline.scriptanimation.unnamedLabel");
}
return L10N.getFormatStr(`timeline.${state.type}.nameLabel`, state.name);
}
exports.getFormattedAnimationTitle = getFormattedAnimationTitle;

Просмотреть файл

@ -178,3 +178,7 @@ timeline.unknown.nameLabel=%S
# %S represents the value in percentage with two decimal points, localized.
# there are two "%" after %S to escape and display "%"
detail.propertiesHeader.percentage=%S%%
# LOCALIZATION NOTE (detail.headerTitle):
# This string is displayed on header label in .animation-detail-header.
detail.headerTitle=Animated properties for

Просмотреть файл

@ -139,8 +139,6 @@ body {
#players {
height: calc(100% - var(--toolbar-height));
overflow-x: hidden;
overflow-y: auto;
}
[empty] #players {
@ -230,15 +228,20 @@ body {
width: 4.5em;
}
.animation-root > .uncontrolled {
overflow: hidden;
}
/* Animation timeline component */
.animation-timeline {
position: relative;
display: flex;
flex-direction: column;
width: 100%;
overflow: auto;
}
/* Useful for positioning animations or keyframes in the timeline */
.animation-detail .track-container,
.animation-timeline .track-container {
position: absolute;
top: 0;
@ -251,41 +254,35 @@ body {
.animation-timeline .scrubber-wrapper {
position: absolute;
z-index: 5;
left: var(--timeline-sidebar-width);
/* Leave the width of a marker right of a track so the 100% markers can be
selected easily */
right: var(--keyframes-marker-size);
height: 100%;
pointer-events: none;
}
.animation-timeline .scrubber {
z-index: 5;
pointer-events: none;
position: absolute;
/* Make the scrubber as tall as the viewport minus the toolbar height and the
header-wrapper's borders */
height: calc(100vh - var(--toolbar-height) - 1px);
min-height: 100%;
width: 0;
border-right: 1px solid red;
box-sizing: border-box;
margin-left: -6px;
}
/* The scrubber handle is a transparent element displayed on top of the scrubber
line that allows users to drag it */
.animation-timeline .scrubber .scrubber-handle {
position: absolute;
position: fixed;
height: 100%;
/* Make it thick enough for easy dragging */
width: 6px;
right: -1.5px;
width: 12px;
cursor: col-resize;
pointer-events: all;
}
.animation-timeline .scrubber .scrubber-handle::before {
content: "";
position: sticky;
position: absolute;
top: 0;
width: 1px;
border-top: 5px solid red;
@ -293,6 +290,14 @@ body {
border-right: 5px solid transparent;
}
.animation-timeline .scrubber .scrubber-handle .scrubber-line {
position: relative;
height: 100%;
left: 5px;
width: 0;
border-right: 1px solid red;
}
.animation-timeline .time-header {
min-height: var(--timeline-animation-height);
cursor: col-resize;
@ -314,29 +319,33 @@ body {
border-bottom: 1px solid var(--time-graduation-border-color);
z-index: 3;
height: var(--timeline-animation-height);
width: 100%;
overflow: hidden;
}
.animation-timeline .time-body {
height: 100%;
top: var(--timeline-animation-height);
}
.progress-tick-container .progress-tick,
.animation-timeline .time-body .time-tick {
-moz-user-select: none;
position: absolute;
width: 0;
/* When scroll bar is shown, make it covers entire time-body */
height: 100%;
/* When scroll bar is hidden, make it as tall as the viewport minus the
timeline animation height and the header-wrapper's borders */
min-height: calc(100vh - var(--timeline-animation-height) - 1px);
}
.progress-tick-container .progress-tick::before,
.animation-timeline .time-body .time-tick::before {
content: "";
position: fixed;
height: 100vh;
width: 0;
border-left: 0.5px solid var(--time-graduation-border-color);
}
.animation-timeline .animations {
position: relative;
width: 100%;
height: 100%;
padding: 0;
list-style-type: none;
margin-top: 0;
@ -350,13 +359,15 @@ body {
position: relative;
}
/* We want animations' background colors to alternate, but each animation has
a sibling (hidden by default) that contains the animated properties and
keyframes, so we need to alternate every 4 elements. */
.animation-timeline .animation:nth-child(4n+1) {
/* Display animations' background colors to alternate. */
.animation-timeline .animation:nth-child(2n+1) {
background-color: var(--even-animation-timeline-background-color);
}
.animation-timeline .animation:last-child {
margin-bottom: calc(var(--timeline-animation-height) / 2);
}
.animation-timeline .animation .target {
width: var(--timeline-sidebar-width);
height: 100%;
@ -487,7 +498,6 @@ body {
}
/* Animation target node gutter, contains a preview of the dom node */
.animation-target {
background-color: var(--theme-toolbar-background);
padding: 0 4px;
@ -520,24 +530,16 @@ body {
/* Inline keyframes info in the timeline */
.animation-timeline .animated-properties:not(.selected) {
display: none;
}
.animation-timeline .animated-properties {
background-color: var(--theme-selection-background-semitransparent);
}
.animation-timeline .animated-properties .property {
.animation-detail .animated-properties .property {
height: var(--timeline-animation-height);
position: relative;
}
.animation-timeline .animated-properties .property:nth-child(2n) {
.animation-detail .animated-properties .property:nth-child(2n) {
background-color: var(--even-animation-timeline-background-color);
}
.animation-timeline .animated-properties .name {
.animation-detail .animated-properties .name {
width: var(--timeline-sidebar-width);
padding-right: var(--keyframes-marker-size);
box-sizing: border-box;
@ -549,24 +551,24 @@ body {
align-items: center;
}
.animation-timeline .animated-properties .name div {
.animation-detail .animated-properties .name div {
overflow: hidden;
text-overflow: ellipsis;
}
.animated-properties.cssanimation {
.animation-detail .animated-properties.cssanimation {
--background-color: var(--theme-contrast-background);
}
.animated-properties.csstransition {
.animation-detail .animated-properties.csstransition {
--background-color: var(--theme-highlight-blue);
}
.animated-properties.scriptanimation {
.animation-detail .animated-properties.scriptanimation {
--background-color: var(--theme-graphs-green);
}
.animation-timeline .animated-properties .oncompositor::before {
.animation-detail .animated-properties .oncompositor::before {
content: "";
display: inline-block;
width: 17px;
@ -576,11 +578,11 @@ body {
vertical-align: middle;
}
.animation-timeline .animated-properties .warning {
.animation-detail .animated-properties .warning {
text-decoration: underline dotted;
}
.animation-timeline .animated-properties .frames {
.animation-detail .animated-properties .frames {
/* The frames list is absolutely positioned and the left and width properties
are dynamically set from javascript to match the animation's startTime and
duration */
@ -606,7 +608,6 @@ body {
top: 0;
width: 100%;
height: 100%;
}
.keyframes .frame {
@ -670,11 +671,62 @@ body {
.keyframes svg path.color {
stroke: none;
height: 100%;
}
.animation-detail {
position: relative;
width: 100%;
background-color: var(--theme-body-background);
z-index: 5;
}
.animation-detail .animation-detail-header {
height: var(--toolbar-height);
width: 100%;
}
.animation-detail .animation-detail-header > div {
position: fixed;
display: flex;
flex-wrap: nowrap;
width: 100%;
height: var(--toolbar-height);
line-height: var(--toolbar-height);
background-color: var(--theme-body-background);
z-index: 5;
}
.animation-detail .animation-detail-header > div > div {
white-space: nowrap;
}
.animation-detail .animation-detail-header > div > div:first-child {
margin-left: 15px;
}
.animation-detail .animation-detail-header > div > div:nth-child(2) {
margin-left: .5em;
}
.animation-detail .animation-detail-body {
position: relative;
background-color: var(--theme-body-background);
}
.animation-detail .animation-detail-body .animated-properties {
position: relative;
height: 100%;
}
.animated-properties-header {
min-height: var(--timeline-animation-height);
-moz-user-select: none;
position: sticky;
top: var(--timeline-animation-height);
min-height: var(--timeline-animation-height);
padding-top: 2px;
z-index: 3;
background-color: var(--theme-body-background);
}
.animated-properties-header .header-item:nth-child(2) {
@ -682,7 +734,7 @@ body {
}
.animated-properties-header .header-item:nth-child(3) {
right: 0;
right: -0.5px;
border-left: none;
border-right: 0.5px solid var(--time-graduation-border-color);
}
@ -694,3 +746,8 @@ body {
.progress-tick-container .progress-tick:nth-child(3) {
left: 100%;
}
.animated-properties-body .property:last-child {
/* To display animation progress graph clealy when the scroll is bottom. */
padding-bottom: calc(var(--timeline-animation-height) / 2);
}