commit 38d86cc30e8039e3c163cc61859bcad75835ffa0
parent b3ee2db4447dbfdd1b73486959e01d038754b5d5
Author: Leah (ctucx) <git@ctu.cx>
Date: Fri, 9 Dec 2022 21:55:37 +0100
parent b3ee2db4447dbfdd1b73486959e01d038754b5d5
Author: Leah (ctucx) <git@ctu.cx>
Date: Fri, 9 Dec 2022 21:55:37 +0100
some refactoring
7 files changed, 389 insertions(+), 496 deletions(-)
diff --git a/config.json b/config.json @@ -31,7 +31,7 @@ "set": "zigbee2mqtt/ikea_lamp_hallway/set" }, "transform": { - "get": "return (value.state == 'ON') ? true : false", + "get": "return (message.state == 'ON') ? true : false", "set": "return JSON.stringify({state: (input) ? 'ON' : 'OFF'})" }, "type": "switch" @@ -47,7 +47,7 @@ "set": "zigbee2mqtt/ikea_lamp_hallway/set" }, "transform": { - "get": "return value.brightness", + "get": "return message.brightness", "set": "return JSON.stringify({brightness: Number(input)})" }, "type": "slider" @@ -63,7 +63,7 @@ "set": "zigbee2mqtt/ikea_lamp_hallway/set" }, "transform": { - "get": "return value.color_temp", + "get": "return message.color_temp", "set": "return JSON.stringify({color_temp: Number(input)})" }, "type": "slider" @@ -81,7 +81,7 @@ "set": "zigbee2mqtt/ikea_lamp_kitchen/set" }, "transform": { - "get": "return (value.state == 'ON') ? true : false", + "get": "return (message.state == 'ON') ? true : false", "set": "return JSON.stringify({state: (input) ? 'ON' : 'OFF'})" }, "type": "switch" @@ -97,7 +97,7 @@ "set": "zigbee2mqtt/ikea_lamp_kitchen/set" }, "transform": { - "get": "return value.brightness", + "get": "return message.brightness", "set": "return JSON.stringify({brightness: Number(input)})" }, "type": "slider" @@ -113,7 +113,7 @@ "set": "zigbee2mqtt/ikea_lamp_kitchen/set" }, "transform": { - "get": "return value.color_temp", + "get": "return message.color_temp", "set": "return JSON.stringify({color_temp: Number(input)})" }, "type": "slider" @@ -131,7 +131,7 @@ "set": "zigbee2mqtt/ikea_lamp_bathroom/set" }, "transform": { - "get": "return (value.state == 'ON') ? true : false", + "get": "return (message.state == 'ON') ? true : false", "set": "return JSON.stringify({state: (input) ? 'ON' : 'OFF'})" }, "type": "switch" @@ -147,7 +147,7 @@ "set": "zigbee2mqtt/ikea_lamp_bathroom/set" }, "transform": { - "get": "return value.brightness", + "get": "return message.brightness", "set": "return JSON.stringify({brightness: Number(input)})" }, "type": "slider" @@ -163,13 +163,32 @@ "set": "zigbee2mqtt/ikea_lamp_bathroom/set" }, "transform": { - "get": "return value.color_temp", + "get": "return message.color_temp", "set": "return JSON.stringify({color_temp: Number(input)})" }, "type": "slider" } ], "title": "Bathroom: Ceiling Light" + }, + { + "items": [ + { + "icon": "icons/temperature.png", + "title": "Fridge", + "topic": "lacrosse2mqtt/33", + "transform": "return Math.round((message.temperature + Number.EPSILON) * 100) / 100 + ' °C'", + "type": "text" + }, + { + "icon": "icons/temperature.png", + "title": "Bathroom", + "topic": "lacrosse2mqtt/5", + "transform": "return Math.round((message.temperature + Number.EPSILON) * 100) / 100 + ' °C - ' + message.humidity + ' %'", + "type": "text" + } + ], + "title": "Temperature-Sensors" } ], "title": "Smart-Home" @@ -188,7 +207,7 @@ "set": "zigbee2mqtt/ikea_lamp_l/set" }, "transform": { - "get": "return (value.state == 'ON') ? true : false", + "get": "return (message.state == 'ON') ? true : false", "set": "return JSON.stringify({state: (input) ? 'ON' : 'OFF'})" }, "type": "switch" @@ -204,7 +223,7 @@ "set": "zigbee2mqtt/ikea_lamp_l/set" }, "transform": { - "get": "return value.brightness", + "get": "return message.brightness", "set": "return JSON.stringify({brightness: Number(input)})" }, "type": "slider" @@ -220,7 +239,7 @@ "set": "zigbee2mqtt/ikea_lamp_l/set" }, "transform": { - "get": "return value.color_temp", + "get": "return message.color_temp", "set": "return JSON.stringify({color_temp: Number(input)})" }, "type": "slider" @@ -238,7 +257,7 @@ "set": "zigbee2mqtt/led_stripe_desk/set" }, "transform": { - "get": "return (value.state == 'ON') ? true : false", + "get": "return (message.state == 'ON') ? true : false", "set": "return JSON.stringify({state: (input) ? 'ON' : 'OFF'})" }, "type": "switch" @@ -254,7 +273,7 @@ "set": "zigbee2mqtt/led_stripe_desk/set" }, "transform": { - "get": "return value.brightness", + "get": "return message.brightness", "set": "return JSON.stringify({brightness: Number(input)})" }, "type": "slider" @@ -272,7 +291,7 @@ "set": "zigbee2mqtt/ikea_lamp_l_rgb/set" }, "transform": { - "get": "return (value.state == 'ON') ? true : false", + "get": "return (message.state == 'ON') ? true : false", "set": "return JSON.stringify({state: (input) ? 'ON' : 'OFF'})" }, "type": "switch" @@ -288,7 +307,7 @@ "set": "zigbee2mqtt/ikea_lamp_l_rgb/set" }, "transform": { - "get": "return value.brightness", + "get": "return message.brightness", "set": "return JSON.stringify({brightness: Number(input)})" }, "type": "slider" @@ -304,7 +323,7 @@ "set": "zigbee2mqtt/ikea_lamp_l_rgb/set" }, "transform": { - "get": "return value.color_temp", + "get": "return message.color_temp", "set": "return JSON.stringify({color_temp: Number(input)})" }, "type": "slider" @@ -331,7 +350,7 @@ "set": "zigbee2mqtt/ikea_lamp_l_rgb/set" }, "transform": { - "get": "return value.color.x + ','+value.color.y", + "get": "return message.color.x + ','+message.color.y", "set": "return JSON.stringify({color: {x: input.split(',')[0], y: input.split(',')[1]}})" }, "type": "select" @@ -345,39 +364,51 @@ "icon": "icons/power.png", "title": "Voltage", "topic": "sdm2mqtt/leah", - "transform": "return Math.round((value.voltage + Number.EPSILON) * 100) / 100 + ' V'", + "transform": "return Math.round((message.voltage + Number.EPSILON) * 100) / 100 + ' V'", "type": "text" }, { "icon": "icons/power.png", "title": "Power", "topic": "sdm2mqtt/leah", - "transform": "return Math.round((value.power + Number.EPSILON) * 100) / 100 + ' W'", + "transform": "return Math.round((message.power + Number.EPSILON) * 100) / 100 + ' W'", "type": "text" }, { "icon": "icons/power.png", "title": "Frequency", "topic": "sdm2mqtt/leah", - "transform": "return value.frequency + ' Hz'", + "transform": "return message.frequency + ' Hz'", "type": "text" }, { "icon": "icons/power.png", "title": "cos φ", "topic": "sdm2mqtt/leah", - "transform": "return Math.round((value.cosphi + Number.EPSILON) * 100) / 100", + "transform": "return Math.round((message.cosphi + Number.EPSILON) * 100) / 100", "type": "text" }, { "icon": "icons/power.png", "title": "Total Import", "topic": "sdm2mqtt/leah", - "transform": "return Math.round((value.import + Number.EPSILON) * 100) / 100 + ' kWh'", + "transform": "return Math.round((message.import + Number.EPSILON) * 100) / 100 + ' kWh'", "type": "text" } ], "title": "Power-Meter" + }, + { + "items": [ + { + "icon": "icons/temperature.png", + "title": "Temperature", + "topic": "lacrosse2mqtt/3a", + "transform": "return Math.round((message.temperature + Number.EPSILON) * 100) / 100 + ' °C'", + "type": "text" + } + ], + "title": "Temperature-Sensors" } ], "title": "Leah's room" @@ -396,7 +427,7 @@ "set": "zigbee2mqtt/ikea_lamp_i/set" }, "transform": { - "get": "return (value.state == 'ON') ? true : false", + "get": "return (message.state == 'ON') ? true : false", "set": "return JSON.stringify({state: (input) ? 'ON' : 'OFF'})" }, "type": "switch" @@ -412,7 +443,7 @@ "set": "zigbee2mqtt/ikea_lamp_i/set" }, "transform": { - "get": "return value.brightness", + "get": "return message.brightness", "set": "return JSON.stringify({brightness: Number(input)})" }, "type": "slider" @@ -428,7 +459,7 @@ "set": "zigbee2mqtt/ikea_lamp_i/set" }, "transform": { - "get": "return value.color_temp", + "get": "return message.color_temp", "set": "return JSON.stringify({color_temp: Number(input)})" }, "type": "slider" @@ -446,7 +477,7 @@ "set": "zigbee2mqtt/ikea_lamp_i_rgb/set" }, "transform": { - "get": "return (value.state == 'ON') ? true : false", + "get": "return (message.state == 'ON') ? true : false", "set": "return JSON.stringify({state: (input) ? 'ON' : 'OFF'})" }, "type": "switch" @@ -462,7 +493,7 @@ "set": "zigbee2mqtt/ikea_lamp_i_rgb/set" }, "transform": { - "get": "return value.brightness", + "get": "return message.brightness", "set": "return JSON.stringify({brightness: Number(input)})" }, "type": "slider" @@ -478,7 +509,7 @@ "set": "zigbee2mqtt/ikea_lamp_i_rgb/set" }, "transform": { - "get": "return value.color_temp", + "get": "return message.color_temp", "set": "return JSON.stringify({color_temp: Number(input)})" }, "type": "slider" @@ -505,7 +536,7 @@ "set": "zigbee2mqtt/ikea_lamp_i_rgb/set" }, "transform": { - "get": "return value.color.x + ','+value.color.y", + "get": "return message.color.x + ','+message.color.y", "set": "return JSON.stringify({color: {x: input.split(',')[0], y: input.split(',')[1]}})" }, "type": "select" @@ -523,7 +554,7 @@ "set": "zigbee2mqtt/ikea_control_outlet_i_desk_l/set" }, "transform": { - "get": "return (value.state == 'ON') ? true : false", + "get": "return (message.state == 'ON') ? true : false", "set": "return JSON.stringify({state: (input) ? 'ON' : 'OFF'})" }, "type": "switch" @@ -536,13 +567,32 @@ "set": "zigbee2mqtt/ikea_control_outlet_i_desk_r/set" }, "transform": { - "get": "return (value.state == 'ON') ? true : false", + "get": "return (message.state == 'ON') ? true : false", "set": "return JSON.stringify({state: (input) ? 'ON' : 'OFF'})" }, "type": "switch" } ], - "title": "Power-Meter" + "title": "Switches" + }, + { + "items": [ + { + "icon": "icons/temperature.png", + "title": "Temperature", + "topic": "lacrosse2mqtt/3c", + "transform": "return Math.round((message.temperature + Number.EPSILON) * 100) / 100 + ' °C'", + "type": "text" + }, + { + "icon": "icons/thermostat.png", + "title": "Humidity", + "topic": "lacrosse2mqtt/3c", + "transform": "return message.humidity + ' %'", + "type": "text" + } + ], + "title": "Temperature-Sensors" } ], "title": "Isa's room"
diff --git a/package.json b/package.json @@ -17,7 +17,6 @@ "dependencies": { "webpack": "5.11.1", "lit-html": "^2.4.0", - "shortid": "^2.2.16", "mqtt": "^4.3.7", "buffer": "^6.0.3", "process": "^0.11.10",
diff --git a/src/webui.css b/src/webui.css @@ -4,7 +4,7 @@ body { margin: 0; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + font-family: system-ui; font-size: 1rem; font-weight: 400; line-height: 1.5; @@ -28,6 +28,9 @@ img { } nav { + margin-left: auto; + margin-right: auto; + max-width: 50em; background-color: #f8f9fa; position: fixed; top: 0; @@ -65,22 +68,30 @@ button .icon { } button .icon svg { + color: #212529; height: 31px; width: 31px; } -.row { +.icon { + height: 32px; + width: 32px; + margin: 4px; +} + + +.page { margin-left: auto; margin-right: auto; max-width: 50em; - margin-bottom: 10px; } -.list-group { +section { border-radius: 0.25rem; + margin-bottom: 10px; } -.list-group-item { +section > div { background-color: #fff; padding: 0.25rem; border: 1px solid rgba(0, 0, 0, 0.125); @@ -89,72 +100,50 @@ button .icon svg { align-self: center !important; } -.list-group-item:first-child { +section > div:first-child { + background-color: #f8f9fa; + padding-left: 1em; border-top-left-radius: inherit; border-top-right-radius: inherit; } -.list-group-item:last-child { +section > div:last-child { border-bottom-right-radius: inherit; border-bottom-left-radius: inherit; border-bottom: 1px solid rgba(0, 0, 0, 0.125); } -.list-group-item * { +section > div * { align-self: center !important; } -.list-group-item .left { +section > div .left { margin-left: auto; font-size: 100%; margin-right: 1em; } -.list-group-item .title { +section > div .title{ padding: 0.5rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } @media screen and (max-width: 700px) { - .row { - margin-bottom: 0; - } - - .list-group { + section { border-radius: 0; - } - .list-group-item { - border-bottom: 1px solid rgba(0, 0, 0, 0.125); + margin-bottom: 0px; } - .list-group-item { - border-width: 0 0 1px; + section > div { + border-bottom: 1px solid rgba(0, 0, 0, 0.125); + border-width: 0 0 1px; } } -.icon { - height: 32px; - width: 32px; - margin: 4px; -} -.no-icon { - display: none; -} - -.title { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.seperator { - background-color: #f8f9fa; - padding-left: 1em!important; -} - -.page { - width: 100%; -} +/* Switch Styling*/ .toggle { font-family: sans-serif; @@ -188,8 +177,6 @@ button .icon svg { /* Slider Styling*/ .slider { width: 100%; -} -.slider { -webkit-appearance: none; height: 10px; border-radius: 5px; @@ -205,7 +192,7 @@ button .icon svg { border-radius: 50%; background: #6c757d; cursor: pointer; - box-shadow:var(--c,0) 0 0 #ccc; /*https://stackoverflow.com/a/51571070/1997890*/ + box-shadow:var(--c,0) 0 0 #ccc; } .slider::-moz-range-thumb { @@ -214,9 +201,10 @@ button .icon svg { border-radius: 50%; background: #6c757d; cursor: pointer; - box-shadow:var(--c,0) 0 0 #ccc; /*https://stackoverflow.com/a/51571070/1997890*/ + box-shadow:var(--c,0) 0 0 #ccc; } + .loader { border: 4px solid #ccc; /* Pick even numbers here */ border-top: 4px solid #6c757d; /* Pick even numbers here */ @@ -233,20 +221,6 @@ button .icon svg { 100% { transform: rotate(360deg); } } -.sh-input-number { - width: 4em!important; - background-color: lightgray; - text-align: center; - border: none; -} - -.sh-input-number:focus { - border-color: inherit; - box-shadow: inherit; - background-color: inherit; -} - -/* Animation during processing */ .working { background-color: lightgray; animation-name: color;
diff --git a/src/webui.js b/src/webui.js @@ -4,32 +4,34 @@ import "./webui.css"; import { html, render } from 'lit-html'; import { map } from 'lit-html/directives/map.js'; -import {ifDefined} from 'lit-html/directives/if-defined.js'; +import { ifDefined } from 'lit-html/directives/if-defined.js'; +import { unsafeSVG } from 'lit-html/directives/unsafe-svg.js'; -import shortid from 'shortid'; -import { connect } from "mqtt"; +import { connect } from "mqtt"; + +var client; + +const connectedSvg = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-wifi"><path d="M5 12.55a11 11 0 0 1 14.08 0"></path><path d="M1.42 9a16 16 0 0 1 21.16 0"></path><path d="M8.53 16.11a6 6 0 0 1 6.95 0"></path><line x1="12" y1="20" x2="12.01" y2="20"></line></svg>'; +const disconnectedSvg = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-wifi-off"><line x1="1" y1="1" x2="23" y2="23"></line><path d="M16.72 11.06A10.94 10.94 0 0 1 19 12.55"></path><path d="M5 12.55a10.94 10.94 0 0 1 5.17-2.39"></path><path d="M10.71 5.05A16 16 0 0 1 22.58 9"></path><path d="M1.42 9a15.91 15.91 0 0 1 4.7-2.88"></path><path d="M8.53 16.11a6 6 0 0 1 6.95 0"></path><line x1="12" y1="20" x2="12.01" y2="20"></line></svg>'; +const backButtonSvg = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-left"><line x1="19" y1="12" x2="5" y2="12"></line><polyline points="12 19 5 12 12 5"></polyline></svg>'; const goToPage = (pageHash) => { - if (typeof pageHash === 'object') pageHash = location.hash || '#mainpage'; + if (typeof pageHash !== 'string') pageHash = location.hash || '#mainpage'; console.log('goToPage', pageHash); document.querySelectorAll(".page").forEach(element => { - if ('#'+element.id !== pageHash) { - element.style.display = 'none'; - } else { - element.style.display = ''; - } + element.style.display = ('#'+element.id !== pageHash) ? 'none' : ''; }); } const transformMessage = (input, meta) => { - if (!meta.transform) return; - if (typeof meta.transform !== 'object') return; + if (typeof meta.transform !== 'object') return input; - if ('set' in meta.transform) { + if (typeof meta.transform.set === 'string') { try { return new Function('input', meta.transform.set)(input); - } catch { + } catch (exception){ + console.log(exception); return; } } @@ -37,19 +39,86 @@ const transformMessage = (input, meta) => { return input; } +const onMessage = (topic, message) => { + message = JSON.parse(message.toString()); + + document.querySelectorAll('[data-mqtt-topic="' + topic + '"]').forEach((element) => { + let value = null; + + if (typeof element.dataset.transform !== 'undefined') { + try { + value = new Function('topic', 'message', element.dataset.transform)(topic, message); + } catch (exception){ + console.log(exception); + return; + } + } + + if (value === null) value = message; + + switch (element.dataset.type) { + case 'text': + if (typeof value === 'object') return; + + element.textContent = value; + break; + + case 'switch': + if (typeof value !== 'boolean') return; + + element.checked = value; + break; + + case 'slider': + if (isNaN(value)) return; + + element.value = value; + element.dataset.lastMqttValue = value; + element.style.setProperty('--c', 0); + break; + + case 'select': + if (typeof value === 'object') return; + + element.value = value; + element.dataset.lastMqttValue = value; + document.getElementById(element.id + '_loader').classList.remove('loader'); + break; + + // untested + case 'button': + if (typeof value === 'object') return; + + if (element.dataset.mqttValue === value) { + element.classList.add('active'); + } else { + element.classList.remove('active'); + } + + break; + + // untested + case 'number': + if (isNaN(value)) return; + + element.value = value; + element.classList.remove('working'); + break; + } + }); +}; + const getConfig = async () => { const response = await fetch('/config.json'); if (!response.ok) { - if (!localStorage.getItem('config')) { - return { - pages: [{ - pageid: "mainpage", - pagetitle: "No Config!", - sections: [] - }] - }; - } + if (!localStorage.getItem('config')) return { + pages: [{ + pageid: "mainpage", + pagetitle: "No Config!", + sections: [] + }] + }; return JSON.parse(localStorage.getItem('config')); } @@ -61,439 +130,252 @@ const getConfig = async () => { }; const renderItem = (itemData) => { - itemData.id = itemData.type + '_' + shortid.generate(); - if (typeof itemData.topic === 'string') { const temporary = itemData.topic; - itemData.topic = {}; + itemData.topic = {}; itemData.topic.get = temporary; itemData.topic.set = null; } + let transform = null; + + if (typeof itemData.transform !== 'undefined') { + if (typeof itemData.transform === 'string') transform = itemData.transform; + if (typeof itemData.transform.get === 'string') transform = itemData.transform.get; + }; + if (itemData.type !== 'html') { let element = html``; - if (itemData.type === 'slider') { - itemData.sliderMinValue = (itemData.sliderMinValue !== undefined) ? itemData.sliderMinValue : 0; - itemData.sliderMaxValue = (itemData.sliderMaxValue !== undefined) ? itemData.sliderMaxValue : 1; - itemData.sliderStepValue = (itemData.sliderStepValue !== undefined) ? itemData.sliderStepValue : 'any'; + if (itemData.type == 'switch' || itemData.type == 'select') { + itemData.id = Math.random().toString(36).slice(2); } switch (itemData.type) { case 'text': - if (itemData.topic === undefined) break; - element = html` - <div class="left" data-mqtt-topic="${itemData.topic.get}" data-meta="${JSON.stringify(itemData)}">undef.</div> - `; + if (typeof itemData.topic === 'undefined') break; + + element = html`<span data-type="${itemData.type}" data-mqtt-topic="${itemData.topic.get}" data-transform="${ifDefined(transform)}">undef.</span>`; break; case 'switch': - element = html` - <div class="left"> - <div class="toggle"> - <input type="checkbox" id="${itemData.id}" data-mqtt-topic="${itemData.topic.get}" data-meta="${JSON.stringify(itemData)}"> - <label for="${itemData.id}"> </label> - </div> - </div> - `; - break; + const switchChangeHandler = (event) => { + if (!client.connected) return false; + if (itemData.topic.set === null) return false; - // untested - case 'button': + client.publish(itemData.topic.set, transformMessage(event.target.checked, itemData), itemData.mqtt); + return false; + } + element = html` - <div class="left"> - <div class="btn-group" role="group"> - ${map(itemData.buttons, (button) => html`<button id="${itemData.id}" type="button" class="btn btn-secondary" data-mqtt-value="${button.value}" data-mqtt-topic="${itemData.topic.get}" data-meta="${JSON.stringify(itemData)}">${button.label}</button>`)} - </div> + <div class="toggle"> + <input id="${itemData.id}" type="checkbox" @change="${switchChangeHandler}" data-type="${itemData.type}" data-mqtt-topic="${itemData.topic.get}" data-transform="${ifDefined(transform)}"> + <label for="${itemData.id}"> </label> </div> `; break; case 'slider': + const sliderChangeHandler = (event) => { + if (!client.connected) return false; + if (itemData.topic.set === null) return false; + + client.publish(itemData.topic.set, transformMessage(event.target.value, itemData), itemData.mqtt); + } + + const sliderInputHandler = (event) => { + if (!client.connected) return false; + if (itemData.topic.set === null) return false; + + const element = event.target; + const value = (element.dataset.lastMqttValue - element.value) / (element.max - element.min) * (element.getBoundingClientRect().width - 20); + + event.target.style.setProperty('--c', value + 'px'); + } + + itemData.sliderMinValue ??= 0; + itemData.sliderMaxValue ??= 1; + itemData.sliderStepValue ??= 'any'; + + element = html`<input class="slider" type="range" @change="${sliderChangeHandler}" @input="${sliderInputHandler}" min="${itemData.sliderMinValue}" max="${itemData.sliderMaxValue}" step="${itemData.sliderStepValue}" data-type="${itemData.type}" data-mqtt-topic="${itemData.topic.get}" data-transform="${ifDefined(transform)}">`; + break; + + case 'select': + const selectChangeHandler = (event) => { + if (!client.connected) return false; + if (itemData.topic.set === null) return false + const element = event.target; + + client.publish(itemData.topic.set, transformMessage(element.value, itemData), itemData.mqtt); + + // Reset to last known state + element.value = element.dataset.lastMqttValue + document.getElementById(element.id + '_loader').classList.add('loader'); + } + element = html` - <div class="left"> - <input id="${itemData.id}" type="range" class="slider" min="${itemData.sliderMinValue}" max="${itemData.sliderMaxValue}" step="${itemData.sliderStepValue}" data-mqtt-topic="${itemData.topic.get}" data-meta="${JSON.stringify(itemData)}"> - </div> + <div id="${itemData.id}_loader"></div> + <select id="${itemData.id}" @change="${selectChangeHandler}" data-type="${itemData.type}" data-mqtt-topic="${itemData.topic.get}" data-transform="${ifDefined(transform)}"> + ${map(itemData.selectOptions, (option) => html`<option value="${option.value}">${option.label}</option>`)} + </select> `; break; // untested - case 'number': - element = html` - <div id="${itemData.id}" class="left" data-mqtt-topic="${itemData.topic.get}" data-meta="${JSON.stringify(itemData)}"> - <div class="input-group"> - <div class="input-group-prepend"> - <button class="btn btn-secondary sh-number-left" type="button">-</button> - </div> - <input type="text" inputmode="decimal" class="sh-input-number form-control"> - <div class="input-group-append"> - <button class="btn btn-secondary sh-number-right" type="button">+</button> - </div> - </div> - </div> - `; + case 'button': + const buttonClickHandler = (event) => { + if (!client.connected) return false; + if (itemData.topic.set === null) return false; + + client.publish(itemData.topic.set, transformMessage(event.target.dataset.mqttValue, meta), itemData.mqtt); + return false; + } + + const buttonElement = (button) => html`<button class="button" type="button" @click="${buttonClickHandler}" data-type="${itemData.type}" data-mqtt-value="${button.value}" data-mqtt-topic="${itemData.topic.get}" data-transform="${ifDefined(transform)}">${button.label}</button>`; + + element = html`<div class="btn-group" role="group">${map(itemData.buttons, buttonElement)}</div>`; break; - case 'select': + // untested + case 'number': + + const numberHandlder = (event) => { + if (!client.connected) return false; + if (itemData.topic.set === null) return false; + + const input = event.target.parentNode.querySelector('input'); + let inputValue = Number(input.value.replace(',', '.')); + + if (element.target.textContent === '-') inputValue = inputValue - itemData.decrement ?? 1; + if (element.target.textContent === '+') inputValue = inputValue - itemData.increment ?? 1; + + const transformedValue = transformMessage(inputValue, meta); + + client.publish(topic, transformedValue, itemData.mqtt); + input.classList.add('working'); + } + element = html` - <div class="left"> - <div id="${itemData.id}_loader"></div> - <select id="${itemData.id}" class="" data-mqtt-topic="${itemData.topic.get}" data-meta="${JSON.stringify(itemData)}"> - ${map(itemData.selectOptions, (option) => html`<option value="${option.value}">${option.label}</option>`)} - </select> + <div class="number-group"> + <button type="button" click="${numberHandlder}">-</button> + <input type="text" inputmode="decimal" @change="${numberHandlder}" data-type="${itemData.type}" data-mqtt-topic="${itemData.topic.get}"> + <button type="button" @click="${numberHandlder}">+</button> </div> `; break; + } + const clickHandler = (itemData.link) ? (event) => { window.location = itemData.link; } : null; + const cursorStyle = (itemData.link) ? 'cursor: pointer;' : null; + return html` - <div class="list-group-item" data-page="${ifDefined(itemData.page)}" data-link="${ifDefined(itemData.link)}"> - <div><img src="${ifDefined(itemData.icon)}" class="icon"></div> + <div @click="${ifDefined(clickHandler)}" style="${ifDefined(cursorStyle)}"> + <img class="icon" src="${ifDefined(itemData.icon)}"> <div class="title">${itemData.title}</div> - ${element} + <div class="left">${element}</div> </div> `; } else { - return html`<div id="${itemData.id}" data-mqtt-topic="${itemData.topic.get}" data-meta="${JSON.stringify(itemData)}">${itemData.html}</div>`; + return html`<div data-mqtt-topic="${itemData.topic.get}" data-transform="${ifDefined(transform)}">${itemData.html}</div>`; } } -const renderSection = (sectionData) => { - const title = (sectionData.title) ? html`<div class="list-group-item seperator">${sectionData.title}</div>` : html`` +const renderSection = (sectionConfig) => { + const title = (sectionConfig.title) ? html`<div>${sectionConfig.title}</div>` : html`` return html` - <div class="row list-group"> + <section> ${title} - ${map(sectionData.items, (itemData) => renderItem(itemData))} - </div> + ${map(sectionConfig.items, (itemData) => renderItem(itemData))} + </section> `; } -const renderPage = (pageData) => { - const reloadHandler = () => window.location.reload(true); - const backButton = - (pageData.id === 'mainpage') ? - html `<span class="icon"></span>` : - html `<button class="icon" onClick="window.history.back();"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-left"><line x1="19" y1="12" x2="5" y2="12"></line><polyline points="12 19 5 12 12 5"></polyline></svg></button>`; - +const renderPage = (pageConfig) => { + const backButton = (pageConfig.id !== 'mainpage') ? html `<button class="icon" onClick="window.history.back();">${unsafeSVG(backButtonSvg)}</button>` : html `<span class="icon"></span>`; return html` - <div id="${pageData.id}" class="page"> - <nav class="row"> + <div id="${pageConfig.id}" class="page"> + <nav> ${backButton} - <a href="#"><img src="${ifDefined(pageData.icon)}" width="30" height="30" class="d-inline-block align-top" alt=""> ${pageData.title}</a> - <button class="icon connectionStatus" @click="${reloadHandler}"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-wifi-off"><line x1="1" y1="1" x2="23" y2="23"></line><path d="M16.72 11.06A10.94 10.94 0 0 1 19 12.55"></path><path d="M5 12.55a10.94 10.94 0 0 1 5.17-2.39"></path><path d="M10.71 5.05A16 16 0 0 1 22.58 9"></path><path d="M1.42 9a15.91 15.91 0 0 1 4.7-2.88"></path><path d="M8.53 16.11a6 6 0 0 1 6.95 0"></path><line x1="12" y1="20" x2="12.01" y2="20"></line></svg></button> + <a href="#"><img src="${ifDefined(pageConfig.icon)}" width="30" height="30"> ${pageConfig.title}</a> + <button class="icon connectionStatus" @click="${() => window.location.reload(true)}">${unsafeSVG(disconnectedSvg)}</button> </nav> - ${map(pageData.sections, (sectionData) => renderSection(sectionData))} + ${map(pageConfig.sections, (sectionConfig) => renderSection(sectionConfig))} </div> `; }; -const renderPages = (config) => html`${map(config.pages, (pageData) => renderPage(pageData))}`; - - -const sw = navigator.serviceWorker; -export let registration; -if (sw) { - sw.register('sw.js', { - scope: './' - }).then(function(reg) { - console.log('serviceWorker registration succeeded.'); - registration = reg; - }); -} - +const renderPages = (config) => html`${map(config.pages, (pageConfig) => renderPage(pageConfig))}`; window.addEventListener('hashchange', goToPage); window.addEventListener('DOMContentLoaded', async (event) => { - const instanceId = localStorage.getItem('instanceId') || shortid.generate(); - localStorage.setItem('instanceId', instanceId); + const instanceId = localStorage.getItem('instanceId') ?? Math.random().toString(36).slice(2); + const username = localStorage.getItem('username') ?? prompt("userame:"); + const password = localStorage.getItem('password') ?? prompt("password:"); + const config = await getConfig(); + const topics = new Set(); - // Get config - const data = await getConfig(); - const topics = new Set(); - - // Get all topics - for (const [i, page] of Object.entries(data.pages)) { - for (const [j, section] of Object.entries(page.sections)) { - for (const [k, item] of Object.entries(section.items)) { - if (item.topic !== undefined) { - if (typeof item.topic === 'string') topics.add(item.topic); - if (item.topic.get !== undefined) topics.add(item.topic.get); - } - } - } - } - - // create UI - render(renderPages(data), document.body); - - document.querySelectorAll('[data-link]').forEach((element) => { - element.style.cursor = 'pointer'; - element.addEventListener('click', () => { - window.location = element.dataset.link; - }); - }); - - document.querySelectorAll('[data-page]').forEach((element) => { - element.style.cursor = 'pointer'; - element.addEventListener('click', () => { - window.location.hash = '#'+element.dataset.page; + localStorage.setItem('instanceId', instanceId); + localStorage.setItem('username', username); + localStorage.setItem('password', password); + + if (navigator.serviceWorker) { + navigator.serviceWorker.register('sw.js', { + scope: './' + }).then(function(reg) { + console.log('serviceWorker registration succeeded.'); }); - }); + } - goToPage(window.location.hash || '#mainpage'); + config.pages.forEach((page) => { + page.sections.forEach((section) => { + section.items.forEach((item) => { + if (typeof item.topic === 'undefined') return; + if (typeof item.topic === 'string') topics.add(item.topic); + if (typeof item.topic.get === 'string') topics.add(item.topic.get); + }) + }) + }) + // create UI + render(renderPages(config), document.body); + goToPage(); // MQTT - if (!localStorage.getItem('username')) localStorage.setItem('username', prompt("userame:")) - if (!localStorage.getItem('password')) localStorage.setItem('password', prompt("password:")) - const mqttUrl = 'ws' + (location.protocol === 'https:' ? 's' : '') + '://' + location.hostname + ((location.port === '') ? '' : ':' + location.port) + '/mqtt'; - console.log('MQTT conencting to', mqttUrl); - const client = connect(mqttUrl, { + console.log('MQTT conencting to', mqttUrl); + client = connect(mqttUrl, { clientId: 'webui_' + instanceId, - logger: console, - username: localStorage.getItem('username'), - password: localStorage.getItem('password') + username: username, + password: password }); client.on('connect', () => { - console.log("Connected to server!") - - // Handle online/offline Button - document.querySelectorAll('.connectionStatus').forEach((element) => { - element.classList.remove('btn-outline-secondary'); - element.classList.add('btn-outline-success'); - element.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-wifi"><path d="M5 12.55a11 11 0 0 1 14.08 0"></path><path d="M1.42 9a16 16 0 0 1 21.16 0"></path><path d="M8.53 16.11a6 6 0 0 1 6.95 0"></path><line x1="12" y1="20" x2="12.01" y2="20"></line></svg>'; - }); + console.log('Connected to mqtt-server!') + document.querySelectorAll('.connectionStatus').forEach((element) => element.innerHTML = connectedSvg); }); - client.on('offline', () => { - console.log("Disconnected to server!") + client.on('reconnect', () => console.log('Trying reconnect to mqtt-server!')); - // Handle online/offline Button - document.querySelectorAll('.connectionStatus').forEach((element) => { - element.classList.remove('btn-outline-success'); - element.classList.add('btn-outline-secondary'); - element.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-wifi-off"><line x1="1" y1="1" x2="23" y2="23"></line><path d="M16.72 11.06A10.94 10.94 0 0 1 19 12.55"></path><path d="M5 12.55a10.94 10.94 0 0 1 5.17-2.39"></path><path d="M10.71 5.05A16 16 0 0 1 22.58 9"></path><path d="M1.42 9a15.91 15.91 0 0 1 4.7-2.88"></path><path d="M8.53 16.11a6 6 0 0 1 6.95 0"></path><line x1="12" y1="20" x2="12.01" y2="20"></line></svg>'; - }); - }); - - client.on('error', () => { - localStorage.setItem('username', prompt("username:")) - localStorage.setItem('password', prompt("password:")) - location.reload() - }); - - client.subscribe([...topics]) - client.on('message', (topic, message) => { - message = JSON.parse(message.toString()); - - const value = (typeof message === 'object' && message !== null && 'val' in message) ? message.val : message; - - document.querySelectorAll('[data-mqtt-topic="' + topic + '"]').forEach((element) => { - const meta = JSON.parse(element.dataset.meta); - - let valueTransformed; - if ('transform' in meta) { - if (typeof meta.transform === 'string') { - try { - valueTransformed = new Function('topic', 'message', 'value', meta.transform)(topic, message, value); - } catch { - return; - } - } - - if (typeof meta.transform === 'object') { - if ('get' in meta.transform) { - try { - valueTransformed = new Function('topic', 'message', 'value', meta.transform.get)(topic, message, value); - } catch { - return; - } - } - } - } - - if (valueTransformed === null) { - return; - } - - const usedValue = (valueTransformed === undefined) ? value : valueTransformed; - - switch (meta.type) { - case 'text': - if (!( - typeof usedValue !== 'object' || - usedValue instanceof String || - usedValue instanceof Number || - usedValue instanceof Boolean) - ) { - return; - } - - element.textContent = usedValue; - break; - - case 'switch': - if (!(typeof usedValue === 'boolean' || usedValue instanceof Boolean)) { - return; - } - - document.getElementById(meta.id).checked = usedValue; - break; - - case 'button': - if (!( - typeof usedValue !== 'object' || - usedValue instanceof String || - usedValue instanceof Number || - usedValue instanceof Boolean) - ) { - return; - } - - if (element.dataset.mqttValue === usedValue) { - element.classList.add('active'); - } else { - element.classList.remove('active'); - } - - break; - - case 'slider': - if (Number.isNaN(usedValue)) { - return; - } - - document.getElementById(meta.id).value = usedValue; - document.getElementById(meta.id).dataset.lastMqttValue = usedValue; - document.getElementById(meta.id).style.setProperty('--c', 0); - break; - - case 'select': - if (!( - typeof usedValue !== 'object' || - usedValue instanceof String || - usedValue instanceof Number || - usedValue instanceof Boolean) - ) { - return; - } - - document.getElementById(meta.id).value = usedValue; - document.getElementById(meta.id).dataset.lastMqttValue = usedValue; - document.getElementById(meta.id + '_loader').classList.remove('loader'); - break; - - case 'number': - if (Number.isNaN(usedValue)) { - return; - } - - element.querySelector('input').value = usedValue; - element.querySelector('input').classList.remove('working'); - break; - - default: - // Do nothing - } - }); - }); - - // Assign user-action events - document.querySelectorAll('[id^=switch]').forEach((element) => { - element.addEventListener('click', () => { - const meta = JSON.parse(element.dataset.meta); - const topic = meta.topic.set; - - if (topic === null) return false; - - client.publish(topic, transformMessage(element.checked, meta), meta.options?.mqtt); - return false; - }); - }); - - document.querySelectorAll('[id^=button]').forEach((element) => { - element.addEventListener('click', () => { - const meta = JSON.parse(element.dataset.meta); - const topic = meta.topic.set; - - if (topic === null) return false; - - client.publish(topic, transformMessage(element.dataset.mqttValue, meta), meta.options?.mqtt); - }); - }); - - document.querySelectorAll('[id^=slider]').forEach((element) => { - const meta = JSON.parse(element.dataset.meta); - const topic = meta.topic.set; - - element.addEventListener('input', () => { - const value = (element.dataset.lastMqttValue - element.value) / (element.max - element.min) * (element.getBoundingClientRect().width - 20) - document.getElementById(meta.id).style.setProperty('--c', value + 'px'); - }); - - element.addEventListener('change', () => { - if (topic === null) return false; - - client.publish(topic, transformMessage(element.value, meta), meta.options?.mqtt); - }); - }); - - document.querySelectorAll('[id^=select]').forEach((element) => { - element.addEventListener('click', () => { - const meta = JSON.parse(element.dataset.meta); - const topic = meta.topic.set; - - if (topic === null) return false; - - client.publish(topic, transformMessage(element.value, meta), meta.options?.mqtt); - - element.value = element.dataset.lastMqttValue - document.getElementById(element.id + '_loader').classList.add('loader'); - }); + client.on('offline', () => { + console.log('Disconnected from mqtt-server!') + document.querySelectorAll('.connectionStatus').forEach((element) => element.innerHTML = disconnectedSvg); }); - document.querySelectorAll('[id^=number]').forEach((element) => { - const meta = JSON.parse(element.dataset.meta); - const topic = meta.topic.set; - - if (topic === null) return false; - - const input = element.querySelector('input'); - const btnLeft = element.querySelector('button.sh-number-left'); - const btnRight = element.querySelector('button.sh-number-right'); - - function getInputValue() { - return Number(input.value.replace(',', '.')); + client.on('error', (error) => { + if (error.message === 'Connection refused: Not authorized') { + alert('Authentication failed!'); + localStorage.setItem('username', prompt('username:') ?? localStorage.getItem('username')); + localStorage.setItem('password', prompt('password:') ?? localStorage.getItem('password')); } - element.addEventListener('change', () => { - const inputValue = getInputValue(); - const transformedValue = transformMessage(inputValue, meta); - - client.publish(topic, transformedValue, meta.options?.mqtt); - input.classList.add('working'); - }); - - btnLeft.addEventListener('click', () => { - const increment = meta.options?.['left-increment'] ?? -1; - const inputValue = getInputValue(); - const transformedValue = transformMessage(inputValue + increment, meta); + location.reload() + }); - client.publish(topic, transformedValue, meta.options?.mqtt); - input.classList.add('working'); - }); + client.on('message', onMessage); - btnRight.addEventListener('click', () => { - const increment = meta.options?.['right-increment'] ?? 1; - const inputValue = getInputValue(); - const transformedValue = transformMessage(inputValue + increment, meta); + client.subscribe([...topics]) - client.publish(topic, transformedValue, meta.options?.mqtt); - input.classList.add('working'); - }); - }); });
diff --git a/www/index.html b/www/index.html @@ -2,7 +2,7 @@ <html lang="en"> <head> <meta charset="utf-8"> - <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> + <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0"> <meta name="apple-mobile-web-app-capable" content="yes"> <meta name="theme-color" content="#f8f9fa"> <link rel="apple-touch-icon" href="./favicon-512x512.png">
diff --git a/www/sw.js b/www/sw.js @@ -50,7 +50,7 @@ let preCache = [ './icons/weather.png' ]; -const CACHE = 'cache-v1'; +const CACHE = 'cache-v2'; self.addEventListener('install', function (evt) { self.skipWaiting();
diff --git a/yarn.lock b/yarn.lock @@ -127,9 +127,9 @@ integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== "@types/node@*": - version "18.11.11" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.11.tgz#1d455ac0211549a8409d3cdb371cd55cc971e8dc" - integrity sha512-KJ021B1nlQUBLopzZmPBVuGU9un7WJd/W4ya7Ih02B4Uwky5Nja0yGYav2EfYIk0RR2Q9oVhf60S2XR1BCWJ2g== + version "18.11.12" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.12.tgz#89e7f8aa8c88abf432f9bd594888144d7dba10aa" + integrity sha512-FgD3NtTAKvyMmD44T07zz2fEf+OKwutgBCEVM8GcvMGVGaDktiLNTDvPwC/LUe3PinMW+X6CuLOF2Ui1mAlSXg== "@types/trusted-types@^2.0.2": version "2.0.2" @@ -465,9 +465,9 @@ caniuse-api@^3.0.0: lodash.uniq "^4.5.0" caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001400: - version "1.0.30001436" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001436.tgz#22d7cbdbbbb60cdc4ca1030ccd6dea9f5de4848b" - integrity sha512-ZmWkKsnC2ifEPoWUvSAIGyOYwT+keAaaWPHiQ9DfMqS1t6tfuyFYoWR78TeZtznkEQ64+vGXH9cZrElwR2Mrxg== + version "1.0.30001439" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001439.tgz#ab7371faeb4adff4b74dad1718a6fd122e45d9cb" + integrity sha512-1MgUzEkoMO6gKfXflStpYgZDlFM7M/ck/bgfVCACO5vnAf0fXoNVHdWtqGU+MYca+4bL9Z5bpOVmR33cWW9G2A== chalk@^4.0.0: version "4.1.2" @@ -1066,9 +1066,9 @@ lilconfig@^2.0.3: integrity sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg== lit-html@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-2.4.0.tgz#b510430f39a56ec959167ed1187241a4e3ab1574" - integrity sha512-G6qXu4JNUpY6aaF2VMfaszhO9hlWw0hOTRFDmuMheg/nDYGB+2RztUSOyrzALAbr8Nh0Y7qjhYkReh3rPnplVg== + version "2.5.0" + resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-2.5.0.tgz#9d4c0bb3652a6b10bc4ccdb627dfa9eff1215474" + integrity sha512-bLHosg1XL3JRUcKdSVI0sLCs0y1wWrj2sqqAN3cZ7bDDPNgmDHH29RV48x6Wz3ZmkxIupaE+z7uXSZ/pXWAO1g== dependencies: "@types/trusted-types" "^2.0.2" @@ -1186,11 +1186,6 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -nanoid@^2.1.0: - version "2.1.11" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-2.1.11.tgz#ec24b8a758d591561531b4176a01e3ab4f0f0280" - integrity sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA== - nanoid@^3.3.4: version "3.3.4" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" @@ -1675,9 +1670,9 @@ sass-loader@^13.2.0: neo-async "^2.6.2" sass@^1.56.1: - version "1.56.1" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.56.1.tgz#94d3910cd468fd075fa87f5bb17437a0b617d8a7" - integrity sha512-VpEyKpyBPCxE7qGDtOcdJ6fFbcpOM+Emu7uZLxVrkX8KVU/Dp5UF7WLvzqRuUhB6mqqQt1xffLoG+AndxTZrCQ== + version "1.56.2" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.56.2.tgz#9433b345ab3872996c82a53a58c014fd244fd095" + integrity sha512-ciEJhnyCRwzlBCB+h5cCPM6ie/6f8HrhZMQOf5vlU60Y1bI1rx5Zb0vlDZvaycHsg/MqFfF1Eq2eokAa32iw8w== dependencies: chokidar ">=3.0.0 <4.0.0" immutable "^4.0.0" @@ -1735,13 +1730,6 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -shortid@^2.2.16: - version "2.2.16" - resolved "https://registry.yarnpkg.com/shortid/-/shortid-2.2.16.tgz#b742b8f0cb96406fd391c76bfc18a67a57fe5608" - integrity sha512-Ugt+GIZqvGXCIItnsL+lvFJOiN7RYqlGy7QE41O3YC1xbNSeDGIRO7xg2JJXIAj1cAGnOeC1r7/T9pgrtQbv4g== - dependencies: - nanoid "^2.1.0" - source-list-map@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34"