diff --git a/images/checkmark.svg b/images/checkmark.svg new file mode 100644 index 0000000..c560639 --- /dev/null +++ b/images/checkmark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/index.html b/index.html index 84d2111..4f10992 100644 --- a/index.html +++ b/index.html @@ -97,8 +97,21 @@
+
+
@@ -199,7 +212,9 @@

Web Bluetooth not available!

flag. However be careful as it would be risky to browse the web with this flag turned on as it enables many other experimental web platform features. Starting with Chromium version 100, enable the about://flags/#enable-web-bluetooth - safer flag instead.

+ safer flag instead. You can also enable Web Bluetooth Binding by enabling the + about://flags/#enable-web-bluetooth-new-permissions-backend + flag instead of the experimental features if it is available.

@@ -325,6 +340,22 @@

Select USB Host Folder

IP Address: + + Build Date: + + + + MCU Name: + + + + Board ID: + + + + UID: + +

More network devices

@@ -333,6 +364,45 @@

More network devicesClose + diff --git a/js/common/ble-file-transfer.js b/js/common/ble-file-transfer.js new file mode 100644 index 0000000..fd7abf9 --- /dev/null +++ b/js/common/ble-file-transfer.js @@ -0,0 +1,43 @@ +import {FileTransferClient as BLEFileTransferClient} from '@adafruit/ble-file-transfer-js'; + +// Wrapper for BLEFileTransferClient to add additional functionality +class FileTransferClient extends BLEFileTransferClient { + constructor(bleDevice, bufferSize) { + super(bleDevice, bufferSize); + } + + async versionInfo() { + // Possibly open /boot_out.txt and read the version info + let versionInfo = {}; + console.log("Reading version info"); + let bootout = await this.readFile('/boot_out.txt', false); + console.log(bootout); + if (!bootout) { + console.error("Unable to read boot_out.txt"); + return null; + } + bootout += "\n"; + + // Add these items as they are found + const searchItems = { + version: /Adafruit CircuitPython (.*?) on/, + build_date: /on ([0-9]{4}-[0-9]{2}-[0-9]{2});/, + board_name: /; (.*?) with/, + mcu_name: /with (.*?)\r?\n/, + board_id: /Board ID:(.*?)\r?\n/, + uid: /UID:([0-9A-F]{12,16})\r?\n/, + } + + for (const [key, regex] of Object.entries(searchItems)) { + const match = bootout.match(regex); + + if (match) { + versionInfo[key] = match[1]; + } + } + + return versionInfo; + } +} + +export {FileTransferClient}; \ No newline at end of file diff --git a/js/common/dialogs.js b/js/common/dialogs.js index 74d110f..73b87e1 100644 --- a/js/common/dialogs.js +++ b/js/common/dialogs.js @@ -329,6 +329,10 @@ class DiscoveryModal extends GenericModal { let ip = this._currentModal.querySelector("#ip"); ip.href = `http://${deviceInfo.ip + port}/code/`; ip.textContent = deviceInfo.ip; + this._currentModal.querySelector("#builddate").textContent = deviceInfo.build_date; + this._currentModal.querySelector("#mcuname").textContent = deviceInfo.mcu_name; + this._currentModal.querySelector("#boardid").textContent = deviceInfo.board_id; + this._currentModal.querySelector("#uid").textContent = deviceInfo.uid; } async _refreshDevices() { @@ -378,11 +382,43 @@ class DiscoveryModal extends GenericModal { } } +class DeviceInfoModal extends GenericModal { + async _getDeviceInfo() { + const deviceInfo = await this._showBusy(this._fileHelper.versionInfo()); + this._currentModal.querySelector("#version").textContent = deviceInfo.version; + const boardLink = this._currentModal.querySelector("#board"); + boardLink.href = `https://circuitpython.org/board/${deviceInfo.board_id}/`; + boardLink.textContent = deviceInfo.board_name; + this._currentModal.querySelector("#builddate").textContent = deviceInfo.build_date; + this._currentModal.querySelector("#mcuname").textContent = deviceInfo.mcu_name; + this._currentModal.querySelector("#boardid").textContent = deviceInfo.board_id; + this._currentModal.querySelector("#uid").textContent = deviceInfo.uid; + } + + async open(workflow, documentState) { + this._workflow = workflow; + this._fileHelper = workflow.fileHelper; + this._showBusy = workflow.showBusy.bind(workflow); + this._docState = documentState; + + let p = super.open(); + const okButton = this._currentModal.querySelector("button.ok-button"); + this._addDialogElement('okButton', okButton, 'click', this._closeModal); + + const refreshIcon = this._currentModal.querySelector("i.refresh"); + this._addDialogElement('refreshIcon', refreshIcon, 'click', this._refreshDevices); + + await this._getDeviceInfo(); + return p; + } +} + export { GenericModal, MessageModal, ButtonValueDialog, UnsavedDialog, DiscoveryModal, - ProgressDialog + ProgressDialog, + DeviceInfoModal }; \ No newline at end of file diff --git a/js/common/file_dialog.js b/js/common/file_dialog.js index 1bc3ec4..3875482 100644 --- a/js/common/file_dialog.js +++ b/js/common/file_dialog.js @@ -13,6 +13,9 @@ const FA_STYLE_REGULAR = "fa-regular"; const FA_STYLE_SOLID = "fa-solid"; const FA_STYLE_BRANDS = "fa-brands"; +const MODIFIER_SHIFT = "shift"; +const MODIFIER_CTRL = "ctrl"; + // Hide any file or folder matching these exact names const HIDDEN_FILES = [".Trashes", ".metadata_never_index", ".fseventsd"]; @@ -27,16 +30,19 @@ const extensionMap = { "gif": {style: FA_STYLE_REGULAR, icon: "file-image", type: "bin"}, "htm": {style: FA_STYLE_REGULAR, icon: "file-code", type: "text"}, "html": {style: FA_STYLE_REGULAR, icon: "file-code", type: "text"}, + "ini": {style: FA_STYLE_REGULAR, icon: "file-code", type: "text"}, + "inf": {style: FA_STYLE_REGULAR, icon: "file-code", type: "text"}, "jpeg": {style: FA_STYLE_REGULAR, icon: "file-image", type: "bin"}, "jpg": {style: FA_STYLE_REGULAR, icon: "file-image", type: "bin"}, "js": {style: FA_STYLE_REGULAR, icon: "file-code", type: "text"}, "json": {style: FA_STYLE_REGULAR, icon: "file-code", type: "text"}, + "md": {style: FA_STYLE_REGULAR, icon: "file-lines", type: "text"}, "mov": {style: FA_STYLE_REGULAR, icon: "file-video", type: "bin"}, "mp3": {style: FA_STYLE_REGULAR, icon: "file-audio", type: "bin"}, "mp4": {style: FA_STYLE_REGULAR, icon: "file-video", type: "bin"}, "mpy": {style: FA_STYLE_REGULAR, icon: "file", type: "bin"}, "pdf": {style: FA_STYLE_REGULAR, icon: "file-pdf", type: "bin"}, - "py": {style: FA_STYLE_REGULAR, icon: "file-lines", type: "text"}, + "py": {style: FA_STYLE_REGULAR, icon: "file-code", type: "text"}, "toml": {style: FA_STYLE_REGULAR, icon: "file-lines", type: "text"}, "txt": {style: FA_STYLE_REGULAR, icon: "file-lines", type: "text"}, "wav": {style: FA_STYLE_REGULAR, icon: "file-audio", type: "bin"}, @@ -47,10 +53,13 @@ const extensionMap = { const FOLDER_ICON = [FA_STYLE_REGULAR, "fa-folder"]; const DEFAULT_FILE_ICON = [FA_STYLE_REGULAR, "fa-file"]; -const FILESIZE_UNITS = ["bytes", "KB", "MB", "GB"]; -const COMPACT_UNITS = ["", "K", "M", "G"]; +const FILESIZE_UNITS = ["bytes", "KB", "MB", "GB", "TB"]; +const COMPACT_UNITS = ["", "K", "M", "G", "T"]; function getFileExtension(filename) { + if (filename === null) { + return null; + } let extension = filename.split('.').pop(); if (extension !== null) { return String(extension).toLowerCase(); @@ -91,6 +100,7 @@ class FileDialog extends GenericModal { this._fileHelper = null; this._readOnlyMode = false; this._progressDialog = null; + this._lastSelectedNode = null; } _removeAllChildNodes(parent) { @@ -110,13 +120,14 @@ class FileDialog extends GenericModal { return "bin"; } - async open(fileHelper, type, hidePaths = null) { + async open(fileHelper, type, hidePaths = null, allowMultiple = true) { if (![FILE_DIALOG_OPEN, FILE_DIALOG_SAVE, FILE_DIALOG_MOVE, FILE_DIALOG_COPY].includes(type)) { return; } this._fileHelper = fileHelper; this._readOnlyMode = await this._showBusy(this._fileHelper.readOnly()); this._hidePaths = hidePaths ? hidePaths : new Set(); + this._allowMultiple = allowMultiple; let p = super.open(); const cancelButton = this._currentModal.querySelector("button.cancel-button"); @@ -176,11 +187,12 @@ class FileDialog extends GenericModal { async _openFolder(path) { const fileList = this._getElement('fileList'); this._removeAllChildNodes(fileList); + this._lastSelectedNode = null; if (path !== undefined) { this._currentPath = path; } const currentPathLabel = this._getElement('currentPathLabel'); - currentPathLabel.innerHTML = this._currentPath; + currentPathLabel.innerHTML = ` ` + this._currentPath; if (this._currentPath != "/") { this._addFile({path: "..", isDir: true}, "fa-folder-open"); @@ -213,28 +225,104 @@ class FileDialog extends GenericModal { if (this._hidePaths.has(this._currentPath)) { return false; } + if (this._multipleItemsSelected()) { + return false; + } return true; } - _handleFileClick(clickedItem) { + _handleFileClick(clickedItem, event) { + // Get a list of nodes that have the data-selected attribute and store them in an array + let listItem; + let previouslySelectedNodes = []; for (let listItem of this._getElement('fileList').childNodes) { - listItem.setAttribute("data-selected", listItem.isEqualNode(clickedItem)); - if (listItem.isEqualNode(clickedItem)) { - listItem.classList.add("selected"); + if (this._isSelected(listItem)) { + previouslySelectedNodes.push(listItem); + } + } + + // Get a list of modifier keys that are currently pressed if event was passed in + let modifierKeys = []; + if (this._allowMultiple && event.shiftKey && this._lastSelectedNode !== null) { + modifierKeys.push(MODIFIER_SHIFT); + } + + // Command for macs, Control for Windows + if (this._allowMultiple && (event.metaKey || event.ctrlKey)) { + modifierKeys.push(MODIFIER_CTRL); + } + + // Go through and add which files should be selected. This will be the key for updating the UI for the + // files that should be selected + let selectedFiles = []; + + // If control is held down, we should start by populating the list with everything currently selected + if (modifierKeys.includes(MODIFIER_CTRL)) { + selectedFiles = previouslySelectedNodes; + } + + // If shift is held down, we should add all the files between the last selected file and the current file + if (modifierKeys.includes(MODIFIER_SHIFT)) { + let lastSelectedIndex = Array.from(this._getElement('fileList').childNodes).indexOf(this._lastSelectedNode); + let currentSelectedIndex = Array.from(this._getElement('fileList').childNodes).indexOf(clickedItem); + let startIndex = Math.min(lastSelectedIndex, currentSelectedIndex); + let endIndex = Math.max(lastSelectedIndex, currentSelectedIndex); + for (let i = startIndex; i <= endIndex; i++) { + selectedFiles.push(this._getElement('fileList').childNodes[i]); + } + } else if (modifierKeys.includes(MODIFIER_CTRL)) { + if (selectedFiles.includes(clickedItem)) { + selectedFiles.splice(selectedFiles.indexOf(clickedItem), 1); } else { - listItem.classList.remove("selected"); + selectedFiles.push(clickedItem); } + } else { + selectedFiles.push(clickedItem); } - if (clickedItem.getAttribute("data-type") != "folder") { + + // Go through and update the UI for all of the files that should be selected or delselected + for (listItem of this._getElement('fileList').childNodes) { + // If Control key is pressed, toggle selection + this._selectItem(listItem, selectedFiles.includes(listItem)); + } + + if (this._multipleItemsSelected()) { + this._getElement('fileNameField').value = ""; + } else if (clickedItem.getAttribute("data-type") != "folder") { this._getElement('fileNameField').value = clickedItem.querySelector("span").innerHTML; } - this._setElementEnabled('okButton', clickedItem.getAttribute("data-type") != "bin"); + + this._lastSelectedNode = clickedItem; + this._setElementEnabled('okButton', !this._multipleItemsSelected() && clickedItem.getAttribute("data-type") != "bin"); this._updateToolbar(); } + _selectItem(listItem, value) { + listItem.setAttribute("data-selected", value); + if (value) { + listItem.classList.add("selected"); + } else { + listItem.classList.remove("selected"); + } + } + + _isSelected(listItem) { + return (/true/i).test(listItem.getAttribute("data-selected")); + } + + _multipleItemsSelected() { + let selectedItems = 0; + for (let listItem of this._getElement('fileList').childNodes) { + if (this._isSelected(listItem)) { + selectedItems++; + } + } + return selectedItems > 1; + } + _updateToolbar() { this._setElementEnabled('delButton', this._canPerformWritableFileOperation()); - this._setElementEnabled('renameButton', this._canPerformWritableFileOperation()); + this._setElementEnabled('renameButton', !this._multipleItemsSelected() && this._canPerformWritableFileOperation()); this._setElementEnabled('moveButton', this._canPerformWritableFileOperation()); this._setElementEnabled('downloadButton', this._canDownload()); } @@ -299,28 +387,38 @@ class FileDialog extends GenericModal { if (this._readOnlyMode) { return false; } - let selectedItem = this._getSelectedFile(); - if (!selectedItem) { + + let selectedItems = this._getSelectedFilesInfo(); + + if (selectedItems.length < 1) { return false; } - let filename = selectedItem.querySelector("span").innerHTML; - if (!this._validName(filename)) { - return false; + + for (let item of selectedItems) { + if (!this._validName(item.filename)) { + return false; + } } - if (!includeFolder && selectedItem.getAttribute("data-type") == "folder") { - return false; + + if (!includeFolder) { + for (let item of selectedItems) { + if (item.filetype == "folder") { + return false; + } + } } + return true; } _canDownload() { - let selectedItem = this._getSelectedFile(); - if (!selectedItem) { - return true; - } - if (!this._validName(selectedItem.querySelector("span").innerHTML)) { - return false; + let selectedItems = this._getSelectedFilesInfo(); + for (let item of selectedItems) { + if (!this._validName(item.filename)) { + return false; + } } + return true; } @@ -331,15 +429,25 @@ class FileDialog extends GenericModal { async _handleDelButton() { if (!this._canPerformWritableFileOperation()) return; - let filename = this._getSelectedFilename(); - filename = this._currentPath + filename; + let filenames = this._getSelectedFilenames(); + let displayFilename = ''; + if (filenames.length == 0) return; + + if (filenames.length > 1) { + displayFilename = `${filenames.length} items`; + } else { + displayFilename = this._currentPath + filenames[0]; + } - if (!confirm(`Are you sure you want to delete ${filename}?`)) { + if (!confirm(`Are you sure you want to delete ${displayFilename}?`)) { return; // If cancelled, do nothing } - // Delete the item - await this._showBusy(this._fileHelper.delete(filename)); + for (let filename of filenames) { + // Delete the item + await this._showBusy(this._fileHelper.delete(filename)); + } + // Refresh the file list await this._openFolder(); }; @@ -440,67 +548,97 @@ class FileDialog extends GenericModal { input.click(); } - // Currently only files are downloadable, but it would be nice to eventually download zipped folders async _handleDownloadButton() { - await this._download(this._getSelectedFilename()); + await this._showBusy(this._download(this._getSelectedFilesInfo())); } - async _download(filename) { + async _download(files) { if (!this._canDownload()) return; - let type, folder, blob; + let blob, filename; + // Function to read the file contents as a blob let getBlob = async (path) => { return await this._fileHelper.readFile(path, true); }; - if (filename) { - type = this._getSelectedFileType(); - } - - if (type == "folder" || !filename) { - folder = this._currentPath; - if (filename) { - folder += filename + "/"; - filename = `${filename}.zip`; + let getParentFolderName = () => { + if (this._currentPath == "/") { + return "CIRCUITPY"; } else { - if (folder == "/") { - filename = "CIRCUITPY.zip"; - } else { - filename = folder.split("/").slice(-2).join("") + ".zip"; + return this._currentPath.split("/").slice(-2).join(""); + } + }; + + let addFileContentsToZip = async (zip, folder, location) => { + let contents = await getBlob(folder + location); + // Get the filename only from the path + zip.file(location, contents); + }; + + if (files.length == 1 && files[0].filetype != "folder") { + // Single File Selected + filename = files[0].filename; + blob = await getBlob(this._currentPath + filename); + } else { + // We either have more than 1 item selected or we have a folder selected or we have no file selected and want to download the current folder + // If we have nothing selected, we will download the current folder + filename = `${getParentFolderName()}.zip`; + if (files.length == 0) { + // No Files Selected, so get everything in current folder + const filesInFolder = await this._fileHelper.listDir(this._currentPath); + + // Add all files in current folder to files array + for (let fileObj of filesInFolder) { + if (this._hidePaths.has(this._currentPath + fileObj.path)) continue; + files.push({filename: fileObj.path, filetype: fileObj.isDir ? "folder" : "file", path: this._currentPath}); } + } else if (files.length == 1) { + // Single Folder Selected + filename = `${files[0].filename}.zip`; } - let files = await this._fileHelper.findContainedFiles(folder, true); let zip = new JSZip(); - for (let location of files) { - let contents = await this._showBusy(getBlob(folder + location)); - zip.file(location, contents); + for (let item of files) { + if (item.filetype == "folder") { + let containedFiles = await this._fileHelper.findContainedFiles(item.path + item.filename + "/", true); + for (let location of containedFiles) { + await addFileContentsToZip(zip, item.path, item.filename + "/" + location); + } + } else { + await addFileContentsToZip(zip, item.path, item.filename); + } } blob = await zip.generateAsync({type: "blob"}); - } else { - blob = await this._showBusy(getBlob(this._currentPath + filename)); } + saveAs(blob, filename); } async _handleMoveButton() { + // Get the new path const newFolderDialog = new FileDialog("folder-select", this._showBusy); let hidePaths = new Set(); hidePaths.add(this._getSelectedFilePath()); hidePaths.add(this._currentPath); - let newFolder = await newFolderDialog.open(this._fileHelper, FILE_DIALOG_MOVE, hidePaths); - + let newFolder = await newFolderDialog.open(this._fileHelper, FILE_DIALOG_MOVE, hidePaths, false); + let errors = false; if (newFolder) { - const filename = this._getSelectedFilename(); - const filetype = this._getSelectedFileType() == "folder" ? "folder" : "file"; - const oldPath = this._currentPath + filename; - const newPath = newFolder + filename; - if (await this._showBusy(this._fileHelper.fileExists(newPath))) { - this._showMessage(`Error moving ${oldPath}. Another ${filetype} with the same name already exists at ${newPath}.`); - } else if (!(await this._showBusy(this._fileHelper.move(oldPath, newPath)))) { - this._showMessage(`Error moving ${oldPath} to ${newPath}. Make sure the ${filetype} you are moving exists.`); - } else { + const files = this._getSelectedFilesInfo(); + for (let file of files) { + const filename = file.filename; + const filetype = file.filetype == "folder" ? "folder" : "file"; + const oldPath = this._currentPath + filename; + const newPath = newFolder + filename; + if (await this._showBusy(this._fileHelper.fileExists(newPath))) { + this._showMessage(`Error moving ${oldPath}. Another ${filetype} with the same name already exists at ${newPath}.`); + errors = true; + } else if (!(await this._showBusy(this._fileHelper.move(oldPath, newPath)))) { + this._showMessage(`Error moving ${oldPath} to ${newPath}. Make sure the file you are moving exists.`); + errors = true; + } + } + if (!errors) { // Go to the new location await this._openFolder(newFolder); } @@ -510,7 +648,11 @@ class FileDialog extends GenericModal { async _handleRenameButton() { if (!this._canPerformWritableFileOperation()) return; - let oldName = this._getSelectedFilename(); + let oldName = this._getSelectedFilenames(); + if (oldName.length != 1) { + return; + } + oldName = oldName[0]; let newName = prompt("Enter a new folder name", oldName); // If cancelled, do nothing if (!newName) { @@ -563,27 +705,46 @@ class FileDialog extends GenericModal { await this._openFolder(); }; - _getSelectedFile() { + _getSelectedFiles() { + let files = []; + // Loop through items and see if any have data-selected for (let listItem of this._getElement('fileList').childNodes) { if ((/true/i).test(listItem.getAttribute("data-selected"))) { - return listItem; + files.push(listItem); } } - return null; + return files; } - _getSelectedFilename() { - let file = this._getSelectedFile(); - if (file) { - return file.querySelector("span").innerHTML; + _getSelectedFilesInfo() { + let files = []; + let selectedFles = this._getSelectedFiles(); + for (let file of selectedFles) { + let info = { + filename: file.querySelector("span").innerHTML, + filetype: file.getAttribute("data-type"), + path: file.getAttribute("data-type") == "folder" ? this._currentPath : this._currentPath + file.querySelector("span").innerHTML, + }; + files.push(info); } - return null; + + return files; + } + + _getSelectedFilenames() { + let filenames = []; + let files = this._getSelectedFiles(); + for (let file of files) { + filenames.push(file.querySelector("span").innerHTML); + } + + return filenames; } _getSelectedFileType() { - let file = this._getSelectedFile(); + let file = this._getSelectedFiles(); if (file) { return file.getAttribute("data-type"); } @@ -591,20 +752,33 @@ class FileDialog extends GenericModal { } _getSelectedFilePath() { - let filename = this._getSelectedFilename(); - if (!filename) return null; - - if (this._getSelectedFileType() != "folder") { - return this._currentPath; + // Get the paths of all selected files. These will not be valid paths to move to. + let paths = []; + let files = this._getSelectedFilesInfo(); + if (files.length < 1) return []; + + for (let file of files) { + if (file.filetype != "folder") { + if (!paths.includes(this._currentPath)) { + paths.push(this._currentPath); + } + } else { + paths.push(this._currentPath + filename); + } } - return this._currentPath + filename; + return paths; } async _openItem(item, forceNavigate = false) { const fileNameField = this._getElement('fileNameField'); let filetype, filename; - let selectedItem = this._getSelectedFile(); + let selectedItem = this._getSelectedFiles(); + if (selectedItem.length > 1) { + // We don't currently support opening multiple items + return; + } + selectedItem = selectedItem.length == 1 ? selectedItem[0] : null; if (item !== undefined) { filetype = item.getAttribute("data-type"); @@ -686,7 +860,7 @@ class FileDialog extends GenericModal { if (clickedItem.tagName.toLowerCase() != "a") { clickedItem = clickedItem.parentNode; } - this._handleFileClick(clickedItem); + this._handleFileClick(clickedItem, event); }); fileItem.addEventListener("dblclick", async (event) => { let clickedItem = event.target; diff --git a/js/common/fsapi-file-transfer.js b/js/common/fsapi-file-transfer.js index 645fdd1..d8a0af5 100644 --- a/js/common/fsapi-file-transfer.js +++ b/js/common/fsapi-file-transfer.js @@ -325,8 +325,10 @@ class FileTransferClient { let bootout = await this.readFile('/boot_out.txt', false); console.log(bootout); if (!bootout) { + console.error("Unable to read boot_out.txt"); return null; } + bootout += "\n"; // Add these items as they are found const searchItems = { @@ -335,7 +337,7 @@ class FileTransferClient { board_name: /; (.*?) with/, mcu_name: /with (.*?)\r?\n/, board_id: /Board ID:(.*?)\r?\n/, - uid: /UID:([0-9A-F]{12})\r?\n/, + uid: /UID:([0-9A-F]{12,16})\r?\n/, } for (const [key, regex] of Object.entries(searchItems)) { diff --git a/js/common/plotter.js b/js/common/plotter.js new file mode 100644 index 0000000..c224b05 --- /dev/null +++ b/js/common/plotter.js @@ -0,0 +1,193 @@ +import Chart from "chart.js/auto"; + +let textLineBuffer = ""; +let textLine; + +let defaultColors = ['#8888ff', '#ff8888', '#88ff88']; + +/** + * @name LineBreakTransformer + * Helper to parse the incoming string messages into lines. + */ +class LineBreakTransformer { + constructor() { + // A container for holding stream data until a new line. + this.container = ''; + } + + transform(chunk, linesList) { + this.container += chunk; + const lines = this.container.split('\n'); + this.container = lines.pop(); + lines.forEach(line => linesList.push(line)); + } + +} + +let lineTransformer = new LineBreakTransformer() + +export function plotValues(chartObj, serialMessage, bufferSize) { + /* + Given a string serialMessage, parse it into the plottable value(s) that + it contains if any, and plot those values onto the given chartObj. If + the serialMessage doesn't represent a complete textLine it will be stored + into a buffer and combined with subsequent serialMessages until a full + textLine is formed. + */ + let currentLines = [] + lineTransformer.transform(serialMessage, currentLines) + + for (textLine of currentLines) { + + textLine = textLine.replace("\r", "").replace("\n", "") + if (textLine.length === 0) { + continue; + } + + let valuesToPlot; + + // handle possible tuple in textLine + if (textLine.startsWith("(") && textLine.endsWith(")")) { + textLine = "[" + textLine.substring(1, textLine.length - 1) + "]"; + console.log("after tuple conversion: " + textLine); + } + + // handle possible list in textLine + if (textLine.startsWith("[") && textLine.endsWith("]")) { + valuesToPlot = JSON.parse(textLine); + for (let i = 0; i < valuesToPlot.length; i++) { + valuesToPlot[i] = parseFloat(valuesToPlot[i]) + } + + } else { // handle possible CSV in textLine + valuesToPlot = textLine.split(",") + for (let i = 0; i < valuesToPlot.length; i++) { + valuesToPlot[i] = parseFloat(valuesToPlot[i]) + } + } + + if (valuesToPlot === undefined || valuesToPlot.length === 0) { + continue; + } + + try { + while (chartObj.data.labels.length > bufferSize) { + chartObj.data.labels.shift(); + for (let i = 0; i < chartObj.data.datasets.length; i++) { + while (chartObj.data.datasets[i].data.length > bufferSize) { + chartObj.data.datasets[i].data.shift(); + } + } + } + chartObj.data.labels.push(""); + + for (let i = 0; i < valuesToPlot.length; i++) { + if (isNaN(valuesToPlot[i])) { + continue; + } + if (i > chartObj.data.datasets.length - 1) { + let curColor = '#000000'; + if (i < defaultColors.length) { + curColor = defaultColors[i]; + } + chartObj.data.datasets.push({ + label: i.toString(), + data: [], + borderColor: curColor, + backgroundColor: curColor + }); + } + chartObj.data.datasets[i].data.push(valuesToPlot[i]); + } + + updatePlotterScales(chartObj); + chartObj.update(); + } catch (e) { + console.log("JSON parse error"); + // This line isn't a valid data value + } + } +} + +function updatePlotterScales(chartObj) { + /* + Update the scale of the plotter so that maximum and minimum values are sure + to be shown within the plotter instead of going outside the visible range. + */ + let allData = [] + for (let i = 0; i < chartObj.data.datasets.length; i++) { + allData = allData.concat(chartObj.data.datasets[i].data) + } + chartObj.options.scales.y.min = Math.min(...allData) - 10 + chartObj.options.scales.y.max = Math.max(...allData) + 10 +} + +export async function setupPlotterChart(workflow) { + /* + Initialize the plotter chart and configure it. + */ + let initialData = [] + Chart.defaults.backgroundColor = '#444444'; + Chart.defaults.borderColor = '#000000'; + Chart.defaults.color = '#000000'; + Chart.defaults.aspectRatio = 3/2; + workflow.plotterChart = new Chart( + document.getElementById('plotter-canvas'), + { + type: 'line', + options: { + animation: false, + scales: { + y: { + min: -1, + max: 1, + grid:{ + color: "#666" + }, + border: { + color: "#444" + } + }, + x:{ + grid: { + display: true, + color: "#666" + }, + border: { + color: "#444" + } + } + } + }, + data: { + labels: initialData.map(row => row.timestamp), + datasets: [ + { + label: '0', + data: initialData.map(row => row.value) + } + ] + } + } + ); + + // Set up a listener to respond to user changing the grid choice configuration + // dropdown + workflow.plotterGridLines.addEventListener('change', (event) => { + let gridChoice = event.target.value; + if (gridChoice === "x"){ + workflow.plotterChart.options.scales.x.grid.display = true; + workflow.plotterChart.options.scales.y.grid.display = false; + }else if (gridChoice === "y"){ + workflow.plotterChart.options.scales.y.grid.display = true; + workflow.plotterChart.options.scales.x.grid.display = false; + }else if (gridChoice === "both"){ + workflow.plotterChart.options.scales.y.grid.display = true; + workflow.plotterChart.options.scales.x.grid.display = true; + }else if (gridChoice === "none"){ + workflow.plotterChart.options.scales.y.grid.display = false; + workflow.plotterChart.options.scales.x.grid.display = false; + } + workflow.plotterChart.update(); + }); +} diff --git a/js/common/repl-file-transfer.js b/js/common/repl-file-transfer.js index c76f9f1..7231887 100644 --- a/js/common/repl-file-transfer.js +++ b/js/common/repl-file-transfer.js @@ -35,7 +35,10 @@ class FileTransferClient { if (contents === null) { return raw ? null : ""; } - return contents; + if (raw) { + return contents; + } + return contents.replaceAll("\r\n", "\n"); } async writeFile(path, offset, contents, modificationTime, raw = false) { @@ -91,8 +94,10 @@ class FileTransferClient { let bootout = await this.readFile('/boot_out.txt', false); console.log(bootout); if (!bootout) { + console.error("Unable to read boot_out.txt"); return null; } + bootout += "\n"; // Add these items as they are found const searchItems = { @@ -101,7 +106,7 @@ class FileTransferClient { board_name: /; (.*?) with/, mcu_name: /with (.*?)\r?\n/, board_id: /Board ID:(.*?)\r?\n/, - uid: /UID:([0-9A-F]{12})\r?\n/, + uid: /UID:([0-9A-F]{12,16})\r?\n/, } for (const [key, regex] of Object.entries(searchItems)) { diff --git a/js/common/utilities.js b/js/common/utilities.js index 39cbc73..25105f7 100644 --- a/js/common/utilities.js +++ b/js/common/utilities.js @@ -122,6 +122,23 @@ function readUploadedFileAsArrayBuffer(inputFile) { }); }; +// Load a setting from local storage with a default value if it doesn't exist +function loadSetting(setting, defaultValue) { + let value = JSON.parse(window.localStorage.getItem(setting)); + console.log(`Loading setting ${setting} with value ${value}`); + if (value == null) { + return defaultValue; + } + + return value; +} + +// Save a setting to local storage +function saveSetting(setting, value) { + console.log(`Saving setting ${setting} with value ${value}`); + window.localStorage.setItem(setting, JSON.stringify(value)); +} + export { isTestHost, buildHash, @@ -135,5 +152,7 @@ export { sleep, switchUrl, switchDevice, - readUploadedFileAsArrayBuffer + readUploadedFileAsArrayBuffer, + loadSetting, + saveSetting }; \ No newline at end of file diff --git a/js/layout.js b/js/layout.js index 4927480..216899a 100644 --- a/js/layout.js +++ b/js/layout.js @@ -1,4 +1,5 @@ import state from './state.js' +import { loadSetting, saveSetting } from './common/utilities.js'; const btnModeEditor = document.getElementById('btn-mode-editor'); const btnModeSerial = document.getElementById('btn-mode-serial'); @@ -8,28 +9,61 @@ const editorPage = document.getElementById('editor-page'); const serialPage = document.getElementById('serial-page'); const pageSeparator = document.getElementById('page-separator'); -btnModeEditor.addEventListener('click', async function (e) { - if (btnModeEditor.classList.contains('active') && !btnModeSerial.classList.contains('active')) { - // this would cause both editor & serial pages to disappear - return; +const SETTING_EDITOR_VISIBLE = "editor-visible"; +const SETTING_TERMINAL_VISIBLE = "terminal-visible"; + +const UPDATE_TYPE_EDITOR = 1; +const UPDATE_TYPE_SERIAL = 2; + +const MINIMUM_COLS = 2; +const MINIMUM_ROWS = 1; + +function isEditorVisible() { + return editorPage.classList.contains('active'); +} + +function isSerialVisible() { + return serialPage.classList.contains('active'); +} + +async function toggleEditor() { + if (isSerialVisible()) { + editorPage.classList.toggle('active'); + saveSetting(SETTING_EDITOR_VISIBLE, isEditorVisible()); + updatePageLayout(UPDATE_TYPE_EDITOR); } - btnModeEditor.classList.toggle('active'); - editorPage.classList.toggle('active') - updatePageLayout(true, false); -}); +} -btnModeSerial.addEventListener('click', async function (e) { - if (btnModeSerial.classList.contains('active') && !btnModeEditor.classList.contains('active')) { - // this would cause both editor & serial pages to disappear - return; +async function toggleSerial() { + if (isEditorVisible()) { + serialPage.classList.toggle('active'); + saveSetting(SETTING_TERMINAL_VISIBLE, isSerialVisible()); + updatePageLayout(UPDATE_TYPE_SERIAL); } - btnModeSerial.classList.toggle('active'); - serialPage.classList.toggle('active') - updatePageLayout(false, true); -}); +} + +btnModeEditor.removeEventListener('click', toggleEditor); +btnModeEditor.addEventListener('click', toggleEditor); + +btnModeSerial.removeEventListener('click', toggleSerial); +btnModeSerial.addEventListener('click', toggleSerial); -function updatePageLayout(editor = false, serial = false) { - if (editorPage.classList.contains('active') && serialPage.classList.contains('active')) { +// Show the editor panel if hidden +export function showEditor() { + editorPage.classList.add('active'); + updatePageLayout(UPDATE_TYPE_EDITOR); +} + +// Show the serial panel if hidden +export function showSerial() { + serialPage.classList.add('active'); + updatePageLayout(UPDATE_TYPE_SERIAL); +} + +// update type is used to indicate which button was clicked +function updatePageLayout(updateType) { + // If both are visible, show the separator + if (isEditorVisible() && isSerialVisible()) { pageSeparator.classList.add('active'); } else { pageSeparator.classList.remove('active'); @@ -40,49 +74,79 @@ function updatePageLayout(editor = false, serial = false) { return; } + // Mobile layout, so only show one or the other if (mainContent.offsetWidth < 768) { - if (editor) { - btnModeSerial.classList.remove('active'); + // Prioritize based on the update type + if (updateType == UPDATE_TYPE_EDITOR && isEditorVisible()) { serialPage.classList.remove('active'); - } else if (serial) { - btnModeEditor.classList.remove('active'); + } else if (updateType == UPDATE_TYPE_SERIAL && isSerialVisible()) { editorPage.classList.remove('active'); } + + // Make sure the separator is hidden for mobile pageSeparator.classList.remove('active'); } else { let w = mainContent.offsetWidth; let s = pageSeparator.offsetWidth; editorPage.style.width = ((w - s) / 2) + 'px'; - editorPage.style.flex = '0 0 auto'; + editorPage.style.flex = 'none'; serialPage.style.width = ((w - s) / 2) + 'px'; - serialPage.style.flex = '0 0 auto'; + serialPage.style.flex = 'none'; } - if (serial) { - refitTerminal(); + // Match the button state to the panel state to avoid getting out of sync + if (isEditorVisible()) { + btnModeEditor.classList.add('active'); + } else { + btnModeEditor.classList.remove('active'); } -} -export function showEditor() { - btnModeEditor.classList.add('active'); - editorPage.classList.add('active'); - updatePageLayout(true, false); -} + if (isSerialVisible()) { + btnModeSerial.classList.add('active'); + } else { + btnModeSerial.classList.remove('active'); + } -export function showSerial() { - btnModeSerial.classList.add('active'); - serialPage.classList.add('active'); - updatePageLayout(false, true); + if (isSerialVisible()) { + refitTerminal(); + } } function refitTerminal() { + // Custom function to replace the terminal refit function as it was a bit buggy + // Re-fitting the terminal requires a full re-layout of the DOM which can be tricky to time right. // see https://www.macarthur.me/posts/when-dom-updates-appear-to-be-asynchronous window.requestAnimationFrame(() => { window.requestAnimationFrame(() => { window.requestAnimationFrame(() => { - if (state.fitter) { - state.fitter.fit(); + const TERMINAL_ROW_HEIGHT = state.terminal._core._renderService.dimensions.css.cell.height; + const TERMINAL_COL_WIDTH = state.terminal._core._renderService.dimensions.css.cell.width; + + // Get the height of the header, footer, and serial bar to determine the height of the terminal + let siteHeader = document.getElementById('site-header'); + let mobileHeader = document.getElementById('mobile-header'); + let headerHeight = siteHeader.offsetHeight; + if (siteHeader.style.display === 'none') { + headerHeight = mobileHeader.offsetHeight; + } + let footerBarHeight = document.getElementById('footer-bar').offsetHeight; + let serialBarHeight = document.getElementById('serial-bar').offsetHeight; + let viewportHeight = window.innerHeight; + let terminalHeight = viewportHeight - headerHeight - footerBarHeight - serialBarHeight; + let terminalWidth = document.getElementById('serial-page').offsetWidth; + let screen = document.querySelector('.xterm-screen'); + if (screen) { + let cols = Math.floor(terminalWidth / TERMINAL_COL_WIDTH); + let rows = Math.floor(terminalHeight / TERMINAL_ROW_HEIGHT); + if (cols < MINIMUM_COLS) { + cols = MINIMUM_COLS; + } + if (rows < MINIMUM_ROWS) { + rows = MINIMUM_ROWS; + } + screen.style.width = (cols * TERMINAL_COL_WIDTH) + 'px'; + screen.style.height = (rows * TERMINAL_ROW_HEIGHT) + 'px'; } }); }); @@ -94,12 +158,11 @@ function refitTerminal() { function fixViewportHeight(e) { let vh = window.innerHeight * 0.01; document.documentElement.style.setProperty('--vh', `${vh}px`); - updatePageLayout(); + updatePageLayout(UPDATE_TYPE_EDITOR); } -fixViewportHeight(); -window.addEventListener("resize", fixViewportHeight); -function resize(e) { +// Resize the panes when the separator is moved +function resizePanels(e) { const w = mainContent.offsetWidth; const gap = pageSeparator.offsetWidth; const ratio = e.clientX / w; @@ -124,12 +187,41 @@ function resize(e) { serialPage.style.width = (w - e.clientX - gap / 2) + 'px'; } +// For the moment, we're going to just use this to keep track of the shown and hidden states +// of the terminal and editor (possibly plotter) +function loadPanelSettings() { + // Load all saved settings or defaults + // Update the terminal first + if (loadSetting(SETTING_EDITOR_VISIBLE, true)) { + editorPage.classList.add('active'); + } else { + editorPage.classList.remove('active'); + } + + if (loadSetting(SETTING_TERMINAL_VISIBLE, false)) { + serialPage.classList.add('active'); + } else { + serialPage.classList.remove('active'); + } + + // Make sure at lest one is visible + if (!isEditorVisible() && !isSerialVisible()) { + editorPage.classList.add('active'); + } + + updatePageLayout(UPDATE_TYPE_SERIAL); +} + function stopResize(e) { - window.removeEventListener('mousemove', resize, false); + window.removeEventListener('mousemove', resizePanels, false); window.removeEventListener('mouseup', stopResize, false); } pageSeparator.addEventListener('mousedown', async function (e) { - window.addEventListener('mousemove', resize, false); + window.addEventListener('mousemove', resizePanels, false); window.addEventListener('mouseup', stopResize, false); }); + +fixViewportHeight(); +window.addEventListener("resize", fixViewportHeight); +loadPanelSettings(); \ No newline at end of file diff --git a/js/script.js b/js/script.js index d562a46..f18886d 100644 --- a/js/script.js +++ b/js/script.js @@ -5,9 +5,9 @@ import {indentWithTab} from "@codemirror/commands" import { python } from "@codemirror/lang-python"; import { syntaxHighlighting, indentUnit } from "@codemirror/language"; import { classHighlighter } from "@lezer/highlight"; +import { getFileIcon } from "./common/file_dialog.js"; import { Terminal } from '@xterm/xterm'; -import { FitAddon } from '@xterm/addon-fit'; import { WebLinksAddon } from '@xterm/addon-web-links'; import state from './state.js' @@ -19,6 +19,7 @@ import { ButtonValueDialog, MessageModal } from './common/dialogs.js'; import { isLocal, switchUrl, getUrlParam } from './common/utilities.js'; import { CONNTYPE } from './constants.js'; import './layout.js'; // load for side effects only +import {setupPlotterChart} from "./common/plotter.js"; import { mainContent, showSerial } from './layout.js'; // Instantiate workflows @@ -32,6 +33,7 @@ let unchanged = 0; let connectionPromise = null; const btnRestart = document.querySelector('.btn-restart'); +const btnPlotter = document.querySelector('.btn-plotter'); const btnClear = document.querySelector('.btn-clear'); const btnConnect = document.querySelectorAll('.btn-connect'); const btnNew = document.querySelectorAll('.btn-new'); @@ -41,6 +43,7 @@ const btnSaveAs = document.querySelectorAll('.btn-save-as'); const btnSaveRun = document.querySelectorAll('.btn-save-run'); const btnInfo = document.querySelector('.btn-info'); const terminalTitle = document.getElementById('terminal-title'); +const serialPlotter = document.getElementById('plotter'); const messageDialog = new MessageModal("message"); const connectionType = new ButtonValueDialog("connection-type"); @@ -129,9 +132,27 @@ btnRestart.addEventListener('click', async function(e) { // Clear Button btnClear.addEventListener('click', async function(e) { + if (workflow.plotterChart){ + workflow.plotterChart.data.datasets.forEach((dataSet, index) => { + workflow.plotterChart.data.datasets[index].data = []; + }); + workflow.plotterChart.data.labels = []; + workflow.plotterChart.options.scales.y.min = -1; + workflow.plotterChart.options.scales.y.max = 1; + workflow.plotterChart.update(); + } state.terminal.clear(); }); +// Plotter Button +btnPlotter.addEventListener('click', async function(e){ + serialPlotter.classList.toggle("hidden"); + if (workflow && !workflow.plotterEnabled){ + await setupPlotterChart(workflow); + workflow.plotterEnabled = true; + } +}); + btnInfo.addEventListener('click', async function(e) { if (await checkConnected()) { await workflow.showInfo(getDocState()); @@ -237,7 +258,13 @@ async function checkReadOnly() { /* Update the filename and update the UI */ function setFilename(path) { + // Use the extension_map to figure out the file icon let filename = path; + + // Prepend an icon to the path + const [style, icon] = getFileIcon(path); + filename = ` ` + filename; + if (path === null) { filename = "[New Document]"; btnSave.forEach((b) => b.style.display = 'none'); @@ -519,9 +546,6 @@ async function setupXterm() { } }); - state.fitter = new FitAddon(); - state.terminal.loadAddon(state.fitter); - state.terminal.loadAddon(new WebLinksAddon()); state.terminal.open(document.getElementById('terminal')); diff --git a/js/workflows/ble.js b/js/workflows/ble.js index 2d2e201..8ff6f0c 100644 --- a/js/workflows/ble.js +++ b/js/workflows/ble.js @@ -2,12 +2,11 @@ * This class will encapsulate all of the workflow functions specific to BLE */ -import {FileTransferClient} from '@adafruit/ble-file-transfer-js'; - -import {CONNTYPE, CONNSTATE} from '../constants.js'; +import {FileTransferClient} from '../common/ble-file-transfer.js'; +import {CONNTYPE} from '../constants.js'; import {Workflow} from './workflow.js'; -import {GenericModal} from '../common/dialogs.js'; -import {sleep, getUrlParam} from '../common/utilities.js'; +import {GenericModal, DeviceInfoModal} from '../common/dialogs.js'; +import {sleep} from '../common/utilities.js'; const bleNusServiceUUID = 'adaf0001-4369-7263-7569-74507974686e'; const bleNusCharRXUUID = 'adaf0002-4369-7263-7569-74507974686e'; @@ -27,8 +26,15 @@ class BLEWorkflow extends Workflow { this.bleDevice = null; this.decoder = new TextDecoder(); this.connectDialog = new GenericModal("ble-connect"); + this.infoDialog = new DeviceInfoModal("device-info"); this.partialWrites = true; this.type = CONNTYPE.Ble; + this.buttonStates = [ + {reconnect: false, request: false, bond: false}, + {reconnect: false, request: true, bond: false}, + {reconnect: true, request: true, bond: false}, + {reconnect: false, request: false, bond: true}, + ]; } // This is called when a user clicks the main disconnect button @@ -50,23 +56,30 @@ class BLEWorkflow extends Workflow { btnBond = modal.querySelector('#promptBond'); btnReconnect = modal.querySelector('#bleReconnect'); - btnRequestBluetoothDevice.addEventListener('click', async (event) => { - await this.onRequestBluetoothDeviceButtonClick(event); - }); - btnBond.addEventListener('click', async (event) => { - await this.onBond(event); - }); - btnReconnect.addEventListener('click', async (event) => { - await this.reconnectButtonHandler(event); - }); + // Map the button states to the buttons + this.connectButtons = { + reconnect: btnReconnect, + request: btnRequestBluetoothDevice, + bond: btnBond + }; + + btnRequestBluetoothDevice.addEventListener('click', this.onRequestBluetoothDeviceButtonClick.bind(this)); + btnBond.addEventListener('click', this.onBond.bind(this)); + btnReconnect.addEventListener('click', this.reconnectButtonHandler.bind(this)); + // Check if Web Bluetooth is available if (!(await this.available() instanceof Error)) { let stepOne; if (stepOne = modal.querySelector('.step:first-of-type')) { stepOne.classList.add("hidden"); } - const devices = await navigator.bluetooth.getDevices(); - this.connectionStep(devices.length > 0 ? 2 : 1); + try { + const devices = await navigator.bluetooth.getDevices(); + console.log(devices); + this.connectionStep(devices.length > 0 ? 2 : 1); + } catch (e) { + console.log("New Permissions backend for Web Bluetooth not enabled. Go to chrome://flags/#enable-web-bluetooth-new-permissions-backend to enable.", e); + } } else { modal.querySelectorAll('.step:not(:first-of-type)').forEach((stepItem) => { stepItem.classList.add("hidden"); @@ -79,7 +92,9 @@ class BLEWorkflow extends Workflow { async onSerialReceive(e) {; // TODO: Make use of super.onSerialReceive() so that title can be extracted - this.writeToTerminal(this.decoder.decode(e.target.value.buffer, {stream: true})); + let output = this.decoder.decode(e.target.value.buffer, {stream: true}); + console.log(output); + this.writeToTerminal(output); } async connectToSerial() { @@ -89,6 +104,8 @@ class BLEWorkflow extends Workflow { this.txCharacteristic = await this.serialService.getCharacteristic(bleNusCharTXUUID); this.rxCharacteristic = await this.serialService.getCharacteristic(bleNusCharRXUUID); + // Remove any existing event listeners to prevent multiple reads + this.txCharacteristic.removeEventListener('characteristicvaluechanged', this.onSerialReceive.bind(this)); this.txCharacteristic.addEventListener('characteristicvaluechanged', this.onSerialReceive.bind(this)); await this.txCharacteristic.startNotifications(); return true; @@ -105,7 +122,7 @@ class BLEWorkflow extends Workflow { console.log('Getting existing permitted Bluetooth devices...'); const devices = await navigator.bluetooth.getDevices(); - console.log('> Got ' + devices.length + ' Bluetooth devices.'); + console.log('> Found ' + devices.length + ' Bluetooth device(s).'); // These devices may not be powered on or in range, so scan for // advertisement packets from them before connecting. for (const device of devices) { @@ -113,34 +130,43 @@ class BLEWorkflow extends Workflow { } } catch (error) { - console.log('Argh! ' + error); + console.error(error); + await this._showMessage(error); } } } + // Bring up a dialog to request a device + async requestDevice() { + return navigator.bluetooth.requestDevice({ + filters: [{services: [0xfebb]},], // <- Prefer filters to save energy & show relevant devices. + optionalServices: [0xfebb, bleNusServiceUUID] + }); + } + async connectToBluetoothDevice(device) { const abortController = new AbortController(); - device.addEventListener('advertisementreceived', async (event) => { + async function onAdvertisementReceived(event) { console.log('> Received advertisement from "' + device.name + '"...'); // Stop watching advertisements to conserve battery life. abortController.abort(); console.log('Connecting to GATT Server from "' + device.name + '"...'); try { - await this.showBusy(device.gatt.connect()); + await device.gatt.connect(); + } catch (error) { + await this._showMessage("Failed to connect to device. Try forgetting device from OS bluetooth devices and try again."); + } + if (device.gatt.connected) { console.log('> Bluetooth device "' + device.name + ' connected.'); await this.switchToDevice(device); + } else { + console.log('Unable to connect to bluetooth device "' + device.name + '.'); } - catch (error) { - console.log('Argh! ' + error); - } - }, {once: true}); + } - //await this.showBusy(device.gatt.connect()); - await navigator.bluetooth.requestDevice({ - filters: [{services: [0xfebb]},], // <- Prefer filters to save energy & show relevant devices. - optionalServices: [0xfebb, bleNusServiceUUID] - }); + device.removeEventListener('advertisementreceived', onAdvertisementReceived.bind(this)); + device.addEventListener('advertisementreceived', onAdvertisementReceived.bind(this)); this.debugLog("connecting to " + device.name); try { @@ -148,49 +174,50 @@ class BLEWorkflow extends Workflow { await device.watchAdvertisements({signal: abortController.signal}); } catch (error) { - console.log('Argh! ' + error); + console.error(error); + await this._showMessage(error); } } // Request Bluetooth Device async onRequestBluetoothDeviceButtonClick(e) { - try { + //try { console.log('Requesting any Bluetooth device...'); this.debugLog("Requesting device. Cancel if empty and try existing"); - let device = await navigator.bluetooth.requestDevice({ - filters: [{services: [0xfebb]},], // <- Prefer filters to save energy & show relevant devices. - optionalServices: [0xfebb, bleNusServiceUUID] - }); + let device = await this.requestDevice(); - await this.showBusy(device.gatt.connect()); console.log('> Requested ' + device.name); + await device.gatt.connect(); await this.switchToDevice(device); - } + /*} catch (error) { - console.log('Argh: ' + error); + console.error(error); + await this._showMessage(error); this.debugLog('No device selected. Try to connect to existing.'); - } + }*/ } async switchToDevice(device) { console.log(device); this.bleDevice = device; + this.bleDevice.removeEventListener("gattserverdisconnected", this.onDisconnected.bind(this)); this.bleDevice.addEventListener("gattserverdisconnected", this.onDisconnected.bind(this)); this.bleServer = this.bleDevice.gatt; console.log("connected", this.bleServer); let services; - try { + console.log(device.gatt.connected); + //try { services = await this.bleServer.getPrimaryServices(); - } catch (e) { + /*} catch (e) { console.log(e, e.stack); - } + }*/ console.log(services); console.log('Initializing File Transfer Client...'); this.initFileClient(new FileTransferClient(this.bleDevice, 65536)); - this.debugLog("connected"); + await this.fileHelper.bond(); await this.connectToSerial(); // Enable/Disable UI buttons @@ -243,6 +270,7 @@ class BLEWorkflow extends Workflow { if (result = await super.connect() instanceof Error) { return result; } + // Is this a new connection? if (!this.bleDevice) { let devices = await navigator.bluetooth.getDevices(); for (const device of devices) { @@ -250,8 +278,9 @@ class BLEWorkflow extends Workflow { } } + // Do we have a connection now but still need to connect serial? if (this.bleDevice && !this.bleServer) { - await await this.showBusy(this.bleDevice.gatt.connect()); + await this.showBusy(this.bleDevice.gatt.connect()); this.switchToDevice(this.bleDevice); } } @@ -270,21 +299,8 @@ class BLEWorkflow extends Workflow { return true; } - // Handle the different button states for various connection steps - connectionStep(step) { - const buttonStates = [ - {reconnect: false, request: false, bond: false}, - {reconnect: false, request: true, bond: false}, - {reconnect: true, request: true, bond: false}, - {reconnect: false, request: false, bond: true}, - ]; - - if (step < 0) step = 0; - if (step > buttonStates.length - 1) step = buttonStates.length - 1; - - btnReconnect.disabled = !buttonStates[step].reconnect; - btnRequestBluetoothDevice.disabled = !buttonStates[step].request; - btnBond.disabled = !buttonStates[step].bond; + async showInfo(documentState) { + return await this.infoDialog.open(this, documentState); } } diff --git a/js/workflows/usb copy.js b/js/workflows/usb copy.js deleted file mode 100644 index f149137..0000000 --- a/js/workflows/usb copy.js +++ /dev/null @@ -1,328 +0,0 @@ -import {CONNTYPE, CONNSTATE} from '../constants.js'; -import {Workflow} from './workflow.js'; -import {GenericModal} from '../common/dialogs.js'; -import {FileTransferClient} from '../common/repl-file-transfer.js'; - -let btnRequestSerialDevice, btnSelectHostFolder, btnUseHostFolder, lblWorkingfolder; - -class USBWorkflow extends Workflow { - constructor() { - super(); - this._serialDevice = null; - this.titleMode = false; - this.reader = null; - this.writer = null; - this.connectDialog = new GenericModal("usb-connect"); - this._fileContents = null; - this.type = CONNTYPE.Usb; - this._partialToken = null; - this._uid = null; - this._readLoopPromise = null; - } - - async init(params) { - await super.init(params); - } - - // This is called when a user clicks the main disconnect button - async disconnectButtonHandler(e) { - await super.disconnectButtonHandler(e); - if (this.connectionStatus()) { - await this.onDisconnected(null, false); - } - } - - async onConnected(e) { - this.connectDialog.close(); - await this.loadEditor(); - super.onConnected(e); - } - - async onDisconnected(e, reconnect = true) { - if (this.reader) { - await this.reader.cancel(); - this.reader = null; - } - if (this.writer) { - await this.writer.releaseLock(); - this.writer = null; - } - - if (this._serialDevice) { - await this._serialDevice.close(); - this._serialDevice = null; - } - - super.onDisconnected(e, reconnect); - } - - async serialTransmit(msg) { - const encoder = new TextEncoder(); - if (this.writer) { - const encMessage = encoder.encode(msg); - await this.writer.ready.catch((err) => { - console.error(`Ready error: ${err}`); - }); - await this.writer.write(encMessage).catch((err) => { - console.error(`Chunk error: ${err}`); - }); - await this.writer.ready; - } - } - - async connect() { - let result; - if (result = await super.connect() instanceof Error) { - return result; - } - - return await this.connectToDevice(); - } - - async connectToDevice() { - return await this.connectToSerial(); - } - - async connectToSerial() { - // There's no way to reference a specific port, so we just hope the user - // only has a single device stored and connected. However, we can check that - // the device on the stored port is currently connected by checking if the - // readable and writable properties are null. - - let allDevices = await navigator.serial.getPorts(); - let connectedDevices = []; - for (let device of allDevices) { - let devInfo = await device.getInfo(); - if (devInfo.readable && devInfo.writable) { - connectedDevices.push(device); - } - } - let device = null; - - if (connectedDevices.length == 1) { - device = connectedDevices[0]; - console.log(await device.getInfo()); - try { - // Attempt to connect to the saved device. If it's not found, this will fail. - await this._switchToDevice(device); - } catch (e) { - // We should probably remove existing devices if it fails here - await device.forget(); - - console.log("Failed to automatically connect to saved device. Prompting user to select a device."); - device = await navigator.serial.requestPort(); - console.log(device); - } - - // TODO: Make it more obvious to user that something happened for smaller screens - // Perhaps providing checkmarks by adding a css class when a step is complete would be helpful - // This would help with other workflows as well - } else { - console.log('Requesting any serial device...'); - device = await navigator.serial.requestPort(); - } - - // If we didn't automatically use a saved device - if (!this._serialDevice) { - console.log('> Requested ', device); - await this._switchToDevice(device); - } - console.log(this._serialDevice); - if (this._serialDevice != null) { - this._connectionStep(2); - return true; - } - - return false; - } - - async showConnect(documentState) { - let p = this.connectDialog.open(); - let modal = this.connectDialog.getModal(); - - btnRequestSerialDevice = modal.querySelector('#requestSerialDevice'); - btnSelectHostFolder = modal.querySelector('#selectHostFolder'); - btnUseHostFolder = modal.querySelector('#useHostFolder'); - lblWorkingfolder = modal.querySelector('#workingFolder'); - - btnRequestSerialDevice.disabled = true; - btnSelectHostFolder.disabled = true; - - btnRequestSerialDevice.addEventListener('click', async (event) => { - try { - await this.connectToSerial(); - } catch (e) { - //console.log(e); - //alert(e.message); - //alert("Unable to connect to device. Make sure it is not already in use."); - // TODO: I think this also occurs if the user cancels the requestPort dialog - } - }); - - btnSelectHostFolder.addEventListener('click', async (event) => { - await this._selectHostFolder(); - }); - - btnUseHostFolder.addEventListener('click', async (event) => { - await this._useHostFolder(); - }); - - if (!(await this.available() instanceof Error)) { - let stepOne; - if (stepOne = modal.querySelector('.step:first-of-type')) { - stepOne.classList.add("hidden"); - } - this._connectionStep(1); - } else { - modal.querySelectorAll('.step:not(:first-of-type)').forEach((stepItem) => { - stepItem.classList.add("hidden"); - }); - this._connectionStep(0); - } - - // TODO: If this is closed before all steps are completed, we should close the serial connection - // probably by calling onDisconnect() - - return await p; - } - - async available() { - if (!('serial' in navigator)) { - return Error("Web Serial is not enabled in this browser"); - } - return true; - } - - // Workflow specific functions - async _selectHostFolder() { - console.log('Initializing File Transfer Client...'); - const fileClient = this.fileHelper.getFileClient(); - const changed = await fileClient.loadDirHandle(false); - if (changed) { - await this._hostFolderChanged(); - } - } - - async _useHostFolder() { - await this.fileHelper.listDir('/'); - this.onConnected(); - } - - async _hostFolderChanged() { - const fileClient = this.fileHelper.getFileClient(); - const folderName = fileClient.getWorkingDirectoryName(); - console.log("New folder name:", folderName); - if (folderName) { - // Set the working folder label - lblWorkingfolder.innerHTML = folderName; - btnUseHostFolder.classList.remove("hidden"); - btnSelectHostFolder.innerHTML = "Select Different Folder"; - btnSelectHostFolder.classList.add("inverted"); - btnSelectHostFolder.classList.remove("first-item"); - } - } - - async _switchToDevice(device) { - device.addEventListener("message", this.onSerialReceive.bind(this)); - - this._serialDevice = device; - console.log("switch to", this._serialDevice); - await this._serialDevice.open({baudRate: 115200}); // TODO: Will fail if something else is already connected or it isn't found. - - // Start the read loop - this._readLoopPromise = this._readSerialLoop().catch( - async function(error) { - await this.onDisconnected(); - }.bind(this) - ); - - if (this._serialDevice.writable) { - this.writer = this._serialDevice.writable.getWriter(); - await this.writer.ready; - } - - await this.showBusy(this._getDeviceUid()); - - this.updateConnected(CONNSTATE.partial); - - // At this point we should see if we should init the file client and check if have a saved dir handle - this.initFileClient(new FileTransferClient(this.connectionStatus.bind(this), this.repl)); - const fileClient = this.fileHelper.getFileClient(); - const result = await fileClient.loadSavedDirHandle(); - if (result) { - console.log("Successfully loaded directory:", fileClient.getWorkingDirectoryName()); - await this._hostFolderChanged(); - } else { - console.log("Failed to load directory"); - } - } - - async _getDeviceUid() { - // TODO: Make this python code more robust for older devices - // For instance what if there is an import error with binascii - // or uid is not set due to older firmware - // or microcontroller is a list - // It might be better to take a minimal python approach and do most of - // the conversion in the javascript code - - console.log("Getting Device UID..."); - let result = await this.repl.runCode( -`import microcontroller -import binascii -binascii.hexlify(microcontroller.cpu.uid).decode('ascii').upper()` - ); - // Strip out whitespace as well as start and end quotes - if (result) { - this._uid = result.trim().slice(1, -1); - console.log("Device UID: " + this._uid); - this.debugLog("Device UID: " + this._uid) - } else { - console.log("Failed to get Device UID, result was", result); - } - } - - async _readSerialLoop() { - console.log("Read Loop Init"); - if (!this._serialDevice) { - return; - } - - const messageEvent = new Event("message"); - const decoder = new TextDecoder(); - - if (this._serialDevice.readable) { - this.reader = this._serialDevice.readable.getReader(); - console.log("Read Loop Started"); - while (true) { - const {value, done} = await this.reader.read(); - if (value) { - messageEvent.data = decoder.decode(value); - this._serialDevice.dispatchEvent(messageEvent); - } - if (done) { - this.reader.releaseLock(); - break; - } - } - } - - console.log("Read Loop Stopped. Closing Serial Port."); - } - - // Handle the different button states for various connection steps - _connectionStep(step) { - const buttonStates = [ - {request: false, select: false}, - {request: true, select: false}, - {request: true, select: true}, - ]; - - if (step < 0) step = 0; - if (step > buttonStates.length - 1) step = buttonStates.length - 1; - - btnRequestSerialDevice.disabled = !buttonStates[step].request; - btnSelectHostFolder.disabled = !buttonStates[step].select; - } -} - -export {USBWorkflow}; diff --git a/js/workflows/usb.js b/js/workflows/usb.js index ba4ed07..00b6ca0 100644 --- a/js/workflows/usb.js +++ b/js/workflows/usb.js @@ -1,6 +1,6 @@ import {CONNTYPE, CONNSTATE} from '../constants.js'; import {Workflow} from './workflow.js'; -import {GenericModal} from '../common/dialogs.js'; +import {GenericModal, DeviceInfoModal} from '../common/dialogs.js'; import {FileOps} from '@adafruit/circuitpython-repl-js'; // Use this to determine which FileTransferClient to load import {FileTransferClient as ReplFileTransferClient} from '../common/repl-file-transfer.js'; import {FileTransferClient as FSAPIFileTransferClient} from '../common/fsapi-file-transfer.js'; @@ -15,11 +15,20 @@ class USBWorkflow extends Workflow { this.reader = null; this.writer = null; this.connectDialog = new GenericModal("usb-connect"); + this.infoDialog = new DeviceInfoModal("device-info"); this._fileContents = null; this.type = CONNTYPE.Usb; this._partialToken = null; this._uid = null; this._readLoopPromise = null; + this._messageCallback = null; + this._btnSelectHostFolderCallback = null; + this._btnUseHostFolderCallback = null; + this.buttonStates = [ + {request: false, select: false}, + {request: true, select: false}, + {request: false, select: true}, + ]; } async init(params) { @@ -121,7 +130,12 @@ class USBWorkflow extends Workflow { // This would help with other workflows as well } else { console.log('Requesting any serial device...'); - device = await navigator.serial.requestPort(); + try { + device = await navigator.serial.requestPort(); + } catch (e) { + console.log(e); + return false; + } } // If we didn't automatically use a saved device @@ -131,7 +145,7 @@ class USBWorkflow extends Workflow { } console.log(this._serialDevice); if (this._serialDevice != null) { - this._connectionStep(2); + this.connectionStep(2); return true; } @@ -147,27 +161,39 @@ class USBWorkflow extends Workflow { btnUseHostFolder = modal.querySelector('#useHostFolder'); lblWorkingfolder = modal.querySelector('#workingFolder'); + // Map the button states to the buttons + this.connectButtons = { + request: btnRequestSerialDevice, + select: btnSelectHostFolder, + }; + btnRequestSerialDevice.disabled = true; btnSelectHostFolder.disabled = true; - - btnRequestSerialDevice.addEventListener('click', async (event) => { + let serialConnect = async (event) => { try { - await this.connectToSerial(); + await this.showBusy(this.connectToSerial()); } catch (e) { //console.log(e); //alert(e.message); //alert("Unable to connect to device. Make sure it is not already in use."); // TODO: I think this also occurs if the user cancels the requestPort dialog } - }); + }; + btnRequestSerialDevice.removeEventListener('click', serialConnect); + btnRequestSerialDevice.addEventListener('click', serialConnect); - btnSelectHostFolder.addEventListener('click', async (event) => { + btnSelectHostFolder.removeEventListener('click', this._btnSelectHostFolderCallback) + this._btnSelectHostFolderCallback = async (event) => { await this._selectHostFolder(); - }); + }; + btnSelectHostFolder.addEventListener('click', this._btnSelectHostFolderCallback); - btnUseHostFolder.addEventListener('click', async (event) => { + + btnUseHostFolder.removeEventListener('click', this._btnUseHostFolderCallback); + this._btnUseHostFolderCallback = async (event) => { await this._useHostFolder(); - }); + } + btnUseHostFolder.addEventListener('click', this._btnUseHostFolderCallback); // Check if WebSerial is available if (!(await this.available() instanceof Error)) { @@ -176,13 +202,13 @@ class USBWorkflow extends Workflow { if (stepOne = modal.querySelector('.step:first-of-type')) { stepOne.classList.add("hidden"); } - this._connectionStep(1); + this.connectionStep(1); } else { // If not, hide all steps beyond the message modal.querySelectorAll('.step:not(:first-of-type)').forEach((stepItem) => { stepItem.classList.add("hidden"); }); - this._connectionStep(0); + this.connectionStep(0); } // Hide the last step until we determine that we need it @@ -235,10 +261,15 @@ class USBWorkflow extends Workflow { // Workflow specific Functions async _switchToDevice(device) { - device.addEventListener("message", this.onSerialReceive.bind(this)); - device.addEventListener("disconnect", async (e) => { + device.removeEventListener("message", this._messageCallback); + this._messageCallback = this.onSerialReceive.bind(this); + device.addEventListener("message", this._messageCallback); + + let onDisconnect = async (e) => { await this.onDisconnected(e, false); - }); + }; + device.removeEventListener("disconnect", onDisconnect); + device.addEventListener("disconnect", onDisconnect); this._serialDevice = device; console.log("switch to", this._serialDevice); @@ -341,19 +372,8 @@ print(binascii.hexlify(microcontroller.cpu.uid).decode('ascii').upper())` console.log("Read Loop Stopped. Closing Serial Port."); } - // Handle the different button states for various connection steps - _connectionStep(step) { - const buttonStates = [ - {request: false, select: false}, - {request: true, select: false}, - {request: true, select: true}, - ]; - - if (step < 0) step = 0; - if (step > buttonStates.length - 1) step = buttonStates.length - 1; - - btnRequestSerialDevice.disabled = !buttonStates[step].request; - btnSelectHostFolder.disabled = !buttonStates[step].select; + async showInfo(documentState) { + return await this.infoDialog.open(this, documentState); } } diff --git a/js/workflows/web.js b/js/workflows/web.js index b78ac28..5c26438 100644 --- a/js/workflows/web.js +++ b/js/workflows/web.js @@ -23,6 +23,8 @@ class WebWorkflow extends Workflow { this.deviceDiscoveryDialog = new DiscoveryModal("device-discovery"); this.connIntervalId = null; this.type = CONNTYPE.Web; + this.buttonStates = []; + this.buttons = {}; } // This is called when a user clicks the main disconnect button diff --git a/js/workflows/workflow.js b/js/workflows/workflow.js index 42f5491..661acb0 100644 --- a/js/workflows/workflow.js +++ b/js/workflows/workflow.js @@ -4,6 +4,7 @@ import {FileHelper} from '../common/file.js'; import {UnsavedDialog} from '../common/dialogs.js'; import {FileDialog, FILE_DIALOG_OPEN, FILE_DIALOG_SAVE} from '../common/file_dialog.js'; import {CONNTYPE, CONNSTATE} from '../constants.js'; +import {plotValues} from '../common/plotter.js' /* * This class will encapsulate all of the common workflow-related functions @@ -47,6 +48,10 @@ class Workflow { this._unsavedDialog = new UnsavedDialog("unsaved"); this._fileDialog = new FileDialog("files", this.showBusy.bind(this)); this.repl = new REPL(); + this.plotterEnabled = false; + this.plotterChart = false; + this.buttonStates = []; + this.connectButtons = {}; } async init(params) { @@ -59,6 +64,8 @@ class Workflow { this._loadFileContents = params.loadFileFunc; this._showMessage = params.showMessageFunc; this.loader = document.getElementById("loader"); + this.plotterBufferSize = document.getElementById('buffer-size'); + this.plotterGridLines = document.getElementById('plot-gridlines-select'); if ("terminalTitle" in params) { this.terminalTitle = params.terminalTitle; } @@ -159,6 +166,9 @@ class Workflow { } writeToTerminal(data) { + if (this.plotterEnabled) { + plotValues(this.plotterChart, data, this.plotterBufferSize.value); + } this.terminal.write(data); } @@ -300,6 +310,33 @@ class Workflow { async available() { return Error("This work flow is not available."); } + + // Handle the different button states for various connection steps + connectionStep(step) { + if (step < 0) step = 0; + if (step > this.buttonStates.length - 1) step = this.buttonStates.length - 1; + + for (let button in this.connectButtons) { + this.connectButtons[button].disabled = !this.buttonStates[step][button]; + } + + // Mark all previous steps as completed (hidden or not) + for (let stepNumber = 0; stepNumber < step; stepNumber++) { + this._markStepCompleted(stepNumber); + } + } + + _markStepCompleted(stepNumber) { + let modal = this.connectDialog.getModal(); + let steps = modal.querySelectorAll('.step'); + // For any steps prior to the last step, add a checkmark + for (let i = 0; i < steps.length - 1; i++) { + let step = steps[stepNumber]; + if (!step.classList.contains('completed')) { + step.classList.add('completed'); + } + } + } } export { diff --git a/package-lock.json b/package-lock.json index fe14a61..b95f7eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,12 +9,13 @@ "version": "0.0.0", "dependencies": { "@adafruit/ble-file-transfer-js": "adafruit/ble-file-transfer-js#1.0.2", - "@adafruit/circuitpython-repl-js": "adafruit/circuitpython-repl-js#3.2.1", + "@adafruit/circuitpython-repl-js": "adafruit/circuitpython-repl-js#3.2.4", "@codemirror/lang-python": "^6.1.6", "@fortawesome/fontawesome-free": "^6.6.0", "@xterm/addon-fit": "^0.10.0", "@xterm/addon-web-links": "^0.11.0", "@xterm/xterm": "^5.5.0", + "chart.js": "^4.4.4", "codemirror": "^6.0.1", "file-saver": "^2.0.5", "focus-trap": "^7.6.1", @@ -23,12 +24,13 @@ }, "devDependencies": { "sass": "^1.80.6", - "vite": "^5.4.11" + "vite": "^5.4.11", + "vite-plugin-mkcert": "^1.17.6" + }, + "optionalDependencies": { + "@rollup/rollup-linux-x64-gnu": "^4.20.0" } }, - "../../circuitpython-repl-js/repl.js": { - "extraneous": true - }, "node_modules/@adafruit/ble-file-transfer-js": { "name": "@adafruit/ble-file-transfer", "version": "1.0.2", @@ -36,15 +38,12 @@ "license": "MIT" }, "node_modules/@adafruit/circuitpython-repl-js": { - "version": "2.0.1", - "resolved": "git+ssh://git@github.com/adafruit/circuitpython-repl-js.git#2defa7c9489c2c84e471e861509027d5418b53ed", - "integrity": "sha512-3r0/MF1yd4w5q9SPQymY4YR5iP+jgam/0yyCPKqTG7HMCC9bRgF+DX2ntjmhsmwjTl0+bdOlUaHVcn8JtIrEsw==", + "version": "3.2.4", + "resolved": "git+ssh://git@github.com/adafruit/circuitpython-repl-js.git#1bd2db05082d245afeb17bb1ffb2d66b7833f1ce", "license": "MIT" }, "node_modules/@codemirror/autocomplete": { "version": "6.16.2", - "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.16.2.tgz", - "integrity": "sha512-MjfDrHy0gHKlPWsvSsikhO1+BOh+eBHNgfH1OXs1+DAf30IonQldgMM3kxLDTG9ktE7kDLaA1j/l7KMPA4KNfw==", "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", @@ -61,8 +60,6 @@ }, "node_modules/@codemirror/commands": { "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.6.0.tgz", - "integrity": "sha512-qnY+b7j1UNcTS31Eenuc/5YJB6gQOzkUoNmJQc0rznwqSRpeaWWpjkWy2C/MPTcePpsKJEM26hXrOXl1+nceXg==", "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", @@ -73,8 +70,6 @@ }, "node_modules/@codemirror/lang-python": { "version": "6.1.6", - "resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.1.6.tgz", - "integrity": "sha512-ai+01WfZhWqM92UqjnvorkxosZ2aq2u28kHvr+N3gu012XqY2CThD67JPMHnGceRfXPDBmn1HnyqowdpF57bNg==", "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.3.2", @@ -86,8 +81,6 @@ }, "node_modules/@codemirror/language": { "version": "6.10.2", - "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.2.tgz", - "integrity": "sha512-kgbTYTo0Au6dCSc/TFy7fK3fpJmgHDv1sG1KNQKJXVi+xBTEeBPY/M30YXiU6mMXeH+YIDLsbrT4ZwNRdtF+SA==", "license": "MIT", "dependencies": { "@codemirror/state": "^6.0.0", @@ -100,8 +93,6 @@ }, "node_modules/@codemirror/lint": { "version": "6.8.0", - "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.0.tgz", - "integrity": "sha512-lsFofvaw0lnPRJlQylNsC4IRt/1lI4OD/yYslrSGVndOJfStc58v+8p9dgGiD90ktOfL7OhBWns1ZETYgz0EJA==", "license": "MIT", "dependencies": { "@codemirror/state": "^6.0.0", @@ -111,8 +102,6 @@ }, "node_modules/@codemirror/search": { "version": "6.5.6", - "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.6.tgz", - "integrity": "sha512-rpMgcsh7o0GuCDUXKPvww+muLA1pDJaFrpq/CCHtpQJYz8xopu4D1hPcKRoDD0YlF8gZaqTNIRa4VRBWyhyy7Q==", "license": "MIT", "dependencies": { "@codemirror/state": "^6.0.0", @@ -122,14 +111,10 @@ }, "node_modules/@codemirror/state": { "version": "6.4.1", - "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.1.tgz", - "integrity": "sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==", "license": "MIT" }, "node_modules/@codemirror/view": { "version": "6.28.1", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.28.1.tgz", - "integrity": "sha512-BUWr+zCJpMkA/u69HlJmR+YkV4yPpM81HeMkOMZuwFa8iM5uJdEPKAs1icIRZKkKmy0Ub1x9/G3PQLTXdpBxrQ==", "license": "MIT", "dependencies": { "@codemirror/state": "^6.4.0", @@ -137,95 +122,8 @@ "w3c-keyname": "^2.2.4" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@esbuild/darwin-x64": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", "cpu": [ "x64" ], @@ -239,344 +137,200 @@ "node": ">=12" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "node_modules/@fortawesome/fontawesome-free": { + "version": "6.6.0", + "license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)", "engines": { - "node": ">=12" + "node": ">=6" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } + "node_modules/@kurkle/color": { + "version": "0.3.2", + "license": "MIT" }, - "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } + "node_modules/@lezer/common": { + "version": "1.2.1", + "license": "MIT" }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "dev": true, + "node_modules/@lezer/highlight": { + "version": "1.2.0", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@lezer/common": "^1.0.0" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "dev": true, + "node_modules/@lezer/lr": { + "version": "1.4.1", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@lezer/common": "^1.0.0" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "dev": true, + "node_modules/@lezer/python": { + "version": "1.1.14", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], + "node_modules/@octokit/auth-token": { + "version": "4.0.0", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=12" + "node": ">= 18" } }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], + "node_modules/@octokit/core": { + "version": "5.2.0", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@octokit/auth-token": "^4.0.0", + "@octokit/graphql": "^7.1.0", + "@octokit/request": "^8.3.1", + "@octokit/request-error": "^5.1.0", + "@octokit/types": "^13.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + }, "engines": { - "node": ">=12" + "node": ">= 18" } }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], + "node_modules/@octokit/endpoint": { + "version": "9.0.6", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, "engines": { - "node": ">=12" + "node": ">= 18" } }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], + "node_modules/@octokit/graphql": { + "version": "7.1.0", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@octokit/request": "^8.3.0", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^6.0.0" + }, "engines": { - "node": ">=12" + "node": ">= 18" } }, - "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], + "node_modules/@octokit/openapi-types": { + "version": "23.0.1", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } + "license": "MIT" }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], + "node_modules/@octokit/plugin-paginate-rest": { + "version": "11.4.4-cjs.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.4.4-cjs.2.tgz", + "integrity": "sha512-2dK6z8fhs8lla5PaOTgqfCGBxgAv/le+EhPs27KklPhm1bKObpu6lXzwfUEQ16ajXzqNrKMujsFyo9K2eaoISw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], + "dependencies": { + "@octokit/types": "^13.7.0" + }, "engines": { - "node": ">=12" + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], + "node_modules/@octokit/plugin-request-log": { + "version": "4.0.1", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], "engines": { - "node": ">=12" + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" } }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "13.3.2-cjs.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.3.2-cjs.1.tgz", + "integrity": "sha512-VUjIjOOvF2oELQmiFpWA1aOPdawpyaCUqcEBc/UOUnj3Xp6DJGrJ1+bjUIIDzdHjnFNO6q57ODMfdEZnoBkCwQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], + "dependencies": { + "@octokit/types": "^13.8.0" + }, "engines": { - "node": ">=12" + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "^5" } }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], + "node_modules/@octokit/request": { + "version": "8.4.1", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "@octokit/endpoint": "^9.0.6", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, "engines": { - "node": ">=12" + "node": ">= 18" } }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], + "node_modules/@octokit/request-error": { + "version": "5.1.1", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "@octokit/types": "^13.1.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, "engines": { - "node": ">=12" + "node": ">= 18" } }, - "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], + "node_modules/@octokit/rest": { + "version": "20.1.2", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-20.1.2.tgz", + "integrity": "sha512-GmYiltypkHHtihFwPRxlaorG5R9VAHuk/vbszVoRTGXnAsY60wYLkh/E2XiFmdZmqrisw+9FaazS1i5SbdWYgA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@fortawesome/fontawesome-free": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.6.0.tgz", - "integrity": "sha512-60G28ke/sXdtS9KZCpZSHHkCbdsOGEhIUGlwq6yhY74UpTiToIh8np7A8yphhM4BWsvNFtIvLpi4co+h9Mr9Ow==", - "engines": { - "node": ">=6" - } - }, - "node_modules/@lezer/common": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.1.tgz", - "integrity": "sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ==", - "license": "MIT" - }, - "node_modules/@lezer/highlight": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.0.tgz", - "integrity": "sha512-WrS5Mw51sGrpqjlh3d4/fOwpEV2Hd3YOkp9DBt4k8XZQcoTHZFB7sx030A6OcahF4J1nDQAa3jXlTVVYH50IFA==", - "license": "MIT", - "dependencies": { - "@lezer/common": "^1.0.0" - } - }, - "node_modules/@lezer/lr": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.1.tgz", - "integrity": "sha512-CHsKq8DMKBf9b3yXPDIU4DbH+ZJd/sJdYOW2llbW/HudP5u0VS6Bfq1hLYfgU7uAYGFIyGGQIsSOXGPEErZiJw==", - "license": "MIT", "dependencies": { - "@lezer/common": "^1.0.0" + "@octokit/core": "^5.0.2", + "@octokit/plugin-paginate-rest": "11.4.4-cjs.2", + "@octokit/plugin-request-log": "^4.0.0", + "@octokit/plugin-rest-endpoint-methods": "13.3.2-cjs.1" + }, + "engines": { + "node": ">= 18" } }, - "node_modules/@lezer/python": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.14.tgz", - "integrity": "sha512-ykDOb2Ti24n76PJsSa4ZoDF0zH12BSw1LGfQXCYJhJyOGiFTfGaX0Du66Ze72R+u/P35U+O6I9m8TFXov1JzsA==", + "node_modules/@octokit/types": { + "version": "13.8.0", + "dev": true, "license": "MIT", "dependencies": { - "@lezer/common": "^1.2.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0" + "@octokit/openapi-types": "^23.0.1" } }, "node_modules/@parcel/watcher": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.0.tgz", - "integrity": "sha512-i0GV1yJnm2n3Yq1qw6QrUrd/LI9bE8WEBOTtOkpCXHHdyN3TAGgqAK/DAT05z4fq2x04cARXt2pDmjWjL92iTQ==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", + "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", "dev": true, "hasInstallScript": true, + "license": "MIT", "optional": true, "dependencies": { "detect-libc": "^1.0.3", @@ -592,29 +346,30 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.5.0", - "@parcel/watcher-darwin-arm64": "2.5.0", - "@parcel/watcher-darwin-x64": "2.5.0", - "@parcel/watcher-freebsd-x64": "2.5.0", - "@parcel/watcher-linux-arm-glibc": "2.5.0", - "@parcel/watcher-linux-arm-musl": "2.5.0", - "@parcel/watcher-linux-arm64-glibc": "2.5.0", - "@parcel/watcher-linux-arm64-musl": "2.5.0", - "@parcel/watcher-linux-x64-glibc": "2.5.0", - "@parcel/watcher-linux-x64-musl": "2.5.0", - "@parcel/watcher-win32-arm64": "2.5.0", - "@parcel/watcher-win32-ia32": "2.5.0", - "@parcel/watcher-win32-x64": "2.5.0" + "@parcel/watcher-android-arm64": "2.5.1", + "@parcel/watcher-darwin-arm64": "2.5.1", + "@parcel/watcher-darwin-x64": "2.5.1", + "@parcel/watcher-freebsd-x64": "2.5.1", + "@parcel/watcher-linux-arm-glibc": "2.5.1", + "@parcel/watcher-linux-arm-musl": "2.5.1", + "@parcel/watcher-linux-arm64-glibc": "2.5.1", + "@parcel/watcher-linux-arm64-musl": "2.5.1", + "@parcel/watcher-linux-x64-glibc": "2.5.1", + "@parcel/watcher-linux-x64-musl": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", + "@parcel/watcher-win32-ia32": "2.5.1", + "@parcel/watcher-win32-x64": "2.5.1" } }, "node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.0.tgz", - "integrity": "sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", + "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -628,13 +383,14 @@ } }, "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.0.tgz", - "integrity": "sha512-hyZ3TANnzGfLpRA2s/4U1kbw2ZI4qGxaRJbBH2DCSREFfubMswheh8TeiC1sGZ3z2jUf3s37P0BBlrD3sjVTUw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", + "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -648,13 +404,14 @@ } }, "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.0.tgz", - "integrity": "sha512-9rhlwd78saKf18fT869/poydQK8YqlU26TMiNg7AIu7eBp9adqbJZqmdFOsbZ5cnLp5XvRo9wcFmNHgHdWaGYA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", + "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -668,13 +425,14 @@ } }, "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.0.tgz", - "integrity": "sha512-syvfhZzyM8kErg3VF0xpV8dixJ+RzbUaaGaeb7uDuz0D3FK97/mZ5AJQ3XNnDsXX7KkFNtyQyFrXZzQIcN49Tw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", + "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -688,13 +446,14 @@ } }, "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.0.tgz", - "integrity": "sha512-0VQY1K35DQET3dVYWpOaPFecqOT9dbuCfzjxoQyif1Wc574t3kOSkKevULddcR9znz1TcklCE7Ht6NIxjvTqLA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", + "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -708,13 +467,14 @@ } }, "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.0.tgz", - "integrity": "sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", + "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -728,13 +488,14 @@ } }, "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.0.tgz", - "integrity": "sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", + "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -748,13 +509,14 @@ } }, "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.0.tgz", - "integrity": "sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", + "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -768,13 +530,14 @@ } }, "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.0.tgz", - "integrity": "sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", + "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -788,13 +551,14 @@ } }, "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.0.tgz", - "integrity": "sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", + "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -808,13 +572,14 @@ } }, "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.0.tgz", - "integrity": "sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", + "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -828,13 +593,14 @@ } }, "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.0.tgz", - "integrity": "sha512-+rgpsNRKwo8A53elqbbHXdOMtY/tAtTzManTWShB5Kk54N8Q9mzNWV7tV+IbGueCbcj826MfWGU3mprWtuf1TA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", + "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -848,13 +614,14 @@ } }, "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.0.tgz", - "integrity": "sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", + "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -868,223 +635,275 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.0.tgz", - "integrity": "sha512-WTWD8PfoSAJ+qL87lE7votj3syLavxunWhzCnx3XFxFiI/BA/r3X7MUM8dVrH8rb2r4AiO8jJsr3ZjdaftmnfA==", + "version": "4.32.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.32.1.tgz", + "integrity": "sha512-/pqA4DmqyCm8u5YIDzIdlLcEmuvxb0v8fZdFhVMszSpDTgbQKdw3/mB3eMUHIbubtJ6F9j+LtmyCnHTEqIHyzA==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.0.tgz", - "integrity": "sha512-a1sR2zSK1B4eYkiZu17ZUZhmUQcKjk2/j9Me2IDjk1GHW7LB5Z35LEzj9iJch6gtUfsnvZs1ZNyDW2oZSThrkA==", + "version": "4.32.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.32.1.tgz", + "integrity": "sha512-If3PDskT77q7zgqVqYuj7WG3WC08G1kwXGVFi9Jr8nY6eHucREHkfpX79c0ACAjLj3QIWKPJR7w4i+f5EdLH5Q==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.0.tgz", - "integrity": "sha512-zOnKWLgDld/svhKO5PD9ozmL6roy5OQ5T4ThvdYZLpiOhEGY+dp2NwUmxK0Ld91LrbjrvtNAE0ERBwjqhZTRAA==", + "version": "4.32.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.32.1.tgz", + "integrity": "sha512-zCpKHioQ9KgZToFp5Wvz6zaWbMzYQ2LJHQ+QixDKq52KKrF65ueu6Af4hLlLWHjX1Wf/0G5kSJM9PySW9IrvHA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.0.tgz", - "integrity": "sha512-7doS8br0xAkg48SKE2QNtMSFPFUlRdw9+votl27MvT46vo44ATBmdZdGysOevNELmZlfd+NEa0UYOA8f01WSrg==", + "version": "4.32.1", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.32.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.32.1.tgz", + "integrity": "sha512-NbOa+7InvMWRcY9RG+B6kKIMD/FsnQPH0MWUvDlQB1iXnF/UcKSudCXZtv4lW+C276g3w5AxPbfry5rSYvyeYA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.32.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.32.1.tgz", + "integrity": "sha512-JRBRmwvHPXR881j2xjry8HZ86wIPK2CcDw0EXchE1UgU0ubWp9nvlT7cZYKc6bkypBt745b4bglf3+xJ7hXWWw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.0.tgz", - "integrity": "sha512-pWJsfQjNWNGsoCq53KjMtwdJDmh/6NubwQcz52aEwLEuvx08bzcy6tOUuawAOncPnxz/3siRtd8hiQ32G1y8VA==", + "version": "4.32.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.32.1.tgz", + "integrity": "sha512-PKvszb+9o/vVdUzCCjL0sKHukEQV39tD3fepXxYrHE3sTKrRdCydI7uldRLbjLmDA3TFDmh418XH19NOsDRH8g==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.0.tgz", - "integrity": "sha512-efRIANsz3UHZrnZXuEvxS9LoCOWMGD1rweciD6uJQIx2myN3a8Im1FafZBzh7zk1RJ6oKcR16dU3UPldaKd83w==", + "version": "4.32.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.32.1.tgz", + "integrity": "sha512-9WHEMV6Y89eL606ReYowXuGF1Yb2vwfKWKdD1A5h+OYnPZSJvxbEjxTRKPgi7tkP2DSnW0YLab1ooy+i/FQp/Q==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.0.tgz", - "integrity": "sha512-ZrPhydkTVhyeGTW94WJ8pnl1uroqVHM3j3hjdquwAcWnmivjAwOYjTEAuEDeJvGX7xv3Z9GAvrBkEzCgHq9U1w==", + "version": "4.32.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.32.1.tgz", + "integrity": "sha512-tZWc9iEt5fGJ1CL2LRPw8OttkCBDs+D8D3oEM8mH8S1ICZCtFJhD7DZ3XMGM8kpqHvhGUTvNUYVDnmkj4BDXnw==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.0.tgz", - "integrity": "sha512-cfaupqd+UEFeURmqNP2eEvXqgbSox/LHOyN9/d2pSdV8xTrjdg3NgOFJCtc1vQ/jEke1qD0IejbBfxleBPHnPw==", + "version": "4.32.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.32.1.tgz", + "integrity": "sha512-FTYc2YoTWUsBz5GTTgGkRYYJ5NGJIi/rCY4oK/I8aKowx1ToXeoVVbIE4LGAjsauvlhjfl0MYacxClLld1VrOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.32.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.32.1.tgz", + "integrity": "sha512-F51qLdOtpS6P1zJVRzYM0v6MrBNypyPEN1GfMiz0gPu9jN8ScGaEFIZQwteSsGKg799oR5EaP7+B2jHgL+d+Kw==", "cpu": [ - "arm64" + "loong64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.0.tgz", - "integrity": "sha512-ZKPan1/RvAhrUylwBXC9t7B2hXdpb/ufeu22pG2psV7RN8roOfGurEghw1ySmX/CmDDHNTDDjY3lo9hRlgtaHg==", + "version": "4.32.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.32.1.tgz", + "integrity": "sha512-wO0WkfSppfX4YFm5KhdCCpnpGbtgQNj/tgvYzrVYFKDpven8w2N6Gg5nB6w+wAMO3AIfSTWeTjfVe+uZ23zAlg==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.0.tgz", - "integrity": "sha512-H1eRaCwd5E8eS8leiS+o/NqMdljkcb1d6r2h4fKSsCXQilLKArq6WS7XBLDu80Yz+nMqHVFDquwcVrQmGr28rg==", + "version": "4.32.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.32.1.tgz", + "integrity": "sha512-iWswS9cIXfJO1MFYtI/4jjlrGb/V58oMu4dYJIKnR5UIwbkzR0PJ09O0PDZT0oJ3LYWXBSWahNf/Mjo6i1E5/g==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.0.tgz", - "integrity": "sha512-zJ4hA+3b5tu8u7L58CCSI0A9N1vkfwPhWd/puGXwtZlsB5bTkwDNW/+JCU84+3QYmKpLi+XvHdmrlwUwDA6kqw==", + "version": "4.32.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.32.1.tgz", + "integrity": "sha512-RKt8NI9tebzmEthMnfVgG3i/XeECkMPS+ibVZjZ6mNekpbbUmkNWuIN2yHsb/mBPyZke4nlI4YqIdFPgKuoyQQ==", "cpu": [ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.0.tgz", - "integrity": "sha512-e2hrvElFIh6kW/UNBQK/kzqMNY5mO+67YtEh9OA65RM5IJXYTWiXjX6fjIiPaqOkBthYF1EqgiZ6OXKcQsM0hg==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.44.0.tgz", + "integrity": "sha512-iUVJc3c0o8l9Sa/qlDL2Z9UP92UZZW1+EmQ4xfjTc1akr0iUFZNfxrXJ/R1T90h/ILm9iXEY6+iPrmYB3pXKjw==", "cpu": [ "x64" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.0.tgz", - "integrity": "sha512-1vvmgDdUSebVGXWX2lIcgRebqfQSff0hMEkLJyakQ9JQUbLDkEaMsPTLOmyccyC6IJ/l3FZuJbmrBw/u0A0uCQ==", + "version": "4.32.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.32.1.tgz", + "integrity": "sha512-BLoiyHDOWoS3uccNSADMza6V6vCNiphi94tQlVIL5de+r6r/CCQuNnerf+1g2mnk2b6edp5dk0nhdZ7aEjOBsA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.0.tgz", - "integrity": "sha512-s5oFkZ/hFcrlAyBTONFY1TWndfyre1wOMwU+6KCpm/iatybvrRgmZVM+vCFwxmC5ZhdlgfE0N4XorsDpi7/4XQ==", + "version": "4.32.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.32.1.tgz", + "integrity": "sha512-w2l3UnlgYTNNU+Z6wOR8YdaioqfEnwPjIsJ66KxKAf0p+AuL2FHeTX6qvM+p/Ue3XPBVNyVSfCrfZiQh7vZHLQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.0.tgz", - "integrity": "sha512-G9+TEqRnAA6nbpqyUqgTiopmnfgnMkR3kMukFBDsiyy23LZvUCpiUwjTRx6ezYCjJODXrh52rBR9oXvm+Fp5wg==", + "version": "4.32.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.32.1.tgz", + "integrity": "sha512-Am9H+TGLomPGkBnaPWie4F3x+yQ2rr4Bk2jpwy+iV+Gel9jLAu/KqT8k3X4jxFPW6Zf8OMnehyutsd+eHoq1WQ==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.0.tgz", - "integrity": "sha512-2jsCDZwtQvRhejHLfZ1JY6w6kEuEtfF9nzYsZxzSlNVKDX+DpsDJ+Rbjkm74nvg2rdx0gwBS+IMdvwJuq3S9pQ==", + "version": "4.32.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.32.1.tgz", + "integrity": "sha512-ar80GhdZb4DgmW3myIS9nRFYcpJRSME8iqWgzH2i44u+IdrzmiXVxeFnExQ5v4JYUSpg94bWjevMG8JHf1Da5Q==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", - "dev": true + "version": "1.0.6", + "dev": true, + "license": "MIT" }, "node_modules/@xterm/addon-fit": { "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz", - "integrity": "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==", "license": "MIT", "peerDependencies": { "@xterm/xterm": "^5.0.0" @@ -1092,8 +911,6 @@ }, "node_modules/@xterm/addon-web-links": { "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@xterm/addon-web-links/-/addon-web-links-0.11.0.tgz", - "integrity": "sha512-nIHQ38pQI+a5kXnRaTgwqSHnX7KE6+4SVoceompgHL26unAxdfP6IPqUTSYPQgSwM56hsElfoNrrW5V7BUED/Q==", "license": "MIT", "peerDependencies": { "@xterm/xterm": "^5.0.0" @@ -1101,15 +918,36 @@ }, "node_modules/@xterm/xterm": { "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", - "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", "license": "MIT" }, + "node_modules/asynckit": { + "version": "0.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", + "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/before-after-hook": { + "version": "2.2.3", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, + "license": "MIT", "optional": true, "dependencies": { "fill-range": "^7.1.1" @@ -1118,11 +956,34 @@ "node": ">=8" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chart.js": { + "version": "4.4.4", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/chokidar": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", - "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, + "license": "MIT", "dependencies": { "readdirp": "^4.0.1" }, @@ -1135,8 +996,6 @@ }, "node_modules/codemirror": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz", - "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==", "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.0.0", @@ -1148,23 +1007,60 @@ "@codemirror/view": "^6.0.0" } }, + "node_modules/combined-stream": { + "version": "1.0.8", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/core-util-is": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "license": "MIT" }, "node_modules/crelt": { "version": "1.0.6", - "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", - "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", "license": "MIT" }, + "node_modules/debug": { + "version": "4.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/deprecation": { + "version": "2.3.1", + "dev": true, + "license": "ISC" + }, "node_modules/detect-libc": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", "dev": true, + "license": "Apache-2.0", "optional": true, "bin": { "detect-libc": "bin/detect-libc.js" @@ -1173,10 +1069,62 @@ "node": ">=0.10" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1214,8 +1162,6 @@ }, "node_modules/file-saver": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", - "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==", "license": "MIT" }, "node_modules/fill-range": { @@ -1223,6 +1169,7 @@ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, + "license": "MIT", "optional": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -1232,19 +1179,50 @@ } }, "node_modules/focus-trap": { - "version": "7.6.1", - "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.1.tgz", - "integrity": "sha512-nB8y4nQl8PshahLpGKZOq1sb0xrMVFSn6at7u/qOsBZTlZRzaapISGENcB6mOkoezbClZyiMwEF/dGY8AZ00rA==", + "version": "7.6.5", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.5.tgz", + "integrity": "sha512-7Ke1jyybbbPZyZXFxEftUtxFGLMpE2n6A+z//m4CRDlj0hW+o3iYSmh8nFlYMurOiJVDmJRilUQtJr08KfIxlg==", + "license": "MIT", "dependencies": { "tabbable": "^6.2.0" } }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fsevents": { "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, - "hasInstallScript": true, "license": "MIT", "optional": true, "os": [ @@ -1254,29 +1232,113 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.7", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "function-bind": "^1.1.2", + "get-proto": "^1.0.0", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/idb-keyval": { "version": "6.2.1", - "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.1.tgz", - "integrity": "sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==", "license": "Apache-2.0" }, "node_modules/immediate": { "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", "license": "MIT" }, "node_modules/immutable": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.6.tgz", - "integrity": "sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ==", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.3.tgz", + "integrity": "sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==", "dev": true, "license": "MIT" }, "node_modules/inherits": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, "node_modules/is-extglob": { @@ -1284,6 +1346,7 @@ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, + "license": "MIT", "optional": true, "engines": { "node": ">=0.10.0" @@ -1294,6 +1357,7 @@ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, + "license": "MIT", "optional": true, "dependencies": { "is-extglob": "^2.1.1" @@ -1307,6 +1371,7 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, + "license": "MIT", "optional": true, "engines": { "node": ">=0.12.0" @@ -1314,14 +1379,10 @@ }, "node_modules/isarray": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "license": "MIT" }, "node_modules/jszip": { "version": "3.10.1", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", - "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", "license": "(MIT OR GPL-3.0-or-later)", "dependencies": { "lie": "~3.3.0", @@ -1332,18 +1393,25 @@ }, "node_modules/lie": { "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", "license": "MIT", "dependencies": { "immediate": "~3.0.5" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, + "license": "MIT", "optional": true, "dependencies": { "braces": "^3.0.3", @@ -1353,10 +1421,32 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "dev": true, + "license": "MIT" + }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.8", "dev": true, "funding": [ { @@ -1364,6 +1454,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -1376,25 +1467,32 @@ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", "dev": true, + "license": "MIT", "optional": true }, + "node_modules/once": { + "version": "1.4.0", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/pako": { "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", "license": "(MIT AND Zlib)" }, "node_modules/picocolors": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", - "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, + "license": "MIT", "optional": true, "engines": { "node": ">=8.6" @@ -1405,8 +1503,6 @@ }, "node_modules/postcss": { "version": "8.4.45", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.45.tgz", - "integrity": "sha512-7KTLTdzdZZYscUc65XmjFiB73vBhBfbPztCYdUNvlaso9PrzjzcmjqBPR0lNGkcVlcO4BjiO5rK/qNz+XAen1Q==", "dev": true, "funding": [ { @@ -1422,6 +1518,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.0.1", @@ -1433,14 +1530,15 @@ }, "node_modules/process-nextick-args": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "dev": true, "license": "MIT" }, "node_modules/readable-stream": { "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", @@ -1453,12 +1551,13 @@ } }, "node_modules/readdirp": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.1.tgz", - "integrity": "sha512-GkMg9uOTpIWWKbSsgwb5fA4EavTR+SG/PMPoAY8hkhHfEEY0/vqljY+XHqtDf2cr2IJtoNRDbrrEpZUiZCkYRw==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "dev": true, + "license": "MIT", "engines": { - "node": ">= 14.16.0" + "node": ">= 14.18.0" }, "funding": { "type": "individual", @@ -1466,12 +1565,11 @@ } }, "node_modules/rollup": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.0.tgz", - "integrity": "sha512-vo+S/lfA2lMS7rZ2Qoubi6I5hwZwzXeUIctILZLbHI+laNtvhhOIon2S1JksA5UEDQ7l3vberd0fxK44lTYjbQ==", + "version": "4.32.1", "dev": true, + "license": "MIT", "dependencies": { - "@types/estree": "1.0.5" + "@types/estree": "1.0.6" }, "bin": { "rollup": "dist/bin/rollup" @@ -1481,39 +1579,55 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.21.0", - "@rollup/rollup-android-arm64": "4.21.0", - "@rollup/rollup-darwin-arm64": "4.21.0", - "@rollup/rollup-darwin-x64": "4.21.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.21.0", - "@rollup/rollup-linux-arm-musleabihf": "4.21.0", - "@rollup/rollup-linux-arm64-gnu": "4.21.0", - "@rollup/rollup-linux-arm64-musl": "4.21.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.21.0", - "@rollup/rollup-linux-riscv64-gnu": "4.21.0", - "@rollup/rollup-linux-s390x-gnu": "4.21.0", - "@rollup/rollup-linux-x64-gnu": "4.21.0", - "@rollup/rollup-linux-x64-musl": "4.21.0", - "@rollup/rollup-win32-arm64-msvc": "4.21.0", - "@rollup/rollup-win32-ia32-msvc": "4.21.0", - "@rollup/rollup-win32-x64-msvc": "4.21.0", + "@rollup/rollup-android-arm-eabi": "4.32.1", + "@rollup/rollup-android-arm64": "4.32.1", + "@rollup/rollup-darwin-arm64": "4.32.1", + "@rollup/rollup-darwin-x64": "4.32.1", + "@rollup/rollup-freebsd-arm64": "4.32.1", + "@rollup/rollup-freebsd-x64": "4.32.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.32.1", + "@rollup/rollup-linux-arm-musleabihf": "4.32.1", + "@rollup/rollup-linux-arm64-gnu": "4.32.1", + "@rollup/rollup-linux-arm64-musl": "4.32.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.32.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.32.1", + "@rollup/rollup-linux-riscv64-gnu": "4.32.1", + "@rollup/rollup-linux-s390x-gnu": "4.32.1", + "@rollup/rollup-linux-x64-gnu": "4.32.1", + "@rollup/rollup-linux-x64-musl": "4.32.1", + "@rollup/rollup-win32-arm64-msvc": "4.32.1", + "@rollup/rollup-win32-ia32-msvc": "4.32.1", + "@rollup/rollup-win32-x64-msvc": "4.32.1", "fsevents": "~2.3.2" } }, + "node_modules/rollup/node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.32.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.32.1.tgz", + "integrity": "sha512-WQFLZ9c42ECqEjwg/GHHsouij3pzLXkFdz0UxHa/0OM12LzvX7DzedlY0SIEly2v18YZLRhCRoHZDxbBSWoGYg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/safe-buffer": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, "node_modules/sass": { - "version": "1.80.6", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.80.6.tgz", - "integrity": "sha512-ccZgdHNiBF1NHBsWvacvT5rju3y1d/Eu+8Ex6c21nHp2lZGLBEtuwc415QfiI1PJa1TpCo3iXwwSRjRpn2Ckjg==", + "version": "1.89.2", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.89.2.tgz", + "integrity": "sha512-xCmtksBKd/jdJ9Bt9p7nPKiuqrlBMBuuGkQlkhZjjQk3Ty48lv93k5Dq6OPkKt4XwxDJ7tvlfrTa1MPA9bf+QA==", "dev": true, + "license": "MIT", "dependencies": { "chokidar": "^4.0.0", - "immutable": "^4.0.0", + "immutable": "^5.0.2", "source-map-js": ">=0.6.2 <2.0.0" }, "bin": { @@ -1528,14 +1642,10 @@ }, "node_modules/setimmediate": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", "license": "MIT" }, "node_modules/source-map-js": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -1544,8 +1654,6 @@ }, "node_modules/string_decoder": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" @@ -1553,14 +1661,10 @@ }, "node_modules/style-mod": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", - "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==", "license": "MIT" }, "node_modules/tabbable": { "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", - "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", "license": "MIT" }, "node_modules/to-regex-range": { @@ -1568,6 +1672,7 @@ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, + "license": "MIT", "optional": true, "dependencies": { "is-number": "^7.0.0" @@ -1576,17 +1681,21 @@ "node": ">=8.0" } }, + "node_modules/universal-user-agent": { + "version": "6.0.1", + "dev": true, + "license": "ISC" + }, "node_modules/util-deprecate": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, "node_modules/vite": { - "version": "5.4.11", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", - "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==", + "version": "5.4.19", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz", + "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", "dev": true, + "license": "MIT", "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -1641,11 +1750,31 @@ } } }, + "node_modules/vite-plugin-mkcert": { + "version": "1.17.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/rest": "^20.1.1", + "axios": "^1.7.4", + "debug": "^4.3.6", + "picocolors": "^1.0.1" + }, + "engines": { + "node": ">=v16.7.0" + }, + "peerDependencies": { + "vite": ">=3" + } + }, "node_modules/w3c-keyname": { "version": "2.2.8", - "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", - "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", "license": "MIT" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "dev": true, + "license": "ISC" } } } diff --git a/package.json b/package.json index f0208c5..eec4e1a 100644 --- a/package.json +++ b/package.json @@ -10,20 +10,25 @@ }, "devDependencies": { "sass": "^1.80.6", - "vite": "^5.4.11" + "vite": "^5.4.11", + "vite-plugin-mkcert": "^1.17.6" }, "dependencies": { "@adafruit/ble-file-transfer-js": "adafruit/ble-file-transfer-js#1.0.2", - "@adafruit/circuitpython-repl-js": "adafruit/circuitpython-repl-js#3.2.1", + "@adafruit/circuitpython-repl-js": "adafruit/circuitpython-repl-js#3.2.4", "@codemirror/lang-python": "^6.1.6", "@fortawesome/fontawesome-free": "^6.6.0", "@xterm/addon-fit": "^0.10.0", "@xterm/addon-web-links": "^0.11.0", "@xterm/xterm": "^5.5.0", + "chart.js": "^4.4.4", "codemirror": "^6.0.1", "file-saver": "^2.0.5", "focus-trap": "^7.6.1", "idb-keyval": "^6.2.1", "jszip": "^3.10.1" + }, + "optionalDependencies": { + "@rollup/rollup-linux-x64-gnu": "^4.20.0" } } diff --git a/sass/layout/_grid.scss b/sass/layout/_grid.scss index fc2e3f2..36c9221 100644 --- a/sass/layout/_grid.scss +++ b/sass/layout/_grid.scss @@ -55,6 +55,17 @@ &.hidden { display: none; } + + &.completed .step-number::after { + content: ""; + background: url('/images/checkmark.svg'); + position: relative; + display: block; + top: 20px; + width: 50px; + height: 50px; + filter: drop-shadow(2px 2px 2px #888); + } } } diff --git a/sass/layout/_layout.scss b/sass/layout/_layout.scss index ec8cbc3..e2fa129 100644 --- a/sass/layout/_layout.scss +++ b/sass/layout/_layout.scss @@ -72,6 +72,18 @@ } #serial-page { + #plotter { + flex: 2 1 0; + background: #777; + position: relative; + width: 99%; + overflow: hidden; + padding: 10px 20px; + + &.hidden{ + display: none; + } + } #terminal { flex: 1 1 0%; background: #333; @@ -99,6 +111,9 @@ } } } + #buffer-size{ + width: 70px; + } } #ble-instructions, @@ -360,6 +375,7 @@ grid-template-columns: 30px minmax(60px, 1fr) 60px 1fr; grid-gap: 10px; cursor: default; + user-select:none; &.hidden-file { @@ -400,7 +416,8 @@ } } - &[data-popup-modal="device-discovery"] { + &[data-popup-modal="device-discovery"], + &[data-popup-modal="device-info"] { .device-info { margin-top: 5px; width: 100%; diff --git a/vite.config.js b/vite.config.js index 3d49619..fb84170 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,5 +1,6 @@ // vite.config.js | https://vitejs.dev/config/ import { defineConfig } from 'vite' +import mkcert from 'vite-plugin-mkcert' export default defineConfig({ build: { @@ -20,5 +21,6 @@ export default defineConfig({ } } } - } + }, + plugins: [ mkcert() ] })