diff --git a/app/app.css b/app/app.css
index 0487480..a9160ad 100644
--- a/app/app.css
+++ b/app/app.css
@@ -95,6 +95,33 @@ select {
.multiline h2 {
font-weight: normal;
}
+x-range {
+ width: 144px;
+ height: 16px;
+ position: relative;
+ --thumb: 6px;
+ --track: 4px;
+ padding: 0 var(--thumb);
+}
+x-range .-track,
+x-range .-elapsed {
+ position: absolute;
+ top: calc(50% - var(--track)/2);
+ height: var(--track);
+ left: 0;
+}
+x-range .-track {
+ width: 100%;
+}
+x-range .-thumb {
+ position: absolute;
+ top: 50%;
+ border-radius: 50%;
+ width: calc(2*var(--thumb));
+ height: calc(2*var(--thumb));
+ background-color: #fff;
+ transform: translate(-50%, -50%);
+}
main {
flex-grow: 1;
overflow: hidden;
diff --git a/app/css/app.less b/app/css/app.less
index ec1b377..cc3a90e 100644
--- a/app/css/app.less
+++ b/app/css/app.less
@@ -55,6 +55,7 @@ select {
@import "font.less";
@import "icons.less";
@import "mixins.less";
+@import "range.less";
@import "main.less";
@import "nav.less";
@import "player.less";
diff --git a/app/css/range.less b/app/css/range.less
new file mode 100644
index 0000000..a6d6e01
--- /dev/null
+++ b/app/css/range.less
@@ -0,0 +1,36 @@
+x-range {
+ width: 144px;
+ height: 16px;
+ position: relative;
+
+ --thumb: 6px;
+ --track: 4px;
+ padding: 0 var(--thumb);
+
+ .-track, .-elapsed {
+ position: absolute;
+ top: calc(50% - var(--track)/2);
+ height: var(--track);
+ left: 0;
+ }
+
+ .-track {
+ width: 100%;
+ }
+
+ .-elapsed {
+
+ }
+
+ .-thumb {
+ position: absolute;
+ top: 50%;
+
+ border-radius: 50%;
+ width: calc(2*var(--thumb));
+ height: calc(2*var(--thumb));
+ background-color: #fff;
+
+ transform: translate(-50%, -50%);
+ }
+}
diff --git a/app/index.html b/app/index.html
index ec21672..a28bfb7 100644
--- a/app/index.html
+++ b/app/index.html
@@ -29,6 +29,7 @@
+
diff --git a/app/js/app.js b/app/js/app.js
index 57dd3e2..0b56910 100644
--- a/app/js/app.js
+++ b/app/js/app.js
@@ -2,6 +2,7 @@ import * as nav from "./nav.js";
import * as mpd from "./lib/mpd.js";
import * as player from "./player.js";
import * as html from "./lib/html.js";
+import * as range from "./lib/range.js";
import * as queue from "./queue.js";
import * as library from "./library.js";
diff --git a/app/js/lib/range.js b/app/js/lib/range.js
new file mode 100644
index 0000000..291dd82
--- /dev/null
+++ b/app/js/lib/range.js
@@ -0,0 +1,59 @@
+import * as html from "./html.js";
+
+class Range extends HTMLElement {
+ static get observedAttributes() { return ["min", "max", "value"]; }
+
+ constructor() {
+ super();
+
+ this._data = {
+ min: 0,
+ max: 100,
+ value: 50
+ };
+
+ this.track = html.node("span", {className:"-track"});
+ this.elapsed = html.node("span", {className:"-elapsed"});
+ this.thumb = html.node("span", {className:"-thumb"});
+
+ this.appendChild(this.track);
+ this.appendChild(this.elapsed);
+ this.appendChild(this.thumb);
+
+ this._update();
+ }
+
+ set value(value) {
+ value = Math.max(value, this._data.min);
+ value = Math.min(value, this._data.max);
+ this._data.value = value;
+ this._update();
+ }
+
+ set min(min) {
+ this._data.min = Math.min(min, this._data.max);
+ this._update();
+ }
+
+ set max(max) {
+ this._data.max = Math.max(max, this._data.max);
+ this._update();
+ }
+
+ attributeChangedCallback(name, oldValue, newValue) {
+ this[name] = newValue;
+ }
+
+ get value() { return this._data.value; }
+ get valueAsNumber() { return Number(this._data.value); }
+ get min() { return this._data.min; }
+ get max() { return this._data.max; }
+
+ _update() {
+ let frac = (this._data.value - this._data.min) / (this._data.max - this._data.min);
+ this.thumb.style.left = `${frac * 100}%`;
+ this.elapsed.style.width = `${frac * 100}%`;
+ }
+}
+
+customElements.define('x-range', Range);