--- a/devtools/client/inspector/animation/animation.js
+++ b/devtools/client/inspector/animation/animation.js
@@ -27,27 +27,30 @@ const {
class AnimationInspector {
constructor(inspector, win) {
this.inspector = inspector;
this.win = win;
this.addAnimationsCurrentTimeListener =
this.addAnimationsCurrentTimeListener.bind(this);
this.getAnimatedPropertyMap = this.getAnimatedPropertyMap.bind(this);
+ this.getAnimationsCurrentTime = this.getAnimationsCurrentTime.bind(this);
this.getComputedStyle = this.getComputedStyle.bind(this);
this.getNodeFromActor = this.getNodeFromActor.bind(this);
this.removeAnimationsCurrentTimeListener =
this.removeAnimationsCurrentTimeListener.bind(this);
this.rewindAnimationsCurrentTime = this.rewindAnimationsCurrentTime.bind(this);
this.selectAnimation = this.selectAnimation.bind(this);
this.setAnimationsCurrentTime = this.setAnimationsCurrentTime.bind(this);
this.setAnimationsPlaybackRate = this.setAnimationsPlaybackRate.bind(this);
this.setAnimationsPlayState = this.setAnimationsPlayState.bind(this);
this.setDetailVisibility = this.setDetailVisibility.bind(this);
this.simulateAnimation = this.simulateAnimation.bind(this);
+ this.simulateAnimationForKeyframesProgressBar =
+ this.simulateAnimationForKeyframesProgressBar.bind(this);
this.toggleElementPicker = this.toggleElementPicker.bind(this);
this.update = this.update.bind(this);
this.onAnimationsCurrentTimeUpdated = this.onAnimationsCurrentTimeUpdated.bind(this);
this.onElementPickerStarted = this.onElementPickerStarted.bind(this);
this.onElementPickerStopped = this.onElementPickerStopped.bind(this);
this.onSidebarResized = this.onSidebarResized.bind(this);
this.onSidebarSelect = this.onSidebarSelect.bind(this);
@@ -66,27 +69,29 @@ class AnimationInspector {
const {
onHideBoxModelHighlighter,
} = this.inspector.getPanel("boxmodel").getComponentProps();
const {
addAnimationsCurrentTimeListener,
emit: emitEventForTest,
getAnimatedPropertyMap,
+ getAnimationsCurrentTime,
getComputedStyle,
getNodeFromActor,
isAnimationsRunning,
removeAnimationsCurrentTimeListener,
rewindAnimationsCurrentTime,
selectAnimation,
setAnimationsCurrentTime,
setAnimationsPlaybackRate,
setAnimationsPlayState,
setDetailVisibility,
simulateAnimation,
+ simulateAnimationForKeyframesProgressBar,
toggleElementPicker,
} = this;
const target = this.inspector.target;
this.animationsFront = new AnimationsFront(target.client, target.form);
this.animationsCurrentTimeListeners = [];
this.isCurrentTimeSet = false;
@@ -97,30 +102,32 @@ class AnimationInspector {
key: "newanimationinspector",
store: this.inspector.store
},
App(
{
addAnimationsCurrentTimeListener,
emitEventForTest,
getAnimatedPropertyMap,
+ getAnimationsCurrentTime,
getComputedStyle,
getNodeFromActor,
isAnimationsRunning,
onHideBoxModelHighlighter,
onShowBoxModelHighlighterForNode,
removeAnimationsCurrentTimeListener,
rewindAnimationsCurrentTime,
selectAnimation,
setAnimationsCurrentTime,
setAnimationsPlaybackRate,
setAnimationsPlayState,
setDetailVisibility,
setSelectedNode,
simulateAnimation,
+ simulateAnimationForKeyframesProgressBar,
toggleElementPicker,
}
)
);
this.provider = provider;
this.inspector.selection.on("new-node-front", this.update);
this.inspector.sidebar.on("newanimationinspector-selected", this.onSidebarSelect);
@@ -141,16 +148,21 @@ class AnimationInspector {
this.simulatedAnimation = null;
}
if (this.simulatedElement) {
this.simulatedElement.remove();
this.simulatedElement = null;
}
+ if (this.simulatedAnimationForKeyframesProgressBar) {
+ this.simulatedAnimationForKeyframesProgressBar.cancel();
+ this.simulatedAnimationForKeyframesProgressBar = null;
+ }
+
this.stopAnimationsCurrentTimeTimer();
this.inspector = null;
this.win = null;
}
get state() {
return this.inspector.store.getState().animations;
@@ -194,16 +206,20 @@ class AnimationInspector {
});
animatedPropertyMap.set(name, keyframes);
}
return animatedPropertyMap;
}
+ getAnimationsCurrentTime() {
+ return this.currentTime;
+ }
+
/**
* Return the computed style of the specified property after setting the given styles
* to the simulated element.
*
* @param {String} property
* CSS property name (e.g. text-align).
* @param {Object} styles
* Map of CSS property name and value.
@@ -233,16 +249,18 @@ class AnimationInspector {
/**
* This method should call when the current time is changed.
* Then, dispatches the current time to listeners that are registered
* by addAnimationsCurrentTimeListener.
*
* @param {Number} currentTime
*/
onAnimationsCurrentTimeUpdated(currentTime) {
+ this.currentTime = currentTime;
+
for (const listener of this.animationsCurrentTimeListeners) {
listener(currentTime);
}
}
onElementPickerStarted() {
this.inspector.store.dispatch(updateElementPickerEnabled(true));
}
@@ -355,16 +373,37 @@ class AnimationInspector {
}
this.simulatedAnimation.effect =
new this.win.KeyframeEffect(targetEl, keyframes, effectTiming);
return this.simulatedAnimation;
}
+ /**
+ * Returns a simulatable efect timing animation for the keyframes progress bar.
+ * The returned animation is implementing Animation interface of Web Animation API.
+ * 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) {
+ if (!this.simulatedAnimationForKeyframesProgressBar) {
+ this.simulatedAnimationForKeyframesProgressBar = new this.win.Animation();
+ }
+
+ this.simulatedAnimationForKeyframesProgressBar.effect =
+ new this.win.KeyframeEffect(null, null, effectTiming);
+
+ return this.simulatedAnimationForKeyframesProgressBar;
+ }
+
stopAnimationsCurrentTimeTimer() {
if (this.currentTimeTimer) {
this.currentTimeTimer.destroy();
this.currentTimeTimer = null;
}
}
startAnimationsCurrentTimeTimer() {
@@ -405,25 +444,16 @@ class AnimationInspector {
});
await Promise.all(promises);
}
updateState(animations) {
this.stopAnimationsCurrentTimeTimer();
- // If number of displayed animations is one, we select the animation automatically.
- // But if selected animation is in given animations, ignores.
- const selectedAnimation = this.state.selectedAnimation;
-
- if (!selectedAnimation ||
- !animations.find(animation => animation.actorID === selectedAnimation.actorID)) {
- this.selectAnimation(animations.length === 1 ? animations[0] : null);
- }
-
this.inspector.store.dispatch(updateAnimations(animations));
if (hasPlayingAnimation(animations)) {
this.startAnimationsCurrentTimeTimer();
}
}
}
--- a/devtools/client/inspector/animation/components/AnimatedPropertyListContainer.js
+++ b/devtools/client/inspector/animation/components/AnimatedPropertyListContainer.js
@@ -9,38 +9,57 @@ const dom = require("devtools/client/sha
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(),
+ AnimatedPropertyListHeader(
+ {
+ addAnimationsCurrentTimeListener,
+ animation,
+ getAnimationsCurrentTime,
+ removeAnimationsCurrentTimeListener,
+ simulateAnimationForKeyframesProgressBar,
+ timeScale,
+ }
+ ),
AnimatedPropertyList(
{
animation,
emitEventForTest,
getAnimatedPropertyMap,
getComputedStyle,
simulateAnimation,
}
--- a/devtools/client/inspector/animation/components/AnimatedPropertyListHeader.js
+++ b/devtools/client/inspector/animation/components/AnimatedPropertyListHeader.js
@@ -1,23 +1,55 @@
/* 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 devtools-toolbar"
},
- KeyframesProgressTickList()
+ 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
@@ -11,33 +11,43 @@ const PropTypes = require("devtools/clie
const AnimationDetailHeader = createFactory(require("./AnimationDetailHeader"));
const AnimatedPropertyListContainer =
createFactory(require("./AnimatedPropertyListContainer"));
class AnimationDetailContainer 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,
setDetailVisibility: PropTypes.func.isRequired,
simulateAnimation: PropTypes.func.isRequired,
+ simulateAnimationForKeyframesProgressBar: PropTypes.func.isRequired,
+ timeScale: PropTypes.object.isRequired,
};
}
render() {
const {
+ addAnimationsCurrentTimeListener,
animation,
emitEventForTest,
getAnimatedPropertyMap,
+ getAnimationsCurrentTime,
getComputedStyle,
+ removeAnimationsCurrentTimeListener,
setDetailVisibility,
simulateAnimation,
+ simulateAnimationForKeyframesProgressBar,
+ timeScale,
} = this.props;
return dom.div(
{
className: "animation-detail-container"
},
animation ?
AnimationDetailHeader(
@@ -46,21 +56,26 @@ class AnimationDetailContainer extends P
setDetailVisibility,
}
)
:
null,
animation ?
AnimatedPropertyListContainer(
{
+ addAnimationsCurrentTimeListener,
animation,
emitEventForTest,
getAnimatedPropertyMap,
+ getAnimationsCurrentTime,
getComputedStyle,
+ removeAnimationsCurrentTimeListener,
simulateAnimation,
+ simulateAnimationForKeyframesProgressBar,
+ timeScale,
}
)
:
null
);
}
}
--- a/devtools/client/inspector/animation/components/App.js
+++ b/devtools/client/inspector/animation/components/App.js
@@ -18,58 +18,62 @@ const SplitBox = createFactory(require("
class App extends PureComponent {
static get propTypes() {
return {
addAnimationsCurrentTimeListener: PropTypes.func.isRequired,
animations: PropTypes.arrayOf(PropTypes.object).isRequired,
detailVisibility: PropTypes.bool.isRequired,
emitEventForTest: PropTypes.func.isRequired,
getAnimatedPropertyMap: PropTypes.func.isRequired,
+ getAnimationsCurrentTime: PropTypes.func.isRequired,
getComputedStyle: PropTypes.func.isRequired,
getNodeFromActor: PropTypes.func.isRequired,
onHideBoxModelHighlighter: PropTypes.func.isRequired,
onShowBoxModelHighlighterForNode: PropTypes.func.isRequired,
removeAnimationsCurrentTimeListener: PropTypes.func.isRequired,
rewindAnimationsCurrentTime: PropTypes.func.isRequired,
selectAnimation: PropTypes.func.isRequired,
setAnimationsCurrentTime: PropTypes.func.isRequired,
setAnimationsPlaybackRate: PropTypes.func.isRequired,
setAnimationsPlayState: PropTypes.func.isRequired,
setDetailVisibility: PropTypes.func.isRequired,
setSelectedNode: PropTypes.func.isRequired,
simulateAnimation: PropTypes.func.isRequired,
+ simulateAnimationForKeyframesProgressBar: PropTypes.func.isRequired,
timeScale: PropTypes.object.isRequired,
toggleElementPicker: PropTypes.func.isRequired,
};
}
shouldComponentUpdate(nextProps, nextState) {
return this.props.animations.length !== 0 || nextProps.animations.length !== 0;
}
render() {
const {
addAnimationsCurrentTimeListener,
animations,
detailVisibility,
emitEventForTest,
getAnimatedPropertyMap,
+ getAnimationsCurrentTime,
getComputedStyle,
getNodeFromActor,
onHideBoxModelHighlighter,
onShowBoxModelHighlighterForNode,
removeAnimationsCurrentTimeListener,
rewindAnimationsCurrentTime,
selectAnimation,
setAnimationsCurrentTime,
setAnimationsPlaybackRate,
setAnimationsPlayState,
setDetailVisibility,
setSelectedNode,
simulateAnimation,
+ simulateAnimationForKeyframesProgressBar,
timeScale,
toggleElementPicker,
} = this.props;
return dom.div(
{
id: "animation-container",
className: detailVisibility ? "animation-detail-visible" : "",
@@ -85,21 +89,26 @@ class App extends PureComponent {
setAnimationsPlaybackRate,
setAnimationsPlayState,
}
),
SplitBox({
className: "animation-container-splitter",
endPanel: AnimationDetailContainer(
{
+ addAnimationsCurrentTimeListener,
emitEventForTest,
getAnimatedPropertyMap,
+ getAnimationsCurrentTime,
getComputedStyle,
+ removeAnimationsCurrentTimeListener,
setDetailVisibility,
simulateAnimation,
+ simulateAnimationForKeyframesProgressBar,
+ timeScale,
}
),
endPanelControl: true,
initialHeight: "50%",
splitterSize: 1,
startPanel: AnimationListContainer(
{
addAnimationsCurrentTimeListener,
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/animation/components/KeyframesProgressBar.js
@@ -0,0 +1,112 @@
+/* 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 PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+
+class KeyframesProgressBar 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,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.onCurrentTimeUpdated = this.onCurrentTimeUpdated.bind(this);
+
+ this.state = {
+ // offset of the position for the progress bar
+ offset: 0,
+ };
+ }
+
+ componentDidMount() {
+ const { addAnimationsCurrentTimeListener } = this.props;
+
+ this.element = ReactDOM.findDOMNode(this);
+ this.setupAnimation(this.props);
+ addAnimationsCurrentTimeListener(this.onCurrentTimeUpdated);
+ }
+
+ componentWillReceiveProps(nextProps) {
+ const { getAnimationsCurrentTime } = nextProps;
+
+ this.setupAnimation(nextProps);
+ this.onCurrentTimeUpdated(getAnimationsCurrentTime());
+ }
+
+ componentWillUnmount() {
+ const { removeAnimationsCurrentTimeListener } = this.props;
+
+ removeAnimationsCurrentTimeListener(this.onCurrentTimeUpdated);
+ this.element = null;
+ this.simulatedAnimation = null;
+ }
+
+ onCurrentTimeUpdated(currentTime) {
+ const {
+ animation,
+ timeScale,
+ } = this.props;
+ const {
+ playbackRate,
+ previousStartTime = 0,
+ } = animation.state;
+
+ this.simulatedAnimation.currentTime =
+ (timeScale.minStartTime + currentTime - previousStartTime) * playbackRate;
+ const offset = this.element.offsetWidth *
+ this.simulatedAnimation.effect.getComputedTiming().progress;
+
+ this.setState({ offset });
+ }
+
+ setupAnimation(props) {
+ const {
+ animation,
+ simulateAnimationForKeyframesProgressBar,
+ } = props;
+
+ if (this.simulatedAnimation) {
+ this.simulatedAnimation.cancel();
+ }
+
+ const timing = Object.assign({}, animation.state, {
+ iterations: animation.state.iterationCount || Infinity
+ });
+
+ this.simulatedAnimation = simulateAnimationForKeyframesProgressBar(timing);
+ }
+
+ render() {
+ const { offset } = this.state;
+
+ return dom.div(
+ {
+ className: "keyframes-progress-bar-area devtools-toolbar",
+ },
+ dom.div(
+ {
+ className: "keyframes-progress-bar",
+ style: {
+ transform: `translateX(${ offset }px)`,
+ },
+ }
+ )
+ );
+ }
+}
+
+module.exports = KeyframesProgressBar;
--- a/devtools/client/inspector/animation/components/moz.build
+++ b/devtools/client/inspector/animation/components/moz.build
@@ -22,15 +22,16 @@ DevToolsModules(
'AnimationTarget.js',
'AnimationTimelineTickItem.js',
'AnimationTimelineTickList.js',
'AnimationToolbar.js',
'App.js',
'CurrentTimeLabel.js',
'CurrentTimeScrubber.js',
'CurrentTimeScrubberController.js',
+ 'KeyframesProgressBar.js',
'KeyframesProgressTickItem.js',
'KeyframesProgressTickList.js',
'NoAnimationPanel.js',
'PauseResumeButton.js',
'PlaybackRateSelector.js',
'RewindButton.js',
)
--- a/devtools/client/inspector/animation/reducers/animations.js
+++ b/devtools/client/inspector/animation/reducers/animations.js
@@ -23,18 +23,29 @@ const INITIAL_STATE = {
height: 0,
width: 0,
},
timeScale: null,
};
const reducers = {
[UPDATE_ANIMATIONS](state, { animations }) {
+ let detailVisibility = state.detailVisibility;
+ let selectedAnimation = state.selectedAnimation;
+
+ if (!state.selectedAnimation ||
+ !animations.find(animation => animation.actorID === selectedAnimation.actorID)) {
+ selectedAnimation = animations.length === 1 ? animations[0] : null;
+ detailVisibility = !!selectedAnimation;
+ }
+
return Object.assign({}, state, {
animations,
+ detailVisibility,
+ selectedAnimation,
timeScale: new TimeScale(animations),
});
},
[UPDATE_DETAIL_VISIBILITY](state, { detailVisibility }) {
return Object.assign({}, state, {
detailVisibility
});
--- a/devtools/client/themes/animation.css
+++ b/devtools/client/themes/animation.css
@@ -9,16 +9,17 @@
--command-pick-image: url(chrome://devtools/skin/images/command-pick.svg);
--fast-track-image: url("images/animation-fast-track.svg");
--fill-color-cssanimation: var(--theme-contrast-background);
--fill-color-csstransition: var(--theme-highlight-blue);
--fill-color-scriptanimation: var(--theme-graphs-green);
--graph-right-offset: 10px;
--keyframe-marker-shadow-color: #c4c4c4;
--pause-image: url(chrome://devtools/skin/images/pause.svg);
+ --progress-bar-color: #909090;
--resume-image: url(chrome://devtools/skin/images/play.svg);
--rewind-image: url(chrome://devtools/skin/images/rewind.svg);
--scrubber-color: #dd00a9;
--sidebar-width: 200px;
--stroke-color-cssanimation: var(--theme-highlight-lightorange);
--stroke-color-csstransition: var(--theme-highlight-bluegrey);
--stroke-color-scriptanimation: var(--theme-highlight-green);
--tick-line-style: 0.5px solid rgba(128, 136, 144, 0.5);
@@ -351,46 +352,80 @@ select.playback-rate-selector.devtools-b
background-image: url(chrome://devtools/skin/images/close.svg);
}
/* Animated Property List Container */
.animated-property-list-container {
display: flex;
flex: 1;
flex-direction: column;
- overflow-y: auto;
+ overflow: hidden;
}
/* Animated Property List Header */
.animated-property-list-header {
- display: flex;
- justify-content: flex-end;
+ display: grid;
+ grid-template-columns: var(--sidebar-width) calc(100% - var(--sidebar-width) - var(--graph-right-offset)) var(--graph-right-offset);
padding: 0;
}
/* Keyframes Progress Tick List */
.keyframes-progress-tick-list {
- margin-right: var(--graph-right-offset);
- position: absolute;
- width: calc(100% - var(--sidebar-width) - var(--graph-right-offset));
+ grid-column: 2 / 3;
+ position: relative;
}
.keyframes-progress-tick-item {
height: 100vh;
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;
+ pointer-events: none;
+ position: relative;
+}
+
+.keyframes-progress-bar {
+ height: 100vh;
+ 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;
+}
+
/* Animated Property List */
.animated-property-list {
flex: 1;
list-style-type: none;
margin: 0;
overflow-y: auto;
padding: 0;
}