ctucx.git: smartie-pwa

[js] smarthome web-gui

commit 444a385c4cb118568fd5d808dce07a6d46e4ad9d
parent e17a1a3f32d4f36e93fc78be56d00b5f6dcc0b2e
Author: Milan Pässler <me@pbb.lc>
Date: Mon, 20 May 2019 00:20:55 +0200

add menu and power meter
7 files changed, 435 insertions(+), 22 deletions(-)
M
package.json
|
4
++++
M
src/index.js
|
64
++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
M
src/lights.js
|
26
++++++++++++++++++--------
M
src/power-meter.js
|
191
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
M
src/settings.js
|
65
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
D
style.css
|
3
---
M
yarn.lock
|
104
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
diff --git a/package.json b/package.json
@@ -9,6 +9,10 @@
   "dependencies": {
     "@authentic/mwc-card": "^0.5.0",
     "@authentic/mwc-circular-progress": "^0.5.0",
+    "@authentic/mwc-drawer": "^0.5.0",
+    "@authentic/mwc-icon": "^0.5.0",
+    "@authentic/mwc-icon-button": "^0.5.0",
+    "@authentic/mwc-ripple": "^0.5.0",
     "@authentic/mwc-switch": "^0.5.0",
     "@authentic/mwc-top-app-bar": "^0.5.0",
     "lit-element": "^2.1.0"
diff --git a/src/index.js b/src/index.js
@@ -2,15 +2,18 @@ import { LitElement, html, css } from "lit-element";
 
 import "@authentic/mwc-top-app-bar";
 import "@authentic/mwc-card";
+import "@authentic/mwc-icon-button";
+import "@authentic/mwc-icon";
+import "@authentic/mwc-drawer";
 
 import "./lights.js";
 import "./power-meter.js";
 import "./settings.js";
 
 const pages = {
-	"#/lights": { name: "Lights", content: html`<smarthome-lights></smarthome-lights>`, path: "/lights" },
-	"#/powermeter": { name: "Power Meter", content: html`<smarthome-power-meter></smarthome-power-meter>` },
-	"#/settings": { name: "Settings", content: html`<smarthome-settings></smarthome-settings>` },
+	"#/lights": { name: "Lights", content: html`<smarthome-lights></smarthome-lights>`, icon: "lightbulb" },
+	"#/powermeter": { name: "Power Meter", content: html`<smarthome-power-meter></smarthome-power-meter>`, icon: "power" },
+	"#/settings": { name: "Settings", content: html`<smarthome-settings></smarthome-settings>`, icon: "settings" },
 };
 
 const notFoundPage = { name: "Sorry", content: html`<h3>The page you tried to access does not exist</h3>` };

@@ -23,21 +26,40 @@ class SmartHomeLayout extends LitElement {
 		this._handleHashChange();
 	}
 
-	_handleHashChange() {
+	async _handleHashChange() {
 		if (!window.location.hash) {
 			this.activePage = defaultPage;
 		} else {
 			this.activePage = pages[window.location.hash] || notFoundPage;
 		}
-		this.requestUpdate();
+		await this.requestUpdate();
+		const drawer = this.shadowRoot.querySelector("mwc-drawer");
+		drawer.open = false;
+	}
+
+	_handleNavEvent(evt) {
+		const drawer = this.shadowRoot.querySelector("mwc-drawer");
+		drawer.open = !drawer.open;
 	}
 
 	render() {
 		return html`
-			<mwc-top-app-bar type="fixed">
+			<mwc-drawer type="modal">
+				<div id="drawer-content">
+					${Object.entries(pages).map(([path, page]) => html`
+						<a href="${path}" class="${this.activePage === page ? "active" : "inactive"}">
+							<mwc-icon>${page.icon}</mwc-icon>
+							<span>${page.name}</span>
+							<mwc-ripple></mwc-ripple>
+						</a>
+					`)}
+				</div>
+			</mwc-drawer>
+			<mwc-top-app-bar type="fixed" @MDCTopAppBar:nav="${this._handleNavEvent}">
 				<div id="title-container" slot="title">
 					<span>${this.activePage.name}</span>
 				</div>
+				<mwc-icon-button slot="navigationIcon" icon="menu"></mwc-icon-button>
 			</mwc-top-app-bar>
 			<div class="top-app-bar-adjust">${this.activePage.content}</div>
 		`;

@@ -51,7 +73,7 @@ class SmartHomeLayout extends LitElement {
 				--mdc-theme-primary: #43a047;
 				--mdc-theme-on-secondary: white;
 				--mdc-theme-secondary: #616161;
-				background-color: #f5f5f5;
+				background-color: #ccc;
 			}
 			mwc-top-app-bar {
 				position: fixed;

@@ -67,9 +89,35 @@ class SmartHomeLayout extends LitElement {
 					width: 100vw;
 					padding-left: calc((100% - 450px) / 2 + 20px);
 				}
+				mwc-icon-button {
+					position: absolute;
+					padding-left: calc(((100% - 450px) / 2) - 75px);
+					z-index: 10;
+				}
+			}
+
+			#drawer-content {
+				display: flex;
+				flex-direction: column;
+			}
+			#drawer-content a {
+				text-decoration: none;
+				color: black;
+				display: flex;
+				flex-direction: row;
+				margin: 0;
+				align-items: center;
+				border-bottom: 1px solid #ccc;
+			}
+			#drawer-content a mwc-icon {
+				padding: .7em;
+			}
+
+			#drawer-content a.active {
+				background-color: #ddd;
 			}
 		`;
 	}
 }
 
-customElements.define('smarthome-layout', SmartHomeLayout);
+customElements.define("smarthome-layout", SmartHomeLayout);
diff --git a/src/lights.js b/src/lights.js
@@ -2,6 +2,7 @@ import { LitElement, html, css } from "lit-element";
 
 import { Switch } from "@authentic/mwc-switch";
 import "@authentic/mwc-circular-progress";
+import "@authentic/mwc-ripple";
 
 class Lights extends LitElement {
 	constructor() {

@@ -12,7 +13,7 @@ class Lights extends LitElement {
 	}
 
 	_initWebSocket() {
-		this.ws = new WebSocket("ws://192.168.1.1:8080/");
+		this.ws = new WebSocket(`ws://${window.location.host}/relay/ws`);
 		this.ws.onopen = async () => {
 			this.connected = true;
 			this.requestUpdate();

@@ -34,8 +35,12 @@ class Lights extends LitElement {
 	}
 
 	_clickHandler(evt) {
-		const sw = evt.composedPath().filter(el => el instanceof Switch)[0];
-		this.ws.send(`set ${Number(sw.id)} ${!sw.checked ? "on" : "off"}`);
+		evt.preventDefault();
+		const p = evt.composedPath().filter(el => el instanceof HTMLParagraphElement)[0];
+		const sw = p.querySelector("mwc-switch");
+		sw.checked = !sw.checked;
+		this.ws.send(`set ${Number(sw.id)} ${sw.checked ? "on" : "off"}`);
+		return true;
 	}
 
 	render() {

@@ -43,9 +48,10 @@ class Lights extends LitElement {
 			${this.connected ? html`
 				<div class="card">
 					${this.switches.map(sw => html`
-						<p>
+						<p @click="${this._clickHandler}">
 							<span>${sw.name}</span>
-							<mwc-switch .id="${sw.id}" ?disabled="${!this.connected}" ?checked="${sw.value}" @click="${this._clickHandler}"></mwc-switch>
+							<mwc-ripple></mwc-ripple>
+							<mwc-switch .id="${sw.id}" ?disabled="${!this.connected}" ?checked="${sw.value}"></mwc-switch>
 						</p>
 					`)}
 				</div>

@@ -67,6 +73,7 @@ class Lights extends LitElement {
 				--mdc-theme-primary: #43a047;
 				--mdc-theme-on-secondary: white;
 				--mdc-theme-secondary: #616161;
+				user-select: none;
 			}
 			p {
 				display: flex;

@@ -84,21 +91,24 @@ class Lights extends LitElement {
 			}
 			.card {
 				background-color: #fff;
-				box-shadow: rgba(0, 0, 0, 0.2) 0px 2px 1px -1px, rgba(0, 0, 0, 0.14) 0px 1px 1px 0px, rgba(0, 0, 0, 0.12) 0px 1px 3px 0px;
-				border-radius: 4px;
 				margin-left: auto;
 				margin-right: auto;
 				width: 100%;
 				overflow: hidden;
+				border-top: 1px solid #ccc;
+				border-bottom: 1px solid #ccc;
 			}
 			@media (min-width: 600px) {
 				.card {
 					max-width: 450px;
 					margin-top: 20px;
+					border: none;
+					border-radius: 4px;
+					box-shadow: 0 4px 5px 0 rgba(0,0,0,0.14), 0 1px 10px 0 rgba(0,0,0,0.12), 0 2px 4px -1px rgba(0,0,0,0.3);
 				}
 			}
 		`;
 	}
 }
 
-customElements.define('smarthome-lights', Lights);
+customElements.define("smarthome-lights", Lights);
diff --git a/src/power-meter.js b/src/power-meter.js
@@ -0,0 +1,191 @@
+import { LitElement, html, css } from "lit-element";
+
+import "@authentic/mwc-circular-progress";
+import "@authentic/mwc-icon";
+
+const counters = [
+	{ id: 60, name: "Kueche" },
+	{ id: 50, name: "Sonstiges" },
+];
+
+const round = (num, places) => {
+	const factor = Math.pow(10, places);
+	return Math.round(num * factor) / factor;
+};
+
+class PowerMeter extends LitElement {
+	constructor() {
+		super(...arguments);
+		this.loaded = false;
+		this.connected = false;
+		this._loadData();
+		this._initWebSocket();
+	}
+
+	_initWebSocket() {
+		this.ws = new WebSocket(`ws://${window.location.host}/gosdm/ws`);
+		this.ws.onopen = async () => {
+			this.connected = true;
+			this.requestUpdate();
+		};
+		this.ws.onclose = () => {
+			this.connected = false;
+			this.requestUpdate();
+			this._initWebSocket();
+		};
+		this.ws.onmessage = (msg) => {
+			clearInterval(this.timeout);
+			this.timeout = setTimeout(() => {
+				this.ws.onclose();
+			}, 2000);
+
+			const m = JSON.parse(msg.data);
+			if (!m.IEC61850) return;
+
+			const d = this.data[m.DeviceId];
+
+			if (m.IEC61850 === "Frequency") {
+				d.Frequency = m.Value;
+			} else if (m.IEC61850 === "VoltageL1") {
+				d.Voltage.L1 = m.Value;
+			} else if (m.IEC61850 === "CosphiL1") {
+				d.Cosphi.L1 = m.Value;
+			} else if (m.IEC61850 === "PowerL1") {
+				d.Power.L1 = m.Value;
+			} else if (m.IEC61850 === "Import") {
+				d.TotalImport = m.Value;
+			}
+
+			this.requestUpdate();
+		};
+	}
+
+	async _loadData() {
+		const newData = {};
+		await Promise.all(counters.map(async (counter) => {
+			const d = await fetch(`http://192.168.1.1:8070/last/${counter.id}`)
+				.then(resp => resp.json());
+
+			newData[d.ModbusDeviceId] = {
+				...d,
+				name: counter.name,
+			};
+		}));
+
+		this.data = newData;
+		this.loaded = true;
+		this.requestUpdate();
+	}
+
+	render() {
+		return html`
+			${this.loaded ? html`
+				<div id="connection-status">
+					${this.connected ? html`
+						<mwc-icon>cloud_queue</mwc-icon> Connected
+					` : html`
+						<mwc-icon>cloud_off</mwc-icon> Disconnected
+					`}
+				</div>
+				<div class="card table">
+					<div class="table-row table-head">
+						<div class="table-column">Name</div>
+						<div class="table-column">Voltage</div>
+						<div class="table-column">Power</div>
+						<div class="table-column">Power Factor</div>
+						<div class="table-column">Frequency</div>
+						<div class="table-column">Import</div>
+					</div>
+					${Object.entries(this.data).map(([_, d]) => html`
+						<div class="table-row">
+							<div class="table-column">${d.name}</div>
+							<div class="table-column">${round(d.Voltage.L1, 2)} V</div>
+							<div class="table-column">${round(d.Power.L1, 2)} W</div>
+							<div class="table-column">${round(d.Cosphi.L1, 2)}</div>
+							<div class="table-column">${round(d.Frequency, 2)}</div>
+							<div class="table-column">${round(d.TotalImport, 2)} kWh</div>
+						</div>
+					`)}
+				</div>
+			` : html`
+				<div id="loading">
+					<h3>Trying to connect...</h3>
+					<mwc-circular-progress></mwc-circular-progress>
+				</div>
+			`}
+		`;
+	}
+
+	static get styles() {
+		return css`
+			:host {
+				display: flex;
+				flex-direction: column;
+				--mdc-theme-on-primary: white;
+				--mdc-theme-primary: #43a047;
+				--mdc-theme-on-secondary: white;
+				--mdc-theme-secondary: #616161;
+			}
+			#loading {
+				display: flex;
+				flex-direction: column;
+				align-items: center;
+			}
+			#connection-status mwc-icon {
+				padding: .5em;
+			}
+			#connection-status {
+				display: flex;
+				flex-direction: row;
+				justify-content: center;
+				align-items: center;
+			}
+			.card {
+				background-color: #fff;
+				margin-left: auto;
+				margin-right: auto;
+				width: 100%;
+				overflow: hidden;
+				border-top: 1px solid #ccc;
+				border-bottom: 1px solid #ccc;
+			}
+			.table {
+				display: flex;
+				flex-direction: row;
+			}
+			.table-row {
+				display: flex;
+				flex-direction: column;
+				width: 100%;
+			}
+			.table-column {
+				padding: 1em;
+				height: 1em;
+				display: flex;
+				flex-direction: column;
+				justify-content: center;
+			}
+
+			.table-column {
+				background-color: #ddd;
+			}
+			.table-column:nth-child(2n) {
+				background-color: #fff;
+			}
+			@media (min-width: 600px) {
+				.card {
+					max-width: 450px;
+					margin-top: 20px;
+					box-shadow: 0 4px 5px 0 rgba(0,0,0,0.14), 0 1px 10px 0 rgba(0,0,0,0.12), 0 2px 4px -1px rgba(0,0,0,0.3);
+					border: none;
+					border-radius: 4px;
+				}
+				#connection-status {
+					margin-top: 20px;
+				}
+			}
+		`;
+	}
+}
+
+customElements.define("smarthome-power-meter", PowerMeter);
diff --git a/src/settings.js b/src/settings.js
@@ -0,0 +1,65 @@
+import { LitElement, html, css } from "lit-element";
+
+import "@authentic/mwc-ripple";
+
+class Settings extends LitElement {
+	_forceUpdate(evt) {
+	}
+
+	render() {
+		return html`
+			<div class="card">
+				<a href="https://www.gnu.org/licenses/agpl-3.0-standalone.html">
+					<span>License</span>
+					<mwc-ripple></mwc-ripple>
+				</a>
+				<p @click="${this._forceUpdate}">
+					<span>Force update</span>
+					<mwc-ripple></mwc-ripple>
+				</p>
+			</div>
+		`;
+	}
+
+	static get styles() {
+		return css`
+			:host {
+				display: flex;
+				flex-direction: column;
+				--mdc-theme-on-primary: white;
+				--mdc-theme-primary: #43a047;
+				--mdc-theme-on-secondary: white;
+				--mdc-theme-secondary: #616161;
+				user-select: none;
+			}
+			p, a {
+				text-decoration: none;
+				color: black;
+				display: flex;
+				flex-direction: row;
+				justify-content: space-between;
+				padding: 1em;
+				margin: 0;
+				height: 14px;
+				border-bottom: 1px solid #ccc;
+			}
+			.card {
+				background-color: #fff;
+				box-shadow: rgba(0, 0, 0, 0.2) 0px 2px 1px -1px, rgba(0, 0, 0, 0.14) 0px 1px 1px 0px, rgba(0, 0, 0, 0.12) 0px 1px 3px 0px;
+				border-radius: 4px;
+				margin-left: auto;
+				margin-right: auto;
+				width: 100%;
+				overflow: hidden;
+			}
+			@media (min-width: 600px) {
+				.card {
+					max-width: 450px;
+					margin-top: 20px;
+				}
+			}
+		`;
+	}
+}
+
+customElements.define("smarthome-settings", Settings);
diff --git a/style.css b/style.css
@@ -1,3 +0,0 @@
-body {
-	background: linear-gradient(#f4f4f4, #cfcfcf) fixed;
-}
diff --git a/yarn.lock b/yarn.lock
@@ -35,6 +35,26 @@
     lit-element "^2.1.0"
     lit-html "^1.0.0"
 
+"@authentic/mwc-drawer@^0.5.0":
+  version "0.5.0"
+  resolved "https://registry.yarnpkg.com/@authentic/mwc-drawer/-/mwc-drawer-0.5.0.tgz#e33598b150ec260cf48299ab03ef5a1bf7f8d7da"
+  integrity sha512-0fUtRno1phJimvE+9Y2unY2kS/bbfUgKNvEVemDHgaz7JvGHRddqyS0JnTCT8A0AqRFfuovlXzzjmuUDzO+uGw==
+  dependencies:
+    "@authentic/mwc-base" "^0.5.0"
+    "@material/drawer" "^2.0.0"
+    blocking-elements "^0.0.2"
+    wicg-inert "^1.1.6"
+
+"@authentic/mwc-icon-button@^0.5.0":
+  version "0.5.0"
+  resolved "https://registry.yarnpkg.com/@authentic/mwc-icon-button/-/mwc-icon-button-0.5.0.tgz#9ba5056ea7b256f783773a8000a0226713fcc2c3"
+  integrity sha512-UbjEWER/AX3FNBbJwVTdpH0pvW7INjdGJC8nN37hbl6I9wYU53IW8K05BtTzlc6SOP1cOEYr9B5u+6Qxw1TAtg==
+  dependencies:
+    "@authentic/mwc-base" "^0.5.0"
+    "@authentic/mwc-icon" "^0.5.0"
+    "@authentic/mwc-ripple" "^0.5.0"
+    "@material/icon-button" "^2.0.0"
+
 "@authentic/mwc-icon@^0.5.0":
   version "0.5.0"
   resolved "https://registry.yarnpkg.com/@authentic/mwc-icon/-/mwc-icon-0.5.0.tgz#ab443a880f53a2b4258c2b4432d3fac4970ea8fb"

@@ -125,6 +145,23 @@
   dependencies:
     tslib "^1.9.3"
 
+"@material/drawer@^2.0.0":
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/@material/drawer/-/drawer-2.1.1.tgz#ccdc3404bba60f3f72b153d04f9a8967c06a7df6"
+  integrity sha512-z0q2Kwb79ZMng4Kb7u2UgIxQJPNLcTq/Bj/J7EJlTlUPSMXe4N/geAhSFHyGBG/iXQupdQw5E2Wu7Fo2sfx7sg==
+  dependencies:
+    "@material/animation" "^1.0.0"
+    "@material/base" "^1.0.0"
+    "@material/elevation" "^1.1.0"
+    "@material/list" "^2.1.1"
+    "@material/ripple" "^2.1.1"
+    "@material/rtl" "^0.42.0"
+    "@material/shape" "^1.1.1"
+    "@material/theme" "^1.1.0"
+    "@material/typography" "^1.0.0"
+    focus-trap "^5.0.0"
+    tslib "^1.9.3"
+
 "@material/elevation@^1.1.0":
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/@material/elevation/-/elevation-1.1.0.tgz#def23c360ae067b43c1632a331b9883b9f679cc5"

@@ -139,6 +176,32 @@
   resolved "https://registry.yarnpkg.com/@material/feature-targeting/-/feature-targeting-0.44.1.tgz#afafc80294e5efab94bee31a187273d43d34979a"
   integrity sha512-90cc7njn4aHbH9UxY8qgZth1W5JgOgcEdWdubH1t7sFkwqFxS5g3zgxSBt46TygFBVIXNZNq35Xmg80wgqO7Pg==
 
+"@material/icon-button@^2.0.0":
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/@material/icon-button/-/icon-button-2.1.1.tgz#574842abc808da774e77c097dfca2d7e74ec23f5"
+  integrity sha512-WUp06uqG458bWCp66hyutNvwiuptbE7vuOtVukG/DmxoGWLDXSRStcUzOXYYElZsmIR14SPOTD4694CaaqHz9w==
+  dependencies:
+    "@material/base" "^1.0.0"
+    "@material/feature-targeting" "^0.44.1"
+    "@material/ripple" "^2.1.1"
+    "@material/theme" "^1.1.0"
+    tslib "^1.9.3"
+
+"@material/list@^2.1.1":
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/@material/list/-/list-2.1.1.tgz#fac8fc886d72c33ffb0f91abbc2483570cda4674"
+  integrity sha512-yZFNLQ6+nTGTJsVuqbYh9gSC+aUh3hC/0aJZzAjVCCY0qSi1vWDHcYdgmgDr4THznlcPr9TxAi6IBiJ/fHXjZg==
+  dependencies:
+    "@material/base" "^1.0.0"
+    "@material/dom" "^1.1.0"
+    "@material/feature-targeting" "^0.44.1"
+    "@material/ripple" "^2.1.1"
+    "@material/rtl" "^0.42.0"
+    "@material/shape" "^1.1.1"
+    "@material/theme" "^1.1.0"
+    "@material/typography" "^1.0.0"
+    tslib "^1.9.3"
+
 "@material/ripple@^2.0.0", "@material/ripple@^2.1.1":
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/@material/ripple/-/ripple-2.1.1.tgz#850e44bafe9db962f400c7e067eb28c1cb11010b"

@@ -279,6 +342,11 @@ base@^0.11.1:
     mixin-deep "^1.2.0"
     pascalcase "^0.1.1"
 
+blocking-elements@^0.0.2:
+  version "0.0.2"
+  resolved "https://registry.yarnpkg.com/blocking-elements/-/blocking-elements-0.0.2.tgz#fd1fb73c090415039e7ad497879decc7c54f20bf"
+  integrity sha512-sMYXYkCAAV4hBrKGZ8ylp761A02uRDjpueW23W43/YvYR6gFD/Z7cIHWvJbSUDmnpzws9VQxSMHdm4/UbL4PKg==
+
 braces@^2.3.1:
   version "2.3.2"
   resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729"

@@ -413,6 +481,11 @@ define-property@^2.0.2:
     is-descriptor "^1.0.2"
     isobject "^3.0.1"
 
+dom-matches@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/dom-matches/-/dom-matches-2.0.0.tgz#d2728b416a87533980eb089b848d253cf23a758c"
+  integrity sha1-0nKLQWqHUzmA6wibhI0lPPI6dYw=
+
 escape-string-regexp@^1.0.5:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"

@@ -480,6 +553,14 @@ fill-range@^4.0.0:
     repeat-string "^1.6.1"
     to-regex-range "^2.1.0"
 
+focus-trap@^5.0.0:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/focus-trap/-/focus-trap-5.0.1.tgz#285f9df2cd9f5ef82dd1abb5d8a70e66cd4f99e3"
+  integrity sha512-vU7zEdL3y+kfkuwBbT9456JH8QfyemdcdZ2gKMfmgLyAs9NQAkSVQBSZmb9nlb1cVMo+iCsddqeGJog00pd2EQ==
+  dependencies:
+    tabbable "^4.0.0"
+    xtend "^4.0.1"
+
 for-in@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"

@@ -880,9 +961,9 @@ rollup-pluginutils@^2.7.0:
     micromatch "^3.1.10"
 
 rollup@^1.12.2:
-  version "1.12.2"
-  resolved "https://registry.yarnpkg.com/rollup/-/rollup-1.12.2.tgz#a7c34a4bef71feb43e3ae69f0b26ae683e75db44"
-  integrity sha512-ePehZfVMIE4eO0/LV6VaMY8kp0D9sbziUabpBeJbHAHa2WJPxuS0lYLmiLamb02e098RIRyq1F2yjM4O08dQVA==
+  version "1.12.3"
+  resolved "https://registry.yarnpkg.com/rollup/-/rollup-1.12.3.tgz#068b1957d5bebf6c0a758cfe42609b512add35a9"
+  integrity sha512-ueWhPijWN+GaPgD3l77hXih/gcDXmYph6sWeQegwBYtaqAE834e8u+MC2wT6FKIUsz1DBOyOXAQXUZB+rjWDoQ==
   dependencies:
     "@types/estree" "0.0.39"
     "@types/node" "^12.0.2"

@@ -1025,6 +1106,11 @@ supports-color@^6.1.0:
   dependencies:
     has-flag "^3.0.0"
 
+tabbable@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-4.0.0.tgz#5bff1d1135df1482cf0f0206434f15eadbeb9261"
+  integrity sha512-H1XoH1URcBOa/rZZWxLxHCtOdVUEev+9vo5YdYhC9tCY4wnybX+VQrCYuy9ubkg69fCBxCONJOSLGfw0DWMffQ==
+
 terser@^3.14.1:
   version "3.17.0"
   resolved "https://registry.yarnpkg.com/terser/-/terser-3.17.0.tgz#f88ffbeda0deb5637f9d24b0da66f4e15ab10cb2"

@@ -1096,3 +1182,15 @@ util-deprecate@~1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
   integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
+
+wicg-inert@^1.1.6:
+  version "1.1.6"
+  resolved "https://registry.yarnpkg.com/wicg-inert/-/wicg-inert-1.1.6.tgz#1d7703bc2f84acc0ea4de01a4c8a5cfcc1fd8a8a"
+  integrity sha512-svnNP2bUZc1luu0erL2Y25Iyxsm0SUk9wNq3FbgTgxcrqG3YAZBPYonRNRGgpveeEqRAnNE5yNcIdEd/F86tbw==
+  dependencies:
+    dom-matches "^2.0.0"
+
+xtend@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
+  integrity sha1-pcbVMr5lbiPbgg77lDofBJmNY68=