From 63156f968e46e4aec5528dbe51dae57b3d383de7 Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Mon, 23 Mar 2026 15:42:30 -0700 Subject: [PATCH 01/67] Remove old keyboard navigation plugin --- README.md | 10 ---------- package-lock.json | 14 -------------- package.json | 1 - src/multiselect.js | 39 --------------------------------------- test/index.js | 6 ------ 5 files changed, 70 deletions(-) diff --git a/README.md b/README.md index 7762ad9..32381d7 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,6 @@ scope of its wrapped input function. - [x] Backpack (Need to update UI in Backpack plugin side) **Workspace-related** -- [x] Keyboard-navigation - [x] Shadow-block-converter - [x] Workspace-content-highlight - [x] Disable-top-blocks @@ -133,15 +132,6 @@ scope of its wrapped input function. For more information, please check out the following [issue page](https://github.com/mit-cml/workspace-multiselect/issues/50). -### Note on keyboard navigation plugin -Currently, the keyboard navigation plugin has no compatibility issues with the multiselect plugin. It is enabled by default -in the test files. However, one thing that developers and users should note is that the regular Blockly core's copy/cut/paste shortcuts -and multiselect plugin's copy/cut/paste shortcuts are completely disabled when the keyboard navigation mode is turned on. This was assumed -to be the expected behavior of the user as it defeats the purpose of having the keyboard navigation mode turned on and the user would try to -select blocks using the cursor and copy/cut/paste with that selection. However, if a developer wants to allow for either copy/cut/paste behavior -(Blockly Core or multiselect plugin) while the keyboard navigation mode is turned on, we would have to make the following changes described in this -[issue](https://github.com/google/blockly-samples/issues/2424). - ### Note on disable top blocks plugin The disable top blocks plugin has to be initialized after the multiselect plugin. The main reason behind this is that the multiselect plugin has its own custom context menu, which allows for the disabling of any blocks (not just top blocks). diff --git a/package-lock.json b/package-lock.json index 7664e44..e07fdfb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,6 @@ "devDependencies": { "@blockly/dev-scripts": "^4.0.9", "@blockly/dev-tools": "^8.1.2", - "@blockly/keyboard-navigation": "^0.6.14", "@blockly/workspace-backpack": "^6.0.16", "@playwright/test": "^1.58.2", "blockly": "^11.2.2" @@ -356,19 +355,6 @@ "eslint": "7.x" } }, - "node_modules/@blockly/keyboard-navigation": { - "version": "0.6.14", - "resolved": "https://registry.npmjs.org/@blockly/keyboard-navigation/-/keyboard-navigation-0.6.14.tgz", - "integrity": "sha512-CBPGQWZxaLvFkDJoa/bh5CYmC1K399ERBt0jmdowhcsk4mGcTDbdifAa71gke3hD8tviIPU0vBEZiSdDzSFlpA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8.17.0" - }, - "peerDependencies": { - "blockly": "^11.0.0" - } - }, "node_modules/@blockly/theme-dark": { "version": "7.0.12", "resolved": "https://registry.npmjs.org/@blockly/theme-dark/-/theme-dark-7.0.12.tgz", diff --git a/package.json b/package.json index 8c28667..8ba473e 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,6 @@ "devDependencies": { "@blockly/dev-scripts": "^4.0.9", "@blockly/dev-tools": "^8.1.2", - "@blockly/keyboard-navigation": "^0.6.14", "@blockly/workspace-backpack": "^6.0.16", "@playwright/test": "^1.58.2", "blockly": "^11.2.2" diff --git a/src/multiselect.js b/src/multiselect.js index 43ac28f..31dea26 100644 --- a/src/multiselect.js +++ b/src/multiselect.js @@ -41,7 +41,6 @@ export class Multiselect { this.useCopyPasteMenu_ = true; this.multiFieldUpdate_ = true; this.multiSelectKeys_ = ['shift']; - this.registeredShortcut_ = true; } /** @@ -140,16 +139,6 @@ export class Multiselect { Blockly.BlockSvg.prototype.bumpNeighbours = function() {}; } - Blockly.browserEvents.conditionalBind( - injectionDiv, 'keydown', this, this.unbindMultiselectCopyPaste_ - ); - - // This is for the keyboard navigation plugin and checks whether it puts - // the workspace into keyboard accessibility mode by default. - if (this.workspace_.keyboardAccessibilityMode) { - Shortcut.unregisterOurShortcut(); - this.registeredShortcut_ = false; - } } /** @@ -432,34 +421,6 @@ export class Multiselect { } } - /** - * Handle a keyboard navigation key-down on the workspace. - * @param {KeyboardEvent} e The keyboard event. - * @private - */ - unbindMultiselectCopyPaste_(e) { - // TODO: Update this to re-register/unregister the original shortcuts after - // Blockly/keyboard navigation plugin update - // This is to unregister the multiselect plugin's shortcuts - // when the user is in the keyboard navigation mode. Currently, - // when the user is in keyboard accessibility mode, they cannot - // use the normal copy/cut/paste functionalities. - // This is because the original (Blockly core) copy/cut/paste - // functions do not allow for collisions. This can be fixed - // either by allowing for collisions in the Blockly core - // copy/cut/paste functions or allowing for unregister/re-registering - // of the keyboard navigation plugin's copy/cut/paste functions. - if (this.workspace_.keyboardAccessibilityMode && - this.registeredShortcut_) { - Shortcut.unregisterOurShortcut(); - this.registeredShortcut_ = false; - } else if (!this.workspace_.keyboardAccessibilityMode && - !this.registeredShortcut_) { - Shortcut.registerOurShortcut(); - this.registeredShortcut_ = true; - } - } - /** * Handle a key-up on the workspace. * @param {KeyboardEvent} e The keyboard event. diff --git a/test/index.js b/test/index.js index 53c3386..8f29d4a 100644 --- a/test/index.js +++ b/test/index.js @@ -14,9 +14,7 @@ import {Multiselect} from '../src/index'; import {multiDraggableWeakMap} from '../src/global'; window.multiDraggableWeakMap = multiDraggableWeakMap; import {Backpack} from '@blockly/workspace-backpack'; -import {NavigationController} from '@blockly/keyboard-navigation'; -const navigationController = new NavigationController(); /** * Create a workspace. * @param {HTMLElement} blocklyDiv The blockly container div. @@ -30,8 +28,6 @@ function createWorkspace(blocklyDiv, options) { const backpack = new Backpack(workspace); backpack.init(); - navigationController.addWorkspace(workspace); - // Initialize multiselect plugin. const multiselectPlugin = new Multiselect(workspace); multiselectPlugin.init(options); @@ -40,8 +36,6 @@ function createWorkspace(blocklyDiv, options) { } Blockly.ContextMenuItems.registerCommentOptions(); -// Initialize keyboard nav plugin. -navigationController.init(); Blockly.Blocks['radix'] = { init() { From a5026166bc23dcd22b141153cf4877cb044bd6f4 Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Wed, 11 Mar 2026 11:59:40 -0700 Subject: [PATCH 02/67] Update dependencies for Blockly 12 --- package-lock.json | 214 ++++++++++++++-------------------------------- package.json | 10 +-- 2 files changed, 69 insertions(+), 155 deletions(-) diff --git a/package-lock.json b/package-lock.json index e07fdfb..6b61532 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,17 +12,17 @@ "dragselect": "^2.7.4" }, "devDependencies": { - "@blockly/dev-scripts": "^4.0.9", - "@blockly/dev-tools": "^8.1.2", - "@blockly/workspace-backpack": "^6.0.16", + "@blockly/dev-scripts": "^4.0.11", + "@blockly/dev-tools": "^9.0.8", + "@blockly/workspace-backpack": "^7.0.9", "@playwright/test": "^1.58.2", - "blockly": "^11.2.2" + "blockly": "^12.4.1" }, "engines": { "node": ">=8.17.0" }, "peerDependencies": { - "blockly": ">=11 <12" + "blockly": "^12.4.1" } }, "node_modules/@asamuzakjp/css-color": { @@ -260,22 +260,22 @@ } }, "node_modules/@blockly/block-test": { - "version": "6.0.13", - "resolved": "https://registry.npmjs.org/@blockly/block-test/-/block-test-6.0.13.tgz", - "integrity": "sha512-nq9qg9/azE73nlDY6bpvUGOjSR4TrQkRtpUTEGGQNXK9TdgwbW97F6Jfx5eutnghLdouRS09Hvo9PjuyffaD0g==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/@blockly/block-test/-/block-test-7.0.5.tgz", + "integrity": "sha512-havzWk4IZUDj/yjId4aWVsHWqeos1Jfp3Ackw56jhK/rOaJZKyRna6qV4g3YSxxIL+cBwNlj33odlNDEzLPOpg==", "dev": true, "license": "Apache 2.0", "engines": { "node": ">=8.17.0" }, "peerDependencies": { - "blockly": "^11.0.0" + "blockly": "^12.0.0" } }, "node_modules/@blockly/dev-scripts": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@blockly/dev-scripts/-/dev-scripts-4.0.9.tgz", - "integrity": "sha512-zoGpvXP7834nBtA2FwtbDbtjvXIvcZkybsrV9Hv+HyQsLxOohkvz9HMU143fV4D0HJTPkhHFC+dvlNIf2teZ9Q==", + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/@blockly/dev-scripts/-/dev-scripts-4.0.11.tgz", + "integrity": "sha512-3AAKRDi/awwOcB6pIaVf1UuZnT8DuKaEyZ4mcP8m0Wv99EhoPijyPlg5jWzuJd+1Ek32SQtqn0EdV6PxK6kF9A==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -309,17 +309,17 @@ } }, "node_modules/@blockly/dev-tools": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/@blockly/dev-tools/-/dev-tools-8.1.2.tgz", - "integrity": "sha512-K54oIKbaLZV3NGC9w4FaE+MzGmlVDxyTNWO+Vuq+WrGRa3F0cons9jvzaMs1RKyHRNbMoKcHy3VzALNalG46SA==", + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@blockly/dev-tools/-/dev-tools-9.0.8.tgz", + "integrity": "sha512-BRSO5k6Cnqc0ahSvu/4FX9/oOAkMHppO7lDrEp66UTeGrXKS94bQ1s9lzujC+pg8fLJBffMpBPcqvbSI6ghprg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@blockly/block-test": "^6.0.13", - "@blockly/theme-dark": "^7.0.12", - "@blockly/theme-deuteranopia": "^6.0.12", - "@blockly/theme-highcontrast": "^6.0.12", - "@blockly/theme-tritanopia": "^6.0.12", + "@blockly/block-test": "^7.0.5", + "@blockly/theme-dark": "^8.0.3", + "@blockly/theme-deuteranopia": "^7.0.3", + "@blockly/theme-highcontrast": "^7.0.3", + "@blockly/theme-tritanopia": "^7.0.3", "chai": "^4.2.0", "dat.gui": "^0.7.7", "lodash.assign": "^4.2.0", @@ -331,7 +331,7 @@ "node": ">=8.0.0" }, "peerDependencies": { - "blockly": "^11.0.0" + "blockly": "^12.0.0" } }, "node_modules/@blockly/eslint-config": { @@ -356,68 +356,68 @@ } }, "node_modules/@blockly/theme-dark": { - "version": "7.0.12", - "resolved": "https://registry.npmjs.org/@blockly/theme-dark/-/theme-dark-7.0.12.tgz", - "integrity": "sha512-DbRiiwSfVTAbAISnCQ1ismCxqc0lnSvtPL4t526X/m5WsLlMh0GT09ivDcP4ajXyWV4yk70wkhv3RYhZkmXuAQ==", + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@blockly/theme-dark/-/theme-dark-8.0.3.tgz", + "integrity": "sha512-ZS2jE9TpmQpxyP0jNpaEgTaEUJFYG885/kNiI9fImKWBHvjFxhyiwtn9UtLUPM07b/btbVWHNB+W7ZQoEbXxBA==", "dev": true, "license": "Apache-2.0", "engines": { "node": ">=8.17.0" }, "peerDependencies": { - "blockly": "^11.0.0" + "blockly": "^12.0.0" } }, "node_modules/@blockly/theme-deuteranopia": { - "version": "6.0.12", - "resolved": "https://registry.npmjs.org/@blockly/theme-deuteranopia/-/theme-deuteranopia-6.0.12.tgz", - "integrity": "sha512-4mlt36kwlaYi0j9IzmPmJTb2XsaOL3bLvHQVTpqoCqFivYY56VpNudZU9aGmknvhH0swNO5VXFrvCcPgWyrruQ==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@blockly/theme-deuteranopia/-/theme-deuteranopia-7.0.3.tgz", + "integrity": "sha512-sUsS7IP6MOFG8UGea0PCEqC5J8gxTAKvdU6ke/SKUT7cgyviUd4cE5nW/xbVSZy3T/yhDaoRu/7x8LAPYP+w9A==", "dev": true, "license": "Apache-2.0", "engines": { "node": ">=8.17.0" }, "peerDependencies": { - "blockly": "^11.0.0" + "blockly": "^12.0.0" } }, "node_modules/@blockly/theme-highcontrast": { - "version": "6.0.12", - "resolved": "https://registry.npmjs.org/@blockly/theme-highcontrast/-/theme-highcontrast-6.0.12.tgz", - "integrity": "sha512-n//Cg7cJFC8A7FXiwoxuoYorFcKk6MQqyUpYZsr2iQI3XMrqqgn0XAyEyvIZRgcqeoRK5g8wSDNiDnGC94McBw==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@blockly/theme-highcontrast/-/theme-highcontrast-7.0.3.tgz", + "integrity": "sha512-1EY07djfcAEqA6/P2Tv8YaUYSLluzPeCGB/6KpgI0lHxhsutu8cqMhqixdJB0tOiOeuBTNmVnBDSUd0HsQkNuw==", "dev": true, "license": "Apache-2.0", "engines": { "node": ">=8.17.0" }, "peerDependencies": { - "blockly": "^11.0.0" + "blockly": "^12.0.0" } }, "node_modules/@blockly/theme-tritanopia": { - "version": "6.0.12", - "resolved": "https://registry.npmjs.org/@blockly/theme-tritanopia/-/theme-tritanopia-6.0.12.tgz", - "integrity": "sha512-KYpKJiGKNy7w4/Nm7oBYe7ezSK/PRGZPh7pDbC+In6OyQm5CXCKaG6EVmA0tgbJ8urJusyOydlPfTFj1QIIO8g==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@blockly/theme-tritanopia/-/theme-tritanopia-7.0.3.tgz", + "integrity": "sha512-poo/pf/qI7PKZTgUhAVO5KZ1c0m4cDxB1CA95tocF9WSOTkqjSxxWDW2vdtb5HhPpgosYat0pWGEtTGYGLToJg==", "dev": true, "license": "Apache-2.0", "engines": { "node": ">=8.17.0" }, "peerDependencies": { - "blockly": "^11.0.0" + "blockly": "^12.0.0" } }, "node_modules/@blockly/workspace-backpack": { - "version": "6.0.16", - "resolved": "https://registry.npmjs.org/@blockly/workspace-backpack/-/workspace-backpack-6.0.16.tgz", - "integrity": "sha512-jZZgThFkGEBGYcBtWzMTTFE3XkepqIBTTcLVKODhw5zPNABtGRwcys4wh/I7c925Wbbm+pKfDa3XdzjbgkQGVQ==", + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/@blockly/workspace-backpack/-/workspace-backpack-7.0.9.tgz", + "integrity": "sha512-aYCGsmBFVXafBRkS087cMRyH58jR15Xu8g44794xZBdfce6HGPKw+wN1bDKTZXnisaMR3F6IgJpYl3WV+wjsKw==", "dev": true, "license": "Apache-2.0", "engines": { "node": ">=8.17.0" }, "peerDependencies": { - "blockly": "^11.0.0" + "blockly": "^12.0.0" } }, "node_modules/@csstools/color-helpers": { @@ -1712,13 +1712,6 @@ "node": ">=8" } }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, - "license": "MIT" - }, "node_modules/babel-eslint": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz", @@ -1799,13 +1792,13 @@ } }, "node_modules/blockly": { - "version": "11.2.2", - "resolved": "https://registry.npmjs.org/blockly/-/blockly-11.2.2.tgz", - "integrity": "sha512-YJW9jMz4qoBXzOOUqxWBOHL35QhOOTAg2a6sVAFJh+/uNTXW0z506JS7qkqZI4PQ9fnUhDqmhF8a0qWSTL1tjg==", + "version": "12.4.1", + "resolved": "https://registry.npmjs.org/blockly/-/blockly-12.4.1.tgz", + "integrity": "sha512-OEF0r8cFMGDkQbX+PWTjifWTe9xi2QzpZS4rO2lYeQhZDWW3/eInklLSdoxAyEQCdfJhQqMTpBct13oDoc0GVQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "jsdom": "25.0.1" + "jsdom": "26.1.0" }, "engines": { "node": ">=18" @@ -2201,19 +2194,6 @@ "dev": true, "license": "MIT" }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -2390,13 +2370,6 @@ "node": ">=18" } }, - "node_modules/cssstyle/node_modules/rrweb-cssom": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", - "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", - "dev": true, - "license": "MIT" - }, "node_modules/dat.gui": { "version": "0.7.9", "resolved": "https://registry.npmjs.org/dat.gui/-/dat.gui-0.7.9.tgz", @@ -2529,16 +2502,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -2782,22 +2745,6 @@ "node": ">= 0.4" } }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "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/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -3503,23 +3450,6 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", - "dev": true, - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -3818,22 +3748,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "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", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -4433,31 +4347,30 @@ } }, "node_modules/jsdom": { - "version": "25.0.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz", - "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", "dependencies": { - "cssstyle": "^4.1.0", + "cssstyle": "^4.2.1", "data-urls": "^5.0.0", - "decimal.js": "^10.4.3", - "form-data": "^4.0.0", + "decimal.js": "^10.5.0", "html-encoding-sniffer": "^4.0.0", "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.5", + "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.12", - "parse5": "^7.1.2", - "rrweb-cssom": "^0.7.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", - "tough-cookie": "^5.0.0", + "tough-cookie": "^5.1.1", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^3.1.1", "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^14.0.0", + "whatwg-url": "^14.1.1", "ws": "^8.18.0", "xml-name-validator": "^5.0.0" }, @@ -4465,7 +4378,7 @@ "node": ">=18" }, "peerDependencies": { - "canvas": "^2.11.2" + "canvas": "^3.0.0" }, "peerDependenciesMeta": { "canvas": { @@ -5046,9 +4959,9 @@ } }, "node_modules/nwsapi": { - "version": "2.2.22", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.22.tgz", - "integrity": "sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==", + "version": "2.2.23", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", + "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", "dev": true, "license": "MIT" }, @@ -5803,9 +5716,9 @@ } }, "node_modules/rrweb-cssom": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", - "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", "dev": true, "license": "MIT" }, @@ -7583,6 +7496,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 8ba473e..117bc12 100644 --- a/package.json +++ b/package.json @@ -43,14 +43,14 @@ "dragselect": "^2.7.4" }, "devDependencies": { - "@blockly/dev-scripts": "^4.0.9", - "@blockly/dev-tools": "^8.1.2", - "@blockly/workspace-backpack": "^6.0.16", + "@blockly/dev-scripts": "^4.0.11", + "@blockly/dev-tools": "^9.0.8", + "@blockly/workspace-backpack": "^7.0.9", "@playwright/test": "^1.58.2", - "blockly": "^11.2.2" + "blockly": "^12.4.1" }, "peerDependencies": { - "blockly": ">=11 <12" + "blockly": "^12.4.1" }, "publishConfig": {}, "eslintConfig": { From 178fe571a4b0f67d0a23c5d850107dbb50f9dc38 Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Sun, 29 Mar 2026 11:29:43 -0700 Subject: [PATCH 03/67] Blockly 12 renamed icon and checkbox CSS classes https://github.com/RaspberryPiFoundation/blockly/commit/9ba791c1446b088e423bfbbb55517ee8fbda5675 https://github.com/RaspberryPiFoundation/blockly/commit/f45270e0837b4da5c899943fae62bcefed28d836 --- test-e2e/actions/multiselect/block.spec.ts | 6 +++--- test-e2e/actions/select/block.spec.ts | 6 +++--- test-e2e/selection/multiselect/mode.spec.ts | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/test-e2e/actions/multiselect/block.spec.ts b/test-e2e/actions/multiselect/block.spec.ts index da5396b..b47bb24 100644 --- a/test-e2e/actions/multiselect/block.spec.ts +++ b/test-e2e/actions/multiselect/block.spec.ts @@ -570,17 +570,17 @@ test("edit block mutator", async ({ page, act }) => { ); await act(page.keyboard.up("Shift")); - await act(page.locator(`g[data-id="block1"] .blockly-icon-mutator`).click()); + await act(page.locator(`g[data-id="block1"] .blocklyMutatorIcon`).click()); expect(await getHighlightedBlockIds(page)).toEqual(["block1", "block2"]); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); - await act(page.locator(".blocklyCheckbox").click()); + await act(page.locator(".blocklyCheckboxField").click()); expect(await getHighlightedBlockIds(page)).toEqual(["block1", "block2"]); // TODO: Blockly bug — should be: // expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); expect(await getSelectedId(page)).not.toBe( await getMultiselectDraggableId(page), ); - await act(page.locator(`g[data-id="block1"] .blockly-icon-mutator`).click()); + await act(page.locator(`g[data-id="block1"] .blocklyMutatorIcon`).click()); expect(await getHighlightedBlockIds(page)).toEqual(["block1", "block2"]); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); diff --git a/test-e2e/actions/select/block.spec.ts b/test-e2e/actions/select/block.spec.ts index 35a0fd3..0deddb3 100644 --- a/test-e2e/actions/select/block.spec.ts +++ b/test-e2e/actions/select/block.spec.ts @@ -269,16 +269,16 @@ test("edit block mutator", async ({ page, act }) => { page.mouse.click(...(await getBlock(page, { id: "block1" })).centerTop), ); - await act(page.locator(`g[data-id="block1"] .blockly-icon-mutator`).click()); + await act(page.locator(`g[data-id="block1"] .blocklyMutatorIcon`).click()); expect(await getHighlightedBlockIds(page)).toEqual(["block1"]); expect(await getSelectedId(page)).toBe("block1"); - await act(page.locator(".blocklyCheckbox").click()); + await act(page.locator(".blocklyCheckboxField").click()); // TODO: Blockly bug — should be: // expect(await getHighlightedBlockIds(page)).toEqual(["block1"]); // expect(await getSelectedId(page)).toBe("block1"); expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).not.toBe("block1"); - await act(page.locator(`g[data-id="block1"] .blockly-icon-mutator`).click()); + await act(page.locator(`g[data-id="block1"] .blocklyMutatorIcon`).click()); expect(await getHighlightedBlockIds(page)).toEqual(["block1"]); expect(await getSelectedId(page)).toBe("block1"); diff --git a/test-e2e/selection/multiselect/mode.spec.ts b/test-e2e/selection/multiselect/mode.spec.ts index e402bb1..128bc0d 100644 --- a/test-e2e/selection/multiselect/mode.spec.ts +++ b/test-e2e/selection/multiselect/mode.spec.ts @@ -142,7 +142,7 @@ test("pressing shift while mutator is open enables multiselect", async ({ await act( page.mouse.click(...(await getBlock(page, { id: "block1" })).centerTop), ); - await act(page.locator(`g[data-id="block1"] .blockly-icon-mutator`).click()); + await act(page.locator(`g[data-id="block1"] .blocklyMutatorIcon`).click()); await act(page.keyboard.down("Shift")); @@ -157,7 +157,7 @@ test("releasing shift while mutator is open disables multiselect", async ({ await act( page.mouse.click(...(await getBlock(page, { id: "block1" })).centerTop), ); - await act(page.locator(`g[data-id="block1"] .blockly-icon-mutator`).click()); + await act(page.locator(`g[data-id="block1"] .blocklyMutatorIcon`).click()); await act(page.keyboard.down("Shift")); await act(page.keyboard.up("Shift")); From 34f8f3947b9df9ba9d123c2d6c66c9a9a01e969b Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Thu, 12 Mar 2026 08:05:55 -0700 Subject: [PATCH 04/67] Blockly 12 no longer preserves IDs when pasting https://github.com/RaspberryPiFoundation/blockly/commit/c661dd1c946d12a41b2fc14b0d1deb5e3e3115f3 --- .../multiselect/workspace/block.spec.ts | 19 ++++++------------- .../multiselect/workspace/comment.spec.ts | 15 ++++++--------- .../actions/select/workspace/block.spec.ts | 10 +++++++--- .../actions/select/workspace/comment.spec.ts | 10 +++++++--- 4 files changed, 26 insertions(+), 28 deletions(-) diff --git a/test-e2e/actions/multiselect/workspace/block.spec.ts b/test-e2e/actions/multiselect/workspace/block.spec.ts index 6fa79d2..755aad6 100644 --- a/test-e2e/actions/multiselect/workspace/block.spec.ts +++ b/test-e2e/actions/multiselect/workspace/block.spec.ts @@ -200,21 +200,14 @@ test("copy and paste blocks via context menu", async ({ page, act }) => { test("cut and paste blocks via keyboard", async ({ page, act }) => { await act(page.keyboard.press("Control+X")); expect(await getAllBlockIds(page)).toEqual(["block4"]); + expect(await getHighlightedBlockIds(page)).toEqual([]); + expect(await getSelectedId(page)).toBeNull(); await act(page.keyboard.press("Control+V")); - expect(await getAllBlockIds(page)).toEqual([ - "block1", - "block2", - "block2-child", - "block3", - "block3-child", - "block4", - ]); - expect(await getHighlightedBlockIds(page)).toEqual([ - "block1", - "block2", - "block3", - ]); + expect(await getAllBlockIds(page)).toHaveLength(6); + const highlightedBlockIds = await getHighlightedBlockIds(page); + expect(highlightedBlockIds).toHaveLength(3); + expect(highlightedBlockIds).not.toContain("block4"); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); }); diff --git a/test-e2e/actions/multiselect/workspace/comment.spec.ts b/test-e2e/actions/multiselect/workspace/comment.spec.ts index 23e4ad2..071a74d 100644 --- a/test-e2e/actions/multiselect/workspace/comment.spec.ts +++ b/test-e2e/actions/multiselect/workspace/comment.spec.ts @@ -127,17 +127,14 @@ test("copy and paste comments via context menu", async ({ page, act }) => { test("cut and paste comments via keyboard", async ({ page, act }) => { await act(page.keyboard.press("Control+X")); expect(await getAllCommentIds(page)).toEqual(["comment3"]); + expect(await getHighlightedCommentIds(page)).toEqual([]); + expect(await getSelectedId(page)).toBeNull(); await act(page.keyboard.press("Control+V")); - expect(await getAllCommentIds(page)).toEqual([ - "comment1", - "comment2", - "comment3", - ]); - expect(await getHighlightedCommentIds(page)).toEqual([ - "comment1", - "comment2", - ]); + expect(await getAllCommentIds(page)).toHaveLength(3); + const highlightedCommentIds = await getHighlightedCommentIds(page); + expect(highlightedCommentIds).toHaveLength(2); + expect(highlightedCommentIds).not.toContain("comment3"); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); }); diff --git a/test-e2e/actions/select/workspace/block.spec.ts b/test-e2e/actions/select/workspace/block.spec.ts index 503ac2c..767c015 100644 --- a/test-e2e/actions/select/workspace/block.spec.ts +++ b/test-e2e/actions/select/workspace/block.spec.ts @@ -94,11 +94,15 @@ test("copy and paste block via context menu", async ({ page, act }) => { test("cut and paste block via keyboard", async ({ page, act }) => { await act(page.keyboard.press("Control+X")); expect(await getAllBlockIds(page)).toEqual(["block2"]); + expect(await getHighlightedBlockIds(page)).toEqual([]); + expect(await getSelectedId(page)).toBeNull(); await act(page.keyboard.press("Control+V")); - expect(await getAllBlockIds(page)).toEqual(["block1", "block2"]); - expect(await getHighlightedBlockIds(page)).toEqual(["block1"]); - expect(await getSelectedId(page)).toBe("block1"); + expect(await getAllBlockIds(page)).toHaveLength(2); + const highlightedBlockIds = await getHighlightedBlockIds(page); + expect(highlightedBlockIds).toHaveLength(1); + expect(highlightedBlockIds).not.toContain("block2"); + expect(await getSelectedId(page)).toBe(highlightedBlockIds[0]); }); test("delete block via keyboard", async ({ page, act }) => { diff --git a/test-e2e/actions/select/workspace/comment.spec.ts b/test-e2e/actions/select/workspace/comment.spec.ts index 185645f..9e12b18 100644 --- a/test-e2e/actions/select/workspace/comment.spec.ts +++ b/test-e2e/actions/select/workspace/comment.spec.ts @@ -88,11 +88,15 @@ test("copy and paste comment via context menu", async ({ page, act }) => { test("cut and paste comment via keyboard", async ({ page, act }) => { await act(page.keyboard.press("Control+X")); expect(await getAllCommentIds(page)).toEqual(["comment2"]); + expect(await getHighlightedCommentIds(page)).toEqual([]); + expect(await getSelectedId(page)).toBeNull(); await act(page.keyboard.press("Control+V")); - expect(await getAllCommentIds(page)).toEqual(["comment1", "comment2"]); - expect(await getHighlightedCommentIds(page)).toEqual(["comment1"]); - expect(await getSelectedId(page)).toBe("comment1"); + expect(await getAllCommentIds(page)).toHaveLength(2); + const highlightedCommentIds = await getHighlightedCommentIds(page); + expect(highlightedCommentIds).toHaveLength(1); + expect(highlightedCommentIds).not.toContain("comment2"); + expect(await getSelectedId(page)).toBe(highlightedCommentIds[0]); }); test("delete comment via keyboard", async ({ page, act }) => { From e1b6cf82d55e6a65a981582d84b2f9d1850ae91f Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Tue, 24 Mar 2026 09:13:24 -0700 Subject: [PATCH 05/67] Blockly 12 selects next block after deleting a block https://github.com/RaspberryPiFoundation/blockly/commit/135da402ef0035ce5a44d4a5d5d73d576e92bc82 --- .../actions/multiselect/workspace/block.spec.ts | 12 ++++++------ test-e2e/actions/select/workspace/block.spec.ts | 16 ++++++++-------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/test-e2e/actions/multiselect/workspace/block.spec.ts b/test-e2e/actions/multiselect/workspace/block.spec.ts index 755aad6..9bb6304 100644 --- a/test-e2e/actions/multiselect/workspace/block.spec.ts +++ b/test-e2e/actions/multiselect/workspace/block.spec.ts @@ -200,8 +200,8 @@ test("copy and paste blocks via context menu", async ({ page, act }) => { test("cut and paste blocks via keyboard", async ({ page, act }) => { await act(page.keyboard.press("Control+X")); expect(await getAllBlockIds(page)).toEqual(["block4"]); - expect(await getHighlightedBlockIds(page)).toEqual([]); - expect(await getSelectedId(page)).toBeNull(); + expect(await getHighlightedBlockIds(page)).toEqual(["block4"]); + expect(await getSelectedId(page)).toBe("block4"); await act(page.keyboard.press("Control+V")); expect(await getAllBlockIds(page)).toHaveLength(6); @@ -215,8 +215,8 @@ test("delete blocks via keyboard", async ({ page, act }) => { await act(page.keyboard.press("Delete")); expect(await getAllBlockIds(page)).toEqual(["block4"]); - expect(await getHighlightedBlockIds(page)).toEqual([]); - expect(await getSelectedId(page)).toBeNull(); + expect(await getHighlightedBlockIds(page)).toEqual(["block4"]); + expect(await getSelectedId(page)).toBe("block4"); }); test("delete blocks via context menu", async ({ page, act }) => { @@ -239,8 +239,8 @@ test("delete blocks via context menu", async ({ page, act }) => { ); expect(await getAllBlockIds(page)).toEqual(["block4"]); - expect(await getHighlightedBlockIds(page)).toEqual([]); - expect(await getSelectedId(page)).toBeNull(); + expect(await getHighlightedBlockIds(page)).toEqual(["block4"]); + expect(await getSelectedId(page)).toBe("block4"); }); test("drag blocks to trash", async ({ page, act }) => { diff --git a/test-e2e/actions/select/workspace/block.spec.ts b/test-e2e/actions/select/workspace/block.spec.ts index 767c015..5aa381d 100644 --- a/test-e2e/actions/select/workspace/block.spec.ts +++ b/test-e2e/actions/select/workspace/block.spec.ts @@ -94,8 +94,8 @@ test("copy and paste block via context menu", async ({ page, act }) => { test("cut and paste block via keyboard", async ({ page, act }) => { await act(page.keyboard.press("Control+X")); expect(await getAllBlockIds(page)).toEqual(["block2"]); - expect(await getHighlightedBlockIds(page)).toEqual([]); - expect(await getSelectedId(page)).toBeNull(); + expect(await getHighlightedBlockIds(page)).toEqual(["block2"]); + expect(await getSelectedId(page)).toBe("block2"); await act(page.keyboard.press("Control+V")); expect(await getAllBlockIds(page)).toHaveLength(2); @@ -109,8 +109,8 @@ test("delete block via keyboard", async ({ page, act }) => { await act(page.keyboard.press("Delete")); expect(await getAllBlockIds(page)).toEqual(["block2"]); - expect(await getHighlightedBlockIds(page)).toEqual([]); - expect(await getSelectedId(page)).toBeNull(); + expect(await getHighlightedBlockIds(page)).toEqual(["block2"]); + expect(await getSelectedId(page)).toBe("block2"); }); test("delete block via context menu", async ({ page, act }) => { @@ -126,8 +126,8 @@ test("delete block via context menu", async ({ page, act }) => { ); expect(await getAllBlockIds(page)).toEqual(["block2"]); - expect(await getHighlightedBlockIds(page)).toEqual([]); - expect(await getSelectedId(page)).toBeNull(); + expect(await getHighlightedBlockIds(page)).toEqual(["block2"]); + expect(await getSelectedId(page)).toBe("block2"); }); test("drag block to trash", async ({ page, act }) => { @@ -139,8 +139,8 @@ test("drag block to trash", async ({ page, act }) => { await act(page.mouse.up()); expect(await getAllBlockIds(page)).toEqual(["block2"]); - expect(await getHighlightedBlockIds(page)).toEqual([]); - expect(await getSelectedId(page)).toBe("block1"); + expect(await getHighlightedBlockIds(page)).toEqual(["block2"]); + expect(await getSelectedId(page)).toBe("block2"); await openTrash(page); await getBlock(page, { type: "logic_boolean", workspace: "trash" }); From f84c3e244117d8bc6bf42f9b032b1ab5722b0ca7 Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Tue, 14 Apr 2026 13:44:33 -0700 Subject: [PATCH 06/67] Blockly 12 focuses workspace after deleting comment https://github.com/RaspberryPiFoundation/blockly/commit/4f01c9937aa0d66891b6bdfc2cf3ef3e6f3fdcd6 --- test-e2e/actions/multiselect/workspace/comment.spec.ts | 2 +- test-e2e/actions/select/workspace/comment.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test-e2e/actions/multiselect/workspace/comment.spec.ts b/test-e2e/actions/multiselect/workspace/comment.spec.ts index 071a74d..f2956b6 100644 --- a/test-e2e/actions/multiselect/workspace/comment.spec.ts +++ b/test-e2e/actions/multiselect/workspace/comment.spec.ts @@ -179,7 +179,7 @@ test("drag comments to trash", async ({ page, act }) => { expect(await getAllCommentIds(page)).toEqual(["comment3"]); expect(await getHighlightedCommentIds(page)).toEqual([]); - expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); + expect(await getSelectedId(page)).toBeNull(); }); test("undo via keyboard", async ({ page, act }) => { diff --git a/test-e2e/actions/select/workspace/comment.spec.ts b/test-e2e/actions/select/workspace/comment.spec.ts index 9e12b18..70e6ae4 100644 --- a/test-e2e/actions/select/workspace/comment.spec.ts +++ b/test-e2e/actions/select/workspace/comment.spec.ts @@ -132,7 +132,7 @@ test("drag comment to trash", async ({ page, act }) => { expect(await getAllCommentIds(page)).toEqual(["comment2"]); expect(await getHighlightedCommentIds(page)).toEqual([]); - expect(await getSelectedId(page)).toBe("comment1"); + expect(await getSelectedId(page)).toBeNull(); }); test("undo via keyboard", async ({ page, act }) => { From e0230a428e5f6cb82e763538c807ff40d0d290e9 Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Mon, 6 Apr 2026 16:34:18 -0700 Subject: [PATCH 07/67] Blockly 12 temporarily removes highlight during interactions https://github.com/RaspberryPiFoundation/blockly/commit/65bc2b5f180cd553dfdf1336222c653841e --- test-e2e/actions/multiselect/block.spec.ts | 48 +++---------------- test-e2e/actions/multiselect/field.spec.ts | 18 ++----- .../multiselect/workspace/block.spec.ts | 21 ++------ .../multiselect/workspace/comment.spec.ts | 15 ++---- test-e2e/actions/select/block.spec.ts | 12 ++--- test-e2e/actions/select/field.spec.ts | 4 +- .../actions/select/workspace/block.spec.ts | 6 +-- .../actions/select/workspace/comment.spec.ts | 6 +-- 8 files changed, 29 insertions(+), 101 deletions(-) diff --git a/test-e2e/actions/multiselect/block.spec.ts b/test-e2e/actions/multiselect/block.spec.ts index b47bb24..4f472f5 100644 --- a/test-e2e/actions/multiselect/block.spec.ts +++ b/test-e2e/actions/multiselect/block.spec.ts @@ -132,13 +132,7 @@ test("add comment to blocks", async ({ page, act }) => { button: "right", }), ); - expect(await getHighlightedBlockIds(page)).toEqual([ - "block2", - "block3", - "block4", - "block5", - "block6", - ]); + expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); await act( page @@ -214,13 +208,7 @@ test("remove comment from blocks", async ({ page, act }) => { button: "right", }), ); - expect(await getHighlightedBlockIds(page)).toEqual([ - "block2", - "block3", - "block4", - "block5", - "block6", - ]); + expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); await act( page @@ -280,13 +268,7 @@ test("switch blocks to external inputs", async ({ page, act }) => { button: "right", }), ); - expect(await getHighlightedBlockIds(page)).toEqual([ - "block2", - "block3", - "block4", - "block5", - "block6", - ]); + expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); await act( page @@ -346,13 +328,7 @@ test("switch blocks to inline inputs", async ({ page, act }) => { button: "right", }), ); - expect(await getHighlightedBlockIds(page)).toEqual([ - "block2", - "block3", - "block4", - "block5", - "block6", - ]); + expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); await act( page @@ -412,13 +388,7 @@ test("disable blocks", async ({ page, act }) => { button: "right", }), ); - expect(await getHighlightedBlockIds(page)).toEqual([ - "block2", - "block3", - "block4", - "block5", - "block6", - ]); + expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); await act( page @@ -478,13 +448,7 @@ test("enable blocks", async ({ page, act }) => { button: "right", }), ); - expect(await getHighlightedBlockIds(page)).toEqual([ - "block2", - "block3", - "block4", - "block5", - "block6", - ]); + expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); await act( page diff --git a/test-e2e/actions/multiselect/field.spec.ts b/test-e2e/actions/multiselect/field.spec.ts index f63cdeb..e8c46fd 100644 --- a/test-e2e/actions/multiselect/field.spec.ts +++ b/test-e2e/actions/multiselect/field.spec.ts @@ -42,11 +42,7 @@ test("editing boolean field updates selected boolean blocks", async ({ ...(await getBlock(page, { id: "block1" })).fields.BOOL.center, ), ); - expect(await getHighlightedBlockIds(page)).toEqual([ - "block1", - "block2", - "block3", - ]); + expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); await act(page.getByRole("option", { name: "false", exact: true }).click()); @@ -134,11 +130,7 @@ test("editing number field updates selected number blocks", async ({ ...(await getBlock(page, { id: "block1" })).fields.NUM.center, ), ); - expect(await getHighlightedBlockIds(page)).toEqual([ - "block1", - "block2", - "block3", - ]); + expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); await act(page.keyboard.type("4")); expect((await getBlock(page, { id: "block1" })).fields.NUM.value).toBe(4); @@ -219,11 +211,7 @@ test("dependent field recalculated during multi-edit", async ({ ...(await getBlock(page, { id: "block1" })).fields.RADIX.center, ), ); - expect(await getHighlightedBlockIds(page)).toEqual([ - "block1", - "block2", - "block3", - ]); + expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); await act(page.getByRole("option", { name: "binary", exact: true }).click()); diff --git a/test-e2e/actions/multiselect/workspace/block.spec.ts b/test-e2e/actions/multiselect/workspace/block.spec.ts index 9bb6304..0a2d75f 100644 --- a/test-e2e/actions/multiselect/workspace/block.spec.ts +++ b/test-e2e/actions/multiselect/workspace/block.spec.ts @@ -60,12 +60,7 @@ test("duplicate blocks via context menu", async ({ page, act }) => { button: "right", }), ); - expect(await getHighlightedBlockIds(page)).toEqual([ - "block1", - "block2", - "block2-child", - "block3", - ]); + expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); await act( page.getByRole("menuitem", { exact: true, name: "Duplicate (3)" }).click(), @@ -143,12 +138,7 @@ test("copy and paste blocks via context menu", async ({ page, act }) => { button: "right", }), ); - expect(await getHighlightedBlockIds(page)).toEqual([ - "block1", - "block2", - "block2-child", - "block3", - ]); + expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); await act( page.getByRole("menuitem", { exact: true, name: "Copy (3)" }).click(), @@ -225,12 +215,7 @@ test("delete blocks via context menu", async ({ page, act }) => { button: "right", }), ); - expect(await getHighlightedBlockIds(page)).toEqual([ - "block1", - "block2", - "block2-child", - "block3", - ]); + expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); await act( page diff --git a/test-e2e/actions/multiselect/workspace/comment.spec.ts b/test-e2e/actions/multiselect/workspace/comment.spec.ts index f2956b6..352a319 100644 --- a/test-e2e/actions/multiselect/workspace/comment.spec.ts +++ b/test-e2e/actions/multiselect/workspace/comment.spec.ts @@ -36,10 +36,7 @@ test("duplicate comments via context menu", async ({ page, act }) => { button: "right", }), ); - expect(await getHighlightedCommentIds(page)).toEqual([ - "comment1", - "comment2", - ]); + expect(await getHighlightedCommentIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); await act( page @@ -88,10 +85,7 @@ test("copy and paste comments via context menu", async ({ page, act }) => { button: "right", }), ); - expect(await getHighlightedCommentIds(page)).toEqual([ - "comment1", - "comment2", - ]); + expect(await getHighlightedCommentIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); await act( page.getByRole("menuitem", { exact: true, name: "Copy (2)" }).click(), @@ -152,10 +146,7 @@ test("delete comments via context menu", async ({ page, act }) => { button: "right", }), ); - expect(await getHighlightedCommentIds(page)).toEqual([ - "comment1", - "comment2", - ]); + expect(await getHighlightedCommentIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); await act( page diff --git a/test-e2e/actions/select/block.spec.ts b/test-e2e/actions/select/block.spec.ts index 0deddb3..a366b65 100644 --- a/test-e2e/actions/select/block.spec.ts +++ b/test-e2e/actions/select/block.spec.ts @@ -65,7 +65,7 @@ test("add comment to block", async ({ page, act }) => { button: "right", }), ); - expect(await getHighlightedBlockIds(page)).toEqual(["block1"]); + expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe("block1"); await act( page.getByRole("menuitem", { exact: true, name: "Add Comment" }).click(), @@ -101,7 +101,7 @@ test("remove comment from block", async ({ page, act }) => { button: "right", }), ); - expect(await getHighlightedBlockIds(page)).toEqual(["block1"]); + expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe("block1"); await act( page.getByRole("menuitem", { exact: true, name: "Remove Comment" }).click(), @@ -129,7 +129,7 @@ test("switch block to external inputs", async ({ page, act }) => { button: "right", }), ); - expect(await getHighlightedBlockIds(page)).toEqual(["block1"]); + expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe("block1"); await act( page @@ -159,7 +159,7 @@ test("switch block to inline inputs", async ({ page, act }) => { button: "right", }), ); - expect(await getHighlightedBlockIds(page)).toEqual(["block1"]); + expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe("block1"); await act( page.getByRole("menuitem", { exact: true, name: "Inline Inputs" }).click(), @@ -187,7 +187,7 @@ test("disable block", async ({ page, act }) => { button: "right", }), ); - expect(await getHighlightedBlockIds(page)).toEqual(["block1"]); + expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe("block1"); await act( page.getByRole("menuitem", { exact: true, name: "Disable Block" }).click(), @@ -215,7 +215,7 @@ test("enable block", async ({ page, act }) => { button: "right", }), ); - expect(await getHighlightedBlockIds(page)).toEqual(["block1"]); + expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe("block1"); await act( page.getByRole("menuitem", { exact: true, name: "Enable Block" }).click(), diff --git a/test-e2e/actions/select/field.spec.ts b/test-e2e/actions/select/field.spec.ts index 6bb5bc7..3096898 100644 --- a/test-e2e/actions/select/field.spec.ts +++ b/test-e2e/actions/select/field.spec.ts @@ -23,7 +23,7 @@ test("edit boolean field", async ({ page, act }) => { ...(await getBlock(page, { id: "block1" })).fields.BOOL.center, ), ); - expect(await getHighlightedBlockIds(page)).toEqual(["block1"]); + expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe("block1"); await act(page.getByRole("option", { name: "false", exact: true }).click()); @@ -53,7 +53,7 @@ test("edit number field", async ({ page, act }) => { ...(await getBlock(page, { id: "block1" })).fields.NUM.center, ), ); - expect(await getHighlightedBlockIds(page)).toEqual(["block1"]); + expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe("block1"); await act(page.keyboard.type("42")); await act(page.keyboard.press("Enter")); diff --git a/test-e2e/actions/select/workspace/block.spec.ts b/test-e2e/actions/select/workspace/block.spec.ts index 5aa381d..f040d90 100644 --- a/test-e2e/actions/select/workspace/block.spec.ts +++ b/test-e2e/actions/select/workspace/block.spec.ts @@ -32,7 +32,7 @@ test("duplicate block via context menu", async ({ page, act }) => { button: "right", }), ); - expect(await getHighlightedBlockIds(page)).toEqual(["block1"]); + expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe("block1"); await act( page.getByRole("menuitem", { exact: true, name: "Duplicate" }).click(), @@ -69,7 +69,7 @@ test("copy and paste block via context menu", async ({ page, act }) => { button: "right", }), ); - expect(await getHighlightedBlockIds(page)).toEqual(["block1"]); + expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe("block1"); await act(page.getByRole("menuitem", { exact: true, name: "Copy" }).click()); expect(await getAllBlockIds(page)).toEqual(["block1", "block2"]); @@ -119,7 +119,7 @@ test("delete block via context menu", async ({ page, act }) => { button: "right", }), ); - expect(await getHighlightedBlockIds(page)).toEqual(["block1"]); + expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe("block1"); await act( page.getByRole("menuitem", { exact: true, name: "Delete Block" }).click(), diff --git a/test-e2e/actions/select/workspace/comment.spec.ts b/test-e2e/actions/select/workspace/comment.spec.ts index 70e6ae4..37e3663 100644 --- a/test-e2e/actions/select/workspace/comment.spec.ts +++ b/test-e2e/actions/select/workspace/comment.spec.ts @@ -24,7 +24,7 @@ test("duplicate comment via context menu", async ({ page, act }) => { button: "right", }), ); - expect(await getHighlightedCommentIds(page)).toEqual(["comment1"]); + expect(await getHighlightedCommentIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe("comment1"); await act( page @@ -63,7 +63,7 @@ test("copy and paste comment via context menu", async ({ page, act }) => { button: "right", }), ); - expect(await getHighlightedCommentIds(page)).toEqual(["comment1"]); + expect(await getHighlightedCommentIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe("comment1"); await act(page.getByRole("menuitem", { exact: true, name: "Copy" }).click()); expect(await getAllCommentIds(page)).toEqual(["comment1", "comment2"]); @@ -113,7 +113,7 @@ test("delete comment via context menu", async ({ page, act }) => { button: "right", }), ); - expect(await getHighlightedCommentIds(page)).toEqual(["comment1"]); + expect(await getHighlightedCommentIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe("comment1"); await act( page.getByRole("menuitem", { exact: true, name: "Remove Comment" }).click(), From 88e32a74c9af1ea0feed469531a90eae215bfe0b Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Mon, 6 Apr 2026 16:55:03 -0700 Subject: [PATCH 08/67] Blockly 12 deselects block when opening toolbox https://github.com/RaspberryPiFoundation/blockly/commit/cac8f0116ce6c724c8062110e9cad02b8db --- test-e2e/actions/select/workspace/block.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test-e2e/actions/select/workspace/block.spec.ts b/test-e2e/actions/select/workspace/block.spec.ts index f040d90..11dd84c 100644 --- a/test-e2e/actions/select/workspace/block.spec.ts +++ b/test-e2e/actions/select/workspace/block.spec.ts @@ -278,8 +278,8 @@ test("drag block", async ({ page, act }) => { test("dragging block from toolbox selects new block", async ({ page, act }) => { await act(page.getByRole("treeitem", { name: "Logic" }).click()); - expect(await getHighlightedBlockIds(page)).toEqual(["block1"]); - expect(await getSelectedId(page)).toBe("block1"); + expect(await getHighlightedBlockIds(page)).toEqual([]); + expect(await getSelectedId(page)).toBeNull(); await act( page.mouse.move( ...(await getBlock(page, { type: "controls_if", workspace: "toolbox" })) From 6a51947ac470bf99d1b6334a95215e3393840a98 Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Mon, 6 Apr 2026 17:33:30 -0700 Subject: [PATCH 09/67] Blockly 12 deselects block when clicking comment/mutator icon https://github.com/RaspberryPiFoundation/blockly/commit/65bc2b5f180cd553dfdf1336222c653841e --- test-e2e/actions/multiselect/block.spec.ts | 30 ++++++++-------------- test-e2e/actions/select/block.spec.ts | 24 +++++++---------- 2 files changed, 19 insertions(+), 35 deletions(-) diff --git a/test-e2e/actions/multiselect/block.spec.ts b/test-e2e/actions/multiselect/block.spec.ts index 4f472f5..913d936 100644 --- a/test-e2e/actions/multiselect/block.spec.ts +++ b/test-e2e/actions/multiselect/block.spec.ts @@ -499,22 +499,16 @@ test("edit block comment", async ({ page, act }) => { await act(page.keyboard.up("Shift")); await act(page.locator(`g[data-id="block1"] .blocklyIconGroup`).click()); - expect(await getHighlightedBlockIds(page)).toEqual(["block1", "block2"]); - expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); + expect(await getHighlightedBlockIds(page)).toEqual([]); + expect(await getSelectedId(page)).toBeNull(); await act(page.locator(".blocklyTextarea").click()); - // TODO: Blockly bug — should be: - // expect(await getHighlightedBlockIds(page)).toEqual(["block1", "block2"]); - // expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBeNull(); await act(page.keyboard.type("hello")); await act(page.locator(`g[data-id="block1"] .blocklyIconGroup`).click()); - // TODO: Blockly bug — should be: - // expect(await getHighlightedBlockIds(page)).toEqual(["block1", "block2"]); - // expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); - expect(await getHighlightedBlockIds(page)).toEqual(["block1"]); - expect(await getSelectedId(page)).toBe("block1"); + expect(await getHighlightedBlockIds(page)).toEqual([]); + expect(await getSelectedId(page)).toBeNull(); }); test("edit block mutator", async ({ page, act }) => { @@ -535,17 +529,13 @@ test("edit block mutator", async ({ page, act }) => { await act(page.keyboard.up("Shift")); await act(page.locator(`g[data-id="block1"] .blocklyMutatorIcon`).click()); - expect(await getHighlightedBlockIds(page)).toEqual(["block1", "block2"]); - expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); + expect(await getHighlightedBlockIds(page)).toEqual([]); + expect(await getSelectedId(page)).toBeNull(); await act(page.locator(".blocklyCheckboxField").click()); - expect(await getHighlightedBlockIds(page)).toEqual(["block1", "block2"]); - // TODO: Blockly bug — should be: - // expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); - expect(await getSelectedId(page)).not.toBe( - await getMultiselectDraggableId(page), - ); + expect(await getHighlightedBlockIds(page)).toEqual([]); + expect(await getSelectedId(page)).not.toBeNull(); await act(page.locator(`g[data-id="block1"] .blocklyMutatorIcon`).click()); - expect(await getHighlightedBlockIds(page)).toEqual(["block1", "block2"]); - expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); + expect(await getHighlightedBlockIds(page)).toEqual([]); + expect(await getSelectedId(page)).toBeNull(); }); diff --git a/test-e2e/actions/select/block.spec.ts b/test-e2e/actions/select/block.spec.ts index a366b65..7eff2f1 100644 --- a/test-e2e/actions/select/block.spec.ts +++ b/test-e2e/actions/select/block.spec.ts @@ -243,19 +243,16 @@ test("edit block comment", async ({ page, act }) => { ); await act(page.locator(`g[data-id="block1"] .blocklyIconGroup`).click()); - expect(await getHighlightedBlockIds(page)).toEqual(["block1"]); - expect(await getSelectedId(page)).toBe("block1"); + expect(await getHighlightedBlockIds(page)).toEqual([]); + expect(await getSelectedId(page)).toBeNull(); await act(page.locator(".blocklyTextarea").click()); - // TODO: Blockly bug — should be: - // expect(await getHighlightedBlockIds(page)).toEqual(["block1"]); - // expect(await getSelectedId(page)).toBe("block1"); expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBeNull(); await act(page.keyboard.type("hello")); await act(page.locator(`g[data-id="block1"] .blocklyIconGroup`).click()); - expect(await getHighlightedBlockIds(page)).toEqual(["block1"]); - expect(await getSelectedId(page)).toBe("block1"); + expect(await getHighlightedBlockIds(page)).toEqual([]); + expect(await getSelectedId(page)).toBeNull(); }); test("edit block mutator", async ({ page, act }) => { @@ -270,16 +267,13 @@ test("edit block mutator", async ({ page, act }) => { ); await act(page.locator(`g[data-id="block1"] .blocklyMutatorIcon`).click()); - expect(await getHighlightedBlockIds(page)).toEqual(["block1"]); - expect(await getSelectedId(page)).toBe("block1"); + expect(await getHighlightedBlockIds(page)).toEqual([]); + expect(await getSelectedId(page)).toBeNull(); await act(page.locator(".blocklyCheckboxField").click()); - // TODO: Blockly bug — should be: - // expect(await getHighlightedBlockIds(page)).toEqual(["block1"]); - // expect(await getSelectedId(page)).toBe("block1"); expect(await getHighlightedBlockIds(page)).toEqual([]); - expect(await getSelectedId(page)).not.toBe("block1"); + expect(await getSelectedId(page)).not.toBeNull(); await act(page.locator(`g[data-id="block1"] .blocklyMutatorIcon`).click()); - expect(await getHighlightedBlockIds(page)).toEqual(["block1"]); - expect(await getSelectedId(page)).toBe("block1"); + expect(await getHighlightedBlockIds(page)).toEqual([]); + expect(await getSelectedId(page)).toBeNull(); }); From 9bdc14e11a82d13bce6902a7f4f28df12621609f Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Tue, 7 Apr 2026 08:17:24 -0700 Subject: [PATCH 10/67] Blockly 12 uses ephemeral focus during context menus and field edits https://github.com/RaspberryPiFoundation/blockly/commit/3ccfba9c4b7d3953caec14a3cfc347c0205c9c06 --- test-e2e/actions/multiselect/block.spec.ts | 19 +++++++++++++++++++ test-e2e/actions/multiselect/field.spec.ts | 7 +++++++ .../multiselect/workspace/block.spec.ts | 7 +++++++ .../multiselect/workspace/comment.spec.ts | 7 +++++++ test-e2e/actions/select/block.spec.ts | 19 +++++++++++++++++++ test-e2e/actions/select/field.spec.ts | 5 +++++ .../actions/select/workspace/block.spec.ts | 9 +++++++++ .../actions/select/workspace/comment.spec.ts | 7 +++++++ test-e2e/test.ts | 3 +++ 9 files changed, 83 insertions(+) diff --git a/test-e2e/actions/multiselect/block.spec.ts b/test-e2e/actions/multiselect/block.spec.ts index 913d936..22c7548 100644 --- a/test-e2e/actions/multiselect/block.spec.ts +++ b/test-e2e/actions/multiselect/block.spec.ts @@ -4,6 +4,7 @@ import { getHighlightedBlockIds, getMultiselectDraggableId, getSelectedId, + isEphemeralFocusTaken, loadBlocks, test, } from "../../test"; @@ -134,6 +135,7 @@ test("add comment to blocks", async ({ page, act }) => { ); expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); + expect(await isEphemeralFocusTaken(page)).toBe(true); await act( page .getByRole("menuitem", { exact: true, name: "Add Comment (2)" }) @@ -155,6 +157,7 @@ test("add comment to blocks", async ({ page, act }) => { "block6", ]); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); + expect(await isEphemeralFocusTaken(page)).toBe(false); }); test("remove comment from blocks", async ({ page, act }) => { @@ -210,6 +213,7 @@ test("remove comment from blocks", async ({ page, act }) => { ); expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); + expect(await isEphemeralFocusTaken(page)).toBe(true); await act( page .getByRole("menuitem", { exact: true, name: "Remove Comment (3)" }) @@ -231,6 +235,7 @@ test("remove comment from blocks", async ({ page, act }) => { "block6", ]); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); + expect(await isEphemeralFocusTaken(page)).toBe(false); }); test("switch blocks to external inputs", async ({ page, act }) => { @@ -270,6 +275,7 @@ test("switch blocks to external inputs", async ({ page, act }) => { ); expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); + expect(await isEphemeralFocusTaken(page)).toBe(true); await act( page .getByRole("menuitem", { exact: true, name: "External Inputs (2)" }) @@ -291,6 +297,7 @@ test("switch blocks to external inputs", async ({ page, act }) => { "block6", ]); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); + expect(await isEphemeralFocusTaken(page)).toBe(false); }); test("switch blocks to inline inputs", async ({ page, act }) => { @@ -330,6 +337,7 @@ test("switch blocks to inline inputs", async ({ page, act }) => { ); expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); + expect(await isEphemeralFocusTaken(page)).toBe(true); await act( page .getByRole("menuitem", { exact: true, name: "Inline Inputs (3)" }) @@ -351,6 +359,7 @@ test("switch blocks to inline inputs", async ({ page, act }) => { "block6", ]); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); + expect(await isEphemeralFocusTaken(page)).toBe(false); }); test("disable blocks", async ({ page, act }) => { @@ -390,6 +399,7 @@ test("disable blocks", async ({ page, act }) => { ); expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); + expect(await isEphemeralFocusTaken(page)).toBe(true); await act( page .getByRole("menuitem", { exact: true, name: "Disable Block (2)" }) @@ -411,6 +421,7 @@ test("disable blocks", async ({ page, act }) => { "block6", ]); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); + expect(await isEphemeralFocusTaken(page)).toBe(false); }); test("enable blocks", async ({ page, act }) => { @@ -450,6 +461,7 @@ test("enable blocks", async ({ page, act }) => { ); expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); + expect(await isEphemeralFocusTaken(page)).toBe(true); await act( page .getByRole("menuitem", { exact: true, name: "Enable Block (3)" }) @@ -471,6 +483,7 @@ test("enable blocks", async ({ page, act }) => { "block6", ]); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); + expect(await isEphemeralFocusTaken(page)).toBe(false); }); test("edit block comment", async ({ page, act }) => { @@ -501,14 +514,17 @@ test("edit block comment", async ({ page, act }) => { await act(page.locator(`g[data-id="block1"] .blocklyIconGroup`).click()); expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBeNull(); + expect(await isEphemeralFocusTaken(page)).toBe(false); await act(page.locator(".blocklyTextarea").click()); expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBeNull(); + expect(await isEphemeralFocusTaken(page)).toBe(false); await act(page.keyboard.type("hello")); await act(page.locator(`g[data-id="block1"] .blocklyIconGroup`).click()); expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBeNull(); + expect(await isEphemeralFocusTaken(page)).toBe(false); }); test("edit block mutator", async ({ page, act }) => { @@ -531,11 +547,14 @@ test("edit block mutator", async ({ page, act }) => { await act(page.locator(`g[data-id="block1"] .blocklyMutatorIcon`).click()); expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBeNull(); + expect(await isEphemeralFocusTaken(page)).toBe(false); await act(page.locator(".blocklyCheckboxField").click()); expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).not.toBeNull(); + expect(await isEphemeralFocusTaken(page)).toBe(false); await act(page.locator(`g[data-id="block1"] .blocklyMutatorIcon`).click()); expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBeNull(); + expect(await isEphemeralFocusTaken(page)).toBe(false); }); diff --git a/test-e2e/actions/multiselect/field.spec.ts b/test-e2e/actions/multiselect/field.spec.ts index e8c46fd..9165dda 100644 --- a/test-e2e/actions/multiselect/field.spec.ts +++ b/test-e2e/actions/multiselect/field.spec.ts @@ -10,6 +10,7 @@ import { getHighlightedBlockIds, getMultiselectDraggableId, getSelectedId, + isEphemeralFocusTaken, loadBlocks, test, } from "../../test"; @@ -44,6 +45,7 @@ test("editing boolean field updates selected boolean blocks", async ({ ); expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); + expect(await isEphemeralFocusTaken(page)).toBe(true); await act(page.getByRole("option", { name: "false", exact: true }).click()); expect((await getBlock(page, { id: "block1" })).fields.BOOL.value).toBe( @@ -62,6 +64,7 @@ test("editing boolean field updates selected boolean blocks", async ({ "block3", ]); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); + expect(await isEphemeralFocusTaken(page)).toBe(false); }); test("undo boolean field multi-edit", async ({ page, act }) => { @@ -132,6 +135,7 @@ test("editing number field updates selected number blocks", async ({ ); expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); + expect(await isEphemeralFocusTaken(page)).toBe(true); await act(page.keyboard.type("4")); expect((await getBlock(page, { id: "block1" })).fields.NUM.value).toBe(4); expect((await getBlock(page, { id: "block2" })).fields.NUM.value).toBe(4); @@ -150,6 +154,7 @@ test("editing number field updates selected number blocks", async ({ "block3", ]); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); + expect(await isEphemeralFocusTaken(page)).toBe(false); }); test("undo number field multi-edit", async ({ page, act }) => { @@ -213,6 +218,7 @@ test("dependent field recalculated during multi-edit", async ({ ); expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); + expect(await isEphemeralFocusTaken(page)).toBe(true); await act(page.getByRole("option", { name: "binary", exact: true }).click()); expect((await getBlock(page, { id: "block1" })).fields.NUM.value).toBe("111"); @@ -227,6 +233,7 @@ test("dependent field recalculated during multi-edit", async ({ "block3", ]); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); + expect(await isEphemeralFocusTaken(page)).toBe(false); }); test("undo multi-edit recalculates dependent field", async ({ page, act }) => { diff --git a/test-e2e/actions/multiselect/workspace/block.spec.ts b/test-e2e/actions/multiselect/workspace/block.spec.ts index 0a2d75f..99a76ce 100644 --- a/test-e2e/actions/multiselect/workspace/block.spec.ts +++ b/test-e2e/actions/multiselect/workspace/block.spec.ts @@ -9,6 +9,7 @@ import { getMultiselectDraggableId, getSelectedId, getTrash, + isEphemeralFocusTaken, loadBlocks, openBackpack, openTrash, @@ -62,6 +63,7 @@ test("duplicate blocks via context menu", async ({ page, act }) => { ); expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); + expect(await isEphemeralFocusTaken(page)).toBe(true); await act( page.getByRole("menuitem", { exact: true, name: "Duplicate (3)" }).click(), ); @@ -84,6 +86,7 @@ test("duplicate blocks via context menu", async ({ page, act }) => { expect(highlightedBlockIds).toHaveLength(3); expect(newBlockIds).toEqual(expect.arrayContaining(highlightedBlockIds)); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); + expect(await isEphemeralFocusTaken(page)).toBe(false); }); test("copy and paste blocks via keyboard", async ({ page, act }) => { @@ -140,6 +143,7 @@ test("copy and paste blocks via context menu", async ({ page, act }) => { ); expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); + expect(await isEphemeralFocusTaken(page)).toBe(true); await act( page.getByRole("menuitem", { exact: true, name: "Copy (3)" }).click(), ); @@ -185,6 +189,7 @@ test("copy and paste blocks via context menu", async ({ page, act }) => { expect(highlightedBlockIds).toHaveLength(3); expect(newBlockIds).toEqual(expect.arrayContaining(highlightedBlockIds)); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); + expect(await isEphemeralFocusTaken(page)).toBe(false); }); test("cut and paste blocks via keyboard", async ({ page, act }) => { @@ -217,6 +222,7 @@ test("delete blocks via context menu", async ({ page, act }) => { ); expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); + expect(await isEphemeralFocusTaken(page)).toBe(true); await act( page .getByRole("menuitem", { exact: true, name: "Delete 5 Blocks" }) @@ -226,6 +232,7 @@ test("delete blocks via context menu", async ({ page, act }) => { expect(await getAllBlockIds(page)).toEqual(["block4"]); expect(await getHighlightedBlockIds(page)).toEqual(["block4"]); expect(await getSelectedId(page)).toBe("block4"); + expect(await isEphemeralFocusTaken(page)).toBe(false); }); test("drag blocks to trash", async ({ page, act }) => { diff --git a/test-e2e/actions/multiselect/workspace/comment.spec.ts b/test-e2e/actions/multiselect/workspace/comment.spec.ts index 352a319..efb574d 100644 --- a/test-e2e/actions/multiselect/workspace/comment.spec.ts +++ b/test-e2e/actions/multiselect/workspace/comment.spec.ts @@ -8,6 +8,7 @@ import { getMultiselectDraggableId, getSelectedId, getTrash, + isEphemeralFocusTaken, loadComments, test, } from "../../../test"; @@ -38,6 +39,7 @@ test("duplicate comments via context menu", async ({ page, act }) => { ); expect(await getHighlightedCommentIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); + expect(await isEphemeralFocusTaken(page)).toBe(true); await act( page .getByRole("menuitem", { @@ -54,6 +56,7 @@ test("duplicate comments via context menu", async ({ page, act }) => { ); expect(await getHighlightedCommentIds(page)).toEqual(newCommentIds); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); + expect(await isEphemeralFocusTaken(page)).toBe(false); }); test("copy and paste comments via keyboard", async ({ page, act }) => { @@ -87,6 +90,7 @@ test("copy and paste comments via context menu", async ({ page, act }) => { ); expect(await getHighlightedCommentIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); + expect(await isEphemeralFocusTaken(page)).toBe(true); await act( page.getByRole("menuitem", { exact: true, name: "Copy (2)" }).click(), ); @@ -116,6 +120,7 @@ test("copy and paste comments via context menu", async ({ page, act }) => { ); expect(await getHighlightedCommentIds(page)).toEqual(newCommentIds); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); + expect(await isEphemeralFocusTaken(page)).toBe(false); }); test("cut and paste comments via keyboard", async ({ page, act }) => { @@ -148,6 +153,7 @@ test("delete comments via context menu", async ({ page, act }) => { ); expect(await getHighlightedCommentIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); + expect(await isEphemeralFocusTaken(page)).toBe(true); await act( page .getByRole("menuitem", { @@ -160,6 +166,7 @@ test("delete comments via context menu", async ({ page, act }) => { expect(await getAllCommentIds(page)).toEqual(["comment3"]); expect(await getHighlightedCommentIds(page)).toEqual([]); expect(await getSelectedId(page)).toBeNull(); + expect(await isEphemeralFocusTaken(page)).toBe(false); }); test("drag comments to trash", async ({ page, act }) => { diff --git a/test-e2e/actions/select/block.spec.ts b/test-e2e/actions/select/block.spec.ts index 7eff2f1..a6ae54a 100644 --- a/test-e2e/actions/select/block.spec.ts +++ b/test-e2e/actions/select/block.spec.ts @@ -3,6 +3,7 @@ import { getBlock, getHighlightedBlockIds, getSelectedId, + isEphemeralFocusTaken, loadBlocks, test, } from "../../test"; @@ -67,6 +68,7 @@ test("add comment to block", async ({ page, act }) => { ); expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe("block1"); + expect(await isEphemeralFocusTaken(page)).toBe(true); await act( page.getByRole("menuitem", { exact: true, name: "Add Comment" }).click(), ); @@ -75,6 +77,7 @@ test("add comment to block", async ({ page, act }) => { expect((await getBlock(page, { id: "block2" })).hasComment).toBe(false); expect(await getHighlightedBlockIds(page)).toEqual(["block1"]); expect(await getSelectedId(page)).toBe("block1"); + expect(await isEphemeralFocusTaken(page)).toBe(false); }); test("remove comment from block", async ({ page, act }) => { @@ -103,6 +106,7 @@ test("remove comment from block", async ({ page, act }) => { ); expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe("block1"); + expect(await isEphemeralFocusTaken(page)).toBe(true); await act( page.getByRole("menuitem", { exact: true, name: "Remove Comment" }).click(), ); @@ -111,6 +115,7 @@ test("remove comment from block", async ({ page, act }) => { expect((await getBlock(page, { id: "block2" })).hasComment).toBe(true); expect(await getHighlightedBlockIds(page)).toEqual(["block1"]); expect(await getSelectedId(page)).toBe("block1"); + expect(await isEphemeralFocusTaken(page)).toBe(false); }); test("switch block to external inputs", async ({ page, act }) => { @@ -131,6 +136,7 @@ test("switch block to external inputs", async ({ page, act }) => { ); expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe("block1"); + expect(await isEphemeralFocusTaken(page)).toBe(true); await act( page .getByRole("menuitem", { exact: true, name: "External Inputs" }) @@ -141,6 +147,7 @@ test("switch block to external inputs", async ({ page, act }) => { expect((await getBlock(page, { id: "block2" })).hasInlineInputs).toBe(true); expect(await getHighlightedBlockIds(page)).toEqual(["block1"]); expect(await getSelectedId(page)).toBe("block1"); + expect(await isEphemeralFocusTaken(page)).toBe(false); }); test("switch block to inline inputs", async ({ page, act }) => { @@ -161,6 +168,7 @@ test("switch block to inline inputs", async ({ page, act }) => { ); expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe("block1"); + expect(await isEphemeralFocusTaken(page)).toBe(true); await act( page.getByRole("menuitem", { exact: true, name: "Inline Inputs" }).click(), ); @@ -169,6 +177,7 @@ test("switch block to inline inputs", async ({ page, act }) => { expect((await getBlock(page, { id: "block2" })).hasInlineInputs).toBe(false); expect(await getHighlightedBlockIds(page)).toEqual(["block1"]); expect(await getSelectedId(page)).toBe("block1"); + expect(await isEphemeralFocusTaken(page)).toBe(false); }); test("disable block", async ({ page, act }) => { @@ -189,6 +198,7 @@ test("disable block", async ({ page, act }) => { ); expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe("block1"); + expect(await isEphemeralFocusTaken(page)).toBe(true); await act( page.getByRole("menuitem", { exact: true, name: "Disable Block" }).click(), ); @@ -197,6 +207,7 @@ test("disable block", async ({ page, act }) => { expect((await getBlock(page, { id: "block2" })).isEnabled).toBe(true); expect(await getHighlightedBlockIds(page)).toEqual(["block1"]); expect(await getSelectedId(page)).toBe("block1"); + expect(await isEphemeralFocusTaken(page)).toBe(false); }); test("enable block", async ({ page, act }) => { @@ -217,6 +228,7 @@ test("enable block", async ({ page, act }) => { ); expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe("block1"); + expect(await isEphemeralFocusTaken(page)).toBe(true); await act( page.getByRole("menuitem", { exact: true, name: "Enable Block" }).click(), ); @@ -225,6 +237,7 @@ test("enable block", async ({ page, act }) => { expect((await getBlock(page, { id: "block2" })).isEnabled).toBe(false); expect(await getHighlightedBlockIds(page)).toEqual(["block1"]); expect(await getSelectedId(page)).toBe("block1"); + expect(await isEphemeralFocusTaken(page)).toBe(false); }); test("edit block comment", async ({ page, act }) => { @@ -245,14 +258,17 @@ test("edit block comment", async ({ page, act }) => { await act(page.locator(`g[data-id="block1"] .blocklyIconGroup`).click()); expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBeNull(); + expect(await isEphemeralFocusTaken(page)).toBe(false); await act(page.locator(".blocklyTextarea").click()); expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBeNull(); + expect(await isEphemeralFocusTaken(page)).toBe(false); await act(page.keyboard.type("hello")); await act(page.locator(`g[data-id="block1"] .blocklyIconGroup`).click()); expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBeNull(); + expect(await isEphemeralFocusTaken(page)).toBe(false); }); test("edit block mutator", async ({ page, act }) => { @@ -269,11 +285,14 @@ test("edit block mutator", async ({ page, act }) => { await act(page.locator(`g[data-id="block1"] .blocklyMutatorIcon`).click()); expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBeNull(); + expect(await isEphemeralFocusTaken(page)).toBe(false); await act(page.locator(".blocklyCheckboxField").click()); expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).not.toBeNull(); + expect(await isEphemeralFocusTaken(page)).toBe(false); await act(page.locator(`g[data-id="block1"] .blocklyMutatorIcon`).click()); expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBeNull(); + expect(await isEphemeralFocusTaken(page)).toBe(false); }); diff --git a/test-e2e/actions/select/field.spec.ts b/test-e2e/actions/select/field.spec.ts index 3096898..b3362c7 100644 --- a/test-e2e/actions/select/field.spec.ts +++ b/test-e2e/actions/select/field.spec.ts @@ -3,6 +3,7 @@ import { getBlock, getHighlightedBlockIds, getSelectedId, + isEphemeralFocusTaken, loadBlocks, test, } from "../../test"; @@ -25,6 +26,7 @@ test("edit boolean field", async ({ page, act }) => { ); expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe("block1"); + expect(await isEphemeralFocusTaken(page)).toBe(true); await act(page.getByRole("option", { name: "false", exact: true }).click()); expect((await getBlock(page, { id: "block1" })).fields.BOOL.value).toBe( @@ -35,6 +37,7 @@ test("edit boolean field", async ({ page, act }) => { ); expect(await getHighlightedBlockIds(page)).toEqual(["block1"]); expect(await getSelectedId(page)).toBe("block1"); + expect(await isEphemeralFocusTaken(page)).toBe(false); }); test("edit number field", async ({ page, act }) => { @@ -55,6 +58,7 @@ test("edit number field", async ({ page, act }) => { ); expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe("block1"); + expect(await isEphemeralFocusTaken(page)).toBe(true); await act(page.keyboard.type("42")); await act(page.keyboard.press("Enter")); @@ -62,4 +66,5 @@ test("edit number field", async ({ page, act }) => { expect((await getBlock(page, { id: "block2" })).fields.NUM.value).toBe(0); expect(await getHighlightedBlockIds(page)).toEqual(["block1"]); expect(await getSelectedId(page)).toBe("block1"); + expect(await isEphemeralFocusTaken(page)).toBe(false); }); diff --git a/test-e2e/actions/select/workspace/block.spec.ts b/test-e2e/actions/select/workspace/block.spec.ts index 11dd84c..fc773e3 100644 --- a/test-e2e/actions/select/workspace/block.spec.ts +++ b/test-e2e/actions/select/workspace/block.spec.ts @@ -8,6 +8,7 @@ import { getHighlightedBlockIds, getSelectedId, getTrash, + isEphemeralFocusTaken, loadBlocks, openBackpack, openTrash, @@ -34,6 +35,7 @@ test("duplicate block via context menu", async ({ page, act }) => { ); expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe("block1"); + expect(await isEphemeralFocusTaken(page)).toBe(true); await act( page.getByRole("menuitem", { exact: true, name: "Duplicate" }).click(), ); @@ -45,6 +47,7 @@ test("duplicate block via context menu", async ({ page, act }) => { ); expect(await getHighlightedBlockIds(page)).toEqual([newBlockId]); expect(await getSelectedId(page)).toBe(newBlockId); + expect(await isEphemeralFocusTaken(page)).toBe(false); }); test("copy and paste block via keyboard", async ({ page, act }) => { @@ -71,6 +74,7 @@ test("copy and paste block via context menu", async ({ page, act }) => { ); expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe("block1"); + expect(await isEphemeralFocusTaken(page)).toBe(true); await act(page.getByRole("menuitem", { exact: true, name: "Copy" }).click()); expect(await getAllBlockIds(page)).toEqual(["block1", "block2"]); expect(await getHighlightedBlockIds(page)).toEqual(["block1"]); @@ -89,6 +93,7 @@ test("copy and paste block via context menu", async ({ page, act }) => { ); expect(await getHighlightedBlockIds(page)).toEqual([newBlockId]); expect(await getSelectedId(page)).toBe(newBlockId); + expect(await isEphemeralFocusTaken(page)).toBe(false); }); test("cut and paste block via keyboard", async ({ page, act }) => { @@ -121,6 +126,7 @@ test("delete block via context menu", async ({ page, act }) => { ); expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe("block1"); + expect(await isEphemeralFocusTaken(page)).toBe(true); await act( page.getByRole("menuitem", { exact: true, name: "Delete Block" }).click(), ); @@ -128,6 +134,7 @@ test("delete block via context menu", async ({ page, act }) => { expect(await getAllBlockIds(page)).toEqual(["block2"]); expect(await getHighlightedBlockIds(page)).toEqual(["block2"]); expect(await getSelectedId(page)).toBe("block2"); + expect(await isEphemeralFocusTaken(page)).toBe(false); }); test("drag block to trash", async ({ page, act }) => { @@ -280,6 +287,7 @@ test("dragging block from toolbox selects new block", async ({ page, act }) => { await act(page.getByRole("treeitem", { name: "Logic" }).click()); expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBeNull(); + expect(await isEphemeralFocusTaken(page)).toBe(false); await act( page.mouse.move( ...(await getBlock(page, { type: "controls_if", workspace: "toolbox" })) @@ -297,4 +305,5 @@ test("dragging block from toolbox selects new block", async ({ page, act }) => { ); expect(await getHighlightedBlockIds(page)).toEqual([newBlockId]); expect(await getSelectedId(page)).toBe(newBlockId); + expect(await isEphemeralFocusTaken(page)).toBe(false); }); diff --git a/test-e2e/actions/select/workspace/comment.spec.ts b/test-e2e/actions/select/workspace/comment.spec.ts index 37e3663..3b22eb8 100644 --- a/test-e2e/actions/select/workspace/comment.spec.ts +++ b/test-e2e/actions/select/workspace/comment.spec.ts @@ -7,6 +7,7 @@ import { getHighlightedCommentIds, getSelectedId, getTrash, + isEphemeralFocusTaken, loadComments, test, } from "../../../test"; @@ -26,6 +27,7 @@ test("duplicate comment via context menu", async ({ page, act }) => { ); expect(await getHighlightedCommentIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe("comment1"); + expect(await isEphemeralFocusTaken(page)).toBe(true); await act( page .getByRole("menuitem", { exact: true, name: "Duplicate Comment" }) @@ -39,6 +41,7 @@ test("duplicate comment via context menu", async ({ page, act }) => { ); expect(await getHighlightedCommentIds(page)).toEqual([newCommentId]); expect(await getSelectedId(page)).toBe(newCommentId); + expect(await isEphemeralFocusTaken(page)).toBe(false); }); test("copy and paste comment via keyboard", async ({ page, act }) => { @@ -65,6 +68,7 @@ test("copy and paste comment via context menu", async ({ page, act }) => { ); expect(await getHighlightedCommentIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe("comment1"); + expect(await isEphemeralFocusTaken(page)).toBe(true); await act(page.getByRole("menuitem", { exact: true, name: "Copy" }).click()); expect(await getAllCommentIds(page)).toEqual(["comment1", "comment2"]); expect(await getHighlightedCommentIds(page)).toEqual(["comment1"]); @@ -83,6 +87,7 @@ test("copy and paste comment via context menu", async ({ page, act }) => { ); expect(await getHighlightedCommentIds(page)).toEqual([newCommentId]); expect(await getSelectedId(page)).toBe(newCommentId); + expect(await isEphemeralFocusTaken(page)).toBe(false); }); test("cut and paste comment via keyboard", async ({ page, act }) => { @@ -115,6 +120,7 @@ test("delete comment via context menu", async ({ page, act }) => { ); expect(await getHighlightedCommentIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe("comment1"); + expect(await isEphemeralFocusTaken(page)).toBe(true); await act( page.getByRole("menuitem", { exact: true, name: "Remove Comment" }).click(), ); @@ -122,6 +128,7 @@ test("delete comment via context menu", async ({ page, act }) => { expect(await getAllCommentIds(page)).toEqual(["comment2"]); expect(await getHighlightedCommentIds(page)).toEqual([]); expect(await getSelectedId(page)).toBeNull(); + expect(await isEphemeralFocusTaken(page)).toBe(false); }); test("drag comment to trash", async ({ page, act }) => { diff --git a/test-e2e/test.ts b/test-e2e/test.ts index e5658c4..a1a0025 100644 --- a/test-e2e/test.ts +++ b/test-e2e/test.ts @@ -278,6 +278,9 @@ export const getHighlightedCommentIds = (page: Page): Promise => export const getSelectedId = (page: Page): Promise => page.evaluate(() => Blockly.getSelected()?.id ?? null); +export const isEphemeralFocusTaken = (page: Page): Promise => + page.evaluate(() => Blockly.getFocusManager().ephemeralFocusTaken()); + export const getMultiselectDraggableId = (page: Page): Promise => page.evaluate(() => { const multiselectDraggable = window.multiDraggableWeakMap.get( From 1f5e6a092a567640e1c7b7832b080fbba2227a55 Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Tue, 7 Apr 2026 14:35:10 -0700 Subject: [PATCH 11/67] Keep multiselection when clicking comment/mutator icon --- test-e2e/actions/multiselect/block.spec.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test-e2e/actions/multiselect/block.spec.ts b/test-e2e/actions/multiselect/block.spec.ts index 22c7548..f2f0ae7 100644 --- a/test-e2e/actions/multiselect/block.spec.ts +++ b/test-e2e/actions/multiselect/block.spec.ts @@ -512,8 +512,8 @@ test("edit block comment", async ({ page, act }) => { await act(page.keyboard.up("Shift")); await act(page.locator(`g[data-id="block1"] .blocklyIconGroup`).click()); - expect(await getHighlightedBlockIds(page)).toEqual([]); - expect(await getSelectedId(page)).toBeNull(); + expect(await getHighlightedBlockIds(page)).toEqual(["block1", "block2"]); + expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); expect(await isEphemeralFocusTaken(page)).toBe(false); await act(page.locator(".blocklyTextarea").click()); expect(await getHighlightedBlockIds(page)).toEqual([]); @@ -522,8 +522,8 @@ test("edit block comment", async ({ page, act }) => { await act(page.keyboard.type("hello")); await act(page.locator(`g[data-id="block1"] .blocklyIconGroup`).click()); - expect(await getHighlightedBlockIds(page)).toEqual([]); - expect(await getSelectedId(page)).toBeNull(); + expect(await getHighlightedBlockIds(page)).toEqual(["block1", "block2"]); + expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); expect(await isEphemeralFocusTaken(page)).toBe(false); }); @@ -545,8 +545,8 @@ test("edit block mutator", async ({ page, act }) => { await act(page.keyboard.up("Shift")); await act(page.locator(`g[data-id="block1"] .blocklyMutatorIcon`).click()); - expect(await getHighlightedBlockIds(page)).toEqual([]); - expect(await getSelectedId(page)).toBeNull(); + expect(await getHighlightedBlockIds(page)).toEqual(["block1", "block2"]); + expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); expect(await isEphemeralFocusTaken(page)).toBe(false); await act(page.locator(".blocklyCheckboxField").click()); expect(await getHighlightedBlockIds(page)).toEqual([]); @@ -554,7 +554,7 @@ test("edit block mutator", async ({ page, act }) => { expect(await isEphemeralFocusTaken(page)).toBe(false); await act(page.locator(`g[data-id="block1"] .blocklyMutatorIcon`).click()); - expect(await getHighlightedBlockIds(page)).toEqual([]); - expect(await getSelectedId(page)).toBeNull(); + expect(await getHighlightedBlockIds(page)).toEqual(["block1", "block2"]); + expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); expect(await isEphemeralFocusTaken(page)).toBe(false); }); From 841ef59fd67e10815f8bff1523debc2d59f7b8ef Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Tue, 24 Mar 2026 09:58:18 -0700 Subject: [PATCH 12/67] Blockly 12 renamed setEnabled to setDisabledReason https://github.com/RaspberryPiFoundation/blockly/commit/cee7f916bb7d67a84b82d5c17f1be751a952c9f1 https://github.com/RaspberryPiFoundation/blockly/commit/556ee39f6f3ad9ed6b82b587ea85c539ff7a50b1 --- src/multiselect_contextmenu.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/multiselect_contextmenu.js b/src/multiselect_contextmenu.js index 5f1efab..169a590 100644 --- a/src/multiselect_contextmenu.js +++ b/src/multiselect_contextmenu.js @@ -524,13 +524,13 @@ const registerDisable = function() { const disableOption = { displayText: function(scope) { let workableBlocksLength = 0; - const state = scope.block.isEnabled(); + const state = !scope.block.hasDisabledReason(Blockly.constants.MANUALLY_DISABLED); const workspace = scope.block.workspace; const dragSelection = dragSelectionWeakMap.get(workspace); dragSelection.forEach(function(id) { const block = workspace.getBlockById(id); if (disableOption.check(block) && - block.isEnabled() === state) { + !block.hasDisabledReason(Blockly.constants.MANUALLY_DISABLED) === state) { workableBlocksLength++; } }); @@ -560,7 +560,11 @@ const registerDisable = function() { const block = scope.block; if (!block.isInFlyout && block.workspace.options.disable && block.isEditable()) { - if (block.getInheritedDisabled()) { + const disabledReasons = block.getDisabledReasons(); + const isDisabledForOtherReason = + disabledReasons.size > + (disabledReasons.has(Blockly.constants.MANUALLY_DISABLED) ? 1 : 0); + if (block.getInheritedDisabled() || isDisabledForOtherReason) { return 'disabled'; } return 'enabled'; @@ -570,13 +574,13 @@ const registerDisable = function() { check: function(block) { return block && disableOption.preconditionFn({block: block}) === 'enabled' && - (!hasSelectedParent(block) || !block.isEnabled()); + (!hasSelectedParent(block) || block.hasDisabledReason(Blockly.constants.MANUALLY_DISABLED)); }, callback: function(scope) { - const state = !scope.block.isEnabled(); + const state = scope.block.hasDisabledReason(Blockly.constants.MANUALLY_DISABLED); const apply = function(block) { if (disableOption.check(block)) { - block.setEnabled(state); + block.setDisabledReason(!state, Blockly.constants.MANUALLY_DISABLED); } }; const workspace = scope.block.workspace; From 9784834ea61711c16287f60886b0ad0bac295837 Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Wed, 1 Apr 2026 13:53:41 -0700 Subject: [PATCH 13/67] Blockly 12 recreates DropDownDiv when clearing it https://github.com/RaspberryPiFoundation/blockly/commit/4fb054c48465c1300c320453724535837768d256 --- src/multiselect.js | 110 +++++++++++++++++++++------------------------ 1 file changed, 50 insertions(+), 60 deletions(-) diff --git a/src/multiselect.js b/src/multiselect.js index 31dea26..0f4f5a6 100644 --- a/src/multiselect.js +++ b/src/multiselect.js @@ -55,32 +55,31 @@ export class Multiselect { }); } const injectionDiv = this.workspace_.getInjectionDiv(); - const dropdownDiv = Blockly.DropDownDiv.getContentDiv(); + this.injectionDivWrappers_ = this.bindListeners_(injectionDiv); const widgetDiv = Blockly.WidgetDiv.getDiv(); - this.onKeyDownWrapper_ = Blockly.browserEvents.conditionalBind( - injectionDiv, 'keydown', this, this.onKeyDown_); - this.onKeyDownDropdownDivWrapper_ = Blockly.browserEvents.conditionalBind( - dropdownDiv, 'keydown', this, this.onKeyDown_); if (widgetDiv) { - this.onKeyDownWidgetDivWrapper_ = Blockly.browserEvents.conditionalBind( - widgetDiv, 'keydown', this, this.onKeyDown_); - } - this.onKeyUpWrapper_ = Blockly.browserEvents.conditionalBind( - injectionDiv, 'keyup', this, this.onKeyUp_); - this.onKeyUpDropdownDivWrapper_ = Blockly.browserEvents.conditionalBind( - dropdownDiv, 'keyup', this, this.onKeyUp_); - if (widgetDiv) { - this.onKeyUpWidgetDivWrapper_ = Blockly.browserEvents.conditionalBind( - widgetDiv, 'keyup', this, this.onKeyUp_); - } - this.onFocusOutWrapper_ = Blockly.browserEvents.conditionalBind( - injectionDiv, 'focusout', this, this.onBlur_); - this.onFocusOutDropdownDivWrapper_ = Blockly.browserEvents.conditionalBind( - dropdownDiv, 'focusout', this, this.onBlur_); - if (widgetDiv) { - this.onFocusOutWidgetDivWrapper_ = Blockly.browserEvents.conditionalBind( - widgetDiv, 'focusout', this, this.onBlur_); - } + this.widgetDivWrappers_ = this.bindListeners_(widgetDiv); + } + this.dropdownDivWrappers_ = this.bindListeners_( + Blockly.DropDownDiv.getContentDiv()); + this.dropdownDivObserver_ = new MutationObserver((mutations) => { + for (const mutation of mutations) { + for (const node of mutation.removedNodes) { + if (node.classList?.contains('blocklyDropDownDiv')) { + this.unbindListeners_(this.dropdownDivWrappers_); + } + } + for (const node of mutation.addedNodes) { + if (node.classList?.contains('blocklyDropDownDiv')) { + this.dropdownDivWrappers_ = this.bindListeners_( + Blockly.DropDownDiv.getContentDiv()); + } + } + } + }); + this.dropdownDivObserver_.observe( + document.querySelector('.blocklyDropDownDiv').parentNode, + {childList: true}); injectionDiv.addEventListener('mouseenter', () => { if (options.workspaceAutoFocus === false || document.activeElement === this.workspace_.getSvgGroup().parentElement || @@ -174,42 +173,16 @@ export class Multiselect { * @param {boolean} keepRegistry Keep the context menu and shortcut registry. */ dispose(keepRegistry = false) { - if (this.onKeyDownWrapper_) { - Blockly.browserEvents.unbind(this.onKeyDownWrapper_); - this.onKeyDownWrapper_ = null; - } - if (this.onKeyDownDropdownDivWrapper_) { - Blockly.browserEvents.unbind(this.onKeyDownDropdownDivWrapper_); - this.onKeyDownDropdownDivWrapper_ = null; - } - if (this.onKeyDownWidgetDivWrapper_) { - Blockly.browserEvents.unbind(this.onKeyDownWidgetDivWrapper_); - this.onKeyDownWidgetDivWrapper_ = null; - } - if (this.onKeyUpWrapper_) { - Blockly.browserEvents.unbind(this.onKeyUpWrapper_); - this.onKeyUpWrapper_ = null; - } - if (this.onKeyUpDropdownDivWrapper_) { - Blockly.browserEvents.unbind(this.onKeyUpDropdownDivWrapper_); - this.onKeyUpDropdownDivWrapper_ = null; - } - if (this.onKeyUpWidgetDivWrapper_) { - Blockly.browserEvents.unbind(this.onKeyUpWidgetDivWrapper_); - this.onKeyUpWidgetDivWrapper_ = null; - } - if (this.onFocusOutWrapper_) { - Blockly.browserEvents.unbind(this.onFocusOutWrapper_); - this.onFocusOutWrapper_ = null; - } - if (this.onFocusOutDropdownDivWrapper_) { - Blockly.browserEvents.unbind(this.onFocusOutDropdownDivWrapper_); - this.onFocusOutDropdownDivWrapper_ = null; - } - if (this.onFocusOutWidgetDivWrapper_) { - Blockly.browserEvents.unbind(this.onFocusOutWidgetDivWrapper_); - this.onFocusOutWidgetDivWrapper_ = null; - } + this.unbindListeners_(this.injectionDivWrappers_); + this.injectionDivWrappers_ = null; + if (this.widgetDivWrappers_) { + this.unbindListeners_(this.widgetDivWrappers_); + this.widgetDivWrappers_ = null; + } + this.unbindListeners_(this.dropdownDivWrappers_); + this.dropdownDivWrappers_ = null; + this.dropdownDivObserver_.disconnect(); + this.dropdownDivObserver_ = null; if (this.eventListenerWrapper_) { this.workspace_.removeChangeListener(this.eventListenerWrapper_); this.eventListenerWrapper_ = null; @@ -249,6 +222,23 @@ export class Multiselect { } } + bindListeners_(div) { + return [ + Blockly.browserEvents.conditionalBind( + div, 'keydown', this, this.onKeyDown_), + Blockly.browserEvents.conditionalBind( + div, 'keyup', this, this.onKeyUp_), + Blockly.browserEvents.conditionalBind( + div, 'focusout', this, this.onBlur_), + ]; + } + + unbindListeners_(wrappers) { + for (const wrapper of wrappers) { + Blockly.browserEvents.unbind(wrapper); + } + } + /** * Add double click to expand/collapse blocks (MIT App Inventor Supported). * @param {!boolean} on Whether to turn on the mode. From 712191ad73eb093bbae432c9d7c308eb54d4dc73 Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Wed, 1 Apr 2026 13:53:56 -0700 Subject: [PATCH 14/67] Blockly 12 added FocusManager API to workspace https://github.com/RaspberryPiFoundation/blockly/commit/d7680cf32ec0fff5b1fabf8d786f996e9e454ff2 --- src/multiselect.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/multiselect.js b/src/multiselect.js index 0f4f5a6..125ae37 100644 --- a/src/multiselect.js +++ b/src/multiselect.js @@ -82,12 +82,10 @@ export class Multiselect { {childList: true}); injectionDiv.addEventListener('mouseenter', () => { if (options.workspaceAutoFocus === false || - document.activeElement === this.workspace_.getSvgGroup().parentElement || - document.activeElement.nodeName.toLowerCase() === 'input' || - document.activeElement.nodeName.toLowerCase() === 'textarea') { + Blockly.getFocusManager().getFocusedTree() === this.workspace_) { return; } - this.workspace_.getSvgGroup().parentElement.focus(); + Blockly.getFocusManager().focusTree(this.workspace_); }); this.eventListenerWrapper_ = this.eventListener_.bind(this); this.workspace_.addChangeListener(this.eventListenerWrapper_); From 545790e1fbdaebe3e3694bf49414042e06eeb60c Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Tue, 7 Apr 2026 17:04:46 -0700 Subject: [PATCH 15/67] Remove no-op setSelected(null) inside !getSelected() check --- src/multiselect_controls.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/multiselect_controls.js b/src/multiselect_controls.js index b67192a..03e1ddc 100644 --- a/src/multiselect_controls.js +++ b/src/multiselect_controls.js @@ -387,7 +387,6 @@ export class MultiselectControls { } this.multiDraggable.clearAll_(); this.dragSelection.clear(); - Blockly.common.setSelected(null); } else if (Blockly.getSelected() && !(Blockly.getSelected() instanceof MultiselectDraggable) && !this.dragSelection.has(Blockly.getSelected().id)) { From cf76c2824b991dd2fb199cc548365c483a91e34f Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Tue, 7 Apr 2026 12:03:26 -0700 Subject: [PATCH 16/67] Blockly 12 replaced setSelected(null) with focusTree(workspace) https://github.com/RaspberryPiFoundation/blockly/commit/4074cee31bb1cdbebaf465ade020805c1ea99045#diff-85e23a17d8e442f164eecab93a4aaa023433a81a3ed895f97f231c0e45e1ab3fL88-R109 --- src/multiselect_contextmenu.js | 10 +++++----- src/multiselect_controls.js | 6 +++--- src/multiselect_shortcut.js | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/multiselect_contextmenu.js b/src/multiselect_contextmenu.js index 169a590..abd1572 100644 --- a/src/multiselect_contextmenu.js +++ b/src/multiselect_contextmenu.js @@ -210,7 +210,7 @@ const registerDuplicate = function() { }); dragSelection.clear(); multiDraggable.clearAll_(); - Blockly.common.setSelected(null); + Blockly.getFocusManager().focusTree(workspace); } else { apply(scope.block); } @@ -703,7 +703,7 @@ const registerDelete = function() { apply(selected); } - Blockly.common.setSelected(null); + Blockly.getFocusManager().focusTree(workspace); Blockly.Events.setGroup(false); }, scopeType: Blockly.ContextMenuRegistry.ScopeType.BLOCK, @@ -858,7 +858,7 @@ const registerSelectAll = function() { } else { Blockly.getSelected().unselect(); } - Blockly.common.setSelected(null); + Blockly.getFocusManager().focusTree(scope.workspace); multiDraggable.clearAll_(); dragSelectionWeakMap.get(scope.workspace).clear(); } @@ -1066,7 +1066,7 @@ const registerCommentDelete = function() { apply(scope.comment); } - Blockly.common.setSelected(null); + Blockly.getFocusManager().focusTree(workspace); Blockly.Events.setGroup(false); }, scopeType: Blockly.ContextMenuRegistry.ScopeType.COMMENT, @@ -1149,7 +1149,7 @@ const registerCommentDuplicate = function() { }); dragSelection.clear(); multiDraggable.clearAll_(); - Blockly.common.setSelected(null); + Blockly.getFocusManager().focusTree(workspace); } else { apply(scope.comment); } diff --git a/src/multiselect_controls.js b/src/multiselect_controls.js index 03e1ddc..a8d8f79 100644 --- a/src/multiselect_controls.js +++ b/src/multiselect_controls.js @@ -410,7 +410,7 @@ export class MultiselectControls { Blockly.common.setSelected(getByID(this.workspace_, this.dragSelection.values().next().value)); } else if (this.dragSelection.size === 0 && Blockly.getSelected() !== null) { - Blockly.common.setSelected(null); + Blockly.getFocusManager().focusTree(this.workspace_); } else if (this.lastSelectedElement_ && !inPasteShortcut.get(this.workspace_)) { this.updateDraggables_(this.lastSelectedElement_); @@ -424,7 +424,7 @@ export class MultiselectControls { Blockly.common.setSelected(getByID(this.workspace_, this.dragSelection.values().next().value)); } else if (this.dragSelection.size === 0 && Blockly.getSelected() !== null) { - Blockly.common.setSelected(null); + Blockly.getFocusManager().focusTree(this.workspace_); } } else if (!this.dragSelection.size && !(Blockly.getSelected() instanceof MultiselectDraggable)) { @@ -548,7 +548,7 @@ export class MultiselectControls { Blockly.common.setSelected(getByID(this.workspace_, this.dragSelection.values().next().value)); } else if (this.dragSelection.size === 0 && Blockly.getSelected() !== null) { - Blockly.common.setSelected(null); + Blockly.getFocusManager().focusTree(this.workspace_); } if (this.hasDisableWorkspaceDrag_) { this.workspace_.options.moveOptions.drag = true; diff --git a/src/multiselect_shortcut.js b/src/multiselect_shortcut.js index cf6d002..e8aded6 100644 --- a/src/multiselect_shortcut.js +++ b/src/multiselect_shortcut.js @@ -97,7 +97,7 @@ const registerShortcutDelete = function() { apply(selected); } - Blockly.common.setSelected(null); + Blockly.getFocusManager().focusTree(workspace); Blockly.Events.setGroup(false); return true; }, @@ -497,7 +497,7 @@ const registerSelectAll = function() { } else { Blockly.getSelected().unselect(); } - Blockly.common.setSelected(null); + Blockly.getFocusManager().focusTree(workspace); multiDraggable.clearAll_(); dragSelectionWeakMap.get(workspace).clear(); } From 1ce23c93d886800a2232ad2cfe125faa351be945 Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Tue, 7 Apr 2026 12:19:44 -0700 Subject: [PATCH 17/67] Blockly 12 made ISelectable extend IFocusableNode https://github.com/RaspberryPiFoundation/blockly/commit/4074cee31bb1cdbebaf465ade020805c1ea99045#diff-9888569b99763f6fa86757ec106a7021021ccaa0e94c2306e3ce20596b61d96cL14-R20 --- src/multiselect_draggable.js | 54 +++++++++++++++++++++++++++++------- 1 file changed, 44 insertions(+), 10 deletions(-) diff --git a/src/multiselect_draggable.js b/src/multiselect_draggable.js index c165dba..a162621 100644 --- a/src/multiselect_draggable.js +++ b/src/multiselect_draggable.js @@ -30,6 +30,19 @@ export class MultiselectDraggable { this.loc = new Blockly.utils.Coordinate(0, 0); this.connectionDBList = []; this.dragSelection = dragSelectionWeakMap.get(workspace); + this.focusableElement = null; + + const origLookUpFocusableNode = workspace.lookUpFocusableNode.bind(workspace); + workspace.lookUpFocusableNode = (id) => { + if (id === this.id) { + if (this.canBeFocused()) { + return this; + } else { + return null; + } + } + return origLookUpFocusableNode(id); + }; } /** @@ -54,6 +67,10 @@ export class MultiselectDraggable { this.addPointerDownEventListener_(subDraggable); } this.subDraggables.set(subDraggable, subDraggable.getRelativeToSurfaceXY()); + if (this.subDraggables.size === 1) { + this.focusableElement = Blockly.utils.dom.createSvgElement( + Blockly.utils.Svg.G, {id: this.id}, this.workspace.getSvgGroup()); + } } /** @@ -67,6 +84,10 @@ export class MultiselectDraggable { this.removePointerDownEventListener_(subDraggable); } this.subDraggables.delete(subDraggable); + if (this.subDraggables.size === 0) { + this.focusableElement.remove(); + this.focusableElement = null; + } } // This is the feature where we added pointer down event listeners. @@ -315,19 +336,32 @@ export class MultiselectDraggable { } } - /** - * A function that turns off the highlight selection - * of the subdraggables. Currently not used as it - * causes a bug when selecting a block by clicking - * while in multiselect mode. - */ unselect() { - // TODO: Look into this after gestures have been updated - // for (const draggable of this.subDraggables) { - // draggable[0].unselect(); - // } + for (const subDraggable of this.subDraggables.keys()) { + Blockly.utils.dom.removeClass(subDraggable.getSvgRoot(), 'blocklySelected'); + } } + // IFocusableNode methods + getFocusableElement() { + return this.focusableElement; + } + + getFocusableTree() { + return this.workspace; + } + + onNodeFocus() { + this.select(); + } + + onNodeBlur() { + this.unselect(); + } + + canBeFocused() { + return this.focusableElement !== null; + } // IDeletable methods /** From 3b63933a051f8da78da2c5da9fad6c2f5942632f Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Tue, 7 Apr 2026 12:48:25 -0700 Subject: [PATCH 18/67] Blockly 12 asynchronously focuses pasted blocks https://github.com/RaspberryPiFoundation/blockly/commit/bbd97eab6770a5ca6019da2b6402d4656a285d4b#diff-f54d1f6dea3d938648ed3d7a91dff6b36ac19e8416774f87b4d71975d1acd510L58-R66 --- src/multiselect_contextmenu.js | 36 ++++++++++++++++++++-------------- src/multiselect_shortcut.js | 12 +++++++----- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/src/multiselect_contextmenu.js b/src/multiselect_contextmenu.js index abd1572..75a571a 100644 --- a/src/multiselect_contextmenu.js +++ b/src/multiselect_contextmenu.js @@ -234,12 +234,14 @@ const registerDuplicate = function() { connectionDBList.forEach(function(connectionDB) { connectionDB[0].connect(connectionDB[1]); }); - if (dragSelection.size === 1) { - Blockly.common.setSelected(getByID(workspace, dragSelection.values().next().value)); - } else { - Blockly.common.setSelected(multiDraggable); - } Blockly.Events.setGroup(false); + Blockly.renderManagement.finishQueuedRenders().then(() => { + if (dragSelection.size === 1) { + Blockly.common.setSelected(getByID(workspace, dragSelection.values().next().value)); + } else { + Blockly.common.setSelected(multiDraggable); + } + }); }, scopeType: Blockly.ContextMenuRegistry.ScopeType.BLOCK, id, @@ -810,11 +812,13 @@ const registerPaste = function(useCopyPasteCrossTab) { blockList[connectionDB[1]].previousConnection); }); Blockly.Events.setGroup(false); - if (dragSelection.size === 1) { - Blockly.common.setSelected(getByID(workspace, dragSelection.values().next().value)); - } else { - Blockly.common.setSelected(multiDraggable); - } + Blockly.renderManagement.finishQueuedRenders().then(() => { + if (dragSelection.size === 1) { + Blockly.common.setSelected(getByID(workspace, dragSelection.values().next().value)); + } else { + Blockly.common.setSelected(multiDraggable); + } + }); return true; }, scopeType: Blockly.ContextMenuRegistry.ScopeType.WORKSPACE, @@ -1161,12 +1165,14 @@ const registerCommentDuplicate = function() { comment.select(); } } - if (dragSelection.size === 1) { - Blockly.common.setSelected(getByID(workspace, dragSelection.values().next().value)); - } else { - Blockly.common.setSelected(multiDraggable); - } Blockly.Events.setGroup(false); + Blockly.renderManagement.finishQueuedRenders().then(() => { + if (dragSelection.size === 1) { + Blockly.common.setSelected(getByID(workspace, dragSelection.values().next().value)); + } else { + Blockly.common.setSelected(multiDraggable); + } + }); }, scopeType: Blockly.ContextMenuRegistry.ScopeType.COMMENT, id, diff --git a/src/multiselect_shortcut.js b/src/multiselect_shortcut.js index e8aded6..e2c8fbc 100644 --- a/src/multiselect_shortcut.js +++ b/src/multiselect_shortcut.js @@ -433,12 +433,14 @@ const registerPaste = function(useCopyPasteCrossTab) { blockList[connectionDB[1]].previousConnection); }); - if (dragSelection.size === 1) { - Blockly.common.setSelected(getByID(workspace, dragSelection.values().next().value)); - } else { - Blockly.common.setSelected(multiDraggable); - } Blockly.Events.setGroup(false); + Blockly.renderManagement.finishQueuedRenders().then(() => { + if (dragSelection.size === 1) { + Blockly.common.setSelected(getByID(workspace, dragSelection.values().next().value)); + } else { + Blockly.common.setSelected(multiDraggable); + } + }); return true; }, }; From c6dae7387c1ca40faf3e988f0fe386dc60801b23 Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Tue, 7 Apr 2026 15:05:17 -0700 Subject: [PATCH 19/67] Blockly 12 dispatches right clicks via IContextMenu https://github.com/RaspberryPiFoundation/blockly/commit/65bc2b5f180cd553dfdf1336222c653841eb1068 --- src/multiselect_draggable.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/multiselect_draggable.js b/src/multiselect_draggable.js index a162621..72adf1f 100644 --- a/src/multiselect_draggable.js +++ b/src/multiselect_draggable.js @@ -363,6 +363,13 @@ export class MultiselectDraggable { return this.focusableElement !== null; } + // IContextMenu methods + showContextMenu(e) { + const targetId = e.target.closest('[data-id]').getAttribute('data-id'); + const target = this.workspace.getBlockById(targetId) || this.workspace.getCommentById(targetId); + target.showContextMenu(e); + } + // IDeletable methods /** * A function that is required for the From 06b0fe2966c05d59d8aa974ab56af152a54993e7 Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Tue, 7 Apr 2026 17:13:20 -0700 Subject: [PATCH 20/67] Blockly 12 deselects multiselection on empty space click https://github.com/RaspberryPiFoundation/blockly/commit/4074cee31bb1cdbebaf465ade020805c1ea99045 --- src/multiselect.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/multiselect.js b/src/multiselect.js index 125ae37..f413481 100644 --- a/src/multiselect.js +++ b/src/multiselect.js @@ -255,6 +255,19 @@ export class Multiselect { const wrappedFunc = function(e, ws) { func.call(this, e, ws); + const dragSelection = dragSelectionWeakMap.get(ws); + const multiDraggable = multiDraggableWeakMap.get(ws); + if (dragSelection) { + const isEmptySpaceClick = !e.target.closest('g[data-id]'); + if (isEmptySpaceClick && dragSelection.size) { + if (inMultipleSelectionModeWeakMap.get(ws)) { + Blockly.common.setSelected(multiDraggable); + } else { + multiDraggable.clearAll_(); + dragSelection.clear(); + } + } + } if (this.targetBlock && e.buttons === 1 && !inMultipleSelectionModeWeakMap.get(ws)) { const preCondition = function(block) { @@ -287,7 +300,6 @@ export class Multiselect { } } else if (selected && selected instanceof MultiselectDraggable) { // Case where the selected is a multidraggable instance - const dragSelection = dragSelectionWeakMap.get(ws); if (dragSelection.size) { // Checking whether any of the blocks in // the dragSelection is not collapsed. From 51c32bd013ca74a5ad2f762f618986d759060e5a Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Tue, 7 Apr 2026 17:13:30 -0700 Subject: [PATCH 21/67] Blockly 12 fires focusout before FocusManager updates https://github.com/RaspberryPiFoundation/blockly/commit/4074cee31bb1cdbebaf465ade020805c1ea99045 --- src/multiselect.js | 32 ++++++++++---------------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/src/multiselect.js b/src/multiselect.js index f413481..163f7a3 100644 --- a/src/multiselect.js +++ b/src/multiselect.js @@ -438,28 +438,16 @@ export class Multiselect { * @private */ onBlur_(e) { - if (inMultipleSelectionModeWeakMap.get(this.workspace_)) { - // Revert last unselected block if the related target - // is a field related element, for accomodating field update - // directly while the multi-selection mode is on. - if (e.relatedTarget && (e.relatedTarget.tagName === 'INPUT' || - e.relatedTarget.tagName === 'TEXTAREA' || - e.relatedTarget.tagName === 'DIV' && - e.relatedTarget.classList.value.indexOf( - 'blocklyDropdownMenu') > -1)) { - this.controls_.revertLastUnselectedBlock(); - } - - const injectionDiv = this.workspace_.getInjectionDiv(); - const dropdownDiv = Blockly.DropDownDiv.getContentDiv(); - const widgetDiv = Blockly.WidgetDiv.getDiv(); - if (e.relatedTarget && ( - injectionDiv.contains(e.relatedTarget) || - dropdownDiv.contains(e.relatedTarget) || - widgetDiv?.contains(e.relatedTarget))) { - return; + setTimeout(() => { + if (inMultipleSelectionModeWeakMap.get(this.workspace_)) { + if (Blockly.getFocusManager().getFocusedTree() === this.workspace_) { + if (Blockly.getFocusManager().ephemeralFocusTaken()) { + this.controls_.revertLastUnselectedBlock(); + } + } else { + this.controls_.disableMultiselect(); + } } - this.controls_.disableMultiselect(); - } + }, 0); } } From 3dce8acc9dfa84ec75d2461ba518048e76b9c439 Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Tue, 14 Apr 2026 14:02:28 -0700 Subject: [PATCH 22/67] Blockly 12 deselects multiselection during drag https://github.com/RaspberryPiFoundation/blockly/commit/4074cee31bb1cdbebaf465ade020805c1ea99045 --- src/multiselect_draggable.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/multiselect_draggable.js b/src/multiselect_draggable.js index 72adf1f..205e412 100644 --- a/src/multiselect_draggable.js +++ b/src/multiselect_draggable.js @@ -250,6 +250,10 @@ export class MultiselectDraggable { for (const draggable of this.topSubDraggables) { draggable.startDrag(); } + for (const subDraggable of this.subDraggables.keys()) { + Blockly.utils.dom.addClass(subDraggable.getSvgRoot(), 'blocklySelected'); + } + Blockly.common.setSelected(this); } /** From db5245ea27051b95230f6cbdbf570e608a261feb Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Wed, 8 Apr 2026 11:17:09 -0700 Subject: [PATCH 23/67] Update Blockly to 12.5.1 --- package-lock.json | 10 +++++----- package.json | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6b61532..0a2f8fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,13 +16,13 @@ "@blockly/dev-tools": "^9.0.8", "@blockly/workspace-backpack": "^7.0.9", "@playwright/test": "^1.58.2", - "blockly": "^12.4.1" + "blockly": "^12.5.1" }, "engines": { "node": ">=8.17.0" }, "peerDependencies": { - "blockly": "^12.4.1" + "blockly": "^12.5.1" } }, "node_modules/@asamuzakjp/css-color": { @@ -1792,9 +1792,9 @@ } }, "node_modules/blockly": { - "version": "12.4.1", - "resolved": "https://registry.npmjs.org/blockly/-/blockly-12.4.1.tgz", - "integrity": "sha512-OEF0r8cFMGDkQbX+PWTjifWTe9xi2QzpZS4rO2lYeQhZDWW3/eInklLSdoxAyEQCdfJhQqMTpBct13oDoc0GVQ==", + "version": "12.5.1", + "resolved": "https://registry.npmjs.org/blockly/-/blockly-12.5.1.tgz", + "integrity": "sha512-etXLpUtEkcRibHGwIJ4BsvnIzMJJs0C0yPIjE/W0NCtj8ACha/a7Q9n7Ib6+j7N4EzQ0p28YPZMnypi5pNIj1g==", "dev": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 117bc12..a438788 100644 --- a/package.json +++ b/package.json @@ -47,10 +47,10 @@ "@blockly/dev-tools": "^9.0.8", "@blockly/workspace-backpack": "^7.0.9", "@playwright/test": "^1.58.2", - "blockly": "^12.4.1" + "blockly": "^12.5.1" }, "peerDependencies": { - "blockly": "^12.4.1" + "blockly": "^12.5.1" }, "publishConfig": {}, "eslintConfig": { From f69960daff10e90330c88c33deb477578766342f Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Wed, 8 Apr 2026 11:04:02 -0700 Subject: [PATCH 24/67] Blockly 12 dropped Ctrl shortcut key on Mac https://github.com/RaspberryPiFoundation/blockly/commit/3b14950a36ed5cb452c72d53d8bb0e71605aacd9 --- package-lock.json | 15 ++++--- package.json | 1 + src/multiselect_shortcut.js | 45 ++----------------- test-e2e/actions/multiselect/field.spec.ts | 7 +-- .../multiselect/workspace/block.spec.ts | 11 ++--- .../multiselect/workspace/comment.spec.ts | 11 ++--- .../actions/select/workspace/block.spec.ts | 11 ++--- .../actions/select/workspace/comment.spec.ts | 11 ++--- test-e2e/selection/multiselect/block.spec.ts | 3 +- test-e2e/test.ts | 3 ++ test-e2e/tsconfig.json | 5 +++ 11 files changed, 51 insertions(+), 72 deletions(-) create mode 100644 test-e2e/tsconfig.json diff --git a/package-lock.json b/package-lock.json index 0a2f8fd..17e4441 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@blockly/dev-tools": "^9.0.8", "@blockly/workspace-backpack": "^7.0.9", "@playwright/test": "^1.58.2", + "@types/node": "^24.12.2", "blockly": "^12.5.1" }, "engines": { @@ -933,13 +934,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.3.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.3.tgz", - "integrity": "sha512-GKBNHjoNw3Kra1Qg5UXttsY5kiWMEfoHq2TmXb+b1rcm6N7B3wTrFYIf/oSZ1xNQ+hVVijgLkiDZh7jRRsh+Gw==", + "version": "24.12.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", + "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.10.0" + "undici-types": "~7.16.0" } }, "node_modules/@types/node-forge": { @@ -6843,9 +6844,9 @@ } }, "node_modules/undici-types": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", - "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index a438788..fa6fd9d 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "@blockly/dev-tools": "^9.0.8", "@blockly/workspace-backpack": "^7.0.9", "@playwright/test": "^1.58.2", + "@types/node": "^24.12.2", "blockly": "^12.5.1" }, "peerDependencies": { diff --git a/src/multiselect_shortcut.js b/src/multiselect_shortcut.js index e2c8fbc..cc8847c 100644 --- a/src/multiselect_shortcut.js +++ b/src/multiselect_shortcut.js @@ -201,19 +201,10 @@ const registerCopy = function(useCopyPasteCrossTab) { Blockly.ShortcutRegistry.registry.register(copyShortcut); const ctrlC = Blockly.ShortcutRegistry.registry.createSerializedKey( - Blockly.utils.KeyCodes.C, [Blockly.utils.KeyCodes.CTRL]); + Blockly.utils.KeyCodes.C, [Blockly.utils.KeyCodes.CTRL_CMD]); Blockly.ShortcutRegistry.registry.addKeyMapping( ctrlC, copyShortcut.name, true); - const altC = Blockly.ShortcutRegistry.registry.createSerializedKey( - Blockly.utils.KeyCodes.C, [Blockly.utils.KeyCodes.ALT]); - Blockly.ShortcutRegistry.registry.addKeyMapping( - altC, copyShortcut.name, true); - - const metaC = Blockly.ShortcutRegistry.registry.createSerializedKey( - Blockly.utils.KeyCodes.C, [Blockly.utils.KeyCodes.META]); - Blockly.ShortcutRegistry.registry.addKeyMapping( - metaC, copyShortcut.name, true); }; @@ -326,19 +317,10 @@ const registerCut = function(useCopyPasteCrossTab) { Blockly.ShortcutRegistry.registry.register(cutShortcut); const ctrlX = Blockly.ShortcutRegistry.registry.createSerializedKey( - Blockly.utils.KeyCodes.X, [Blockly.utils.KeyCodes.CTRL]); + Blockly.utils.KeyCodes.X, [Blockly.utils.KeyCodes.CTRL_CMD]); Blockly.ShortcutRegistry.registry.addKeyMapping( ctrlX, cutShortcut.name, true); - const altX = Blockly.ShortcutRegistry.registry.createSerializedKey( - Blockly.utils.KeyCodes.X, [Blockly.utils.KeyCodes.ALT]); - Blockly.ShortcutRegistry.registry.addKeyMapping( - altX, cutShortcut.name, true); - - const metaX = Blockly.ShortcutRegistry.registry.createSerializedKey( - Blockly.utils.KeyCodes.X, [Blockly.utils.KeyCodes.META]); - Blockly.ShortcutRegistry.registry.addKeyMapping( - metaX, cutShortcut.name, true); }; // TODO: Look into undo stack and adding/removing blocks from multidraggable @@ -451,19 +433,10 @@ const registerPaste = function(useCopyPasteCrossTab) { Blockly.ShortcutRegistry.registry.register(pasteShortcut); const ctrlV = Blockly.ShortcutRegistry.registry.createSerializedKey( - Blockly.utils.KeyCodes.V, [Blockly.utils.KeyCodes.CTRL]); + Blockly.utils.KeyCodes.V, [Blockly.utils.KeyCodes.CTRL_CMD]); Blockly.ShortcutRegistry.registry.addKeyMapping( ctrlV, pasteShortcut.name, true); - const altV = Blockly.ShortcutRegistry.registry.createSerializedKey( - Blockly.utils.KeyCodes.V, [Blockly.utils.KeyCodes.ALT]); - Blockly.ShortcutRegistry.registry.addKeyMapping( - altV, pasteShortcut.name, true); - - const metaV = Blockly.ShortcutRegistry.registry.createSerializedKey( - Blockly.utils.KeyCodes.V, [Blockly.utils.KeyCodes.META]); - Blockly.ShortcutRegistry.registry.addKeyMapping( - metaV, pasteShortcut.name, true); }; /** @@ -532,20 +505,10 @@ const registerSelectAll = function() { Blockly.ShortcutRegistry.registry.register(selectAllShortcut); const ctrlA = Blockly.ShortcutRegistry.registry.createSerializedKey( - Blockly.utils.KeyCodes.A, [Blockly.utils.KeyCodes.CTRL]); + Blockly.utils.KeyCodes.A, [Blockly.utils.KeyCodes.CTRL_CMD]); Blockly.ShortcutRegistry.registry.addKeyMapping( ctrlA, selectAllShortcut.name); - const altA = - Blockly.ShortcutRegistry.registry.createSerializedKey( - Blockly.utils.KeyCodes.A, [Blockly.utils.KeyCodes.ALT]); - Blockly.ShortcutRegistry.registry.addKeyMapping( - altA, selectAllShortcut.name); - - const metaA = Blockly.ShortcutRegistry.registry.createSerializedKey( - Blockly.utils.KeyCodes.A, [Blockly.utils.KeyCodes.META]); - Blockly.ShortcutRegistry.registry.addKeyMapping( - metaA, selectAllShortcut.name); }; /** diff --git a/test-e2e/actions/multiselect/field.spec.ts b/test-e2e/actions/multiselect/field.spec.ts index 9165dda..3399e24 100644 --- a/test-e2e/actions/multiselect/field.spec.ts +++ b/test-e2e/actions/multiselect/field.spec.ts @@ -6,6 +6,7 @@ declare global { import { expect } from "@playwright/test"; import { + cmdOrCtrl, getBlock, getHighlightedBlockIds, getMultiselectDraggableId, @@ -95,7 +96,7 @@ test("undo boolean field multi-edit", async ({ page, act }) => { "FALSE", ); - await act(page.keyboard.press("Control+Z")); + await act(page.keyboard.press(cmdOrCtrl("Z"))); expect((await getBlock(page, { id: "block1" })).fields.BOOL.value).toBe( "TRUE", @@ -182,7 +183,7 @@ test("undo number field multi-edit", async ({ page, act }) => { expect((await getBlock(page, { id: "block1" })).fields.NUM.value).toBe(42); expect((await getBlock(page, { id: "block2" })).fields.NUM.value).toBe(42); - await act(page.keyboard.press("Control+Z")); + await act(page.keyboard.press(cmdOrCtrl("Z"))); expect((await getBlock(page, { id: "block1" })).fields.NUM.value).toBe(0); expect((await getBlock(page, { id: "block2" })).fields.NUM.value).toBe(0); @@ -262,7 +263,7 @@ test("undo multi-edit recalculates dependent field", async ({ page, act }) => { "101010", ); - await act(page.keyboard.press("Control+Z")); + await act(page.keyboard.press(cmdOrCtrl("Z"))); expect((await getBlock(page, { id: "block1" })).fields.NUM.value).toBe("7"); expect((await getBlock(page, { id: "block2" })).fields.NUM.value).toBe("42"); diff --git a/test-e2e/actions/multiselect/workspace/block.spec.ts b/test-e2e/actions/multiselect/workspace/block.spec.ts index 99a76ce..4d2a43a 100644 --- a/test-e2e/actions/multiselect/workspace/block.spec.ts +++ b/test-e2e/actions/multiselect/workspace/block.spec.ts @@ -1,5 +1,6 @@ import { expect } from "@playwright/test"; import { + cmdOrCtrl, getAllBlockIds, getBackpack, getBlock, @@ -90,7 +91,7 @@ test("duplicate blocks via context menu", async ({ page, act }) => { }); test("copy and paste blocks via keyboard", async ({ page, act }) => { - await act(page.keyboard.press("Control+C")); + await act(page.keyboard.press(cmdOrCtrl("C"))); expect(await getAllBlockIds(page)).toEqual([ "block1", "block2", @@ -114,7 +115,7 @@ test("copy and paste blocks via keyboard", async ({ page, act }) => { expect(await getSelectedId(page)).toBe("block4"); await act(page.locator(".blocklyMultiselect image").click()); - await act(page.keyboard.press("Control+V")); + await act(page.keyboard.press(cmdOrCtrl("V"))); const allBlockIds = await getAllBlockIds(page); expect(allBlockIds).toHaveLength(11); const newBlockIds = allBlockIds.filter( @@ -193,12 +194,12 @@ test("copy and paste blocks via context menu", async ({ page, act }) => { }); test("cut and paste blocks via keyboard", async ({ page, act }) => { - await act(page.keyboard.press("Control+X")); + await act(page.keyboard.press(cmdOrCtrl("X"))); expect(await getAllBlockIds(page)).toEqual(["block4"]); expect(await getHighlightedBlockIds(page)).toEqual(["block4"]); expect(await getSelectedId(page)).toBe("block4"); - await act(page.keyboard.press("Control+V")); + await act(page.keyboard.press(cmdOrCtrl("V"))); expect(await getAllBlockIds(page)).toHaveLength(6); const highlightedBlockIds = await getHighlightedBlockIds(page); expect(highlightedBlockIds).toHaveLength(3); @@ -302,7 +303,7 @@ test("undo via keyboard", async ({ page, act }) => { await act(page.keyboard.press("Delete")); expect(await getAllBlockIds(page)).toEqual(["block4"]); - await act(page.keyboard.press("Control+Z")); + await act(page.keyboard.press(cmdOrCtrl("Z"))); expect(await getAllBlockIds(page)).toEqual([ "block1", diff --git a/test-e2e/actions/multiselect/workspace/comment.spec.ts b/test-e2e/actions/multiselect/workspace/comment.spec.ts index efb574d..2af8577 100644 --- a/test-e2e/actions/multiselect/workspace/comment.spec.ts +++ b/test-e2e/actions/multiselect/workspace/comment.spec.ts @@ -1,5 +1,6 @@ import { expect } from "@playwright/test"; import { + cmdOrCtrl, getAllCommentIds, getComment, getEmptySpace, @@ -60,7 +61,7 @@ test("duplicate comments via context menu", async ({ page, act }) => { }); test("copy and paste comments via keyboard", async ({ page, act }) => { - await act(page.keyboard.press("Control+C")); + await act(page.keyboard.press(cmdOrCtrl("C"))); expect(await getAllCommentIds(page)).toEqual([ "comment1", "comment2", @@ -72,7 +73,7 @@ test("copy and paste comments via keyboard", async ({ page, act }) => { ]); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); - await act(page.keyboard.press("Control+V")); + await act(page.keyboard.press(cmdOrCtrl("V"))); const allCommentIds = await getAllCommentIds(page); expect(allCommentIds).toHaveLength(5); const newCommentIds = allCommentIds.filter( @@ -124,12 +125,12 @@ test("copy and paste comments via context menu", async ({ page, act }) => { }); test("cut and paste comments via keyboard", async ({ page, act }) => { - await act(page.keyboard.press("Control+X")); + await act(page.keyboard.press(cmdOrCtrl("X"))); expect(await getAllCommentIds(page)).toEqual(["comment3"]); expect(await getHighlightedCommentIds(page)).toEqual([]); expect(await getSelectedId(page)).toBeNull(); - await act(page.keyboard.press("Control+V")); + await act(page.keyboard.press(cmdOrCtrl("V"))); expect(await getAllCommentIds(page)).toHaveLength(3); const highlightedCommentIds = await getHighlightedCommentIds(page); expect(highlightedCommentIds).toHaveLength(2); @@ -184,7 +185,7 @@ test("undo via keyboard", async ({ page, act }) => { await act(page.keyboard.press("Delete")); expect(await getAllCommentIds(page)).toEqual(["comment3"]); - await act(page.keyboard.press("Control+Z")); + await act(page.keyboard.press(cmdOrCtrl("Z"))); expect(await getAllCommentIds(page)).toEqual([ "comment1", diff --git a/test-e2e/actions/select/workspace/block.spec.ts b/test-e2e/actions/select/workspace/block.spec.ts index fc773e3..eb4c55c 100644 --- a/test-e2e/actions/select/workspace/block.spec.ts +++ b/test-e2e/actions/select/workspace/block.spec.ts @@ -1,5 +1,6 @@ import { expect } from "@playwright/test"; import { + cmdOrCtrl, getAllBlockIds, getBackpack, getBlock, @@ -51,12 +52,12 @@ test("duplicate block via context menu", async ({ page, act }) => { }); test("copy and paste block via keyboard", async ({ page, act }) => { - await act(page.keyboard.press("Control+C")); + await act(page.keyboard.press(cmdOrCtrl("C"))); expect(await getAllBlockIds(page)).toEqual(["block1", "block2"]); expect(await getHighlightedBlockIds(page)).toEqual(["block1"]); expect(await getSelectedId(page)).toBe("block1"); - await act(page.keyboard.press("Control+V")); + await act(page.keyboard.press(cmdOrCtrl("V"))); const allBlockIds = await getAllBlockIds(page); expect(allBlockIds).toHaveLength(3); const [newBlockId] = allBlockIds.filter( @@ -97,12 +98,12 @@ test("copy and paste block via context menu", async ({ page, act }) => { }); test("cut and paste block via keyboard", async ({ page, act }) => { - await act(page.keyboard.press("Control+X")); + await act(page.keyboard.press(cmdOrCtrl("X"))); expect(await getAllBlockIds(page)).toEqual(["block2"]); expect(await getHighlightedBlockIds(page)).toEqual(["block2"]); expect(await getSelectedId(page)).toBe("block2"); - await act(page.keyboard.press("Control+V")); + await act(page.keyboard.press(cmdOrCtrl("V"))); expect(await getAllBlockIds(page)).toHaveLength(2); const highlightedBlockIds = await getHighlightedBlockIds(page); expect(highlightedBlockIds).toHaveLength(1); @@ -231,7 +232,7 @@ test("undo via keyboard", async ({ page, act }) => { await act(page.keyboard.press("Delete")); expect(await getAllBlockIds(page)).toEqual(["block2"]); - await act(page.keyboard.press("Control+Z")); + await act(page.keyboard.press(cmdOrCtrl("Z"))); expect(await getAllBlockIds(page)).toEqual(["block1", "block2"]); }); diff --git a/test-e2e/actions/select/workspace/comment.spec.ts b/test-e2e/actions/select/workspace/comment.spec.ts index 3b22eb8..30f5c1b 100644 --- a/test-e2e/actions/select/workspace/comment.spec.ts +++ b/test-e2e/actions/select/workspace/comment.spec.ts @@ -1,5 +1,6 @@ import { expect } from "@playwright/test"; import { + cmdOrCtrl, getAllCommentIds, getComment, getEmptySpace, @@ -45,12 +46,12 @@ test("duplicate comment via context menu", async ({ page, act }) => { }); test("copy and paste comment via keyboard", async ({ page, act }) => { - await act(page.keyboard.press("Control+C")); + await act(page.keyboard.press(cmdOrCtrl("C"))); expect(await getAllCommentIds(page)).toEqual(["comment1", "comment2"]); expect(await getHighlightedCommentIds(page)).toEqual(["comment1"]); expect(await getSelectedId(page)).toBe("comment1"); - await act(page.keyboard.press("Control+V")); + await act(page.keyboard.press(cmdOrCtrl("V"))); const allCommentIds = await getAllCommentIds(page); expect(allCommentIds).toHaveLength(3); const [newCommentId] = allCommentIds.filter( @@ -91,12 +92,12 @@ test("copy and paste comment via context menu", async ({ page, act }) => { }); test("cut and paste comment via keyboard", async ({ page, act }) => { - await act(page.keyboard.press("Control+X")); + await act(page.keyboard.press(cmdOrCtrl("X"))); expect(await getAllCommentIds(page)).toEqual(["comment2"]); expect(await getHighlightedCommentIds(page)).toEqual([]); expect(await getSelectedId(page)).toBeNull(); - await act(page.keyboard.press("Control+V")); + await act(page.keyboard.press(cmdOrCtrl("V"))); expect(await getAllCommentIds(page)).toHaveLength(2); const highlightedCommentIds = await getHighlightedCommentIds(page); expect(highlightedCommentIds).toHaveLength(1); @@ -146,7 +147,7 @@ test("undo via keyboard", async ({ page, act }) => { await act(page.keyboard.press("Delete")); expect(await getAllCommentIds(page)).toEqual(["comment2"]); - await act(page.keyboard.press("Control+Z")); + await act(page.keyboard.press(cmdOrCtrl("Z"))); expect(await getAllCommentIds(page)).toEqual(["comment1", "comment2"]); }); diff --git a/test-e2e/selection/multiselect/block.spec.ts b/test-e2e/selection/multiselect/block.spec.ts index 408d13e..dda4519 100644 --- a/test-e2e/selection/multiselect/block.spec.ts +++ b/test-e2e/selection/multiselect/block.spec.ts @@ -1,5 +1,6 @@ import { expect } from "@playwright/test"; import { + cmdOrCtrl, getBlock, getEmptySpace, getGridSpacing, @@ -346,7 +347,7 @@ test("select all blocks via keyboard", async ({ page, act }) => { ]), ); - await act(page.keyboard.press("Control+A")); + await act(page.keyboard.press(cmdOrCtrl("A"))); expect(await getHighlightedBlockIds(page)).toEqual(["block1", "block2"]); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); diff --git a/test-e2e/test.ts b/test-e2e/test.ts index a1a0025..b3f96f9 100644 --- a/test-e2e/test.ts +++ b/test-e2e/test.ts @@ -36,6 +36,9 @@ type BlockJSON = { }; type CommentJSON = { centerTop: Point; bounds: Bounds }; +export const cmdOrCtrl = (key: string): string => + `${process.platform === "darwin" ? "Meta" : "Control"}+${key}`; + export const test = base.extend<{ act: Act }>({ page: async ( { page }: { page: Page }, diff --git a/test-e2e/tsconfig.json b/test-e2e/tsconfig.json new file mode 100644 index 0000000..571593c --- /dev/null +++ b/test-e2e/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "types": ["node"] + } +} From 230ed0dcaf649ae6fd18f409c90ce35655c45db4 Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Wed, 8 Apr 2026 11:17:37 -0700 Subject: [PATCH 25/67] Update Backpack to 7.0.10 --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 17e4441..db22af6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "devDependencies": { "@blockly/dev-scripts": "^4.0.11", "@blockly/dev-tools": "^9.0.8", - "@blockly/workspace-backpack": "^7.0.9", + "@blockly/workspace-backpack": "^7.0.10", "@playwright/test": "^1.58.2", "@types/node": "^24.12.2", "blockly": "^12.5.1" @@ -409,9 +409,9 @@ } }, "node_modules/@blockly/workspace-backpack": { - "version": "7.0.9", - "resolved": "https://registry.npmjs.org/@blockly/workspace-backpack/-/workspace-backpack-7.0.9.tgz", - "integrity": "sha512-aYCGsmBFVXafBRkS087cMRyH58jR15Xu8g44794xZBdfce6HGPKw+wN1bDKTZXnisaMR3F6IgJpYl3WV+wjsKw==", + "version": "7.0.10", + "resolved": "https://registry.npmjs.org/@blockly/workspace-backpack/-/workspace-backpack-7.0.10.tgz", + "integrity": "sha512-059e+bw10PMgxbQXnJhfnYAlP6PvSWxq4ImSZn5R1jL1K52IkVTb25QemGRNcQvfBpxamxAl1QMqbIStEnsdrw==", "dev": true, "license": "Apache-2.0", "engines": { diff --git a/package.json b/package.json index fa6fd9d..d392106 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "devDependencies": { "@blockly/dev-scripts": "^4.0.11", "@blockly/dev-tools": "^9.0.8", - "@blockly/workspace-backpack": "^7.0.9", + "@blockly/workspace-backpack": "^7.0.10", "@playwright/test": "^1.58.2", "@types/node": "^24.12.2", "blockly": "^12.5.1" From 5fd4e8fc5377244f50cf9717751a749f97bbb191 Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Thu, 16 Apr 2026 07:09:27 -0700 Subject: [PATCH 26/67] Add new keyboard navigation plugin --- README.md | 4 ++++ package-lock.json | 11 +++++++++++ package.json | 1 + test/index.html | 1 + test/index.js | 15 +++++++++++++++ 5 files changed, 32 insertions(+) diff --git a/README.md b/README.md index 32381d7..0501ddc 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,7 @@ scope of its wrapped input function. - [x] Backpack (Need to update UI in Backpack plugin side) **Workspace-related** +- [x] Keyboard-navigation - [x] Shadow-block-converter - [x] Workspace-content-highlight - [x] Disable-top-blocks @@ -132,6 +133,9 @@ scope of its wrapped input function. For more information, please check out the following [issue page](https://github.com/mit-cml/workspace-multiselect/issues/50). +### Note on keyboard navigation plugin +The keyboard navigation plugin must be initialized after the multiselect plugin. + ### Note on disable top blocks plugin The disable top blocks plugin has to be initialized after the multiselect plugin. The main reason behind this is that the multiselect plugin has its own custom context menu, which allows for the disabling of any blocks (not just top blocks). diff --git a/package-lock.json b/package-lock.json index db22af6..6a5e521 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "devDependencies": { "@blockly/dev-scripts": "^4.0.11", "@blockly/dev-tools": "^9.0.8", + "@blockly/keyboard-navigation": "^3.0.5", "@blockly/workspace-backpack": "^7.0.10", "@playwright/test": "^1.58.2", "@types/node": "^24.12.2", @@ -356,6 +357,16 @@ "eslint": "7.x" } }, + "node_modules/@blockly/keyboard-navigation": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@blockly/keyboard-navigation/-/keyboard-navigation-3.0.5.tgz", + "integrity": "sha512-ZsqWrq/8K1dYhNEkOt3F8YcWkZ3ZIybjnsb5q11lSyhYaVUva+nKKCqFO0mifQl6ZsuPCaapUmlvGG9Gy/EgsQ==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "blockly": "^12.3.0" + } + }, "node_modules/@blockly/theme-dark": { "version": "8.0.3", "resolved": "https://registry.npmjs.org/@blockly/theme-dark/-/theme-dark-8.0.3.tgz", diff --git a/package.json b/package.json index d392106..974c95b 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "devDependencies": { "@blockly/dev-scripts": "^4.0.11", "@blockly/dev-tools": "^9.0.8", + "@blockly/keyboard-navigation": "^3.0.5", "@blockly/workspace-backpack": "^7.0.10", "@playwright/test": "^1.58.2", "@types/node": "^24.12.2", diff --git a/test/index.html b/test/index.html index 086fdb2..fc5911c 100644 --- a/test/index.html +++ b/test/index.html @@ -13,6 +13,7 @@ +
diff --git a/test/index.js b/test/index.js index 8f29d4a..1c8fa4b 100644 --- a/test/index.js +++ b/test/index.js @@ -14,6 +14,7 @@ import {Multiselect} from '../src/index'; import {multiDraggableWeakMap} from '../src/global'; window.multiDraggableWeakMap = multiDraggableWeakMap; import {Backpack} from '@blockly/workspace-backpack'; +import {KeyboardNavigation} from '@blockly/keyboard-navigation'; /** * Create a workspace. @@ -32,10 +33,24 @@ function createWorkspace(blocklyDiv, options) { const multiselectPlugin = new Multiselect(workspace); multiselectPlugin.init(options); + // Initialize keyboard navigation plugin. + new KeyboardNavigation(workspace); + return workspace; } Blockly.ContextMenuItems.registerCommentOptions(); +KeyboardNavigation.registerKeyboardNavigationStyles(); +class NavigationDeferringToolbox extends Blockly.Toolbox { + onKeyDown_() {} +} +Blockly.registry.register( + Blockly.registry.Type.TOOLBOX, + Blockly.registry.DEFAULT, + NavigationDeferringToolbox, + true, +); +KeyboardNavigation.registerFlyoutCursor(); Blockly.Blocks['radix'] = { init() { From 63c862eb805178f5821f5ef5bcb56a6241360717 Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Thu, 16 Apr 2026 16:48:31 -0700 Subject: [PATCH 27/67] Update context menu tests for keyboard nav labels --- .../multiselect/workspace/block.spec.ts | 63 +++++++++++++++-- .../multiselect/workspace/comment.spec.ts | 59 +++++++++++++++- .../actions/select/workspace/block.spec.ts | 67 +++++++++++++++++-- .../actions/select/workspace/comment.spec.ts | 63 ++++++++++++++++- test-e2e/test.ts | 3 + 5 files changed, 241 insertions(+), 14 deletions(-) diff --git a/test-e2e/actions/multiselect/workspace/block.spec.ts b/test-e2e/actions/multiselect/workspace/block.spec.ts index 4d2a43a..bec0124 100644 --- a/test-e2e/actions/multiselect/workspace/block.spec.ts +++ b/test-e2e/actions/multiselect/workspace/block.spec.ts @@ -1,6 +1,7 @@ import { expect } from "@playwright/test"; import { cmdOrCtrl, + cmdOrCtrlLabel, getAllBlockIds, getBackpack, getBlock, @@ -66,7 +67,9 @@ test("duplicate blocks via context menu", async ({ page, act }) => { expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); expect(await isEphemeralFocusTaken(page)).toBe(true); await act( - page.getByRole("menuitem", { exact: true, name: "Duplicate (3)" }).click(), + page + .getByRole("menuitem", { exact: true, name: "Duplicate (3) D" }) + .click(), ); const allBlockIds = await getAllBlockIds(page); @@ -146,7 +149,12 @@ test("copy and paste blocks via context menu", async ({ page, act }) => { expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); expect(await isEphemeralFocusTaken(page)).toBe(true); await act( - page.getByRole("menuitem", { exact: true, name: "Copy (3)" }).click(), + page + .getByRole("menuitem", { + exact: true, + name: `Copy (3) ${cmdOrCtrlLabel("C")}`, + }) + .click(), ); expect(await getAllBlockIds(page)).toEqual([ "block1", @@ -170,7 +178,12 @@ test("copy and paste blocks via context menu", async ({ page, act }) => { }), ); await act( - page.getByRole("menuitem", { exact: true, name: "Paste (3)" }).click(), + page + .getByRole("menuitem", { + exact: true, + name: `Paste (3) ${cmdOrCtrlLabel("V")}`, + }) + .click(), ); const allBlockIds = await getAllBlockIds(page); expect(allBlockIds).toHaveLength(11); @@ -207,6 +220,48 @@ test("cut and paste blocks via keyboard", async ({ page, act }) => { expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); }); +test("cut and paste blocks via context menu", async ({ page, act }) => { + await act( + page.mouse.click(...(await getBlock(page, { id: "block1" })).centerTop, { + button: "right", + }), + ); + expect(await getHighlightedBlockIds(page)).toEqual([]); + expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); + expect(await isEphemeralFocusTaken(page)).toBe(true); + await act( + page + .getByRole("menuitem", { + exact: true, + name: `Cut (3) ${cmdOrCtrlLabel("X")}`, + }) + .click(), + ); + expect(await getAllBlockIds(page)).toEqual(["block4"]); + expect(await getHighlightedBlockIds(page)).toEqual(["block4"]); + expect(await getSelectedId(page)).toBe("block4"); + + await act( + page.mouse.click(...(await getEmptySpace(page)), { + button: "right", + }), + ); + await act( + page + .getByRole("menuitem", { + exact: true, + name: `Paste (3) ${cmdOrCtrlLabel("V")}`, + }) + .click(), + ); + expect(await getAllBlockIds(page)).toHaveLength(6); + const highlightedBlockIds = await getHighlightedBlockIds(page); + expect(highlightedBlockIds).toHaveLength(3); + expect(highlightedBlockIds).not.toContain("block4"); + expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); + expect(await isEphemeralFocusTaken(page)).toBe(false); +}); + test("delete blocks via keyboard", async ({ page, act }) => { await act(page.keyboard.press("Delete")); @@ -226,7 +281,7 @@ test("delete blocks via context menu", async ({ page, act }) => { expect(await isEphemeralFocusTaken(page)).toBe(true); await act( page - .getByRole("menuitem", { exact: true, name: "Delete 5 Blocks" }) + .getByRole("menuitem", { exact: true, name: "Delete 5 Blocks Delete" }) .click(), ); diff --git a/test-e2e/actions/multiselect/workspace/comment.spec.ts b/test-e2e/actions/multiselect/workspace/comment.spec.ts index 2af8577..3ee762b 100644 --- a/test-e2e/actions/multiselect/workspace/comment.spec.ts +++ b/test-e2e/actions/multiselect/workspace/comment.spec.ts @@ -1,6 +1,7 @@ import { expect } from "@playwright/test"; import { cmdOrCtrl, + cmdOrCtrlLabel, getAllCommentIds, getComment, getEmptySpace, @@ -45,7 +46,7 @@ test("duplicate comments via context menu", async ({ page, act }) => { page .getByRole("menuitem", { exact: true, - name: "Duplicate Comment (2)", + name: "Duplicate Comment (2) D", }) .click(), ); @@ -93,7 +94,12 @@ test("copy and paste comments via context menu", async ({ page, act }) => { expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); expect(await isEphemeralFocusTaken(page)).toBe(true); await act( - page.getByRole("menuitem", { exact: true, name: "Copy (2)" }).click(), + page + .getByRole("menuitem", { + exact: true, + name: `Copy (2) ${cmdOrCtrlLabel("C")}`, + }) + .click(), ); expect(await getAllCommentIds(page)).toEqual([ "comment1", @@ -112,7 +118,12 @@ test("copy and paste comments via context menu", async ({ page, act }) => { }), ); await act( - page.getByRole("menuitem", { exact: true, name: "Paste (2)" }).click(), + page + .getByRole("menuitem", { + exact: true, + name: `Paste (2) ${cmdOrCtrlLabel("V")}`, + }) + .click(), ); const allCommentIds = await getAllCommentIds(page); expect(allCommentIds).toHaveLength(5); @@ -138,6 +149,48 @@ test("cut and paste comments via keyboard", async ({ page, act }) => { expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); }); +test("cut and paste comments via context menu", async ({ page, act }) => { + await act( + page.mouse.click(...(await getComment(page, "comment1")).centerTop, { + button: "right", + }), + ); + expect(await getHighlightedCommentIds(page)).toEqual([]); + expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); + expect(await isEphemeralFocusTaken(page)).toBe(true); + await act( + page + .getByRole("menuitem", { + exact: true, + name: `Cut (2) ${cmdOrCtrlLabel("X")}`, + }) + .click(), + ); + expect(await getAllCommentIds(page)).toEqual(["comment3"]); + expect(await getHighlightedCommentIds(page)).toEqual([]); + expect(await getSelectedId(page)).toBeNull(); + + await act( + page.mouse.click(...(await getEmptySpace(page)), { + button: "right", + }), + ); + await act( + page + .getByRole("menuitem", { + exact: true, + name: `Paste (2) ${cmdOrCtrlLabel("V")}`, + }) + .click(), + ); + expect(await getAllCommentIds(page)).toHaveLength(3); + const highlightedCommentIds = await getHighlightedCommentIds(page); + expect(highlightedCommentIds).toHaveLength(2); + expect(highlightedCommentIds).not.toContain("comment3"); + expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); + expect(await isEphemeralFocusTaken(page)).toBe(false); +}); + test("delete comments via keyboard", async ({ page, act }) => { await act(page.keyboard.press("Delete")); diff --git a/test-e2e/actions/select/workspace/block.spec.ts b/test-e2e/actions/select/workspace/block.spec.ts index eb4c55c..896f850 100644 --- a/test-e2e/actions/select/workspace/block.spec.ts +++ b/test-e2e/actions/select/workspace/block.spec.ts @@ -1,6 +1,7 @@ import { expect } from "@playwright/test"; import { cmdOrCtrl, + cmdOrCtrlLabel, getAllBlockIds, getBackpack, getBlock, @@ -38,7 +39,7 @@ test("duplicate block via context menu", async ({ page, act }) => { expect(await getSelectedId(page)).toBe("block1"); expect(await isEphemeralFocusTaken(page)).toBe(true); await act( - page.getByRole("menuitem", { exact: true, name: "Duplicate" }).click(), + page.getByRole("menuitem", { exact: true, name: "Duplicate D" }).click(), ); const allBlockIds = await getAllBlockIds(page); @@ -76,7 +77,14 @@ test("copy and paste block via context menu", async ({ page, act }) => { expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe("block1"); expect(await isEphemeralFocusTaken(page)).toBe(true); - await act(page.getByRole("menuitem", { exact: true, name: "Copy" }).click()); + await act( + page + .getByRole("menuitem", { + exact: true, + name: `Copy ${cmdOrCtrlLabel("C")}`, + }) + .click(), + ); expect(await getAllBlockIds(page)).toEqual(["block1", "block2"]); expect(await getHighlightedBlockIds(page)).toEqual(["block1"]); expect(await getSelectedId(page)).toBe("block1"); @@ -86,7 +94,14 @@ test("copy and paste block via context menu", async ({ page, act }) => { button: "right", }), ); - await act(page.getByRole("menuitem", { exact: true, name: "Paste" }).click()); + await act( + page + .getByRole("menuitem", { + exact: true, + name: `Paste ${cmdOrCtrlLabel("V")}`, + }) + .click(), + ); const allBlockIds = await getAllBlockIds(page); expect(allBlockIds).toHaveLength(3); const [newBlockId] = allBlockIds.filter( @@ -111,6 +126,48 @@ test("cut and paste block via keyboard", async ({ page, act }) => { expect(await getSelectedId(page)).toBe(highlightedBlockIds[0]); }); +test("cut and paste block via context menu", async ({ page, act }) => { + await act( + page.mouse.click(...(await getBlock(page, { id: "block1" })).centerTop, { + button: "right", + }), + ); + expect(await getHighlightedBlockIds(page)).toEqual([]); + expect(await getSelectedId(page)).toBe("block1"); + expect(await isEphemeralFocusTaken(page)).toBe(true); + await act( + page + .getByRole("menuitem", { + exact: true, + name: `Cut ${cmdOrCtrlLabel("X")}`, + }) + .click(), + ); + expect(await getAllBlockIds(page)).toEqual(["block2"]); + expect(await getHighlightedBlockIds(page)).toEqual(["block2"]); + expect(await getSelectedId(page)).toBe("block2"); + + await act( + page.mouse.click(...(await getEmptySpace(page)), { + button: "right", + }), + ); + await act( + page + .getByRole("menuitem", { + exact: true, + name: `Paste ${cmdOrCtrlLabel("V")}`, + }) + .click(), + ); + expect(await getAllBlockIds(page)).toHaveLength(2); + const highlightedBlockIds = await getHighlightedBlockIds(page); + expect(highlightedBlockIds).toHaveLength(1); + expect(highlightedBlockIds).not.toContain("block2"); + expect(await getSelectedId(page)).toBe(highlightedBlockIds[0]); + expect(await isEphemeralFocusTaken(page)).toBe(false); +}); + test("delete block via keyboard", async ({ page, act }) => { await act(page.keyboard.press("Delete")); @@ -129,7 +186,9 @@ test("delete block via context menu", async ({ page, act }) => { expect(await getSelectedId(page)).toBe("block1"); expect(await isEphemeralFocusTaken(page)).toBe(true); await act( - page.getByRole("menuitem", { exact: true, name: "Delete Block" }).click(), + page + .getByRole("menuitem", { exact: true, name: "Delete Block Delete" }) + .click(), ); expect(await getAllBlockIds(page)).toEqual(["block2"]); diff --git a/test-e2e/actions/select/workspace/comment.spec.ts b/test-e2e/actions/select/workspace/comment.spec.ts index 30f5c1b..2102655 100644 --- a/test-e2e/actions/select/workspace/comment.spec.ts +++ b/test-e2e/actions/select/workspace/comment.spec.ts @@ -1,6 +1,7 @@ import { expect } from "@playwright/test"; import { cmdOrCtrl, + cmdOrCtrlLabel, getAllCommentIds, getComment, getEmptySpace, @@ -31,7 +32,7 @@ test("duplicate comment via context menu", async ({ page, act }) => { expect(await isEphemeralFocusTaken(page)).toBe(true); await act( page - .getByRole("menuitem", { exact: true, name: "Duplicate Comment" }) + .getByRole("menuitem", { exact: true, name: "Duplicate Comment D" }) .click(), ); @@ -70,7 +71,14 @@ test("copy and paste comment via context menu", async ({ page, act }) => { expect(await getHighlightedCommentIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe("comment1"); expect(await isEphemeralFocusTaken(page)).toBe(true); - await act(page.getByRole("menuitem", { exact: true, name: "Copy" }).click()); + await act( + page + .getByRole("menuitem", { + exact: true, + name: `Copy ${cmdOrCtrlLabel("C")}`, + }) + .click(), + ); expect(await getAllCommentIds(page)).toEqual(["comment1", "comment2"]); expect(await getHighlightedCommentIds(page)).toEqual(["comment1"]); expect(await getSelectedId(page)).toBe("comment1"); @@ -80,7 +88,14 @@ test("copy and paste comment via context menu", async ({ page, act }) => { button: "right", }), ); - await act(page.getByRole("menuitem", { exact: true, name: "Paste" }).click()); + await act( + page + .getByRole("menuitem", { + exact: true, + name: `Paste ${cmdOrCtrlLabel("V")}`, + }) + .click(), + ); const allCommentIds = await getAllCommentIds(page); expect(allCommentIds).toHaveLength(3); const [newCommentId] = allCommentIds.filter( @@ -105,6 +120,48 @@ test("cut and paste comment via keyboard", async ({ page, act }) => { expect(await getSelectedId(page)).toBe(highlightedCommentIds[0]); }); +test("cut and paste comment via context menu", async ({ page, act }) => { + await act( + page.mouse.click(...(await getComment(page, "comment1")).centerTop, { + button: "right", + }), + ); + expect(await getHighlightedCommentIds(page)).toEqual([]); + expect(await getSelectedId(page)).toBe("comment1"); + expect(await isEphemeralFocusTaken(page)).toBe(true); + await act( + page + .getByRole("menuitem", { + exact: true, + name: `Cut ${cmdOrCtrlLabel("X")}`, + }) + .click(), + ); + expect(await getAllCommentIds(page)).toEqual(["comment2"]); + expect(await getHighlightedCommentIds(page)).toEqual([]); + expect(await getSelectedId(page)).toBeNull(); + + await act( + page.mouse.click(...(await getEmptySpace(page)), { + button: "right", + }), + ); + await act( + page + .getByRole("menuitem", { + exact: true, + name: `Paste ${cmdOrCtrlLabel("V")}`, + }) + .click(), + ); + expect(await getAllCommentIds(page)).toHaveLength(2); + const highlightedCommentIds = await getHighlightedCommentIds(page); + expect(highlightedCommentIds).toHaveLength(1); + expect(highlightedCommentIds).not.toContain("comment2"); + expect(await getSelectedId(page)).toBe(highlightedCommentIds[0]); + expect(await isEphemeralFocusTaken(page)).toBe(false); +}); + test("delete comment via keyboard", async ({ page, act }) => { await act(page.keyboard.press("Delete")); diff --git a/test-e2e/test.ts b/test-e2e/test.ts index b3f96f9..e3b51d7 100644 --- a/test-e2e/test.ts +++ b/test-e2e/test.ts @@ -39,6 +39,9 @@ type CommentJSON = { centerTop: Point; bounds: Bounds }; export const cmdOrCtrl = (key: string): string => `${process.platform === "darwin" ? "Meta" : "Control"}+${key}`; +export const cmdOrCtrlLabel = (key: string): string => + `${process.platform === "darwin" ? "⌘" : "Ctrl +"} ${key}`; + export const test = base.extend<{ act: Act }>({ page: async ( { page }: { page: Page }, From 4b0b2bb2bb91f745f76feacc0c9140e86a7aeccc Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Thu, 16 Apr 2026 07:32:16 -0700 Subject: [PATCH 28/67] Keyboard nav expects shortcuts registered under core names --- src/multiselect_shortcut.js | 49 ++++++++++++------------------------- 1 file changed, 15 insertions(+), 34 deletions(-) diff --git a/src/multiselect_shortcut.js b/src/multiselect_shortcut.js index cc8847c..dbb1ded 100644 --- a/src/multiselect_shortcut.js +++ b/src/multiselect_shortcut.js @@ -12,7 +12,7 @@ import * as Blockly from 'blockly/core'; import { dragSelectionWeakMap, hasSelectedParent, copyData, connectionDBList, dataCopyToStorage, dataCopyFromStorage, registeredShortcut, - multiDraggableWeakMap, inPasteShortcut, getByID, shortcutNames, + multiDraggableWeakMap, inPasteShortcut, getByID, } from './global'; import {MultiselectDraggable} from './multiselect_draggable'; @@ -21,7 +21,7 @@ import {MultiselectDraggable} from './multiselect_draggable'; * for multiple blocks. */ const registerShortcutDelete = function() { - const name = shortcutNames.MULTIDELETE; + const name = Blockly.ShortcutItems.names.DELETE; const deleteShortcut = { name, preconditionFn: function(workspace) { @@ -101,15 +101,12 @@ const registerShortcutDelete = function() { Blockly.Events.setGroup(false); return true; }, + keyCodes: [Blockly.utils.KeyCodes.DELETE, Blockly.utils.KeyCodes.BACKSPACE], }; if (name in Blockly.ShortcutRegistry.registry.getRegistry()) { Blockly.ShortcutRegistry.registry.unregister(name); } Blockly.ShortcutRegistry.registry.register(deleteShortcut); - Blockly.ShortcutRegistry.registry.addKeyMapping( - Blockly.utils.KeyCodes.DELETE, deleteShortcut.name, true); - Blockly.ShortcutRegistry.registry.addKeyMapping( - Blockly.utils.KeyCodes.BACKSPACE, deleteShortcut.name, true); }; @@ -119,7 +116,7 @@ const registerShortcutDelete = function() { * @param {boolean} useCopyPasteCrossTab Whether or not to use copy/paste */ const registerCopy = function(useCopyPasteCrossTab) { - const name = shortcutNames.MULTICOPY; + const name = Blockly.ShortcutItems.names.COPY; const copyShortcut = { name, preconditionFn: function(workspace) { @@ -194,17 +191,13 @@ const registerCopy = function(useCopyPasteCrossTab) { Blockly.Events.setGroup(false); return true; }, + keyCodes: [Blockly.ShortcutRegistry.registry.createSerializedKey( + Blockly.utils.KeyCodes.C, [Blockly.utils.KeyCodes.CTRL_CMD])], }; if (name in Blockly.ShortcutRegistry.registry.getRegistry()) { Blockly.ShortcutRegistry.registry.unregister(name); } Blockly.ShortcutRegistry.registry.register(copyShortcut); - - const ctrlC = Blockly.ShortcutRegistry.registry.createSerializedKey( - Blockly.utils.KeyCodes.C, [Blockly.utils.KeyCodes.CTRL_CMD]); - Blockly.ShortcutRegistry.registry.addKeyMapping( - ctrlC, copyShortcut.name, true); - }; @@ -214,7 +207,7 @@ const registerCopy = function(useCopyPasteCrossTab) { * @param {boolean} useCopyPasteCrossTab Whether or not to use copy/paste */ const registerCut = function(useCopyPasteCrossTab) { - const name = shortcutNames.MULTICUT; + const name = Blockly.ShortcutItems.names.CUT; const cutShortcut = { name, preconditionFn: function(workspace) { @@ -309,18 +302,14 @@ const registerCut = function(useCopyPasteCrossTab) { Blockly.Events.setGroup(false); return true; }, + keyCodes: [Blockly.ShortcutRegistry.registry.createSerializedKey( + Blockly.utils.KeyCodes.X, [Blockly.utils.KeyCodes.CTRL_CMD])], }; if (name in Blockly.ShortcutRegistry.registry.getRegistry()) { Blockly.ShortcutRegistry.registry.unregister(name); } Blockly.ShortcutRegistry.registry.register(cutShortcut); - - const ctrlX = Blockly.ShortcutRegistry.registry.createSerializedKey( - Blockly.utils.KeyCodes.X, [Blockly.utils.KeyCodes.CTRL_CMD]); - Blockly.ShortcutRegistry.registry.addKeyMapping( - ctrlX, cutShortcut.name, true); - }; // TODO: Look into undo stack and adding/removing blocks from multidraggable @@ -330,7 +319,7 @@ const registerCut = function(useCopyPasteCrossTab) { * @param {boolean} useCopyPasteCrossTab Whether or not to use copy/paste */ const registerPaste = function(useCopyPasteCrossTab) { - const name = shortcutNames.MULTIPASTE; + const name = Blockly.ShortcutItems.names.PASTE; const pasteShortcut = { name, preconditionFn: function(workspace) { @@ -425,18 +414,14 @@ const registerPaste = function(useCopyPasteCrossTab) { }); return true; }, + keyCodes: [Blockly.ShortcutRegistry.registry.createSerializedKey( + Blockly.utils.KeyCodes.V, [Blockly.utils.KeyCodes.CTRL_CMD])], }; if (name in Blockly.ShortcutRegistry.registry.getRegistry()) { Blockly.ShortcutRegistry.registry.unregister(name); } Blockly.ShortcutRegistry.registry.register(pasteShortcut); - - const ctrlV = Blockly.ShortcutRegistry.registry.createSerializedKey( - Blockly.utils.KeyCodes.V, [Blockly.utils.KeyCodes.CTRL_CMD]); - Blockly.ShortcutRegistry.registry.addKeyMapping( - ctrlV, pasteShortcut.name, true); - }; /** @@ -529,17 +514,13 @@ export const unregisterOrigShortcut = function() { export const unregisterOurShortcut = function() { registeredShortcut.length = 0; - for (const name of [shortcutNames.MULTIDELETE, - shortcutNames.MULTICOPY, - shortcutNames.MULTICUT, shortcutNames.MULTIPASTE]) { + for (const name of [Blockly.ShortcutItems.names.DELETE, + Blockly.ShortcutItems.names.COPY, + Blockly.ShortcutItems.names.CUT, Blockly.ShortcutItems.names.PASTE]) { if (Object.entries(Blockly.ShortcutRegistry.registry.getRegistry()) .map(([_, value]) => value.name).includes(name)) { Blockly.ShortcutRegistry.registry.unregister(name); } - } - for (const name of [Blockly.ShortcutItems.names.DELETE, - Blockly.ShortcutItems.names.COPY, - Blockly.ShortcutItems.names.CUT, Blockly.ShortcutItems.names.PASTE]) { registeredShortcut.push(name); } }; From 221325da6c7ebb660504d9711d124273b0b39815 Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Thu, 16 Apr 2026 20:05:31 -0700 Subject: [PATCH 29/67] Fix copy/cut disabled in keyboard nav context menus --- src/multiselect_shortcut.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/multiselect_shortcut.js b/src/multiselect_shortcut.js index dbb1ded..4c75942 100644 --- a/src/multiselect_shortcut.js +++ b/src/multiselect_shortcut.js @@ -25,7 +25,7 @@ const registerShortcutDelete = function() { const deleteShortcut = { name, preconditionFn: function(workspace) { - if (workspace.options.readOnly || Blockly.Gesture.inProgress()) { + if (workspace.options.readOnly || workspace.isDragging()) { return false; } const selected = Blockly.common.getSelected(); @@ -120,7 +120,7 @@ const registerCopy = function(useCopyPasteCrossTab) { const copyShortcut = { name, preconditionFn: function(workspace) { - if (workspace.options.readOnly || Blockly.Gesture.inProgress()) { + if (workspace.options.readOnly || workspace.isDragging()) { return false; } const selected = Blockly.common.getSelected(); @@ -211,7 +211,7 @@ const registerCut = function(useCopyPasteCrossTab) { const cutShortcut = { name, preconditionFn: function(workspace) { - if (workspace.options.readOnly || Blockly.Gesture.inProgress()) { + if (workspace.options.readOnly || workspace.isDragging()) { return false; } const selected = Blockly.common.getSelected(); @@ -323,7 +323,7 @@ const registerPaste = function(useCopyPasteCrossTab) { const pasteShortcut = { name, preconditionFn: function(workspace) { - return !workspace.options.readOnly && !Blockly.Gesture.inProgress(); + return !workspace.options.readOnly && !workspace.isDragging(); }, callback: function(workspace) { inPasteShortcut.set(workspace, true); From a049992296cb6e940f236bc611772d047d6ebd53 Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Fri, 17 Apr 2026 10:44:21 -0700 Subject: [PATCH 30/67] Add multiselect counts to keyboard nav context menus X --- README.md | 11 +- src/multiselect.js | 5 + src/multiselect_contextmenu.js | 260 ++++++++++++++++++++++++--------- test/index.js | 1 + 4 files changed, 204 insertions(+), 73 deletions(-) diff --git a/README.md b/README.md index 0501ddc..9a79309 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,15 @@ scope of its wrapped input function. For more information, please check out the following [issue page](https://github.com/mit-cml/workspace-multiselect/issues/50). ### Note on keyboard navigation plugin -The keyboard navigation plugin must be initialized after the multiselect plugin. +The keyboard navigation plugin must be initialized after the multiselect plugin. After initializing keyboard navigation, call `onKeyboardNavigationInit()`: + +```javascript +const multiselectPlugin = new Multiselect(workspace); +multiselectPlugin.init(options); + +new KeyboardNavigation(workspace); +multiselectPlugin.onKeyboardNavigationInit(); +``` ### Note on disable top blocks plugin The disable top blocks plugin has to be initialized after the multiselect plugin. The main reason behind this is that @@ -145,6 +153,7 @@ customization. If we install the disable top blocks plugin after the multiselect ## API - `Multiselect.init`: Initialize the plugin. +- `Multiselect.onKeyboardNavigationInit`: Call after initializing the keyboard navigation plugin. - `Multiselect.dispose`: Dispose the plugin. - `MultiselectDraggable`: The customized draggable object unique to each workspace that contains the blocks in the multiselection. - `dragSelectionWeakMap`: The WeakMap storing set of currently selected block ids by workspace svg. diff --git a/src/multiselect.js b/src/multiselect.js index 163f7a3..8ca684a 100644 --- a/src/multiselect.js +++ b/src/multiselect.js @@ -138,6 +138,10 @@ export class Multiselect { } + onKeyboardNavigationInit() { + ContextMenu.registerOurKeyboardNavigationMenuItems(this.useCopyPasteCrossTab_); + } + /** * Ignore multi-field updates within the given function. * @param {Function} func The function to call. @@ -201,6 +205,7 @@ export class Multiselect { Blockly.ContextMenuRegistry.registry.unregister('workspaceSelectAll'); Blockly.ContextMenuRegistry.registry.unregister('copy_to_backpack'); ContextMenu.registerOrigContextMenu(); + ContextMenu.registerOrigKeyboardNavigationMenuItems(); Shortcut.unregisterOrigShortcut(); Blockly.ShortcutRegistry.registry.unregister('selectall'); diff --git a/src/multiselect_contextmenu.js b/src/multiselect_contextmenu.js index 75a571a..a00391a 100644 --- a/src/multiselect_contextmenu.js +++ b/src/multiselect_contextmenu.js @@ -16,6 +16,37 @@ import { } from './global'; import {MultiselectDraggable} from './multiselect_draggable'; +const multiselectDisplayText = {}; + +const blockCheck = function(block) { + return block && block.isDeletable() && block.isMovable() && + !hasSelectedParent(block); +}; + +const blockCopyDisplayText = function(scope) { + let workableBlocksLength = 0; + const workspace = scope.block.workspace; + const dragSelection = dragSelectionWeakMap.get(workspace); + dragSelection.forEach(function(id) { + const block = workspace.getBlockById(id); + if (block && blockCheck(block)) { + workableBlocksLength++; + } + }); + if (workableBlocksLength <= 1) { + return Blockly.Msg['CROSS_TAB_COPY']? + Blockly.Msg['CROSS_TAB_COPY'] : Blockly.Msg['COPY_SHORTCUT']; + } else { + return Blockly.Msg['CROSS_TAB_COPY_X_BLOCKS']? + Blockly.Msg['CROSS_TAB_COPY_X_BLOCKS'].replace( + '%1', workableBlocksLength) : + (Blockly.Msg['CROSS_TAB_COPY']? + Blockly.Msg['CROSS_TAB_COPY'] : Blockly.Msg['COPY_SHORTCUT'] + ) + ' (' + + workableBlocksLength + ')'; + } +}; + /** * Copy multiple selected blocks to clipboard. * @param {boolean} useCopyPasteCrossTab Whether or not to use @@ -24,29 +55,7 @@ import {MultiselectDraggable} from './multiselect_draggable'; const registerCopy = function(useCopyPasteCrossTab) { const id = 'blockCopyToStorage'; const copyOptions = { - displayText: function(scope) { - let workableBlocksLength = 0; - const workspace = scope.block.workspace; - const dragSelection = dragSelectionWeakMap.get(workspace); - dragSelection.forEach(function(id) { - const block = workspace.getBlockById(id); - if (block && copyOptions.check(block)) { - workableBlocksLength++; - } - }); - if (workableBlocksLength <= 1) { - return Blockly.Msg['CROSS_TAB_COPY']? - Blockly.Msg['CROSS_TAB_COPY'] : 'Copy'; - } else { - return Blockly.Msg['CROSS_TAB_COPY_X_BLOCKS']? - Blockly.Msg['CROSS_TAB_COPY_X_BLOCKS'].replace( - '%1', workableBlocksLength) : - (Blockly.Msg['CROSS_TAB_COPY']? - Blockly.Msg['CROSS_TAB_COPY'] : 'Copy' - ) + ' (' + - workableBlocksLength + ')'; - } - }, + displayText: blockCopyDisplayText, preconditionFn: function(scope) { const workspace = scope.block.workspace; if (workspace.options.readOnly && !useCopyPasteCrossTab) { @@ -75,10 +84,7 @@ const registerCopy = function(useCopyPasteCrossTab) { } return 'disabled'; }, - check: function(block) { - return block && block.isDeletable() && block.isMovable() && - !hasSelectedParent(block); - }, + check: blockCheck, callback: function(scope) { const workspace = scope.block.workspace; copyData.clear(); @@ -130,6 +136,31 @@ const registerCopy = function(useCopyPasteCrossTab) { Blockly.ContextMenuRegistry.registry.unregister(id); } Blockly.ContextMenuRegistry.registry.register(copyOptions); + multiselectDisplayText[id] = copyOptions.displayText; +}; + +const blockCutDisplayText = function(scope) { + let workableBlocksLength = 0; + const workspace = scope.block.workspace; + const dragSelection = dragSelectionWeakMap.get(workspace); + dragSelection.forEach(function(id) { + const block = workspace.getBlockById(id); + if (block && blockCheck(block)) { + workableBlocksLength++; + } + }); + if (workableBlocksLength <= 1) { + return Blockly.Msg['CROSS_TAB_CUT']? + Blockly.Msg['CROSS_TAB_CUT'] : Blockly.Msg['CUT_SHORTCUT']; + } else { + return Blockly.Msg['CROSS_TAB_CUT_X_BLOCKS']? + Blockly.Msg['CROSS_TAB_CUT_X_BLOCKS'].replace( + '%1', workableBlocksLength) : + (Blockly.Msg['CROSS_TAB_CUT']? + Blockly.Msg['CROSS_TAB_CUT'] : Blockly.Msg['CUT_SHORTCUT'] + ) + ' (' + + workableBlocksLength + ')'; + } }; /** @@ -251,6 +282,7 @@ const registerDuplicate = function() { Blockly.ContextMenuRegistry.registry.unregister(id); } Blockly.ContextMenuRegistry.registry.register(duplicateOption); + multiselectDisplayText[id] = duplicateOption.displayText; }; /** @@ -718,6 +750,23 @@ const registerDelete = function() { Blockly.ContextMenuRegistry.registry.register(deleteOption); }; +const pasteDisplayText = function(useCopyPasteCrossTab) { + const workableDraggableLength = + blockNumGetFromStorage(useCopyPasteCrossTab); + if (workableDraggableLength <= 1) { + return Blockly.Msg['CROSS_TAB_PASTE']? + Blockly.Msg['CROSS_TAB_PASTE'] : Blockly.Msg['PASTE_SHORTCUT']; + } else { + return Blockly.Msg['CROSS_TAB_PASTE_X_ELEMENTS']? + Blockly.Msg['CROSS_TAB_PASTE_X_ELEMENTS'].replace( + '%1', workableDraggableLength) : + (Blockly.Msg['CROSS_TAB_PASTE']? + Blockly.Msg['CROSS_TAB_PASTE'] : Blockly.Msg['PASTE_SHORTCUT'] + ) + ' (' + + workableDraggableLength + ')'; + } +}; + /** * Paste multiple selected draggables from clipboard. * @param {boolean} useCopyPasteCrossTab Whether to use cross tab copy paste. @@ -725,22 +774,7 @@ const registerDelete = function() { const registerPaste = function(useCopyPasteCrossTab) { const id = 'blockPasteFromStorage'; const pasteOption = { - displayText: function() { - const workableDraggableLength = - blockNumGetFromStorage(useCopyPasteCrossTab); - if (workableDraggableLength <= 1) { - return Blockly.Msg['CROSS_TAB_PASTE']? - Blockly.Msg['CROSS_TAB_PASTE'] : 'Paste'; - } else { - return Blockly.Msg['CROSS_TAB_PASTE_X_ELEMENTS']? - Blockly.Msg['CROSS_TAB_PASTE_X_ELEMENTS'].replace( - '%1', workableDraggableLength) : - (Blockly.Msg['CROSS_TAB_PASTE']? - Blockly.Msg['CROSS_TAB_PASTE'] : 'Paste' - ) + ' (' + - workableDraggableLength + ')'; - } - }, + displayText: () => pasteDisplayText(useCopyPasteCrossTab), preconditionFn: function(scope) { return scope.workspace.options.readOnly? 'hidden': (blockNumGetFromStorage(useCopyPasteCrossTab) < 1? @@ -829,6 +863,7 @@ const registerPaste = function(useCopyPasteCrossTab) { Blockly.ContextMenuRegistry.registry.unregister(id); } Blockly.ContextMenuRegistry.registry.register(pasteOption); + multiselectDisplayText[id] = pasteOption.displayText; }; /** @@ -1182,6 +1217,38 @@ const registerCommentDuplicate = function() { Blockly.ContextMenuRegistry.registry.unregister(id); } Blockly.ContextMenuRegistry.registry.register(duplicateOption); + multiselectDisplayText[id] = duplicateOption.displayText; +}; + +const commentCheck = function(comment) { + return comment && comment.isDeletable() && + comment.isMovable(); +}; + +const commentCopyDisplayText = function(scope) { + let workableCommentsLength = 0; + const workspace = scope.comment.workspace; + const dragSelection = dragSelectionWeakMap.get(workspace); + dragSelection.forEach(function(id) { + const comment = workspace.getCommentById(id); + if (comment) { + if (commentCheck(comment)) { + workableCommentsLength++; + } + } + }); + if (workableCommentsLength <= 1) { + return Blockly.Msg['CROSS_TAB_COPY']? + Blockly.Msg['CROSS_TAB_COPY'] : Blockly.Msg['COPY_SHORTCUT']; + } else { + return Blockly.Msg['CROSS_TAB_COPY_X_COMMENTS']? + Blockly.Msg['CROSS_TAB_COPY_X_COMMENTS'].replace( + '%1', workableCommentsLength) : + (Blockly.Msg['CROSS_TAB_COPY']? + Blockly.Msg['CROSS_TAB_COPY'] : Blockly.Msg['COPY_SHORTCUT'] + ) + ' (' + + workableCommentsLength + ')'; + } }; /** @@ -1192,31 +1259,7 @@ const registerCommentDuplicate = function() { const registerCommentCopy = function(useCopyPasteCrossTab) { const id = 'commentCopyToStorage'; const copyOptions = { - displayText: function(scope) { - let workableCommentsLength = 0; - const workspace = scope.comment.workspace; - const dragSelection = dragSelectionWeakMap.get(workspace); - dragSelection.forEach(function(id) { - const comment = workspace.getCommentById(id); - if (comment) { - if (copyOptions.check(comment)) { - workableCommentsLength++; - } - } - }); - if (workableCommentsLength <= 1) { - return Blockly.Msg['CROSS_TAB_COPY']? - Blockly.Msg['CROSS_TAB_COPY'] : 'Copy'; - } else { - return Blockly.Msg['CROSS_TAB_COPY_X_COMMENTS']? - Blockly.Msg['CROSS_TAB_COMMENTS'].replace( - '%1', workableCommentsLength) : - (Blockly.Msg['CROSS_TAB_COPY']? - Blockly.Msg['CROSS_TAB_COPY'] : 'Copy' - ) + ' (' + - workableCommentsLength + ')'; - } - }, + displayText: commentCopyDisplayText, preconditionFn: function(scope) { const workspace = scope.comment.workspace; if (workspace.options.readOnly && !useCopyPasteCrossTab) { @@ -1247,10 +1290,7 @@ const registerCommentCopy = function(useCopyPasteCrossTab) { } return 'disabled'; }, - check: function(comment) { - return comment && comment.isDeletable() && - comment.isMovable(); - }, + check: commentCheck, callback: function(scope) { const workspace = scope.comment.workspace; copyData.clear(); @@ -1291,6 +1331,33 @@ const registerCommentCopy = function(useCopyPasteCrossTab) { Blockly.ContextMenuRegistry.registry.unregister(id); } Blockly.ContextMenuRegistry.registry.register(copyOptions); + multiselectDisplayText[id] = copyOptions.displayText; +}; + +const commentCutDisplayText = function(scope) { + let workableCommentsLength = 0; + const workspace = scope.comment.workspace; + const dragSelection = dragSelectionWeakMap.get(workspace); + dragSelection.forEach(function(id) { + const comment = workspace.getCommentById(id); + if (comment) { + if (commentCheck(comment)) { + workableCommentsLength++; + } + } + }); + if (workableCommentsLength <= 1) { + return Blockly.Msg['CROSS_TAB_CUT']? + Blockly.Msg['CROSS_TAB_CUT'] : Blockly.Msg['CUT_SHORTCUT']; + } else { + return Blockly.Msg['CROSS_TAB_CUT_X_COMMENTS']? + Blockly.Msg['CROSS_TAB_CUT_X_COMMENTS'].replace( + '%1', workableCommentsLength) : + (Blockly.Msg['CROSS_TAB_CUT']? + Blockly.Msg['CROSS_TAB_CUT'] : Blockly.Msg['CUT_SHORTCUT'] + ) + ' (' + + workableCommentsLength + ')'; + } }; /** @@ -1354,3 +1421,52 @@ export const registerOurContextMenu = function(useCopyPasteMenu, useCopyPasteCro registerSelectAll(); updateToMultiCopyToBackpack(); }; + +const origKeyboardNavigationMenuItems = []; + +export const registerOurKeyboardNavigationMenuItems = function(useCopyPasteCrossTab) { + const addMultiselectCount = (keyboardNavMenuItem, multiselectDisplayText) => { + origKeyboardNavigationMenuItems.push(keyboardNavMenuItem); + Blockly.ContextMenuRegistry.registry.unregister(keyboardNavMenuItem.id); + Blockly.ContextMenuRegistry.registry.register({ + ...keyboardNavMenuItem, + displayText: (scope) => { + const element = keyboardNavMenuItem.displayText(scope); + element.firstChild.textContent = multiselectDisplayText(scope); + return element; + }, + }); + }; + + addMultiselectCount( + Blockly.ContextMenuRegistry.registry.getItem('blockCopyFromContextMenu'), + (scope) => { + if (scope.block) return blockCopyDisplayText(scope); + if (scope.comment) return commentCopyDisplayText(scope); + }); + addMultiselectCount( + Blockly.ContextMenuRegistry.registry.getItem('blockCutFromContextMenu'), + (scope) => { + if (scope.block) return blockCutDisplayText(scope); + if (scope.comment) return commentCutDisplayText(scope); + }); + addMultiselectCount( + Blockly.ContextMenuRegistry.registry.getItem('blockDuplicate'), + multiselectDisplayText['blockDuplicate']); + addMultiselectCount( + Blockly.ContextMenuRegistry.registry.getItem('blockPasteFromContextMenu'), + () => pasteDisplayText(useCopyPasteCrossTab)); + addMultiselectCount( + Blockly.ContextMenuRegistry.registry.getItem('commentDuplicate'), + multiselectDisplayText['commentDuplicate']); +}; + +export const registerOrigKeyboardNavigationMenuItems = function() { + for (const item of origKeyboardNavigationMenuItems) { + if (Blockly.ContextMenuRegistry.registry.getItem(item.id) !== null) { + Blockly.ContextMenuRegistry.registry.unregister(item.id); + } + Blockly.ContextMenuRegistry.registry.register(item); + } + origKeyboardNavigationMenuItems.length = 0; +}; diff --git a/test/index.js b/test/index.js index 1c8fa4b..02d1a8e 100644 --- a/test/index.js +++ b/test/index.js @@ -35,6 +35,7 @@ function createWorkspace(blocklyDiv, options) { // Initialize keyboard navigation plugin. new KeyboardNavigation(workspace); + multiselectPlugin.onKeyboardNavigationInit(); return workspace; } From 375e8620ec1f8c7995c9d3b0470df95bf2c15c8b Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Fri, 17 Apr 2026 10:49:59 -0700 Subject: [PATCH 31/67] Fix remaining block not highlighted after multiselect cut --- src/multiselect_shortcut.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/multiselect_shortcut.js b/src/multiselect_shortcut.js index 4c75942..3d7dca4 100644 --- a/src/multiselect_shortcut.js +++ b/src/multiselect_shortcut.js @@ -299,6 +299,7 @@ const registerCut = function(useCopyPasteCrossTab) { if (useCopyPasteCrossTab) { dataCopyToStorage(); } + Blockly.getFocusManager().focusTree(workspace); Blockly.Events.setGroup(false); return true; }, From 478dc486deb48c2092a95d1256c493971a38b11f Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Fri, 17 Apr 2026 11:58:00 -0700 Subject: [PATCH 32/67] Fix paste disabled in keyboard nav context menus --- README.md | 2 +- test/index.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9a79309..99cc922 100644 --- a/README.md +++ b/README.md @@ -140,7 +140,7 @@ The keyboard navigation plugin must be initialized after the multiselect plugin. const multiselectPlugin = new Multiselect(workspace); multiselectPlugin.init(options); -new KeyboardNavigation(workspace); +new KeyboardNavigation(workspace, {allowCrossWorkspacePaste: true}); multiselectPlugin.onKeyboardNavigationInit(); ``` diff --git a/test/index.js b/test/index.js index 02d1a8e..8fe9503 100644 --- a/test/index.js +++ b/test/index.js @@ -34,7 +34,7 @@ function createWorkspace(blocklyDiv, options) { multiselectPlugin.init(options); // Initialize keyboard navigation plugin. - new KeyboardNavigation(workspace); + new KeyboardNavigation(workspace, {allowCrossWorkspacePaste: true}); multiselectPlugin.onKeyboardNavigationInit(); return workspace; From 42bbce10f24107ddf3b5713907941ace368c3c1a Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Fri, 17 Apr 2026 14:50:08 -0700 Subject: [PATCH 33/67] Remove duplicate clipboard context menu items when using keyboard nav --- README.md | 4 ++-- test/index.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 99cc922..b677deb 100644 --- a/README.md +++ b/README.md @@ -134,11 +134,11 @@ scope of its wrapped input function. For more information, please check out the following [issue page](https://github.com/mit-cml/workspace-multiselect/issues/50). ### Note on keyboard navigation plugin -The keyboard navigation plugin must be initialized after the multiselect plugin. After initializing keyboard navigation, call `onKeyboardNavigationInit()`: +The keyboard navigation plugin must be initialized after the multiselect plugin. Set `multiselectCopyPaste.menu` to `false` to avoid duplicate clipboard context menu items, pass `allowCrossWorkspacePaste: true` to keyboard navigation, and call `onKeyboardNavigationInit()` after: ```javascript const multiselectPlugin = new Multiselect(workspace); -multiselectPlugin.init(options); +multiselectPlugin.init({multiselectCopyPaste: {menu: false}}); new KeyboardNavigation(workspace, {allowCrossWorkspacePaste: true}); multiselectPlugin.onKeyboardNavigationInit(); diff --git a/test/index.js b/test/index.js index 8fe9503..74a534f 100644 --- a/test/index.js +++ b/test/index.js @@ -112,7 +112,7 @@ document.addEventListener('DOMContentLoaded', function() { multiSelectKeys: ['Shift'], multiselectCopyPaste: { crossTab: true, - menu: true, + menu: false, }, grid: { spacing: 25, From 2f85b3535ce04ce79d641e57e49faad90981ee11 Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Sat, 18 Apr 2026 09:08:10 -0700 Subject: [PATCH 34/67] Fix workspace context menu empty via keyboard nav --- test-e2e/keyboard/workspace.spec.ts | 24 ++++++++++++++++++++++++ test/index.js | 9 +++++++++ 2 files changed, 33 insertions(+) create mode 100644 test-e2e/keyboard/workspace.spec.ts diff --git a/test-e2e/keyboard/workspace.spec.ts b/test-e2e/keyboard/workspace.spec.ts new file mode 100644 index 0000000..99f2154 --- /dev/null +++ b/test-e2e/keyboard/workspace.spec.ts @@ -0,0 +1,24 @@ +import { expect } from "@playwright/test"; +import { cmdOrCtrl, getEmptySpace, loadBlocks, test } from "../test"; + +test.beforeEach(async ({ page, act }) => { + await act(loadBlocks(page, [{ type: "logic_boolean", id: "block1" }])); +}); + +test("workspace context menu", async ({ page, act }) => { + await act( + page.mouse.click(...(await getEmptySpace(page)), { button: "right" }), + ); + await expect(page.getByRole("menu")).toBeVisible(); + const rightClickMenuItems = await page + .getByRole("menuitem") + .allTextContents(); + await act(page.keyboard.press("Escape")); + + await act(page.keyboard.press("W")); + await act(page.keyboard.press(cmdOrCtrl("Enter"))); + await expect(page.getByRole("menu")).toBeVisible(); + const keyboardMenuItems = await page.getByRole("menuitem").allTextContents(); + + expect(keyboardMenuItems).toEqual(rightClickMenuItems); +}); diff --git a/test/index.js b/test/index.js index 74a534f..ef1f21f 100644 --- a/test/index.js +++ b/test/index.js @@ -29,6 +29,15 @@ function createWorkspace(blocklyDiv, options) { const backpack = new Backpack(workspace); backpack.init(); + // Work around backpack plugin bug where its configureContextMenu clears + // all menu items for non-PointerEvent events (e.g. keyboard nav). + const origConfigureContextMenu = workspace.configureContextMenu; + workspace.configureContextMenu = (menuOptions, event) => { + if (event instanceof PointerEvent) { + origConfigureContextMenu.call(null, menuOptions, event); + } + }; + // Initialize multiselect plugin. const multiselectPlugin = new Multiselect(workspace); multiselectPlugin.init(options); From 121d91130fa9dedffaf60d37128c95415b627a95 Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Sat, 18 Apr 2026 15:18:34 -0700 Subject: [PATCH 35/67] Add navigation for multiselect --- src/multiselect.js | 9 + src/multiselect_navigation_policy.js | 159 ++++++++++++++++++ test-e2e/keyboard/multiselect/block.spec.ts | 59 +++++++ test-e2e/keyboard/multiselect/comment.spec.ts | 59 +++++++ test-e2e/keyboard/multiselect/stack.spec.ts | 83 +++++++++ test-e2e/keyboard/select/block.spec.ts | 56 ++++++ test-e2e/keyboard/select/comment.spec.ts | 56 ++++++ test-e2e/keyboard/select/stack.spec.ts | 63 +++++++ test-e2e/test.ts | 31 ++++ 9 files changed, 575 insertions(+) create mode 100644 src/multiselect_navigation_policy.js create mode 100644 test-e2e/keyboard/multiselect/block.spec.ts create mode 100644 test-e2e/keyboard/multiselect/comment.spec.ts create mode 100644 test-e2e/keyboard/multiselect/stack.spec.ts create mode 100644 test-e2e/keyboard/select/block.spec.ts create mode 100644 test-e2e/keyboard/select/comment.spec.ts create mode 100644 test-e2e/keyboard/select/stack.spec.ts diff --git a/src/multiselect.js b/src/multiselect.js index 8ca684a..f6085db 100644 --- a/src/multiselect.js +++ b/src/multiselect.js @@ -19,6 +19,7 @@ import { } from './global'; import {MultiselectControls} from './multiselect_controls'; import {MultiselectDraggable} from './multiselect_draggable'; +import {MultiselectNavigationPolicy} from './multiselect_navigation_policy'; /** * Class for using multiple select blocks on workspace. @@ -140,6 +141,9 @@ export class Multiselect { onKeyboardNavigationInit() { ContextMenu.registerOurKeyboardNavigationMenuItems(this.useCopyPasteCrossTab_); + this.navigationPolicy_ = new MultiselectNavigationPolicy(); + this.workspace_.getNavigator().addNavigationPolicy(this.navigationPolicy_); + this.navigationPolicy_.install(); } /** @@ -223,6 +227,11 @@ export class Multiselect { if (this.origBumpNeighbours) { Blockly.BlockSvg.prototype.bumpNeighbours = this.origBumpNeighbours; } + + if (this.navigationPolicy_) { + this.navigationPolicy_.uninstall(); + this.navigationPolicy_ = null; + } } bindListeners_(div) { diff --git a/src/multiselect_navigation_policy.js b/src/multiselect_navigation_policy.js new file mode 100644 index 0000000..bcb8b6b --- /dev/null +++ b/src/multiselect_navigation_policy.js @@ -0,0 +1,159 @@ +/** + * @license + * Copyright 2026 MIT + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @fileoverview NavigationPolicy for MultiselectDraggable. + */ + +import * as Blockly from "blockly/core"; +import { MultiselectDraggable } from "./multiselect_draggable"; + +/** + * NavigationPolicy that treats MultiselectDraggable as + * a top-level block with no navigable children. + */ +export class MultiselectNavigationPolicy { + getFirstChild(_selection) { + return null; + } + + getParent(selection) { + return selection.workspace; + } + + getNextSibling(selection) { + const { bottomBlock } = + MultiselectNavigationPolicy.getSelectionBounds(selection); + if (bottomBlock instanceof Blockly.comments.RenderedWorkspaceComment) { + return Blockly.navigateStacks(bottomBlock, 1); + } + const anchor = + MultiselectNavigationPolicy.findStatementAncestor(bottomBlock); + return anchor.getNextBlock() || Blockly.navigateStacks(anchor, 1); + } + + getPreviousSibling(selection) { + const { topBlock } = + MultiselectNavigationPolicy.getSelectionBounds(selection); + if (topBlock instanceof Blockly.comments.RenderedWorkspaceComment) { + return Blockly.navigateStacks(topBlock, -1); + } + const anchor = MultiselectNavigationPolicy.findStatementAncestor(topBlock); + return anchor.getPreviousBlock() || Blockly.navigateStacks(anchor, -1); + } + + isNavigable(selection) { + return selection.canBeFocused(); + } + + isApplicable(selection) { + return selection instanceof MultiselectDraggable; + } + + static findStatementAncestor(block) { + let current = block; + while (current && !current.previousConnection) { + current = current.getParent(); + } + return current || block; + } + + static getSelectionBounds(selection) { + const selected = [...selection.subDraggables.keys()]; + selected.sort((a, b) => + MultiselectNavigationPolicy.sortByOrigin(selection.workspace, a, b), + ); + return { + topBlock: selected[0], + bottomBlock: selected[selected.length - 1], + }; + } + + static getSelectedStacks(selection) { + const stacks = new Map(); + for (const [selected] of selection.subDraggables) { + const stack = + selected instanceof Blockly.comments.RenderedWorkspaceComment + ? selected + : selected.getRootBlock(); + stacks.set(stack.id, stack); + } + return [...stacks.values()].sort((a, b) => + MultiselectNavigationPolicy.sortByOrigin(selection.workspace, a, b), + ); + } + + /** + * Angle away from the horizontal to sweep for blocks. Order of execution is + * generally top to bottom, but a small angle changes the scan to give a bit + * of a left to right bias (reversed in RTL). Units are in degrees. See: + * https://tvtropes.org/pmwiki/pmwiki.php/Main/DiagonalBilling + * + * Copied from https://github.com/RaspberryPiFoundation/blockly/blob/blockly-v12.5.1/packages/blockly/core/workspace.ts#L48-L54 + * + */ + static SCAN_ANGLE = 3; + + /** + * Sorts bounded elements on the workspace by their relative position, top to + * bottom (with slight LTR or RTL bias). + * + * Copied from https://github.com/RaspberryPiFoundation/blockly/blob/blockly-v12.5.1/packages/blockly/core/workspace.ts#L198-L212 + * + * @param workspace The workspace the elements belong to. + * @param a The first element to sort. + * @param b The second elment to sort. + * @returns -1, 0 or 1 depending on the sort order. + */ + static sortByOrigin(workspace, a, b) { + const offset = + Math.sin( + Blockly.utils.math.toRadians(MultiselectNavigationPolicy.SCAN_ANGLE), + ) * (workspace.RTL ? -1 : 1); + const aXY = a.getBoundingRectangle().getOrigin(); + const bXY = b.getBoundingRectangle().getOrigin(); + return aXY.y + offset * aXY.x - (bXY.y + offset * bXY.x); + } + + install() { + Blockly.ShortcutRegistry.registry.register({ + name: "multiselect_previous_stack", + allowCollision: true, + preconditionFn: () => + Blockly.getSelected() instanceof MultiselectDraggable, + callback: (workspace) => { + const stacks = MultiselectNavigationPolicy.getSelectedStacks( + Blockly.getSelected(), + ); + workspace.getCursor().setCurNode(Blockly.navigateStacks(stacks[0], -1)); + return true; + }, + keyCodes: [Blockly.utils.KeyCodes.B], + }); + + Blockly.ShortcutRegistry.registry.register({ + name: "multiselect_next_stack", + allowCollision: true, + preconditionFn: () => + Blockly.getSelected() instanceof MultiselectDraggable, + callback: (workspace) => { + const stacks = MultiselectNavigationPolicy.getSelectedStacks( + Blockly.getSelected(), + ); + workspace + .getCursor() + .setCurNode(Blockly.navigateStacks(stacks[stacks.length - 1], 1)); + return true; + }, + keyCodes: [Blockly.utils.KeyCodes.N], + }); + } + + uninstall() { + Blockly.ShortcutRegistry.registry.unregister("multiselect_previous_stack"); + Blockly.ShortcutRegistry.registry.unregister("multiselect_next_stack"); + } +} diff --git a/test-e2e/keyboard/multiselect/block.spec.ts b/test-e2e/keyboard/multiselect/block.spec.ts new file mode 100644 index 0000000..5446e98 --- /dev/null +++ b/test-e2e/keyboard/multiselect/block.spec.ts @@ -0,0 +1,59 @@ +import { expect } from "@playwright/test"; +import { + getBlock, + getFocusedField, + getHighlightedBlockIds, + getSelectedId, + loadBlocks, + test, +} from "../../test"; + +test.beforeEach(async ({ page, act }) => { + await act( + loadBlocks(page, [ + { type: "logic_boolean", id: "block1" }, + { type: "logic_boolean", id: "block2" }, + { type: "math_number", id: "block3" }, + { type: "math_number", id: "block4" }, + ]), + ); + await act(page.keyboard.down("Shift")); + await act( + page.mouse.click(...(await getBlock(page, { id: "block2" })).centerTop), + ); + await act( + page.mouse.click(...(await getBlock(page, { id: "block3" })).centerTop), + ); + await act(page.keyboard.up("Shift")); +}); + +test("navigate up", async ({ page, act }) => { + await act(page.keyboard.press("ArrowUp")); + + expect(await getHighlightedBlockIds(page)).toEqual(["block1"]); + expect(await getSelectedId(page)).toBe("block1"); +}); + +test("navigate down", async ({ page, act }) => { + await act(page.keyboard.press("ArrowDown")); + + expect(await getHighlightedBlockIds(page)).toEqual(["block4"]); + expect(await getSelectedId(page)).toBe("block4"); +}); + +test("navigate left", async ({ page, act }) => { + await act(page.keyboard.press("ArrowLeft")); + + expect(await getHighlightedBlockIds(page)).toEqual([]); + expect(await getFocusedField(page)).toEqual({ + blockId: "block1", + name: "BOOL", + }); +}); + +test("navigate right", async ({ page, act }) => { + await act(page.keyboard.press("ArrowRight")); + + expect(await getHighlightedBlockIds(page)).toEqual(["block4"]); + expect(await getSelectedId(page)).toBe("block4"); +}); diff --git a/test-e2e/keyboard/multiselect/comment.spec.ts b/test-e2e/keyboard/multiselect/comment.spec.ts new file mode 100644 index 0000000..05fee65 --- /dev/null +++ b/test-e2e/keyboard/multiselect/comment.spec.ts @@ -0,0 +1,59 @@ +import { expect } from "@playwright/test"; +import { + getComment, + getFocusedCommentButton, + getHighlightedCommentIds, + getSelectedId, + loadComments, + test, +} from "../../test"; + +test.beforeEach(async ({ page, act }) => { + await act( + loadComments(page, [ + { id: "comment1" }, + { id: "comment2" }, + { id: "comment3" }, + { id: "comment4" }, + ]), + ); + await act(page.keyboard.down("Shift")); + await act( + page.mouse.click(...(await getComment(page, "comment2")).centerTop), + ); + await act( + page.mouse.click(...(await getComment(page, "comment3")).centerTop), + ); + await act(page.keyboard.up("Shift")); +}); + +test("navigate up", async ({ page, act }) => { + await act(page.keyboard.press("ArrowUp")); + + expect(await getHighlightedCommentIds(page)).toEqual(["comment1"]); + expect(await getSelectedId(page)).toBe("comment1"); +}); + +test("navigate down", async ({ page, act }) => { + await act(page.keyboard.press("ArrowDown")); + + expect(await getHighlightedCommentIds(page)).toEqual(["comment4"]); + expect(await getSelectedId(page)).toBe("comment4"); +}); + +test("navigate left", async ({ page, act }) => { + await act(page.keyboard.press("ArrowLeft")); + + expect(await getHighlightedCommentIds(page)).toEqual([]); + expect(await getFocusedCommentButton(page)).toEqual({ + commentId: "comment1", + type: "delete", + }); +}); + +test("navigate right", async ({ page, act }) => { + await act(page.keyboard.press("ArrowRight")); + + expect(await getHighlightedCommentIds(page)).toEqual(["comment4"]); + expect(await getSelectedId(page)).toBe("comment4"); +}); diff --git a/test-e2e/keyboard/multiselect/stack.spec.ts b/test-e2e/keyboard/multiselect/stack.spec.ts new file mode 100644 index 0000000..02cac0d --- /dev/null +++ b/test-e2e/keyboard/multiselect/stack.spec.ts @@ -0,0 +1,83 @@ +import { expect } from "@playwright/test"; +import { + getBlock, + getHighlightedBlockIds, + getSelectedId, + loadBlocks, + test, +} from "../../test"; + +test.beforeEach(async ({ page, act }) => { + await act( + loadBlocks(page, [ + { type: "math_number", id: "stack1" }, + { + type: "text_print", + id: "stack2_top", + next: { + block: { + type: "text_print", + id: "stack2_middle", + next: { + block: { type: "text_print", id: "stack2_bottom" }, + }, + }, + }, + }, + { + type: "text_print", + id: "stack3_top", + next: { + block: { + type: "text_print", + id: "stack3_middle", + next: { + block: { type: "text_print", id: "stack3_bottom" }, + }, + }, + }, + }, + { type: "math_number", id: "stack4" }, + ]), + ); + await act(page.keyboard.down("Shift")); + await act( + page.mouse.click( + ...(await getBlock(page, { id: "stack2_middle" })).centerTop, + ), + ); + await act( + page.mouse.click( + ...(await getBlock(page, { id: "stack3_middle" })).centerTop, + ), + ); + await act(page.keyboard.up("Shift")); +}); + +test("navigate up", async ({ page, act }) => { + await act(page.keyboard.press("ArrowUp")); + + expect(await getHighlightedBlockIds(page)).toEqual(["stack2_top"]); + expect(await getSelectedId(page)).toBe("stack2_top"); +}); + +test("navigate down", async ({ page, act }) => { + await act(page.keyboard.press("ArrowDown")); + + expect(await getHighlightedBlockIds(page)).toEqual(["stack3_bottom"]); + expect(await getSelectedId(page)).toBe("stack3_bottom"); +}); + +test("navigate to previous stack", async ({ page, act }) => { + await act(page.keyboard.press("B")); + + expect(await getHighlightedBlockIds(page)).toEqual(["stack1"]); + expect(await getSelectedId(page)).toBe("stack1"); +}); + +test("navigate to next stack", async ({ page, act }) => { + await act(page.keyboard.press("N")); + + expect(await getHighlightedBlockIds(page)).toEqual(["stack4"]); + expect(await getSelectedId(page)).toBe("stack4"); +}); diff --git a/test-e2e/keyboard/select/block.spec.ts b/test-e2e/keyboard/select/block.spec.ts new file mode 100644 index 0000000..9988a28 --- /dev/null +++ b/test-e2e/keyboard/select/block.spec.ts @@ -0,0 +1,56 @@ +import { expect } from "@playwright/test"; +import { + getBlock, + getFocusedField, + getHighlightedBlockIds, + getSelectedId, + loadBlocks, + test, +} from "../../test"; + +test.beforeEach(async ({ page, act }) => { + await act( + loadBlocks(page, [ + { type: "logic_boolean", id: "block1" }, + { type: "math_number", id: "block2" }, + { type: "math_number", id: "block3" }, + ]), + ); + await act( + page.mouse.click(...(await getBlock(page, { id: "block2" })).centerTop), + ); +}); + +test("navigate up", async ({ page, act }) => { + await act(page.keyboard.press("ArrowUp")); + + expect(await getHighlightedBlockIds(page)).toEqual(["block1"]); + expect(await getSelectedId(page)).toBe("block1"); +}); + +test("navigate down", async ({ page, act }) => { + await act(page.keyboard.press("ArrowDown")); + + expect(await getHighlightedBlockIds(page)).toEqual(["block3"]); + expect(await getSelectedId(page)).toBe("block3"); +}); + +test("navigate left", async ({ page, act }) => { + await act(page.keyboard.press("ArrowLeft")); + + expect(await getHighlightedBlockIds(page)).toEqual([]); + expect(await getFocusedField(page)).toEqual({ + blockId: "block1", + name: "BOOL", + }); +}); + +test("navigate right", async ({ page, act }) => { + await act(page.keyboard.press("ArrowRight")); + + expect(await getHighlightedBlockIds(page)).toEqual([]); + expect(await getFocusedField(page)).toEqual({ + blockId: "block2", + name: "NUM", + }); +}); diff --git a/test-e2e/keyboard/select/comment.spec.ts b/test-e2e/keyboard/select/comment.spec.ts new file mode 100644 index 0000000..246af10 --- /dev/null +++ b/test-e2e/keyboard/select/comment.spec.ts @@ -0,0 +1,56 @@ +import { expect } from "@playwright/test"; +import { + getComment, + getFocusedCommentButton, + getHighlightedCommentIds, + getSelectedId, + loadComments, + test, +} from "../../test"; + +test.beforeEach(async ({ page, act }) => { + await act( + loadComments(page, [ + { id: "comment1" }, + { id: "comment2" }, + { id: "comment3" }, + ]), + ); + await act( + page.mouse.click(...(await getComment(page, "comment2")).centerTop), + ); +}); + +test("navigate up", async ({ page, act }) => { + await act(page.keyboard.press("ArrowUp")); + + expect(await getHighlightedCommentIds(page)).toEqual(["comment1"]); + expect(await getSelectedId(page)).toBe("comment1"); +}); + +test("navigate down", async ({ page, act }) => { + await act(page.keyboard.press("ArrowDown")); + + expect(await getHighlightedCommentIds(page)).toEqual(["comment3"]); + expect(await getSelectedId(page)).toBe("comment3"); +}); + +test("navigate left", async ({ page, act }) => { + await act(page.keyboard.press("ArrowLeft")); + + expect(await getHighlightedCommentIds(page)).toEqual([]); + expect(await getFocusedCommentButton(page)).toEqual({ + commentId: "comment1", + type: "delete", + }); +}); + +test("navigate right", async ({ page, act }) => { + await act(page.keyboard.press("ArrowRight")); + + expect(await getHighlightedCommentIds(page)).toEqual([]); + expect(await getFocusedCommentButton(page)).toEqual({ + commentId: "comment2", + type: "collapse", + }); +}); diff --git a/test-e2e/keyboard/select/stack.spec.ts b/test-e2e/keyboard/select/stack.spec.ts new file mode 100644 index 0000000..32317b6 --- /dev/null +++ b/test-e2e/keyboard/select/stack.spec.ts @@ -0,0 +1,63 @@ +import { expect } from "@playwright/test"; +import { + getBlock, + getHighlightedBlockIds, + getSelectedId, + loadBlocks, + test, +} from "../../test"; + +test.beforeEach(async ({ page, act }) => { + await act( + loadBlocks(page, [ + { type: "math_number", id: "stack1" }, + { + type: "text_print", + id: "stack2_top", + next: { + block: { + type: "text_print", + id: "stack2_middle", + next: { + block: { type: "text_print", id: "stack2_bottom" }, + }, + }, + }, + }, + { type: "math_number", id: "stack3" }, + ]), + ); + await act( + page.mouse.click( + ...(await getBlock(page, { id: "stack2_middle" })).centerTop, + ), + ); +}); + +test("navigate up within stack", async ({ page, act }) => { + await act(page.keyboard.press("ArrowUp")); + + expect(await getHighlightedBlockIds(page)).toEqual(["stack2_top"]); + expect(await getSelectedId(page)).toBe("stack2_top"); +}); + +test("navigate down within stack", async ({ page, act }) => { + await act(page.keyboard.press("ArrowDown")); + + expect(await getHighlightedBlockIds(page)).toEqual(["stack2_bottom"]); + expect(await getSelectedId(page)).toBe("stack2_bottom"); +}); + +test("navigate to previous stack", async ({ page, act }) => { + await act(page.keyboard.press("B")); + + expect(await getHighlightedBlockIds(page)).toEqual(["stack1"]); + expect(await getSelectedId(page)).toBe("stack1"); +}); + +test("navigate to next stack", async ({ page, act }) => { + await act(page.keyboard.press("N")); + + expect(await getHighlightedBlockIds(page)).toEqual(["stack3"]); + expect(await getSelectedId(page)).toBe("stack3"); +}); diff --git a/test-e2e/test.ts b/test-e2e/test.ts index e3b51d7..0b7b665 100644 --- a/test-e2e/test.ts +++ b/test-e2e/test.ts @@ -284,6 +284,37 @@ export const getHighlightedCommentIds = (page: Page): Promise => export const getSelectedId = (page: Page): Promise => page.evaluate(() => Blockly.getSelected()?.id ?? null); +export const getFocusedField = ( + page: Page, +): Promise<{ blockId: string; name: string } | null> => + page.evaluate(() => { + const focusedNode = Blockly.getFocusManager().getFocusedNode(); + if (focusedNode instanceof Blockly.Field) { + const sourceBlock = focusedNode.getSourceBlock(); + if (sourceBlock && focusedNode.name) { + return { blockId: sourceBlock.id, name: focusedNode.name }; + } + } + return null; + }); + +export const getFocusedCommentButton = ( + page: Page, +): Promise<{ commentId: string; type: "collapse" | "delete" } | null> => + page.evaluate(() => { + const focusedNode = Blockly.getFocusManager().getFocusedNode(); + if (!(focusedNode instanceof Blockly.comments.CommentBarButton)) + return null; + const commentId = (focusedNode as unknown as { id: string }).id; + if (focusedNode instanceof Blockly.comments.CollapseCommentBarButton) { + return { commentId, type: "collapse" as const }; + } + if (focusedNode instanceof Blockly.comments.DeleteCommentBarButton) { + return { commentId, type: "delete" as const }; + } + return null; + }); + export const isEphemeralFocusTaken = (page: Page): Promise => page.evaluate(() => Blockly.getFocusManager().ephemeralFocusTaken()); From bcedae974b87f03691c66a061b70687b4022b98b Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Mon, 20 Apr 2026 10:51:19 -0700 Subject: [PATCH 36/67] Add right-click context menu tests --- .../multiselect/workspace/block.spec.ts | 36 +++++++++++++++++++ .../multiselect/workspace/comment.spec.ts | 32 +++++++++++++++++ .../actions/select/workspace/block.spec.ts | 36 +++++++++++++++++++ .../actions/select/workspace/comment.spec.ts | 32 +++++++++++++++++ 4 files changed, 136 insertions(+) diff --git a/test-e2e/actions/multiselect/workspace/block.spec.ts b/test-e2e/actions/multiselect/workspace/block.spec.ts index bec0124..29d494b 100644 --- a/test-e2e/actions/multiselect/workspace/block.spec.ts +++ b/test-e2e/actions/multiselect/workspace/block.spec.ts @@ -57,6 +57,42 @@ test.beforeEach(async ({ page, act }) => { await act(page.keyboard.up("Shift")); }); +test("open context menu", async ({ page, act }) => { + await act( + page.mouse.click(...(await getBlock(page, { id: "block1" })).centerTop, { + button: "right", + }), + ); + + await expect(page.getByRole("menu")).toBeVisible(); + const expectedMenuItems: [string, boolean][] = [ + ["Duplicate (3) D", true], + ["Add Comment (4)", true], + ["Collapse Block (3)", true], + ["Disable Block (3)", true], + ["Delete 5 Blocks Delete", true], + ["Help", true], + [`Cut (3) ${cmdOrCtrlLabel("X")}`, true], + [`Copy (3) ${cmdOrCtrlLabel("C")}`, true], + [`Paste ${cmdOrCtrlLabel("V")}`, true], + ["Copy to Backpack (3)", true], + ]; + expect(await page.getByRole("menuitem").allTextContents()).toEqual( + expectedMenuItems.map(([name]) => name), + ); + for (const [name, enabled] of expectedMenuItems) { + const menuItem = page.getByRole("menuitem", { exact: true, name }); + if (enabled) { + await expect(menuItem).toBeEnabled(); + } else { + await expect(menuItem).toBeDisabled(); + } + } + expect(await getHighlightedBlockIds(page)).toEqual([]); + expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); + expect(await isEphemeralFocusTaken(page)).toBe(true); +}); + test("duplicate blocks via context menu", async ({ page, act }) => { await act( page.mouse.click(...(await getBlock(page, { id: "block1" })).centerTop, { diff --git a/test-e2e/actions/multiselect/workspace/comment.spec.ts b/test-e2e/actions/multiselect/workspace/comment.spec.ts index 3ee762b..f8f7038 100644 --- a/test-e2e/actions/multiselect/workspace/comment.spec.ts +++ b/test-e2e/actions/multiselect/workspace/comment.spec.ts @@ -33,6 +33,38 @@ test.beforeEach(async ({ page, act }) => { await act(page.keyboard.up("Shift")); }); +test("open context menu", async ({ page, act }) => { + await act( + page.mouse.click(...(await getComment(page, "comment1")).centerTop, { + button: "right", + }), + ); + + await expect(page.getByRole("menu")).toBeVisible(); + const expectedMenuItems: [string, boolean][] = [ + ["Duplicate Comment (2) D", true], + ["Remove Comment (2)", true], + ["Move Comment M", true], + [`Cut (2) ${cmdOrCtrlLabel("X")}`, true], + [`Copy (2) ${cmdOrCtrlLabel("C")}`, true], + [`Paste ${cmdOrCtrlLabel("V")}`, true], + ]; + expect(await page.getByRole("menuitem").allTextContents()).toEqual( + expectedMenuItems.map(([name]) => name), + ); + for (const [name, enabled] of expectedMenuItems) { + const menuItem = page.getByRole("menuitem", { exact: true, name }); + if (enabled) { + await expect(menuItem).toBeEnabled(); + } else { + await expect(menuItem).toBeDisabled(); + } + } + expect(await getHighlightedCommentIds(page)).toEqual([]); + expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); + expect(await isEphemeralFocusTaken(page)).toBe(true); +}); + test("duplicate comments via context menu", async ({ page, act }) => { await act( page.mouse.click(...(await getComment(page, "comment1")).centerTop, { diff --git a/test-e2e/actions/select/workspace/block.spec.ts b/test-e2e/actions/select/workspace/block.spec.ts index 896f850..c27e23a 100644 --- a/test-e2e/actions/select/workspace/block.spec.ts +++ b/test-e2e/actions/select/workspace/block.spec.ts @@ -29,6 +29,42 @@ test.beforeEach(async ({ page, act }) => { ); }); +test("open context menu", async ({ page, act }) => { + await act( + page.mouse.click(...(await getBlock(page, { id: "block1" })).centerTop, { + button: "right", + }), + ); + + await expect(page.getByRole("menu")).toBeVisible(); + const expectedMenuItems: [string, boolean][] = [ + ["Duplicate D", true], + ["Add Comment", true], + ["Collapse Block", true], + ["Disable Block", true], + ["Delete Block Delete", true], + ["Help", true], + [`Cut ${cmdOrCtrlLabel("X")}`, true], + [`Copy ${cmdOrCtrlLabel("C")}`, true], + [`Paste ${cmdOrCtrlLabel("V")}`, true], + ["Copy to Backpack", true], + ]; + expect(await page.getByRole("menuitem").allTextContents()).toEqual( + expectedMenuItems.map(([name]) => name), + ); + for (const [name, enabled] of expectedMenuItems) { + const menuItem = page.getByRole("menuitem", { exact: true, name }); + if (enabled) { + await expect(menuItem).toBeEnabled(); + } else { + await expect(menuItem).toBeDisabled(); + } + } + expect(await getHighlightedBlockIds(page)).toEqual([]); + expect(await getSelectedId(page)).toBe("block1"); + expect(await isEphemeralFocusTaken(page)).toBe(true); +}); + test("duplicate block via context menu", async ({ page, act }) => { await act( page.mouse.click(...(await getBlock(page, { id: "block1" })).centerTop, { diff --git a/test-e2e/actions/select/workspace/comment.spec.ts b/test-e2e/actions/select/workspace/comment.spec.ts index 2102655..fc77f05 100644 --- a/test-e2e/actions/select/workspace/comment.spec.ts +++ b/test-e2e/actions/select/workspace/comment.spec.ts @@ -21,6 +21,38 @@ test.beforeEach(async ({ page, act }) => { ); }); +test("open context menu", async ({ page, act }) => { + await act( + page.mouse.click(...(await getComment(page, "comment1")).centerTop, { + button: "right", + }), + ); + + await expect(page.getByRole("menu")).toBeVisible(); + const expectedMenuItems: [string, boolean][] = [ + ["Duplicate Comment D", true], + ["Remove Comment", true], + ["Move Comment M", true], + [`Cut ${cmdOrCtrlLabel("X")}`, true], + [`Copy ${cmdOrCtrlLabel("C")}`, true], + [`Paste ${cmdOrCtrlLabel("V")}`, true], + ]; + expect(await page.getByRole("menuitem").allTextContents()).toEqual( + expectedMenuItems.map(([name]) => name), + ); + for (const [name, enabled] of expectedMenuItems) { + const menuItem = page.getByRole("menuitem", { exact: true, name }); + if (enabled) { + await expect(menuItem).toBeEnabled(); + } else { + await expect(menuItem).toBeDisabled(); + } + } + expect(await getHighlightedCommentIds(page)).toEqual([]); + expect(await getSelectedId(page)).toBe("comment1"); + expect(await isEphemeralFocusTaken(page)).toBe(true); +}); + test("duplicate comment via context menu", async ({ page, act }) => { await act( page.mouse.click(...(await getComment(page, "comment1")).centerTop, { From 31bd1c0acad27008bf217d97c92bea5f85ec3f98 Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Sat, 18 Apr 2026 18:41:36 -0700 Subject: [PATCH 37/67] Add Cmd+Enter context menu shortcut for multiselect --- src/multiselect_draggable.js | 8 +++- test-e2e/keyboard/multiselect/block.spec.ts | 37 +++++++++++++++++++ test-e2e/keyboard/multiselect/comment.spec.ts | 32 ++++++++++++++++ test-e2e/keyboard/select/block.spec.ts | 37 +++++++++++++++++++ test-e2e/keyboard/select/comment.spec.ts | 31 ++++++++++++++++ 5 files changed, 143 insertions(+), 2 deletions(-) diff --git a/src/multiselect_draggable.js b/src/multiselect_draggable.js index 205e412..f666ec6 100644 --- a/src/multiselect_draggable.js +++ b/src/multiselect_draggable.js @@ -9,6 +9,7 @@ */ import * as Blockly from 'blockly/core'; import {dragSelectionWeakMap, hasSelectedParent, inMultipleSelectionModeWeakMap} from './global'; +import {MultiselectNavigationPolicy} from './multiselect_navigation_policy'; /** @@ -369,8 +370,11 @@ export class MultiselectDraggable { // IContextMenu methods showContextMenu(e) { - const targetId = e.target.closest('[data-id]').getAttribute('data-id'); - const target = this.workspace.getBlockById(targetId) || this.workspace.getCommentById(targetId); + const targetId = e.target?.closest('[data-id]')?.getAttribute('data-id'); + const target = targetId + ? this.workspace.getBlockById(targetId) || + this.workspace.getCommentById(targetId) + : MultiselectNavigationPolicy.getSelectionBounds(this).topBlock; target.showContextMenu(e); } diff --git a/test-e2e/keyboard/multiselect/block.spec.ts b/test-e2e/keyboard/multiselect/block.spec.ts index 5446e98..2425e2c 100644 --- a/test-e2e/keyboard/multiselect/block.spec.ts +++ b/test-e2e/keyboard/multiselect/block.spec.ts @@ -1,9 +1,13 @@ import { expect } from "@playwright/test"; import { + cmdOrCtrl, + cmdOrCtrlLabel, getBlock, getFocusedField, getHighlightedBlockIds, + getMultiselectDraggableId, getSelectedId, + isEphemeralFocusTaken, loadBlocks, test, } from "../../test"; @@ -57,3 +61,36 @@ test("navigate right", async ({ page, act }) => { expect(await getHighlightedBlockIds(page)).toEqual(["block4"]); expect(await getSelectedId(page)).toBe("block4"); }); + +test("open context menu", async ({ page, act }) => { + await act(page.keyboard.press(cmdOrCtrl("Enter"))); + + await expect(page.getByRole("menu")).toBeVisible(); + const expectedMenuItems: [string, boolean][] = [ + ["Duplicate (2) D", true], + ["Add Comment (2)", true], + ["Collapse Block (2)", true], + ["Disable Block (2)", true], + ["Delete 2 Blocks Delete", true], + ["Help", true], + ["Move Block M", false], + [`Cut (2) ${cmdOrCtrlLabel("X")}`, true], + [`Copy (2) ${cmdOrCtrlLabel("C")}`, true], + [`Paste ${cmdOrCtrlLabel("V")}`, true], + ["Copy to Backpack (2)", true], + ]; + expect(await page.getByRole("menuitem").allTextContents()).toEqual( + expectedMenuItems.map(([name]) => name), + ); + for (const [name, enabled] of expectedMenuItems) { + const menuItem = page.getByRole("menuitem", { exact: true, name }); + if (enabled) { + await expect(menuItem).toBeEnabled(); + } else { + await expect(menuItem).toBeDisabled(); + } + } + expect(await getHighlightedBlockIds(page)).toEqual([]); + expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); + expect(await isEphemeralFocusTaken(page)).toBe(true); +}); diff --git a/test-e2e/keyboard/multiselect/comment.spec.ts b/test-e2e/keyboard/multiselect/comment.spec.ts index 05fee65..e57360b 100644 --- a/test-e2e/keyboard/multiselect/comment.spec.ts +++ b/test-e2e/keyboard/multiselect/comment.spec.ts @@ -1,9 +1,13 @@ import { expect } from "@playwright/test"; import { + cmdOrCtrl, + cmdOrCtrlLabel, getComment, getFocusedCommentButton, getHighlightedCommentIds, + getMultiselectDraggableId, getSelectedId, + isEphemeralFocusTaken, loadComments, test, } from "../../test"; @@ -57,3 +61,31 @@ test("navigate right", async ({ page, act }) => { expect(await getHighlightedCommentIds(page)).toEqual(["comment4"]); expect(await getSelectedId(page)).toBe("comment4"); }); + +test("open context menu", async ({ page, act }) => { + await act(page.keyboard.press(cmdOrCtrl("Enter"))); + + await expect(page.getByRole("menu")).toBeVisible(); + const expectedMenuItems: [string, boolean][] = [ + ["Duplicate Comment (2) D", true], + ["Remove Comment (2)", true], + ["Move Comment M", true], + [`Cut (2) ${cmdOrCtrlLabel("X")}`, true], + [`Copy (2) ${cmdOrCtrlLabel("C")}`, true], + [`Paste ${cmdOrCtrlLabel("V")}`, true], + ]; + expect(await page.getByRole("menuitem").allTextContents()).toEqual( + expectedMenuItems.map(([name]) => name), + ); + for (const [name, enabled] of expectedMenuItems) { + const menuItem = page.getByRole("menuitem", { exact: true, name }); + if (enabled) { + await expect(menuItem).toBeEnabled(); + } else { + await expect(menuItem).toBeDisabled(); + } + } + expect(await getHighlightedCommentIds(page)).toEqual([]); + expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); + expect(await isEphemeralFocusTaken(page)).toBe(true); +}); diff --git a/test-e2e/keyboard/select/block.spec.ts b/test-e2e/keyboard/select/block.spec.ts index 9988a28..dd89c93 100644 --- a/test-e2e/keyboard/select/block.spec.ts +++ b/test-e2e/keyboard/select/block.spec.ts @@ -1,9 +1,12 @@ import { expect } from "@playwright/test"; import { + cmdOrCtrl, + cmdOrCtrlLabel, getBlock, getFocusedField, getHighlightedBlockIds, getSelectedId, + isEphemeralFocusTaken, loadBlocks, test, } from "../../test"; @@ -54,3 +57,37 @@ test("navigate right", async ({ page, act }) => { name: "NUM", }); }); + +test("open context menu", async ({ page, act }) => { + await act(page.keyboard.press(cmdOrCtrl("Enter"))); + + await expect(page.getByRole("menu")).toBeVisible(); + const expectedMenuItems: [string, boolean][] = [ + ["Duplicate D", true], + ["Add Comment", true], + ["Collapse Block", true], + ["Disable Block", true], + ["Delete Block Delete", true], + ["Help", true], + ["Move Block M", true], + ["Edit Block contents Right", true], + [`Cut ${cmdOrCtrlLabel("X")}`, true], + [`Copy ${cmdOrCtrlLabel("C")}`, true], + [`Paste ${cmdOrCtrlLabel("V")}`, true], + ["Copy to Backpack", true], + ]; + expect(await page.getByRole("menuitem").allTextContents()).toEqual( + expectedMenuItems.map(([name]) => name), + ); + for (const [name, enabled] of expectedMenuItems) { + const menuItem = page.getByRole("menuitem", { exact: true, name }); + if (enabled) { + await expect(menuItem).toBeEnabled(); + } else { + await expect(menuItem).toBeDisabled(); + } + } + expect(await getHighlightedBlockIds(page)).toEqual([]); + expect(await getSelectedId(page)).toBe("block2"); + expect(await isEphemeralFocusTaken(page)).toBe(true); +}); diff --git a/test-e2e/keyboard/select/comment.spec.ts b/test-e2e/keyboard/select/comment.spec.ts index 246af10..45f26da 100644 --- a/test-e2e/keyboard/select/comment.spec.ts +++ b/test-e2e/keyboard/select/comment.spec.ts @@ -1,9 +1,12 @@ import { expect } from "@playwright/test"; import { + cmdOrCtrl, + cmdOrCtrlLabel, getComment, getFocusedCommentButton, getHighlightedCommentIds, getSelectedId, + isEphemeralFocusTaken, loadComments, test, } from "../../test"; @@ -54,3 +57,31 @@ test("navigate right", async ({ page, act }) => { type: "collapse", }); }); + +test("open context menu", async ({ page, act }) => { + await act(page.keyboard.press(cmdOrCtrl("Enter"))); + + await expect(page.getByRole("menu")).toBeVisible(); + const expectedMenuItems: [string, boolean][] = [ + ["Duplicate Comment D", true], + ["Remove Comment", true], + ["Move Comment M", true], + [`Cut ${cmdOrCtrlLabel("X")}`, true], + [`Copy ${cmdOrCtrlLabel("C")}`, true], + [`Paste ${cmdOrCtrlLabel("V")}`, true], + ]; + expect(await page.getByRole("menuitem").allTextContents()).toEqual( + expectedMenuItems.map(([name]) => name), + ); + for (const [name, enabled] of expectedMenuItems) { + const menuItem = page.getByRole("menuitem", { exact: true, name }); + if (enabled) { + await expect(menuItem).toBeEnabled(); + } else { + await expect(menuItem).toBeDisabled(); + } + } + expect(await getHighlightedCommentIds(page)).toEqual([]); + expect(await getSelectedId(page)).toBe("comment2"); + expect(await isEphemeralFocusTaken(page)).toBe(true); +}); From 200fd0c1a3ba1911eb4f0df7e59753e74f43e00a Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Mon, 20 Apr 2026 12:44:59 -0700 Subject: [PATCH 38/67] Hide move menu item for multiselect and right-click comments --- src/multiselect_contextmenu.js | 24 +++++++++++++++++++ .../multiselect/workspace/comment.spec.ts | 1 - .../actions/select/workspace/comment.spec.ts | 1 - test-e2e/keyboard/multiselect/block.spec.ts | 1 - test-e2e/keyboard/multiselect/comment.spec.ts | 1 - 5 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/multiselect_contextmenu.js b/src/multiselect_contextmenu.js index a00391a..3b992ec 100644 --- a/src/multiselect_contextmenu.js +++ b/src/multiselect_contextmenu.js @@ -1459,6 +1459,30 @@ export const registerOurKeyboardNavigationMenuItems = function(useCopyPasteCross addMultiselectCount( Blockly.ContextMenuRegistry.registry.getItem('commentDuplicate'), multiselectDisplayText['commentDuplicate']); + + const hideForMultiselect = (keyboardNavMenuItem) => { + const originalPrecondition = keyboardNavMenuItem.preconditionFn; + origKeyboardNavigationMenuItems.push(keyboardNavMenuItem); + Blockly.ContextMenuRegistry.registry.unregister(keyboardNavMenuItem.id); + Blockly.ContextMenuRegistry.registry.register({ + ...keyboardNavMenuItem, + preconditionFn: (scope, menuOpenEvent) => { + if (Blockly.getSelected() instanceof MultiselectDraggable) return 'hidden'; + // MOVE_COMMENT is missing the PointerEvent check that MOVE_BLOCK + // has, so it incorrectly appears in right-click menus. + // https://github.com/RaspberryPiFoundation/blockly-keyboard-experimentation/blob/%40blockly/keyboard-navigation%403.0.5/src/actions/move.ts#L187-L194 + if (menuOpenEvent instanceof PointerEvent) return 'hidden'; + return originalPrecondition(scope, menuOpenEvent); + }, + }); + }; + + if (Blockly.ContextMenuRegistry.registry.getItem('move')) { + hideForMultiselect(Blockly.ContextMenuRegistry.registry.getItem('move')); + } + if (Blockly.ContextMenuRegistry.registry.getItem('move_comment')) { + hideForMultiselect(Blockly.ContextMenuRegistry.registry.getItem('move_comment')); + } }; export const registerOrigKeyboardNavigationMenuItems = function() { diff --git a/test-e2e/actions/multiselect/workspace/comment.spec.ts b/test-e2e/actions/multiselect/workspace/comment.spec.ts index f8f7038..2de0315 100644 --- a/test-e2e/actions/multiselect/workspace/comment.spec.ts +++ b/test-e2e/actions/multiselect/workspace/comment.spec.ts @@ -44,7 +44,6 @@ test("open context menu", async ({ page, act }) => { const expectedMenuItems: [string, boolean][] = [ ["Duplicate Comment (2) D", true], ["Remove Comment (2)", true], - ["Move Comment M", true], [`Cut (2) ${cmdOrCtrlLabel("X")}`, true], [`Copy (2) ${cmdOrCtrlLabel("C")}`, true], [`Paste ${cmdOrCtrlLabel("V")}`, true], diff --git a/test-e2e/actions/select/workspace/comment.spec.ts b/test-e2e/actions/select/workspace/comment.spec.ts index fc77f05..f348751 100644 --- a/test-e2e/actions/select/workspace/comment.spec.ts +++ b/test-e2e/actions/select/workspace/comment.spec.ts @@ -32,7 +32,6 @@ test("open context menu", async ({ page, act }) => { const expectedMenuItems: [string, boolean][] = [ ["Duplicate Comment D", true], ["Remove Comment", true], - ["Move Comment M", true], [`Cut ${cmdOrCtrlLabel("X")}`, true], [`Copy ${cmdOrCtrlLabel("C")}`, true], [`Paste ${cmdOrCtrlLabel("V")}`, true], diff --git a/test-e2e/keyboard/multiselect/block.spec.ts b/test-e2e/keyboard/multiselect/block.spec.ts index 2425e2c..c04bf23 100644 --- a/test-e2e/keyboard/multiselect/block.spec.ts +++ b/test-e2e/keyboard/multiselect/block.spec.ts @@ -73,7 +73,6 @@ test("open context menu", async ({ page, act }) => { ["Disable Block (2)", true], ["Delete 2 Blocks Delete", true], ["Help", true], - ["Move Block M", false], [`Cut (2) ${cmdOrCtrlLabel("X")}`, true], [`Copy (2) ${cmdOrCtrlLabel("C")}`, true], [`Paste ${cmdOrCtrlLabel("V")}`, true], diff --git a/test-e2e/keyboard/multiselect/comment.spec.ts b/test-e2e/keyboard/multiselect/comment.spec.ts index e57360b..3fffb3e 100644 --- a/test-e2e/keyboard/multiselect/comment.spec.ts +++ b/test-e2e/keyboard/multiselect/comment.spec.ts @@ -69,7 +69,6 @@ test("open context menu", async ({ page, act }) => { const expectedMenuItems: [string, boolean][] = [ ["Duplicate Comment (2) D", true], ["Remove Comment (2)", true], - ["Move Comment M", true], [`Cut (2) ${cmdOrCtrlLabel("X")}`, true], [`Copy (2) ${cmdOrCtrlLabel("C")}`, true], [`Paste ${cmdOrCtrlLabel("V")}`, true], From 684f287ce59b78a631436757a0617ad6522c4f47 Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Sun, 19 Apr 2026 10:27:06 -0700 Subject: [PATCH 39/67] Add tests for dragging blocks out of stacks --- .../multiselect/workspace/stack.spec.ts | 75 +++++++++++++++++++ .../actions/select/workspace/stack.spec.ts | 45 +++++++++++ 2 files changed, 120 insertions(+) create mode 100644 test-e2e/actions/multiselect/workspace/stack.spec.ts create mode 100644 test-e2e/actions/select/workspace/stack.spec.ts diff --git a/test-e2e/actions/multiselect/workspace/stack.spec.ts b/test-e2e/actions/multiselect/workspace/stack.spec.ts new file mode 100644 index 0000000..01d7283 --- /dev/null +++ b/test-e2e/actions/multiselect/workspace/stack.spec.ts @@ -0,0 +1,75 @@ +import { expect } from "@playwright/test"; +import { + getBlock, + getEmptySpace, + getStackBlockIds, + loadBlocks, + test, +} from "../../../test"; + +test.beforeEach(async ({ page, act }) => { + await act( + loadBlocks(page, [ + { + type: "text_print", + id: "stack1_top", + next: { + block: { + type: "text_print", + id: "stack1_middle", + next: { + block: { type: "text_print", id: "stack1_bottom" }, + }, + }, + }, + }, + { + type: "text_print", + id: "stack2_top", + next: { + block: { + type: "text_print", + id: "stack2_middle", + next: { + block: { type: "text_print", id: "stack2_bottom" }, + }, + }, + }, + }, + ]), + ); + await act(page.keyboard.down("Shift")); + await act( + page.mouse.click( + ...(await getBlock(page, { id: "stack1_middle" })).centerTop, + ), + ); + await act( + page.mouse.click( + ...(await getBlock(page, { id: "stack2_middle" })).centerTop, + ), + ); + await act(page.keyboard.up("Shift")); +}); + +test("drag middle blocks from stacks", async ({ page, act }) => { + await act( + page.mouse.move( + ...(await getBlock(page, { id: "stack1_middle" })).centerTop, + ), + ); + await act(page.mouse.down()); + await act(page.mouse.move(...(await getEmptySpace(page)))); + await act(page.mouse.up()); + + expect(await getStackBlockIds(page, "stack1_top")).toEqual(["stack1_top"]); + expect(await getStackBlockIds(page, "stack1_middle")).toEqual([ + "stack1_middle", + "stack1_bottom", + ]); + expect(await getStackBlockIds(page, "stack2_top")).toEqual(["stack2_top"]); + expect(await getStackBlockIds(page, "stack2_middle")).toEqual([ + "stack2_middle", + "stack2_bottom", + ]); +}); diff --git a/test-e2e/actions/select/workspace/stack.spec.ts b/test-e2e/actions/select/workspace/stack.spec.ts new file mode 100644 index 0000000..428b9a1 --- /dev/null +++ b/test-e2e/actions/select/workspace/stack.spec.ts @@ -0,0 +1,45 @@ +import { expect } from "@playwright/test"; +import { + getBlock, + getEmptySpace, + getStackBlockIds, + loadBlocks, + test, +} from "../../../test"; + +test.beforeEach(async ({ page, act }) => { + await act( + loadBlocks(page, [ + { + type: "text_print", + id: "stack_top", + next: { + block: { + type: "text_print", + id: "stack_middle", + next: { + block: { type: "text_print", id: "stack_bottom" }, + }, + }, + }, + }, + ]), + ); +}); + +test("drag middle block from stack", async ({ page, act }) => { + await act( + page.mouse.move( + ...(await getBlock(page, { id: "stack_middle" })).centerTop, + ), + ); + await act(page.mouse.down()); + await act(page.mouse.move(...(await getEmptySpace(page)))); + await act(page.mouse.up()); + + expect(await getStackBlockIds(page, "stack_top")).toEqual(["stack_top"]); + expect(await getStackBlockIds(page, "stack_middle")).toEqual([ + "stack_middle", + "stack_bottom", + ]); +}); From 72c4534a9598b8fdd04b42117d8b9112c9931e16 Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Sun, 19 Apr 2026 10:16:15 -0700 Subject: [PATCH 40/67] Add single-select keyboard move tests --- .../actions/select/workspace/block.spec.ts | 4 +- test-e2e/keyboard/select/block.spec.ts | 97 +++++++++ test-e2e/keyboard/select/comment.spec.ts | 193 ++++++++++++++++++ test-e2e/keyboard/select/stack.spec.ts | 189 +++++++++++++++++ test-e2e/test.ts | 18 ++ 5 files changed, 499 insertions(+), 2 deletions(-) diff --git a/test-e2e/actions/select/workspace/block.spec.ts b/test-e2e/actions/select/workspace/block.spec.ts index c27e23a..46c4e7a 100644 --- a/test-e2e/actions/select/workspace/block.spec.ts +++ b/test-e2e/actions/select/workspace/block.spec.ts @@ -284,8 +284,8 @@ test("drag block to backpack", async ({ page, act }) => { expect(await getHighlightedBlockIds(page)).toEqual(["block1"]); expect(await getSelectedId(page)).toBe("block1"); const block1BoundsEnd = (await getBlock(page, { id: "block1" })).bounds; - expect(block1BoundsEnd.left).toBe(block1BoundsStart.left); - expect(block1BoundsEnd.top).toBe(block1BoundsStart.top); + expect(block1BoundsEnd.left).toBeCloseTo(block1BoundsStart.left); + expect(block1BoundsEnd.top).toBeCloseTo(block1BoundsStart.top); await openBackpack(page); await getBlock(page, { type: "logic_boolean", workspace: "backpack" }); diff --git a/test-e2e/keyboard/select/block.spec.ts b/test-e2e/keyboard/select/block.spec.ts index dd89c93..e095878 100644 --- a/test-e2e/keyboard/select/block.spec.ts +++ b/test-e2e/keyboard/select/block.spec.ts @@ -4,6 +4,7 @@ import { cmdOrCtrlLabel, getBlock, getFocusedField, + getGridSpacing, getHighlightedBlockIds, getSelectedId, isEphemeralFocusTaken, @@ -58,6 +59,102 @@ test("navigate right", async ({ page, act }) => { }); }); +test("unconstrained move via shortcut", async ({ page, act }) => { + const gridSpacing = await getGridSpacing(page); + if (gridSpacing === null) throw new Error("Workspace has no grid"); + const block1BoundsStart = (await getBlock(page, { id: "block1" })).bounds; + const block2BoundsStart = (await getBlock(page, { id: "block2" })).bounds; + const block3BoundsStart = (await getBlock(page, { id: "block3" })).bounds; + + await act(page.keyboard.press("M")); + await expect(page.locator(".blocklyMoveIndicatorBubble")).toBeVisible(); + await act(page.keyboard.press("Alt+ArrowRight")); + await act(page.keyboard.press("Enter")); + await expect(page.locator(".blocklyMoveIndicatorBubble")).not.toBeVisible(); + + const block1BoundsEnd = (await getBlock(page, { id: "block1" })).bounds; + const block2BoundsEnd = (await getBlock(page, { id: "block2" })).bounds; + const block3BoundsEnd = (await getBlock(page, { id: "block3" })).bounds; + expect(block2BoundsEnd.left).toBeCloseTo( + block2BoundsStart.left + gridSpacing, + ); + expect(block2BoundsEnd.top).toBeCloseTo(block2BoundsStart.top); + expect(block1BoundsEnd.left).toBeCloseTo(block1BoundsStart.left); + expect(block1BoundsEnd.top).toBeCloseTo(block1BoundsStart.top); + expect(block3BoundsEnd.left).toBeCloseTo(block3BoundsStart.left); + expect(block3BoundsEnd.top).toBeCloseTo(block3BoundsStart.top); + expect(await getHighlightedBlockIds(page)).toEqual(["block2"]); + expect(await getSelectedId(page)).toBe("block2"); +}); + +test("abort unconstrained move via shortcut", async ({ page, act }) => { + const block2BoundsStart = (await getBlock(page, { id: "block2" })).bounds; + + await act(page.keyboard.press("M")); + await expect(page.locator(".blocklyMoveIndicatorBubble")).toBeVisible(); + await act(page.keyboard.press("Alt+ArrowRight")); + await act(page.keyboard.press("Escape")); + await expect(page.locator(".blocklyMoveIndicatorBubble")).not.toBeVisible(); + + const block2BoundsEnd = (await getBlock(page, { id: "block2" })).bounds; + expect(block2BoundsEnd.left).toBeCloseTo(block2BoundsStart.left); + expect(block2BoundsEnd.top).toBeCloseTo(block2BoundsStart.top); + expect(await getHighlightedBlockIds(page)).toEqual(["block2"]); + expect(await getSelectedId(page)).toBe("block2"); +}); + +test("unconstrained move via context menu", async ({ page, act }) => { + const gridSpacing = await getGridSpacing(page); + if (gridSpacing === null) throw new Error("Workspace has no grid"); + const block1BoundsStart = (await getBlock(page, { id: "block1" })).bounds; + const block2BoundsStart = (await getBlock(page, { id: "block2" })).bounds; + const block3BoundsStart = (await getBlock(page, { id: "block3" })).bounds; + + await act(page.keyboard.press(cmdOrCtrl("Enter"))); + for (let i = 0; i < 6; i++) { + await act(page.keyboard.press("ArrowDown")); + } + await act(page.keyboard.press("Enter")); + await expect(page.locator(".blocklyMoveIndicatorBubble")).toBeVisible(); + await act(page.keyboard.press("Alt+ArrowRight")); + await act(page.keyboard.press("Enter")); + await expect(page.locator(".blocklyMoveIndicatorBubble")).not.toBeVisible(); + + const block1BoundsEnd = (await getBlock(page, { id: "block1" })).bounds; + const block2BoundsEnd = (await getBlock(page, { id: "block2" })).bounds; + const block3BoundsEnd = (await getBlock(page, { id: "block3" })).bounds; + expect(block2BoundsEnd.left).toBeCloseTo( + block2BoundsStart.left + gridSpacing, + ); + expect(block2BoundsEnd.top).toBeCloseTo(block2BoundsStart.top); + expect(block1BoundsEnd.left).toBeCloseTo(block1BoundsStart.left); + expect(block1BoundsEnd.top).toBeCloseTo(block1BoundsStart.top); + expect(block3BoundsEnd.left).toBeCloseTo(block3BoundsStart.left); + expect(block3BoundsEnd.top).toBeCloseTo(block3BoundsStart.top); + expect(await getHighlightedBlockIds(page)).toEqual(["block2"]); + expect(await getSelectedId(page)).toBe("block2"); +}); + +test("abort unconstrained move via context menu", async ({ page, act }) => { + const block2BoundsStart = (await getBlock(page, { id: "block2" })).bounds; + + await act(page.keyboard.press(cmdOrCtrl("Enter"))); + for (let i = 0; i < 6; i++) { + await act(page.keyboard.press("ArrowDown")); + } + await act(page.keyboard.press("Enter")); + await expect(page.locator(".blocklyMoveIndicatorBubble")).toBeVisible(); + await act(page.keyboard.press("Alt+ArrowRight")); + await act(page.keyboard.press("Escape")); + await expect(page.locator(".blocklyMoveIndicatorBubble")).not.toBeVisible(); + + const block2BoundsEnd = (await getBlock(page, { id: "block2" })).bounds; + expect(block2BoundsEnd.left).toBeCloseTo(block2BoundsStart.left); + expect(block2BoundsEnd.top).toBeCloseTo(block2BoundsStart.top); + expect(await getHighlightedBlockIds(page)).toEqual(["block2"]); + expect(await getSelectedId(page)).toBe("block2"); +}); + test("open context menu", async ({ page, act }) => { await act(page.keyboard.press(cmdOrCtrl("Enter"))); diff --git a/test-e2e/keyboard/select/comment.spec.ts b/test-e2e/keyboard/select/comment.spec.ts index 45f26da..b0803c5 100644 --- a/test-e2e/keyboard/select/comment.spec.ts +++ b/test-e2e/keyboard/select/comment.spec.ts @@ -4,6 +4,7 @@ import { cmdOrCtrlLabel, getComment, getFocusedCommentButton, + getGridSpacing, getHighlightedCommentIds, getSelectedId, isEphemeralFocusTaken, @@ -58,6 +59,198 @@ test("navigate right", async ({ page, act }) => { }); }); +test("constrained move via shortcut", async ({ page, act }) => { + const gridSpacing = await getGridSpacing(page); + if (gridSpacing === null) throw new Error("Workspace has no grid"); + const comment1BoundsStart = (await getComment(page, "comment1")).bounds; + const comment2BoundsStart = (await getComment(page, "comment2")).bounds; + const comment3BoundsStart = (await getComment(page, "comment3")).bounds; + + await act(page.keyboard.press("M")); + await expect(page.locator(".blocklyMoveIndicatorBubble")).toBeVisible(); + await act(page.keyboard.press("ArrowRight")); + await act(page.keyboard.press("Enter")); + await expect(page.locator(".blocklyMoveIndicatorBubble")).not.toBeVisible(); + + const comment1BoundsEnd = (await getComment(page, "comment1")).bounds; + const comment2BoundsEnd = (await getComment(page, "comment2")).bounds; + const comment3BoundsEnd = (await getComment(page, "comment3")).bounds; + expect(comment2BoundsEnd.left).toBeCloseTo( + comment2BoundsStart.left + gridSpacing, + ); + expect(comment2BoundsEnd.top).toBeCloseTo(comment2BoundsStart.top); + expect(comment1BoundsEnd.left).toBeCloseTo(comment1BoundsStart.left); + expect(comment1BoundsEnd.top).toBeCloseTo(comment1BoundsStart.top); + expect(comment3BoundsEnd.left).toBeCloseTo(comment3BoundsStart.left); + expect(comment3BoundsEnd.top).toBeCloseTo(comment3BoundsStart.top); + expect(await getHighlightedCommentIds(page)).toEqual(["comment2"]); + expect(await getSelectedId(page)).toBe("comment2"); +}); + +test("abort constrained move via shortcut", async ({ page, act }) => { + const comment2BoundsStart = (await getComment(page, "comment2")).bounds; + + await act(page.keyboard.press("M")); + await expect(page.locator(".blocklyMoveIndicatorBubble")).toBeVisible(); + await act(page.keyboard.press("ArrowRight")); + await act(page.keyboard.press("Escape")); + await expect(page.locator(".blocklyMoveIndicatorBubble")).not.toBeVisible(); + + const comment2BoundsEnd = (await getComment(page, "comment2")).bounds; + expect(comment2BoundsEnd.top).toBeCloseTo(comment2BoundsStart.top); + expect(comment2BoundsEnd.left).toBeCloseTo(comment2BoundsStart.left); + expect(await getHighlightedCommentIds(page)).toEqual(["comment2"]); + expect(await getSelectedId(page)).toBe("comment2"); +}); + +test("constrained move via context menu", async ({ page, act }) => { + const gridSpacing = await getGridSpacing(page); + if (gridSpacing === null) throw new Error("Workspace has no grid"); + const comment1BoundsStart = (await getComment(page, "comment1")).bounds; + const comment2BoundsStart = (await getComment(page, "comment2")).bounds; + const comment3BoundsStart = (await getComment(page, "comment3")).bounds; + + await act(page.keyboard.press(cmdOrCtrl("Enter"))); + for (let i = 0; i < 2; i++) { + await act(page.keyboard.press("ArrowDown")); + } + await act(page.keyboard.press("Enter")); + await expect(page.locator(".blocklyMoveIndicatorBubble")).toBeVisible(); + await act(page.keyboard.press("ArrowRight")); + await act(page.keyboard.press("Enter")); + await expect(page.locator(".blocklyMoveIndicatorBubble")).not.toBeVisible(); + + const comment1BoundsEnd = (await getComment(page, "comment1")).bounds; + const comment2BoundsEnd = (await getComment(page, "comment2")).bounds; + const comment3BoundsEnd = (await getComment(page, "comment3")).bounds; + expect(comment2BoundsEnd.left).toBeCloseTo( + comment2BoundsStart.left + gridSpacing, + ); + expect(comment2BoundsEnd.top).toBeCloseTo(comment2BoundsStart.top); + expect(comment1BoundsEnd.left).toBeCloseTo(comment1BoundsStart.left); + expect(comment1BoundsEnd.top).toBeCloseTo(comment1BoundsStart.top); + expect(comment3BoundsEnd.left).toBeCloseTo(comment3BoundsStart.left); + expect(comment3BoundsEnd.top).toBeCloseTo(comment3BoundsStart.top); + expect(await getHighlightedCommentIds(page)).toEqual(["comment2"]); + expect(await getSelectedId(page)).toBe("comment2"); +}); + +test("abort constrained move via context menu", async ({ page, act }) => { + const comment2BoundsStart = (await getComment(page, "comment2")).bounds; + + await act(page.keyboard.press(cmdOrCtrl("Enter"))); + for (let i = 0; i < 2; i++) { + await act(page.keyboard.press("ArrowDown")); + } + await act(page.keyboard.press("Enter")); + await expect(page.locator(".blocklyMoveIndicatorBubble")).toBeVisible(); + await act(page.keyboard.press("ArrowRight")); + await act(page.keyboard.press("Escape")); + await expect(page.locator(".blocklyMoveIndicatorBubble")).not.toBeVisible(); + + const comment2BoundsEnd = (await getComment(page, "comment2")).bounds; + expect(comment2BoundsEnd.top).toBeCloseTo(comment2BoundsStart.top); + expect(comment2BoundsEnd.left).toBeCloseTo(comment2BoundsStart.left); + expect(await getHighlightedCommentIds(page)).toEqual(["comment2"]); + expect(await getSelectedId(page)).toBe("comment2"); +}); + +test("unconstrained move via shortcut", async ({ page, act }) => { + const gridSpacing = await getGridSpacing(page); + if (gridSpacing === null) throw new Error("Workspace has no grid"); + const comment1BoundsStart = (await getComment(page, "comment1")).bounds; + const comment2BoundsStart = (await getComment(page, "comment2")).bounds; + const comment3BoundsStart = (await getComment(page, "comment3")).bounds; + + await act(page.keyboard.press("M")); + await expect(page.locator(".blocklyMoveIndicatorBubble")).toBeVisible(); + await act(page.keyboard.press("Alt+ArrowRight")); + await act(page.keyboard.press("Enter")); + await expect(page.locator(".blocklyMoveIndicatorBubble")).not.toBeVisible(); + + const comment1BoundsEnd = (await getComment(page, "comment1")).bounds; + const comment2BoundsEnd = (await getComment(page, "comment2")).bounds; + const comment3BoundsEnd = (await getComment(page, "comment3")).bounds; + expect(comment2BoundsEnd.left).toBeCloseTo( + comment2BoundsStart.left + gridSpacing, + ); + expect(comment2BoundsEnd.top).toBeCloseTo(comment2BoundsStart.top); + expect(comment1BoundsEnd.left).toBeCloseTo(comment1BoundsStart.left); + expect(comment1BoundsEnd.top).toBeCloseTo(comment1BoundsStart.top); + expect(comment3BoundsEnd.left).toBeCloseTo(comment3BoundsStart.left); + expect(comment3BoundsEnd.top).toBeCloseTo(comment3BoundsStart.top); + expect(await getHighlightedCommentIds(page)).toEqual(["comment2"]); + expect(await getSelectedId(page)).toBe("comment2"); +}); + +test("abort unconstrained move via shortcut", async ({ page, act }) => { + const comment2BoundsStart = (await getComment(page, "comment2")).bounds; + + await act(page.keyboard.press("M")); + await expect(page.locator(".blocklyMoveIndicatorBubble")).toBeVisible(); + await act(page.keyboard.press("Alt+ArrowRight")); + await act(page.keyboard.press("Escape")); + await expect(page.locator(".blocklyMoveIndicatorBubble")).not.toBeVisible(); + + const comment2BoundsEnd = (await getComment(page, "comment2")).bounds; + expect(comment2BoundsEnd.top).toBeCloseTo(comment2BoundsStart.top); + expect(comment2BoundsEnd.left).toBeCloseTo(comment2BoundsStart.left); + expect(await getHighlightedCommentIds(page)).toEqual(["comment2"]); + expect(await getSelectedId(page)).toBe("comment2"); +}); + +test("unconstrained move via context menu", async ({ page, act }) => { + const gridSpacing = await getGridSpacing(page); + if (gridSpacing === null) throw new Error("Workspace has no grid"); + const comment1BoundsStart = (await getComment(page, "comment1")).bounds; + const comment2BoundsStart = (await getComment(page, "comment2")).bounds; + const comment3BoundsStart = (await getComment(page, "comment3")).bounds; + + await act(page.keyboard.press(cmdOrCtrl("Enter"))); + for (let i = 0; i < 2; i++) { + await act(page.keyboard.press("ArrowDown")); + } + await act(page.keyboard.press("Enter")); + await expect(page.locator(".blocklyMoveIndicatorBubble")).toBeVisible(); + await act(page.keyboard.press("Alt+ArrowRight")); + await act(page.keyboard.press("Enter")); + await expect(page.locator(".blocklyMoveIndicatorBubble")).not.toBeVisible(); + + const comment1BoundsEnd = (await getComment(page, "comment1")).bounds; + const comment2BoundsEnd = (await getComment(page, "comment2")).bounds; + const comment3BoundsEnd = (await getComment(page, "comment3")).bounds; + expect(comment2BoundsEnd.left).toBeCloseTo( + comment2BoundsStart.left + gridSpacing, + ); + expect(comment2BoundsEnd.top).toBeCloseTo(comment2BoundsStart.top); + expect(comment1BoundsEnd.left).toBeCloseTo(comment1BoundsStart.left); + expect(comment1BoundsEnd.top).toBeCloseTo(comment1BoundsStart.top); + expect(comment3BoundsEnd.left).toBeCloseTo(comment3BoundsStart.left); + expect(comment3BoundsEnd.top).toBeCloseTo(comment3BoundsStart.top); + expect(await getHighlightedCommentIds(page)).toEqual(["comment2"]); + expect(await getSelectedId(page)).toBe("comment2"); +}); + +test("abort unconstrained move via context menu", async ({ page, act }) => { + const comment2BoundsStart = (await getComment(page, "comment2")).bounds; + + await act(page.keyboard.press(cmdOrCtrl("Enter"))); + for (let i = 0; i < 2; i++) { + await act(page.keyboard.press("ArrowDown")); + } + await act(page.keyboard.press("Enter")); + await expect(page.locator(".blocklyMoveIndicatorBubble")).toBeVisible(); + await act(page.keyboard.press("Alt+ArrowRight")); + await act(page.keyboard.press("Escape")); + await expect(page.locator(".blocklyMoveIndicatorBubble")).not.toBeVisible(); + + const comment2BoundsEnd = (await getComment(page, "comment2")).bounds; + expect(comment2BoundsEnd.top).toBeCloseTo(comment2BoundsStart.top); + expect(comment2BoundsEnd.left).toBeCloseTo(comment2BoundsStart.left); + expect(await getHighlightedCommentIds(page)).toEqual(["comment2"]); + expect(await getSelectedId(page)).toBe("comment2"); +}); + test("open context menu", async ({ page, act }) => { await act(page.keyboard.press(cmdOrCtrl("Enter"))); diff --git a/test-e2e/keyboard/select/stack.spec.ts b/test-e2e/keyboard/select/stack.spec.ts index 32317b6..b6dae9d 100644 --- a/test-e2e/keyboard/select/stack.spec.ts +++ b/test-e2e/keyboard/select/stack.spec.ts @@ -1,8 +1,11 @@ import { expect } from "@playwright/test"; import { + cmdOrCtrl, getBlock, + getGridSpacing, getHighlightedBlockIds, getSelectedId, + getStackBlockIds, loadBlocks, test, } from "../../test"; @@ -61,3 +64,189 @@ test("navigate to next stack", async ({ page, act }) => { expect(await getHighlightedBlockIds(page)).toEqual(["stack3"]); expect(await getSelectedId(page)).toBe("stack3"); }); + +test("constrained move via shortcut", async ({ page, act }) => { + await act(page.keyboard.press("M")); + await expect(page.locator(".blocklyMoveIndicatorBubble")).toBeVisible(); + await act(page.keyboard.press("ArrowDown")); + await act(page.keyboard.press("Enter")); + await expect(page.locator(".blocklyMoveIndicatorBubble")).not.toBeVisible(); + + expect(await getStackBlockIds(page, "stack2_top")).toEqual([ + "stack2_top", + "stack2_bottom", + "stack2_middle", + ]); +}); + +test("abort constrained move via shortcut", async ({ page, act }) => { + await act(page.keyboard.press("M")); + await expect(page.locator(".blocklyMoveIndicatorBubble")).toBeVisible(); + await act(page.keyboard.press("ArrowDown")); + await act(page.keyboard.press("Escape")); + await expect(page.locator(".blocklyMoveIndicatorBubble")).not.toBeVisible(); + + expect(await getStackBlockIds(page, "stack2_top")).toEqual([ + "stack2_top", + "stack2_middle", + "stack2_bottom", + ]); +}); + +test("constrained move via context menu", async ({ page, act }) => { + await act(page.keyboard.press(cmdOrCtrl("Enter"))); + for (let i = 0; i < 6; i++) { + await act(page.keyboard.press("ArrowDown")); + } + await act(page.keyboard.press("Enter")); + await expect(page.locator(".blocklyMoveIndicatorBubble")).toBeVisible(); + await act(page.keyboard.press("ArrowDown")); + await act(page.keyboard.press("Enter")); + await expect(page.locator(".blocklyMoveIndicatorBubble")).not.toBeVisible(); + + expect(await getStackBlockIds(page, "stack2_top")).toEqual([ + "stack2_top", + "stack2_bottom", + "stack2_middle", + ]); +}); + +test("abort constrained move via context menu", async ({ page, act }) => { + await act(page.keyboard.press(cmdOrCtrl("Enter"))); + for (let i = 0; i < 6; i++) { + await act(page.keyboard.press("ArrowDown")); + } + await act(page.keyboard.press("Enter")); + await expect(page.locator(".blocklyMoveIndicatorBubble")).toBeVisible(); + await act(page.keyboard.press("ArrowDown")); + await act(page.keyboard.press("Escape")); + await expect(page.locator(".blocklyMoveIndicatorBubble")).not.toBeVisible(); + + expect(await getStackBlockIds(page, "stack2_top")).toEqual([ + "stack2_top", + "stack2_middle", + "stack2_bottom", + ]); +}); + +test("unconstrained move via shortcut", async ({ page, act }) => { + const gridSpacing = await getGridSpacing(page); + if (gridSpacing === null) throw new Error("Workspace has no grid"); + const stack1BoundsStart = (await getBlock(page, { id: "stack1" })).bounds; + const stack2TopBoundsStart = (await getBlock(page, { id: "stack2_top" })) + .bounds; + const stack3BoundsStart = (await getBlock(page, { id: "stack3" })).bounds; + + await act(page.keyboard.press("M")); + await expect(page.locator(".blocklyMoveIndicatorBubble")).toBeVisible(); + for (let i = 0; i < 3; i++) { + await act(page.keyboard.press("Alt+ArrowRight")); + } + await act(page.keyboard.press("Enter")); + await expect(page.locator(".blocklyMoveIndicatorBubble")).not.toBeVisible(); + + expect(await getStackBlockIds(page, "stack2_top")).toEqual([ + "stack2_top", + "stack2_bottom", + ]); + expect(await getStackBlockIds(page, "stack2_middle")).toEqual([ + "stack2_middle", + ]); + const stack2MiddleBoundsEnd = (await getBlock(page, { id: "stack2_middle" })) + .bounds; + expect(stack2MiddleBoundsEnd.left - stack2TopBoundsStart.left).toBeCloseTo( + 3 * gridSpacing, + ); + expect(stack2MiddleBoundsEnd.top - stack2TopBoundsStart.top).toBeCloseTo( + gridSpacing, + ); + const stack1BoundsEnd = (await getBlock(page, { id: "stack1" })).bounds; + expect(stack1BoundsEnd.left).toBeCloseTo(stack1BoundsStart.left); + expect(stack1BoundsEnd.top).toBeCloseTo(stack1BoundsStart.top); + const stack3BoundsEnd = (await getBlock(page, { id: "stack3" })).bounds; + expect(stack3BoundsEnd.left).toBeCloseTo(stack3BoundsStart.left); + expect(stack3BoundsEnd.top).toBeCloseTo(stack3BoundsStart.top); + expect(await getHighlightedBlockIds(page)).toEqual(["stack2_middle"]); + expect(await getSelectedId(page)).toBe("stack2_middle"); +}); + +test("abort unconstrained move via shortcut", async ({ page, act }) => { + await act(page.keyboard.press("M")); + await expect(page.locator(".blocklyMoveIndicatorBubble")).toBeVisible(); + for (let i = 0; i < 3; i++) { + await act(page.keyboard.press("Alt+ArrowRight")); + } + await act(page.keyboard.press("Escape")); + await expect(page.locator(".blocklyMoveIndicatorBubble")).not.toBeVisible(); + + expect(await getStackBlockIds(page, "stack2_top")).toEqual([ + "stack2_top", + "stack2_middle", + "stack2_bottom", + ]); +}); + +test("unconstrained move via context menu", async ({ page, act }) => { + const gridSpacing = await getGridSpacing(page); + if (gridSpacing === null) throw new Error("Workspace has no grid"); + const stack1BoundsStart = (await getBlock(page, { id: "stack1" })).bounds; + const stack2TopBoundsStart = (await getBlock(page, { id: "stack2_top" })) + .bounds; + const stack3BoundsStart = (await getBlock(page, { id: "stack3" })).bounds; + + await act(page.keyboard.press(cmdOrCtrl("Enter"))); + for (let i = 0; i < 6; i++) { + await act(page.keyboard.press("ArrowDown")); + } + await act(page.keyboard.press("Enter")); + await expect(page.locator(".blocklyMoveIndicatorBubble")).toBeVisible(); + for (let i = 0; i < 3; i++) { + await act(page.keyboard.press("Alt+ArrowRight")); + } + await act(page.keyboard.press("Enter")); + await expect(page.locator(".blocklyMoveIndicatorBubble")).not.toBeVisible(); + + expect(await getStackBlockIds(page, "stack2_top")).toEqual([ + "stack2_top", + "stack2_bottom", + ]); + expect(await getStackBlockIds(page, "stack2_middle")).toEqual([ + "stack2_middle", + ]); + const stack2MiddleBoundsEnd = (await getBlock(page, { id: "stack2_middle" })) + .bounds; + expect(stack2MiddleBoundsEnd.left - stack2TopBoundsStart.left).toBeCloseTo( + 3 * gridSpacing, + ); + expect(stack2MiddleBoundsEnd.top - stack2TopBoundsStart.top).toBeCloseTo( + gridSpacing, + ); + const stack1BoundsEnd = (await getBlock(page, { id: "stack1" })).bounds; + expect(stack1BoundsEnd.left).toBeCloseTo(stack1BoundsStart.left); + expect(stack1BoundsEnd.top).toBeCloseTo(stack1BoundsStart.top); + const stack3BoundsEnd = (await getBlock(page, { id: "stack3" })).bounds; + expect(stack3BoundsEnd.left).toBeCloseTo(stack3BoundsStart.left); + expect(stack3BoundsEnd.top).toBeCloseTo(stack3BoundsStart.top); + expect(await getHighlightedBlockIds(page)).toEqual(["stack2_middle"]); + expect(await getSelectedId(page)).toBe("stack2_middle"); +}); + +test("abort unconstrained move via context menu", async ({ page, act }) => { + await act(page.keyboard.press(cmdOrCtrl("Enter"))); + for (let i = 0; i < 6; i++) { + await act(page.keyboard.press("ArrowDown")); + } + await act(page.keyboard.press("Enter")); + await expect(page.locator(".blocklyMoveIndicatorBubble")).toBeVisible(); + for (let i = 0; i < 3; i++) { + await act(page.keyboard.press("Alt+ArrowRight")); + } + await act(page.keyboard.press("Escape")); + await expect(page.locator(".blocklyMoveIndicatorBubble")).not.toBeVisible(); + + expect(await getStackBlockIds(page, "stack2_top")).toEqual([ + "stack2_top", + "stack2_middle", + "stack2_bottom", + ]); +}); diff --git a/test-e2e/test.ts b/test-e2e/test.ts index 0b7b665..f7a5dc4 100644 --- a/test-e2e/test.ts +++ b/test-e2e/test.ts @@ -98,6 +98,7 @@ export const loadBlocks = ( workspace, ); workspace.cleanUp(); + workspace.scrollCenter(); }, blocks); export const getBlock = (page: Page, query: BlockQuery): Promise => @@ -206,6 +207,22 @@ export const getHighlightedBlockIds = (page: Page): Promise => .sort(), ); +export const getStackBlockIds = ( + page: Page, + topBlockId: string, +): Promise => + page.evaluate((topBlockId) => { + const workspace = Blockly.getMainWorkspace() as WorkspaceSvg; + const stackBlockIds: string[] = []; + let block = workspace.getBlockById(topBlockId); + if (!block) throw new Error(`Block "${topBlockId}" not found`); + while (block) { + stackBlockIds.push(block.id); + block = block.getNextBlock(); + } + return stackBlockIds; + }, topBlockId); + export const loadComments = ( page: Page, comments: serialization.workspaceComments.State[], @@ -229,6 +246,7 @@ export const loadComments = ( for (const comment of workspace.getTopComments() as RenderedWorkspaceComment[]) { comment.snapToGrid(); } + workspace.scrollCenter(); }, comments); export const getComment = (page: Page, id: string): Promise => From ca1258ccc2e981ccbd8c21c9ec02152cee9f1257 Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Mon, 20 Apr 2026 13:55:34 -0700 Subject: [PATCH 41/67] Add configurable keybindings for keyboard navigation shortcuts --- src/multiselect.js | 13 ++++++++++- src/navigation_shortcut_keybindings.js | 30 ++++++++++++++++++++++++++ test/index.js | 17 +++++++++++++-- 3 files changed, 57 insertions(+), 3 deletions(-) create mode 100644 src/navigation_shortcut_keybindings.js diff --git a/src/multiselect.js b/src/multiselect.js index f6085db..f5fa6c5 100644 --- a/src/multiselect.js +++ b/src/multiselect.js @@ -20,6 +20,7 @@ import { import {MultiselectControls} from './multiselect_controls'; import {MultiselectDraggable} from './multiselect_draggable'; import {MultiselectNavigationPolicy} from './multiselect_navigation_policy'; +import {applyShortcutKeybindings} from './navigation_shortcut_keybindings'; /** * Class for using multiple select blocks on workspace. @@ -139,11 +140,21 @@ export class Multiselect { } - onKeyboardNavigationInit() { + onKeyboardNavigationInit(options = {}) { ContextMenu.registerOurKeyboardNavigationMenuItems(this.useCopyPasteCrossTab_); this.navigationPolicy_ = new MultiselectNavigationPolicy(); this.workspace_.getNavigator().addNavigationPolicy(this.navigationPolicy_); this.navigationPolicy_.install(); + + if (options.shortcutKeybindings) { + if (!options.instance) { + throw new Error( + 'KeyboardNavigation instance required when using shortcutKeybindings'); + } + applyShortcutKeybindings(options.shortcutKeybindings); + options.instance.navigationController + .shortcutDialog.createModalContent(); + } } /** diff --git a/src/navigation_shortcut_keybindings.js b/src/navigation_shortcut_keybindings.js new file mode 100644 index 0000000..3f68b26 --- /dev/null +++ b/src/navigation_shortcut_keybindings.js @@ -0,0 +1,30 @@ +import * as Blockly from "blockly/core"; + +const createSerializedKey = + Blockly.ShortcutRegistry.registry.createSerializedKey.bind( + Blockly.ShortcutRegistry.registry, + ); + +/** + * Re-maps keyboard navigation shortcuts to new keybindings. + * @param keybindings Object mapping shortcut registry names to key arrays, + * e.g. {toolbox: [KeyCodes.ALT, KeyCodes.T]}. + */ +export function applyShortcutKeybindings(keybindings) { + const serialized = (keyCodes) => + createSerializedKey(keyCodes[keyCodes.length - 1], keyCodes.slice(0, -1)); + + for (const [name, keyCodes] of Object.entries(keybindings)) { + for (const shortcut of [name, `multiselect_${name}`]) { + const existing = + Blockly.ShortcutRegistry.registry.getKeyCodesByShortcutName(shortcut); + if (existing.length === 0) continue; + Blockly.ShortcutRegistry.registry.removeAllKeyMappings(shortcut); + Blockly.ShortcutRegistry.registry.addKeyMapping( + serialized(keyCodes), + shortcut, + true, + ); + } + } +} diff --git a/test/index.js b/test/index.js index ef1f21f..7a1138f 100644 --- a/test/index.js +++ b/test/index.js @@ -43,8 +43,21 @@ function createWorkspace(blocklyDiv, options) { multiselectPlugin.init(options); // Initialize keyboard navigation plugin. - new KeyboardNavigation(workspace, {allowCrossWorkspacePaste: true}); - multiselectPlugin.onKeyboardNavigationInit(); + const keyboardNav = new KeyboardNavigation(workspace, {allowCrossWorkspacePaste: true}); + multiselectPlugin.onKeyboardNavigationInit({ + // instance: keyboardNav, + // shortcutKeybindings: { + // toolbox: [Blockly.utils.KeyCodes.ALT, Blockly.utils.KeyCodes.T], + // clean_up_workspace: [Blockly.utils.KeyCodes.ALT, Blockly.utils.KeyCodes.C], + // list_shortcuts: [Blockly.utils.KeyCodes.ALT, Blockly.utils.KeyCodes.SLASH], + // disconnect: [Blockly.utils.KeyCodes.ALT, Blockly.utils.KeyCodes.X], + // start_move: [Blockly.utils.KeyCodes.ALT, Blockly.utils.KeyCodes.M], + // duplicate: [Blockly.utils.KeyCodes.ALT, Blockly.utils.KeyCodes.D], + // next_stack: [Blockly.utils.KeyCodes.ALT, Blockly.utils.KeyCodes.N], + // previous_stack: [Blockly.utils.KeyCodes.ALT, Blockly.utils.KeyCodes.B], + // to_workspace: [Blockly.utils.KeyCodes.ALT, Blockly.utils.KeyCodes.W], + // }, + }); return workspace; } From f26447c663db75ce685edf10ccb8df006a4f76ad Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Sat, 16 May 2026 16:05:40 -0700 Subject: [PATCH 42/67] Fix D duplicate multiselect shortcut --- src/multiselect.js | 2 + src/multiselect_shortcut.js | 139 ++++++++++++++++++ .../multiselect/workspace/block.spec.ts | 39 +++++ .../multiselect/workspace/comment.spec.ts | 23 +++ .../actions/select/workspace/block.spec.ts | 16 ++ .../actions/select/workspace/comment.spec.ts | 16 ++ 6 files changed, 235 insertions(+) diff --git a/src/multiselect.js b/src/multiselect.js index f5fa6c5..cfce83c 100644 --- a/src/multiselect.js +++ b/src/multiselect.js @@ -142,6 +142,7 @@ export class Multiselect { onKeyboardNavigationInit(options = {}) { ContextMenu.registerOurKeyboardNavigationMenuItems(this.useCopyPasteCrossTab_); + Shortcut.registerDuplicateShortcut(); this.navigationPolicy_ = new MultiselectNavigationPolicy(); this.workspace_.getNavigator().addNavigationPolicy(this.navigationPolicy_); this.navigationPolicy_.install(); @@ -222,6 +223,7 @@ export class Multiselect { ContextMenu.registerOrigContextMenu(); ContextMenu.registerOrigKeyboardNavigationMenuItems(); + Shortcut.unregisterDuplicateShortcut(); Shortcut.unregisterOrigShortcut(); Blockly.ShortcutRegistry.registry.unregister('selectall'); Shortcut.registerOrigShortcut(); diff --git a/src/multiselect_shortcut.js b/src/multiselect_shortcut.js index 3d7dca4..c8f6fc7 100644 --- a/src/multiselect_shortcut.js +++ b/src/multiselect_shortcut.js @@ -16,6 +16,8 @@ import { } from './global'; import {MultiselectDraggable} from './multiselect_draggable'; +let originalDuplicateShortcut = null; + /** * Modification for keyboard shortcut 'Delete' to be available * for multiple blocks. @@ -425,6 +427,132 @@ const registerPaste = function(useCopyPasteCrossTab) { Blockly.ShortcutRegistry.registry.register(pasteShortcut); }; +/** + * Modification for keyboard shortcut 'Duplicate' to be available + * for multiple blocks or comments. + */ +export const registerDuplicateShortcut = function() { + const name = 'duplicate'; + originalDuplicateShortcut = + Blockly.ShortcutRegistry.registry.getRegistry()[name] || null; + const duplicateShortcut = { + name, + preconditionFn: function(workspace) { + if (workspace.options.readOnly || workspace.isDragging()) { + return false; + } + const selected = Blockly.common.getSelected(); + const dragSelection = dragSelectionWeakMap.get(workspace); + if (!dragSelection.size) { + return duplicateShortcut.check(selected); + } + for (const id of dragSelection) { + const element = getByID(workspace, id); + if (duplicateShortcut.check(element)) { + return true; + } + } + return false; + }, + check: function(element) { + if (element instanceof Blockly.BlockSvg) { + return element && !element.isInFlyout && element.isDeletable() && + element.isMovable() && element.isDuplicatable() && + !hasSelectedParent(element); + } else if (element instanceof + Blockly.comments.RenderedWorkspaceComment) { + return element && element.isDeletable() && element.isMovable(); + } + return false; + }, + callback: function(workspace, e) { + e.preventDefault(); + const selected = Blockly.common.getSelected(); + const dragSelection = dragSelectionWeakMap.get(workspace); + + const duplicatedElements = {}; + const connectionDBList = []; + const multiDraggable = multiDraggableWeakMap.get(workspace); + const apply = function(element) { + if (duplicateShortcut.check(element)) { + // Get the copy data of the block + const copyData = element.toCopyData(); + if (!copyData) { + return; + } + // Set the new ID in the copy data + if (copyData.blockState) { + copyData.blockState.id = Blockly.utils.idGenerator.genUid(); + } else if (copyData.commentState) { + copyData.commentState.id = Blockly.utils.idGenerator.genUid(); + } + // Paste the block with the modified copy data + duplicatedElements[element.id] = + Blockly.clipboard.paste(copyData, workspace); + } + }; + Blockly.Events.setGroup(true); + + // We want to update the dragSelection and the multiDraggable object to + // remove subdraggables from the current selection prior to duplicating. + if (dragSelection.size) { + dragSelection.forEach(function(id) { + const element = getByID(workspace, id); + if (element) { + element.unselect(); + apply(element); + } + }); + dragSelection.clear(); + multiDraggable.clearAll_(); + Blockly.getFocusManager().focusTree(workspace); + } else { + apply(selected); + } + + for (const [id, element] of Object.entries(duplicatedElements)) { + if (!element || !element.id) { + continue; + } + if (element instanceof Blockly.BlockSvg) { + const origBlock = workspace.getBlockById(id); + const origParentBlock = origBlock && origBlock.getParent(); + if (origParentBlock && origParentBlock.id in duplicatedElements && + origParentBlock.getNextBlock() === origBlock) { + connectionDBList.push([ + duplicatedElements[origParentBlock.id].nextConnection, + element.previousConnection, + ]); + } + if (element.type === 'drag_to_dupe') { + continue; + } + } + dragSelection.add(element.id); + multiDraggable.addSubDraggable_(element); + } + connectionDBList.forEach(function(connectionDB) { + connectionDB[0].connect(connectionDB[1]); + }); + Blockly.Events.setGroup(false); + Blockly.renderManagement.finishQueuedRenders().then(() => { + if (dragSelection.size === 1) { + Blockly.common.setSelected( + getByID(workspace, dragSelection.values().next().value)); + } else { + Blockly.common.setSelected(multiDraggable); + } + }); + return true; + }, + keyCodes: [Blockly.utils.KeyCodes.D], + }; + if (name in Blockly.ShortcutRegistry.registry.getRegistry()) { + Blockly.ShortcutRegistry.registry.unregister(name); + } + Blockly.ShortcutRegistry.registry.register(duplicateShortcut); +}; + /** * Keyboard shortcut to select all top blocks in the workspace on * ctrl+a, cmd+a, or alt+a. @@ -526,6 +654,17 @@ export const unregisterOurShortcut = function() { } }; +export const unregisterDuplicateShortcut = function() { + const name = 'duplicate'; + if (name in Blockly.ShortcutRegistry.registry.getRegistry()) { + Blockly.ShortcutRegistry.registry.unregister(name); + } + if (originalDuplicateShortcut) { + Blockly.ShortcutRegistry.registry.register(originalDuplicateShortcut); + originalDuplicateShortcut = null; + } +}; + /** * Register default keyboard shortcut item. */ diff --git a/test-e2e/actions/multiselect/workspace/block.spec.ts b/test-e2e/actions/multiselect/workspace/block.spec.ts index 29d494b..2731802 100644 --- a/test-e2e/actions/multiselect/workspace/block.spec.ts +++ b/test-e2e/actions/multiselect/workspace/block.spec.ts @@ -93,6 +93,45 @@ test("open context menu", async ({ page, act }) => { expect(await isEphemeralFocusTaken(page)).toBe(true); }); +test("duplicate blocks via keyboard", async ({ page, act }) => { + expect(await getAllBlockIds(page)).toEqual([ + "block1", + "block2", + "block2-child", + "block3", + "block3-child", + "block4", + ]); + expect(await getHighlightedBlockIds(page)).toEqual([ + "block1", + "block2", + "block2-child", + "block3", + ]); + expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); + + await act(page.keyboard.press("D")); + + const allBlockIds = await getAllBlockIds(page); + expect(allBlockIds).toHaveLength(11); + const newBlockIds = allBlockIds.filter( + (id) => + ![ + "block1", + "block2", + "block2-child", + "block3", + "block3-child", + "block4", + ].includes(id), + ); + const highlightedBlockIds = await getHighlightedBlockIds(page); + expect(newBlockIds).toHaveLength(5); + expect(highlightedBlockIds).toHaveLength(3); + expect(newBlockIds).toEqual(expect.arrayContaining(highlightedBlockIds)); + expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); +}); + test("duplicate blocks via context menu", async ({ page, act }) => { await act( page.mouse.click(...(await getBlock(page, { id: "block1" })).centerTop, { diff --git a/test-e2e/actions/multiselect/workspace/comment.spec.ts b/test-e2e/actions/multiselect/workspace/comment.spec.ts index 2de0315..316322b 100644 --- a/test-e2e/actions/multiselect/workspace/comment.spec.ts +++ b/test-e2e/actions/multiselect/workspace/comment.spec.ts @@ -64,6 +64,29 @@ test("open context menu", async ({ page, act }) => { expect(await isEphemeralFocusTaken(page)).toBe(true); }); +test("duplicate comments via keyboard", async ({ page, act }) => { + expect(await getAllCommentIds(page)).toEqual([ + "comment1", + "comment2", + "comment3", + ]); + expect(await getHighlightedCommentIds(page)).toEqual([ + "comment1", + "comment2", + ]); + expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); + + await act(page.keyboard.press("D")); + + const allCommentIds = await getAllCommentIds(page); + expect(allCommentIds).toHaveLength(5); + const newCommentIds = allCommentIds.filter( + (id) => !["comment1", "comment2", "comment3"].includes(id), + ); + expect(await getHighlightedCommentIds(page)).toEqual(newCommentIds); + expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); +}); + test("duplicate comments via context menu", async ({ page, act }) => { await act( page.mouse.click(...(await getComment(page, "comment1")).centerTop, { diff --git a/test-e2e/actions/select/workspace/block.spec.ts b/test-e2e/actions/select/workspace/block.spec.ts index 46c4e7a..a2cb86c 100644 --- a/test-e2e/actions/select/workspace/block.spec.ts +++ b/test-e2e/actions/select/workspace/block.spec.ts @@ -65,6 +65,22 @@ test("open context menu", async ({ page, act }) => { expect(await isEphemeralFocusTaken(page)).toBe(true); }); +test("duplicate block via keyboard", async ({ page, act }) => { + expect(await getAllBlockIds(page)).toEqual(["block1", "block2"]); + expect(await getHighlightedBlockIds(page)).toEqual(["block1"]); + expect(await getSelectedId(page)).toBe("block1"); + + await act(page.keyboard.press("D")); + + const allBlockIds = await getAllBlockIds(page); + expect(allBlockIds).toHaveLength(3); + const [newBlockId] = allBlockIds.filter( + (id) => !["block1", "block2"].includes(id), + ); + expect(await getHighlightedBlockIds(page)).toEqual([newBlockId]); + expect(await getSelectedId(page)).toBe(newBlockId); +}); + test("duplicate block via context menu", async ({ page, act }) => { await act( page.mouse.click(...(await getBlock(page, { id: "block1" })).centerTop, { diff --git a/test-e2e/actions/select/workspace/comment.spec.ts b/test-e2e/actions/select/workspace/comment.spec.ts index f348751..4f9db72 100644 --- a/test-e2e/actions/select/workspace/comment.spec.ts +++ b/test-e2e/actions/select/workspace/comment.spec.ts @@ -52,6 +52,22 @@ test("open context menu", async ({ page, act }) => { expect(await isEphemeralFocusTaken(page)).toBe(true); }); +test("duplicate comment via keyboard", async ({ page, act }) => { + expect(await getAllCommentIds(page)).toEqual(["comment1", "comment2"]); + expect(await getHighlightedCommentIds(page)).toEqual(["comment1"]); + expect(await getSelectedId(page)).toBe("comment1"); + + await act(page.keyboard.press("D")); + + const allCommentIds = await getAllCommentIds(page); + expect(allCommentIds).toHaveLength(3); + const [newCommentId] = allCommentIds.filter( + (id) => !["comment1", "comment2"].includes(id), + ); + expect(await getHighlightedCommentIds(page)).toEqual([newCommentId]); + expect(await getSelectedId(page)).toBe(newCommentId); +}); + test("duplicate comment via context menu", async ({ page, act }) => { await act( page.mouse.click(...(await getComment(page, "comment1")).centerTop, { From 5cf08079712569ac3dbe9abe646e15b8418f9f70 Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Mon, 3 Aug 2026 11:25:06 -0700 Subject: [PATCH 43/67] Remove configurable keybindings for keyboard nav shortcuts --- src/multiselect.js | 13 +---------- src/navigation_shortcut_keybindings.js | 30 -------------------------- test/index.js | 17 ++------------- 3 files changed, 3 insertions(+), 57 deletions(-) delete mode 100644 src/navigation_shortcut_keybindings.js diff --git a/src/multiselect.js b/src/multiselect.js index cfce83c..598e0c9 100644 --- a/src/multiselect.js +++ b/src/multiselect.js @@ -20,7 +20,6 @@ import { import {MultiselectControls} from './multiselect_controls'; import {MultiselectDraggable} from './multiselect_draggable'; import {MultiselectNavigationPolicy} from './multiselect_navigation_policy'; -import {applyShortcutKeybindings} from './navigation_shortcut_keybindings'; /** * Class for using multiple select blocks on workspace. @@ -140,22 +139,12 @@ export class Multiselect { } - onKeyboardNavigationInit(options = {}) { + onKeyboardNavigationInit() { ContextMenu.registerOurKeyboardNavigationMenuItems(this.useCopyPasteCrossTab_); Shortcut.registerDuplicateShortcut(); this.navigationPolicy_ = new MultiselectNavigationPolicy(); this.workspace_.getNavigator().addNavigationPolicy(this.navigationPolicy_); this.navigationPolicy_.install(); - - if (options.shortcutKeybindings) { - if (!options.instance) { - throw new Error( - 'KeyboardNavigation instance required when using shortcutKeybindings'); - } - applyShortcutKeybindings(options.shortcutKeybindings); - options.instance.navigationController - .shortcutDialog.createModalContent(); - } } /** diff --git a/src/navigation_shortcut_keybindings.js b/src/navigation_shortcut_keybindings.js deleted file mode 100644 index 3f68b26..0000000 --- a/src/navigation_shortcut_keybindings.js +++ /dev/null @@ -1,30 +0,0 @@ -import * as Blockly from "blockly/core"; - -const createSerializedKey = - Blockly.ShortcutRegistry.registry.createSerializedKey.bind( - Blockly.ShortcutRegistry.registry, - ); - -/** - * Re-maps keyboard navigation shortcuts to new keybindings. - * @param keybindings Object mapping shortcut registry names to key arrays, - * e.g. {toolbox: [KeyCodes.ALT, KeyCodes.T]}. - */ -export function applyShortcutKeybindings(keybindings) { - const serialized = (keyCodes) => - createSerializedKey(keyCodes[keyCodes.length - 1], keyCodes.slice(0, -1)); - - for (const [name, keyCodes] of Object.entries(keybindings)) { - for (const shortcut of [name, `multiselect_${name}`]) { - const existing = - Blockly.ShortcutRegistry.registry.getKeyCodesByShortcutName(shortcut); - if (existing.length === 0) continue; - Blockly.ShortcutRegistry.registry.removeAllKeyMappings(shortcut); - Blockly.ShortcutRegistry.registry.addKeyMapping( - serialized(keyCodes), - shortcut, - true, - ); - } - } -} diff --git a/test/index.js b/test/index.js index 7a1138f..ef1f21f 100644 --- a/test/index.js +++ b/test/index.js @@ -43,21 +43,8 @@ function createWorkspace(blocklyDiv, options) { multiselectPlugin.init(options); // Initialize keyboard navigation plugin. - const keyboardNav = new KeyboardNavigation(workspace, {allowCrossWorkspacePaste: true}); - multiselectPlugin.onKeyboardNavigationInit({ - // instance: keyboardNav, - // shortcutKeybindings: { - // toolbox: [Blockly.utils.KeyCodes.ALT, Blockly.utils.KeyCodes.T], - // clean_up_workspace: [Blockly.utils.KeyCodes.ALT, Blockly.utils.KeyCodes.C], - // list_shortcuts: [Blockly.utils.KeyCodes.ALT, Blockly.utils.KeyCodes.SLASH], - // disconnect: [Blockly.utils.KeyCodes.ALT, Blockly.utils.KeyCodes.X], - // start_move: [Blockly.utils.KeyCodes.ALT, Blockly.utils.KeyCodes.M], - // duplicate: [Blockly.utils.KeyCodes.ALT, Blockly.utils.KeyCodes.D], - // next_stack: [Blockly.utils.KeyCodes.ALT, Blockly.utils.KeyCodes.N], - // previous_stack: [Blockly.utils.KeyCodes.ALT, Blockly.utils.KeyCodes.B], - // to_workspace: [Blockly.utils.KeyCodes.ALT, Blockly.utils.KeyCodes.W], - // }, - }); + new KeyboardNavigation(workspace, {allowCrossWorkspacePaste: true}); + multiselectPlugin.onKeyboardNavigationInit(); return workspace; } From 6c1ccae4b1185684b83250d3b7807bea57e14482 Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Mon, 3 Aug 2026 11:25:06 -0700 Subject: [PATCH 44/67] Remove keyboard nav plugin --- README.md | 13 ------------- package-lock.json | 11 ----------- package.json | 1 - src/multiselect.js | 9 +++------ test/index.html | 1 - test/index.js | 16 ---------------- 6 files changed, 3 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index b677deb..32381d7 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,6 @@ scope of its wrapped input function. - [x] Backpack (Need to update UI in Backpack plugin side) **Workspace-related** -- [x] Keyboard-navigation - [x] Shadow-block-converter - [x] Workspace-content-highlight - [x] Disable-top-blocks @@ -133,17 +132,6 @@ scope of its wrapped input function. For more information, please check out the following [issue page](https://github.com/mit-cml/workspace-multiselect/issues/50). -### Note on keyboard navigation plugin -The keyboard navigation plugin must be initialized after the multiselect plugin. Set `multiselectCopyPaste.menu` to `false` to avoid duplicate clipboard context menu items, pass `allowCrossWorkspacePaste: true` to keyboard navigation, and call `onKeyboardNavigationInit()` after: - -```javascript -const multiselectPlugin = new Multiselect(workspace); -multiselectPlugin.init({multiselectCopyPaste: {menu: false}}); - -new KeyboardNavigation(workspace, {allowCrossWorkspacePaste: true}); -multiselectPlugin.onKeyboardNavigationInit(); -``` - ### Note on disable top blocks plugin The disable top blocks plugin has to be initialized after the multiselect plugin. The main reason behind this is that the multiselect plugin has its own custom context menu, which allows for the disabling of any blocks (not just top blocks). @@ -153,7 +141,6 @@ customization. If we install the disable top blocks plugin after the multiselect ## API - `Multiselect.init`: Initialize the plugin. -- `Multiselect.onKeyboardNavigationInit`: Call after initializing the keyboard navigation plugin. - `Multiselect.dispose`: Dispose the plugin. - `MultiselectDraggable`: The customized draggable object unique to each workspace that contains the blocks in the multiselection. - `dragSelectionWeakMap`: The WeakMap storing set of currently selected block ids by workspace svg. diff --git a/package-lock.json b/package-lock.json index 6a5e521..db22af6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,6 @@ "devDependencies": { "@blockly/dev-scripts": "^4.0.11", "@blockly/dev-tools": "^9.0.8", - "@blockly/keyboard-navigation": "^3.0.5", "@blockly/workspace-backpack": "^7.0.10", "@playwright/test": "^1.58.2", "@types/node": "^24.12.2", @@ -357,16 +356,6 @@ "eslint": "7.x" } }, - "node_modules/@blockly/keyboard-navigation": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@blockly/keyboard-navigation/-/keyboard-navigation-3.0.5.tgz", - "integrity": "sha512-ZsqWrq/8K1dYhNEkOt3F8YcWkZ3ZIybjnsb5q11lSyhYaVUva+nKKCqFO0mifQl6ZsuPCaapUmlvGG9Gy/EgsQ==", - "dev": true, - "license": "Apache-2.0", - "peerDependencies": { - "blockly": "^12.3.0" - } - }, "node_modules/@blockly/theme-dark": { "version": "8.0.3", "resolved": "https://registry.npmjs.org/@blockly/theme-dark/-/theme-dark-8.0.3.tgz", diff --git a/package.json b/package.json index 974c95b..d392106 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,6 @@ "devDependencies": { "@blockly/dev-scripts": "^4.0.11", "@blockly/dev-tools": "^9.0.8", - "@blockly/keyboard-navigation": "^3.0.5", "@blockly/workspace-backpack": "^7.0.10", "@playwright/test": "^1.58.2", "@types/node": "^24.12.2", diff --git a/src/multiselect.js b/src/multiselect.js index 598e0c9..6fbb725 100644 --- a/src/multiselect.js +++ b/src/multiselect.js @@ -106,7 +106,10 @@ export class Multiselect { this.useCopyPasteMenu_ = false; } + this.navigationPolicy_ = new MultiselectNavigationPolicy(); + this.workspace_.getNavigator().addNavigationPolicy(this.navigationPolicy_); if (!Blockly.ContextMenuRegistry.registry.getItem('workspaceSelectAll')) { + this.navigationPolicy_.install(); ContextMenu.unregisterContextMenu(); ContextMenu.registerOurContextMenu(this.useCopyPasteMenu_, this.useCopyPasteCrossTab_); @@ -137,14 +140,8 @@ export class Multiselect { Blockly.BlockSvg.prototype.bumpNeighbours = function() {}; } - } - - onKeyboardNavigationInit() { ContextMenu.registerOurKeyboardNavigationMenuItems(this.useCopyPasteCrossTab_); Shortcut.registerDuplicateShortcut(); - this.navigationPolicy_ = new MultiselectNavigationPolicy(); - this.workspace_.getNavigator().addNavigationPolicy(this.navigationPolicy_); - this.navigationPolicy_.install(); } /** diff --git a/test/index.html b/test/index.html index fc5911c..086fdb2 100644 --- a/test/index.html +++ b/test/index.html @@ -13,7 +13,6 @@ -
diff --git a/test/index.js b/test/index.js index ef1f21f..95dd59a 100644 --- a/test/index.js +++ b/test/index.js @@ -14,7 +14,6 @@ import {Multiselect} from '../src/index'; import {multiDraggableWeakMap} from '../src/global'; window.multiDraggableWeakMap = multiDraggableWeakMap; import {Backpack} from '@blockly/workspace-backpack'; -import {KeyboardNavigation} from '@blockly/keyboard-navigation'; /** * Create a workspace. @@ -42,25 +41,10 @@ function createWorkspace(blocklyDiv, options) { const multiselectPlugin = new Multiselect(workspace); multiselectPlugin.init(options); - // Initialize keyboard navigation plugin. - new KeyboardNavigation(workspace, {allowCrossWorkspacePaste: true}); - multiselectPlugin.onKeyboardNavigationInit(); - return workspace; } Blockly.ContextMenuItems.registerCommentOptions(); -KeyboardNavigation.registerKeyboardNavigationStyles(); -class NavigationDeferringToolbox extends Blockly.Toolbox { - onKeyDown_() {} -} -Blockly.registry.register( - Blockly.registry.Type.TOOLBOX, - Blockly.registry.DEFAULT, - NavigationDeferringToolbox, - true, -); -KeyboardNavigation.registerFlyoutCursor(); Blockly.Blocks['radix'] = { init() { From b6a569fd180c78ae187e7f9df7a2951f246e806a Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Mon, 3 Aug 2026 12:22:50 -0700 Subject: [PATCH 45/67] Update dependencies to Blockly 13 --- package-lock.json | 6858 +++++++++++++++++---------------------------- package.json | 10 +- 2 files changed, 2525 insertions(+), 4343 deletions(-) diff --git a/package-lock.json b/package-lock.json index db22af6..9d1488c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,294 +12,129 @@ "dragselect": "^2.7.4" }, "devDependencies": { - "@blockly/dev-scripts": "^4.0.11", - "@blockly/dev-tools": "^9.0.8", - "@blockly/workspace-backpack": "^7.0.10", + "@blockly/dev-scripts": "^13.3.0", + "@blockly/dev-tools": "^13.3.0", + "@blockly/workspace-backpack": "^13.3.0", "@playwright/test": "^1.58.2", "@types/node": "^24.12.2", - "blockly": "^12.5.1" + "blockly": "^13.3.0" }, "engines": { "node": ">=8.17.0" }, "peerDependencies": { - "blockly": "^12.5.1" + "blockly": "^13.3.0" } }, "node_modules/@asamuzakjp/css-color": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", - "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@csstools/css-calc": "^2.1.3", - "@csstools/css-color-parser": "^3.0.9", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "lru-cache": "^10.4.3" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/generator": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", - "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.28.3", - "@babel/types": "^7.28.2", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.9.tgz", - "integrity": "sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" }, "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "has-flag": "^3.0.0" + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" }, "engines": { - "node": ">=4" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, - "node_modules/@babel/parser": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", - "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", "dev": true, "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.4" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, + "peer": true, "engines": { - "node": ">=6.0.0" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, - "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", "dev": true, "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } + "peer": true }, - "node_modules/@babel/traverse": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", - "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "node_modules/@babel/code-frame": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-8.0.0.tgz", + "integrity": "sha512-dYYg153EyN2Ekbqw2zAsbd6/JR+9N2SEoC7YV2GyyqMM7x9bLDTjBD6XBhSMLH0wtIVyJj03jWNriQhaN+eoCw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.4", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4", - "debug": "^4.3.1" + "@babel/helper-validator-identifier": "^8.0.0", + "js-tokens": "^10.0.0" }, "engines": { - "node": ">=6.9.0" + "node": "^22.18.0 || >=24.11.0" } }, - "node_modules/@babel/types": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", - "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "node_modules/@babel/helper-validator-identifier": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz", + "integrity": "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==", "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" - }, "engines": { - "node": ">=6.9.0" + "node": "^22.18.0 || >=24.11.0" } }, "node_modules/@blockly/block-test": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/@blockly/block-test/-/block-test-7.0.5.tgz", - "integrity": "sha512-havzWk4IZUDj/yjId4aWVsHWqeos1Jfp3Ackw56jhK/rOaJZKyRna6qV4g3YSxxIL+cBwNlj33odlNDEzLPOpg==", + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@blockly/block-test/-/block-test-13.3.0.tgz", + "integrity": "sha512-GWgwzHkMvp0ux/lJtFFx1/eGu5Cr63GtdH/TEb9gNataCLffUJvTo3ucngDnLcr5gjoCLVETF92BXXyCMDdDuw==", "dev": true, "license": "Apache 2.0", - "engines": { - "node": ">=8.17.0" - }, "peerDependencies": { - "blockly": "^12.0.0" + "blockly": "^13.2.0" } }, "node_modules/@blockly/dev-scripts": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/@blockly/dev-scripts/-/dev-scripts-4.0.11.tgz", - "integrity": "sha512-3AAKRDi/awwOcB6pIaVf1UuZnT8DuKaEyZ4mcP8m0Wv99EhoPijyPlg5jWzuJd+1Ek32SQtqn0EdV6PxK6kF9A==", + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@blockly/dev-scripts/-/dev-scripts-13.3.0.tgz", + "integrity": "sha512-W5isjBiBKeG3k98npp36nJPdM0TwQb9Fkcy6QfzWArf0Eebi9Mfh2kcQHOBl4g93v6v3Op6pIZ8zPhmrsou2BQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@babel/code-frame": "^7.8.3", - "@blockly/eslint-config": "^4.0.1", - "chalk": "^4.0.0", - "eslint": "^7.15.0", - "eslint-webpack-plugin": "^3.0.0", - "fork-ts-checker-webpack-plugin": "^8.0.0", - "mocha": "^10.7.0", - "rimraf": "^3.0.2", - "source-map-loader": "^1.0.0", - "ts-loader": "^9.4.1", - "webpack": "^5.0.0", - "webpack-cli": "^5.1.4", - "webpack-dev-server": "^5.0.4" + "@babel/code-frame": "^8.0.0", + "chalk": "^4.1.2", + "fork-ts-checker-webpack-plugin": "^9.1.0", + "mocha": "^12.0.0", + "rimraf": "^6.1.3", + "source-map-loader": "^5.0.0", + "ts-loader": "^9.6.1", + "webpack": "^5.107.2", + "webpack-dev-server": "^6.0.0" }, "bin": { "blockly-scripts": "bin/blockly-scripts.js" }, - "engines": { - "node": ">=8.0.0" - }, "peerDependencies": { "typescript": "^4.3.2 || ^5" }, @@ -310,121 +145,96 @@ } }, "node_modules/@blockly/dev-tools": { - "version": "9.0.8", - "resolved": "https://registry.npmjs.org/@blockly/dev-tools/-/dev-tools-9.0.8.tgz", - "integrity": "sha512-BRSO5k6Cnqc0ahSvu/4FX9/oOAkMHppO7lDrEp66UTeGrXKS94bQ1s9lzujC+pg8fLJBffMpBPcqvbSI6ghprg==", + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@blockly/dev-tools/-/dev-tools-13.3.0.tgz", + "integrity": "sha512-b/wKIkCrxrg5Uh0kLoBZyW/c1e2Rp2/3ixN/1TLcp2ShSidjjy2dkm7ZjV7u84/rrOkYXXL8P6bMdr5uGoBFwg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@blockly/block-test": "^7.0.5", - "@blockly/theme-dark": "^8.0.3", - "@blockly/theme-deuteranopia": "^7.0.3", - "@blockly/theme-highcontrast": "^7.0.3", - "@blockly/theme-tritanopia": "^7.0.3", - "chai": "^4.2.0", - "dat.gui": "^0.7.7", + "@blockly/block-test": "^13.3.0", + "@blockly/theme-dark": "^13.3.0", + "@blockly/theme-deuteranopia": "^13.3.0", + "@blockly/theme-highcontrast": "^13.3.0", + "@blockly/theme-tritanopia": "^13.3.0", + "chai": "^6.2.2", + "dat.gui": "^0.7.9", "lodash.assign": "^4.2.0", "lodash.merge": "^4.6.2", - "monaco-editor": "^0.20.0", - "sinon": "^9.0.2" - }, - "engines": { - "node": ">=8.0.0" - }, - "peerDependencies": { - "blockly": "^12.0.0" - } - }, - "node_modules/@blockly/eslint-config": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@blockly/eslint-config/-/eslint-config-4.0.1.tgz", - "integrity": "sha512-yY6aeX6gv0T/+lFwxP35yVbZT2Q6kOgeDfBXTmIsul+S6Qr0ZqMJIkwrCzl1z45YRrB1WBGUJw40rimFwspeyg==", - "deprecated": "The Blockly ESLint config is no longer supported. See https://github.com/google/blockly-samples/issues/1975 for more information.", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@typescript-eslint/eslint-plugin": "^5.0.0", - "@typescript-eslint/parser": "^5.0.0", - "babel-eslint": "^10.1.0", - "eslint-config-google": "^0.14.0", - "eslint-plugin-jsdoc": "^46.8.0" - }, - "engines": { - "node": ">=10.0.0" + "monaco-editor": "^0.56.0", + "sinon": "^22.1.0" }, "peerDependencies": { - "eslint": "7.x" + "blockly": "^13.2.0" } }, "node_modules/@blockly/theme-dark": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/@blockly/theme-dark/-/theme-dark-8.0.3.tgz", - "integrity": "sha512-ZS2jE9TpmQpxyP0jNpaEgTaEUJFYG885/kNiI9fImKWBHvjFxhyiwtn9UtLUPM07b/btbVWHNB+W7ZQoEbXxBA==", + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@blockly/theme-dark/-/theme-dark-13.3.0.tgz", + "integrity": "sha512-0dGj2NINVoxdgKmbqm71Y/3+JL7+unbjXeHUWhvjCo3HB0k5tKnbTtW5HS3wVgdarI6OxhZo17t1om5tx8f4yg==", "dev": true, "license": "Apache-2.0", - "engines": { - "node": ">=8.17.0" - }, "peerDependencies": { - "blockly": "^12.0.0" + "blockly": "^13.2.0" } }, "node_modules/@blockly/theme-deuteranopia": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@blockly/theme-deuteranopia/-/theme-deuteranopia-7.0.3.tgz", - "integrity": "sha512-sUsS7IP6MOFG8UGea0PCEqC5J8gxTAKvdU6ke/SKUT7cgyviUd4cE5nW/xbVSZy3T/yhDaoRu/7x8LAPYP+w9A==", + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@blockly/theme-deuteranopia/-/theme-deuteranopia-13.3.0.tgz", + "integrity": "sha512-EaWbOgvEhO2zYCFs+R1BAa1vECOvWwGBmX7r7CS7w5ZBc5VUMu2gRduIJr315/E/J57/jZzfZjLi0vjRifPhFw==", "dev": true, "license": "Apache-2.0", - "engines": { - "node": ">=8.17.0" - }, "peerDependencies": { - "blockly": "^12.0.0" + "blockly": "^13.2.0" } }, "node_modules/@blockly/theme-highcontrast": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@blockly/theme-highcontrast/-/theme-highcontrast-7.0.3.tgz", - "integrity": "sha512-1EY07djfcAEqA6/P2Tv8YaUYSLluzPeCGB/6KpgI0lHxhsutu8cqMhqixdJB0tOiOeuBTNmVnBDSUd0HsQkNuw==", + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@blockly/theme-highcontrast/-/theme-highcontrast-13.3.0.tgz", + "integrity": "sha512-tw8klpzeArFW16qmGwUOhRVKSj/1xeQOrqAJLtKF9K2MLjmSRNgUdhzUqaW2rPmbGwAMkNqf6SmBp7zLE5ZcPw==", "dev": true, "license": "Apache-2.0", - "engines": { - "node": ">=8.17.0" - }, "peerDependencies": { - "blockly": "^12.0.0" + "blockly": "^13.2.0" } }, "node_modules/@blockly/theme-tritanopia": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@blockly/theme-tritanopia/-/theme-tritanopia-7.0.3.tgz", - "integrity": "sha512-poo/pf/qI7PKZTgUhAVO5KZ1c0m4cDxB1CA95tocF9WSOTkqjSxxWDW2vdtb5HhPpgosYat0pWGEtTGYGLToJg==", + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@blockly/theme-tritanopia/-/theme-tritanopia-13.3.0.tgz", + "integrity": "sha512-k0VH5jzMANV5jkKQX3VffJXHe/1+/yLnqGhhJPPoHWTeck1oazXtJkAB3i5uy5X1BC5sp4jX89zsNrRW9Qz1hw==", "dev": true, "license": "Apache-2.0", - "engines": { - "node": ">=8.17.0" - }, "peerDependencies": { - "blockly": "^12.0.0" + "blockly": "^13.2.0" } }, "node_modules/@blockly/workspace-backpack": { - "version": "7.0.10", - "resolved": "https://registry.npmjs.org/@blockly/workspace-backpack/-/workspace-backpack-7.0.10.tgz", - "integrity": "sha512-059e+bw10PMgxbQXnJhfnYAlP6PvSWxq4ImSZn5R1jL1K52IkVTb25QemGRNcQvfBpxamxAl1QMqbIStEnsdrw==", + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@blockly/workspace-backpack/-/workspace-backpack-13.3.0.tgz", + "integrity": "sha512-bTkv79OLXdY+rpYYWX8ZJn+yed84+oWy3ew22j9cx9uFwdFCPxCPDuI8+5j1ipgzxokiGcMIcTkSmI80Lnfzdw==", "dev": true, "license": "Apache-2.0", - "engines": { - "node": ">=8.17.0" - }, "peerDependencies": { - "blockly": "^12.0.0" + "blockly": "^13.2.0" + } + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" } }, "node_modules/@csstools/color-helpers": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", - "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.0.tgz", + "integrity": "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==", "dev": true, "funding": [ { @@ -437,14 +247,15 @@ } ], "license": "MIT-0", + "peer": true, "engines": { - "node": ">=18" + "node": ">=20.19.0" } }, "node_modules/@csstools/css-calc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", - "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.3.0.tgz", + "integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==", "dev": true, "funding": [ { @@ -457,18 +268,19 @@ } ], "license": "MIT", + "peer": true, "engines": { - "node": ">=18" + "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" } }, "node_modules/@csstools/css-color-parser": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", - "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.10.tgz", + "integrity": "sha512-UZhQLIUyJaaMepqehrCODwCg2KW25vFvLWBmqYFaPclYvvxzj/sG8LBOhBFCp11i9uE7t1EyS+RAoV9tztPFyw==", "dev": true, "funding": [ { @@ -481,22 +293,23 @@ } ], "license": "MIT", + "peer": true, "dependencies": { - "@csstools/color-helpers": "^5.1.0", - "@csstools/css-calc": "^2.1.4" + "@csstools/color-helpers": "^6.1.0", + "@csstools/css-calc": "^3.3.0" }, "engines": { - "node": ">=18" + "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" } }, "node_modules/@csstools/css-parser-algorithms": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", - "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", "dev": true, "funding": [ { @@ -509,17 +322,18 @@ } ], "license": "MIT", + "peer": true, "engines": { - "node": ">=18" + "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-tokenizer": "^3.0.4" + "@csstools/css-tokenizer": "^4.0.0" } }, - "node_modules/@csstools/css-tokenizer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", - "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.7.tgz", + "integrity": "sha512-fQ+05118eQS1cofO3aJpB5efgpBZMvIzwr/sbC8kDLVA5XLG8q1kJV5yzrUAI1f7lvhPnm8fgIjzFB8/O/5Dig==", "dev": true, "funding": [ { @@ -531,120 +345,57 @@ "url": "https://opencollective.com/csstools" } ], - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@discoveryjs/json-ext": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", - "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@es-joy/jsdoccomment": { - "version": "0.41.0", - "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.41.0.tgz", - "integrity": "sha512-aKUhyn1QI5Ksbqcr3fFJj16p99QdjUxXAEuFst1Z47DRyoiMwivIH9MV/ARcJOCXVjPfjITciej8ZD2O/6qUmw==", - "dev": true, - "license": "MIT", - "dependencies": { - "comment-parser": "1.4.1", - "esquery": "^1.5.0", - "jsdoc-type-pratt-parser": "~4.0.0" + "license": "MIT-0", + "peer": true, + "peerDependencies": { + "css-tree": "^3.2.1" }, - "engines": { - "node": ">=16" + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, + "peer": true, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + "node": ">=20.19.0" } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", - "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.1.1", - "espree": "^7.3.0", - "globals": "^13.9.0", - "ignore": "^4.0.6", - "import-fresh": "^3.2.1", - "js-yaml": "^3.13.1", - "minimatch": "^3.0.4", - "strip-json-comments": "^3.1.1" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/@eslint/eslintrc/node_modules/ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", - "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", - "deprecated": "Use @eslint/config-array instead", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanwhocodes/object-schema": "^1.2.0", - "debug": "^4.1.1", - "minimatch": "^3.0.4" + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" }, - "engines": { - "node": ">=10.10.0" + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", - "deprecated": "Use @eslint/object-schema instead", - "dev": true, - "license": "BSD-3-Clause" - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -695,546 +446,859 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@leichtgewicht/ip-codec": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", - "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "node_modules/@jsonjoy.com/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, + "license": "Apache-2.0", "engines": { - "node": ">= 8" + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "node_modules/@jsonjoy.com/buffers": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-17.67.0.tgz", + "integrity": "sha512-tfExRpYxBvi32vPs9ZHaTjSP4fHAfzSmcahOfNxtvGHcyJel+aibkPlGeBB+7AoC6hL7lXIE++8okecBxx7lcw==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "engines": { - "node": ">= 8" + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "node_modules/@jsonjoy.com/codegen": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz", + "integrity": "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-core": { + "version": "4.78.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-core/-/fs-core-4.78.0.tgz", + "integrity": "sha512-0YE5XQj383AwNj5fG0Mm1gyx/ViVe2DJzS5FeVbJulw0pteEK9mfbeB0MWH3/ARoXb2AhHHuiiNthdWF0Y6EYQ==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "@jsonjoy.com/fs-node-builtins": "4.78.0", + "@jsonjoy.com/fs-node-utils": "4.78.0", + "thingies": "^2.5.0" }, "engines": { - "node": ">= 8" + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/@playwright/test": { - "version": "1.58.2", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", - "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", + "node_modules/@jsonjoy.com/fs-fsa": { + "version": "4.78.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-fsa/-/fs-fsa-4.78.0.tgz", + "integrity": "sha512-V1lUo5k4xUypsfdmvCrrUJ44BCixbgvpus71WI3a3BOQt39bK2LasU/HgpJ1P9or8a2d5mWR3kk26sRoehDlaw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.58.2" + "@jsonjoy.com/fs-core": "4.78.0", + "@jsonjoy.com/fs-node-builtins": "4.78.0", + "@jsonjoy.com/fs-node-utils": "4.78.0", + "thingies": "^2.5.0" }, - "bin": { - "playwright": "cli.js" + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node": { + "version": "4.78.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node/-/fs-node-4.78.0.tgz", + "integrity": "sha512-AhoKCEEzsTU5C+9pSegNkErrQijvpHj10kzVhaXIxjs/0taxjCCLG7zE+Y+EWCMNCVmgFEyca+oTQQWm2RO+sg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-core": "4.78.0", + "@jsonjoy.com/fs-node-builtins": "4.78.0", + "@jsonjoy.com/fs-node-utils": "4.78.0", + "@jsonjoy.com/fs-print": "4.78.0", + "@jsonjoy.com/fs-snapshot": "4.78.0", + "glob-to-regex.js": "^1.3.1", + "thingies": "^2.5.0" }, "engines": { - "node": ">=18" + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/@sinonjs/commons": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", - "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", + "node_modules/@jsonjoy.com/fs-node-builtins": { + "version": "4.78.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.78.0.tgz", + "integrity": "sha512-yiyMh9uKnQvRVDYzINP9P2AXZPdHYM2iVsQ/2U2V+g+gfTr4vPcT1Ox0aQs0IIXGQRAnksyA63KSqpOOJ/O3lA==", "dev": true, - "license": "BSD-3-Clause", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node-to-fsa": { + "version": "4.78.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.78.0.tgz", + "integrity": "sha512-L1e0UgRJ/igqt3JUDJh3z8YXTKqJc9r9MY4ZakEMOqqEDq/C5fmSZDDzq03d7HqdG1rehkmCDpKyXWqQEVpQGw==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "type-detect": "4.0.8" + "@jsonjoy.com/fs-fsa": "4.78.0", + "@jsonjoy.com/fs-node-builtins": "4.78.0", + "@jsonjoy.com/fs-node-utils": "4.78.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/@sinonjs/commons/node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "node_modules/@jsonjoy.com/fs-node-utils": { + "version": "4.78.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-utils/-/fs-node-utils-4.78.0.tgz", + "integrity": "sha512-Am25+Y/rEFtkJBZCFgs84XPXqeqXq4FvXmCYMZ0bEC/M+vT0e6tHBU4Camp9/Yzuf6NoxaInC3iXIN791wVu9g==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-node-builtins": "4.78.0", + "glob-to-regex.js": "^1.3.1" + }, "engines": { - "node": ">=4" + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/@sinonjs/fake-timers": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz", - "integrity": "sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==", + "node_modules/@jsonjoy.com/fs-print": { + "version": "4.78.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-print/-/fs-print-4.78.0.tgz", + "integrity": "sha512-Uwk0SvDN1X+5FNW3MxibrFVXXUnd0nTUP6qwvPikg3QDeXRiRww91mz34lXYa/xO5eVuUqa1WXmd0ljy1+Ej0w==", "dev": true, - "license": "BSD-3-Clause", + "license": "Apache-2.0", "dependencies": { - "@sinonjs/commons": "^1.7.0" + "@jsonjoy.com/fs-node-utils": "4.78.0", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/@sinonjs/samsam": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-5.3.1.tgz", - "integrity": "sha512-1Hc0b1TtyfBu8ixF/tpfSHTVWKwCBLY4QJbkgnE7HcwyvT2xArDxb4K7dMgqRm3szI+LJbzmW/s4xxEhv6hwDg==", + "node_modules/@jsonjoy.com/fs-snapshot": { + "version": "4.78.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.78.0.tgz", + "integrity": "sha512-+HvPGwV1IcXtwFFxMBK5x8rmqBuIkPpEFZMGkO8BolJH+AuSvQGVwvlKulUjQ9KECpN4GTCcgeQtyUvYC8heSA==", "dev": true, - "license": "BSD-3-Clause", + "license": "Apache-2.0", "dependencies": { - "@sinonjs/commons": "^1.6.0", - "lodash.get": "^4.4.2", - "type-detect": "^4.0.8" + "@jsonjoy.com/buffers": "^17.65.0", + "@jsonjoy.com/fs-node-utils": "4.78.0", + "@jsonjoy.com/json-pack": "^17.65.0", + "@jsonjoy.com/util": "^17.65.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/@sinonjs/text-encoding": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz", - "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==", + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/base64": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-17.67.0.tgz", + "integrity": "sha512-5SEsJGsm15aP8TQGkDfJvz9axgPwAEm98S5DxOuYe8e1EbfajcDmgeXXzccEjh+mLnjqEKrkBdjHWS5vFNwDdw==", "dev": true, - "license": "(Unlicense OR Apache-2.0)" + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } }, - "node_modules/@types/body-parser": { - "version": "1.19.6", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", - "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/codegen": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-17.67.0.tgz", + "integrity": "sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pack": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-17.67.0.tgz", + "integrity": "sha512-t0ejURcGaZsn1ClbJ/3kFqSOjlryd92eQY465IYrezsXmPcfHPE/av4twRSxf6WE+TkZgLY+71vCZbiIiFKA/w==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@types/connect": "*", - "@types/node": "*" + "@jsonjoy.com/base64": "17.67.0", + "@jsonjoy.com/buffers": "17.67.0", + "@jsonjoy.com/codegen": "17.67.0", + "@jsonjoy.com/json-pointer": "17.67.0", + "@jsonjoy.com/util": "17.67.0", + "hyperdyperid": "^1.2.0", + "thingies": "^2.5.0", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/@types/bonjour": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", - "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pointer": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-17.67.0.tgz", + "integrity": "sha512-+iqOFInH+QZGmSuaybBUNdh7yvNrXvqR+h3wjXm0N/3JK1EyyFAeGJvqnmQL61d1ARLlk/wJdFKSL+LHJ1eaUA==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "@types/node": "*" + "@jsonjoy.com/util": "17.67.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/util": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-17.67.0.tgz", + "integrity": "sha512-6+8xBaz1rLSohlGh68D1pdw3AwDi9xydm8QNlAFkvnavCJYSze+pxoW2VKP8p308jtlMRLs5NTHfPlZLd4w7ew==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "@types/node": "*" + "@jsonjoy.com/buffers": "17.67.0", + "@jsonjoy.com/codegen": "17.67.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/@types/connect-history-api-fallback": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", - "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", + "node_modules/@jsonjoy.com/json-pack": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz", + "integrity": "sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "@types/express-serve-static-core": "*", - "@types/node": "*" + "@jsonjoy.com/base64": "^1.1.2", + "@jsonjoy.com/buffers": "^1.2.0", + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/json-pointer": "^1.0.2", + "@jsonjoy.com/util": "^1.9.0", + "hyperdyperid": "^1.2.0", + "thingies": "^2.5.0", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/@types/eslint": { - "version": "8.56.12", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.12.tgz", - "integrity": "sha512-03ruubjWyOHlmljCVoxSuNDdmfZDzsrrz0P2LeJsOXr+ZwFQ+0yQIwNCwt/GYhV7Z31fgtXJTAEs+FYlEL851g==", + "node_modules/@jsonjoy.com/json-pack/node_modules/@jsonjoy.com/buffers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", + "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pointer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz", + "integrity": "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/util": "^1.9.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "node_modules/@jsonjoy.com/util": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.9.0.tgz", + "integrity": "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" + "@jsonjoy.com/buffers": "^1.0.0", + "@jsonjoy.com/codegen": "^1.0.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "node_modules/@jsonjoy.com/util/node_modules/@jsonjoy.com/buffers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", + "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", "dev": true, "license": "MIT" }, - "node_modules/@types/express": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.23.tgz", - "integrity": "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==", + "node_modules/@peculiar/asn1-cms": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.9.4.tgz", + "integrity": "sha512-cben7oxmQsUGZqotus7yt0srYdncOT6RNWcTQ77T2RFOXejYVYkXadrfePdRcrVpO9K95IRLKKglG2k38jKXuw==", "dev": true, "license": "MIT", "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "*" + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/asn1-x509": "^2.9.4", + "@peculiar/asn1-x509-attr": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" } }, - "node_modules/@types/express-serve-static-core": { - "version": "4.19.6", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", - "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "node_modules/@peculiar/asn1-csr": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.9.4.tgz", + "integrity": "sha512-xd4YN4vpRjkDAQWVfZZkeu12IEND7DOpkqaHSIHxZl1uggUNa9Ju0QxY2jHvDAS9pP0zhRBytg8ifsnGo3V0jw==", "dev": true, "license": "MIT", "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/asn1-x509": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" } }, - "node_modules/@types/http-errors": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", - "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "node_modules/@peculiar/asn1-ecc": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.9.4.tgz", + "integrity": "sha512-JJXefFshRAuVAjWQo/39bkg1ywc1VaiO44S8RRC+Ykvf/u2KDmYffoDb0ZBPCR5uJy4AGKQhl8mX+Q8ShcWaXQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/asn1-x509": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } }, - "node_modules/@types/http-proxy": { - "version": "1.17.16", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.16.tgz", - "integrity": "sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w==", + "node_modules/@peculiar/asn1-pfx": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.9.4.tgz", + "integrity": "sha512-khuGzHTzNzk4GDlIBEILyIs6Lce0yn0ZBdoI9v93kmNncfZRhD+AQ5ODFqdhvoE8cMJF/JMTQ8yA+t1D14kqCw==", "dev": true, "license": "MIT", "dependencies": { - "@types/node": "*" + "@peculiar/asn1-cms": "^2.9.4", + "@peculiar/asn1-pkcs8": "^2.9.4", + "@peculiar/asn1-rsa": "^2.9.4", + "@peculiar/asn1-schema": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" } }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "node_modules/@peculiar/asn1-pkcs8": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.9.4.tgz", + "integrity": "sha512-duRdotlUx9eDZe6QrQpQKl61RbWykCHBCkKayP8V8XdEFwlKHZ8qGGDMyS6Pye7OX7nLFttTTpRkJeet78ckwQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/asn1-x509": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "node_modules/@peculiar/asn1-pkcs9": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.9.4.tgz", + "integrity": "sha512-kaL4cNxBpdQE2dKlyZBqz4ygCrwffO+8wfoxTEqM1Z8RadvCeELBRzcv0dzM8aY9azHMwODO5nxU65zXmhToOQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.9.4", + "@peculiar/asn1-pfx": "^2.9.4", + "@peculiar/asn1-pkcs8": "^2.9.4", + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/asn1-x509": "^2.9.4", + "@peculiar/asn1-x509-attr": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } }, - "node_modules/@types/node": { - "version": "24.12.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", - "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", + "node_modules/@peculiar/asn1-rsa": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.9.4.tgz", + "integrity": "sha512-pZ96eD1PptovcWQ/GSmuNFXd/7EQJNlKfDaNCyE2rx3W0v6QFelkzquVqRSRyyDXXCYD69ZXJDzZ8GhIiQzKoA==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.16.0" + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/asn1-x509": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" } }, - "node_modules/@types/node-forge": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.14.tgz", - "integrity": "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==", + "node_modules/@peculiar/asn1-schema": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.9.4.tgz", + "integrity": "sha512-GjzePcT9Iw8NzeOPf73iNS9xM+TBhd/FilAfP+RQGkTMQJTVWtytN3JHJACCjf/ABNau5S7mS3g+DcuxmRgYEg==", "dev": true, "license": "MIT", "dependencies": { - "@types/node": "*" + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" } }, - "node_modules/@types/parse-json": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", - "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "node_modules/@peculiar/asn1-x509": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.9.4.tgz", + "integrity": "sha512-CxhBo/RdEbMMob7T31ZdQjGuoyRFLVwrDzTn25bihzBasRg9kRm/0IxIPvhgQtcK/9dNcO1XQL2fuPugwELL0Q==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } }, - "node_modules/@types/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "node_modules/@peculiar/asn1-x509-attr": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.9.4.tgz", + "integrity": "sha512-ehQXbpQaQYycgu8OrvigwSPTFfVRcu0ECNYCWw+yzBp02Lw5paRqzzhUpfOgO2K38+WfFZuEz/0RPtam5g0OMg==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/asn1-x509": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "node_modules/@peculiar/utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@peculiar/utils/-/utils-2.0.3.tgz", + "integrity": "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } }, - "node_modules/@types/retry": { - "version": "0.12.2", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", - "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", + "node_modules/@peculiar/x509": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.3.tgz", + "integrity": "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.0", + "@peculiar/asn1-csr": "^2.6.0", + "@peculiar/asn1-ecc": "^2.6.0", + "@peculiar/asn1-pkcs9": "^2.6.0", + "@peculiar/asn1-rsa": "^2.6.0", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "pvtsutils": "^1.3.6", + "reflect-metadata": "^0.2.2", + "tslib": "^2.8.1", + "tsyringe": "^4.10.0" + }, + "engines": { + "node": ">=20.0.0" + } }, - "node_modules/@types/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", + "node_modules/@playwright/test": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", + "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", "dev": true, - "license": "MIT" + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } }, - "node_modules/@types/send": { - "version": "0.17.5", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", - "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", "dependencies": { - "@types/mime": "^1", - "@types/node": "*" + "type-detect": "4.0.8" } }, - "node_modules/@types/serve-index": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", - "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", + "node_modules/@sinonjs/fake-timers": { + "version": "15.4.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.4.0.tgz", + "integrity": "sha512-DsG+8/LscQIQg68J6Ef3dv10u6nVyetYn923s3/sus5eaGfTo1of5WMZSLf0UJc9KDuKPilPH0UDJCjvNbDNCA==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", "dependencies": { - "@types/express": "*" + "@sinonjs/commons": "^3.0.1" } }, - "node_modules/@types/serve-static": { - "version": "1.15.8", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", - "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", + "node_modules/@sinonjs/samsam": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-10.0.2.tgz", + "integrity": "sha512-8lVwD1Df1BmzoaOLhMcGGcz/Jyr5QY2KSB75/YK1QgKzoabTeLdIVyhXNZK9ojfSKSdirbXqdbsXXqP9/Ve8+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "type-detect": "^4.1.0" + } + }, + "node_modules/@sinonjs/samsam/node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", "dev": true, "license": "MIT", "dependencies": { - "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "*" + "@types/connect": "*", + "@types/node": "*" } }, - "node_modules/@types/sockjs": { - "version": "0.3.36", - "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", - "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", + "node_modules/@types/bonjour": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" } }, - "node_modules/@types/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" } }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", - "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "node_modules/@types/connect-history-api-fallback": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/type-utils": "5.62.0", - "@typescript-eslint/utils": "5.62.0", - "debug": "^4.3.4", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "@types/express-serve-static-core": "*", + "@types/node": "*" } }, - "node_modules/@typescript-eslint/parser": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", - "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/typescript-estree": "5.62.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", - "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "node_modules/@types/express-serve-static-core": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.3.tgz", + "integrity": "sha512-dPfW8NFiOF4wOHc7+N/QSxlY9cfSsenewGbAz8C8U/MULPd/YZ27LvJUIlzaXie7e6Ove9YunJGgC9tbHD2cKw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/visitor-keys": "5.62.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" } }, - "node_modules/@typescript-eslint/type-utils": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", - "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.12.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", + "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "5.62.0", - "@typescript-eslint/utils": "5.62.0", - "debug": "^4.3.4", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "undici-types": "~7.16.0" } }, - "node_modules/@typescript-eslint/types": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", - "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", "dev": true, "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "dependencies": { + "@types/node": "*" } }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", - "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "node_modules/@types/serve-index": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/visitor-keys": "5.62.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "@types/express": "*" } }, - "node_modules/@typescript-eslint/utils": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", - "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/typescript-estree": "5.62.0", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "@types/http-errors": "*", + "@types/node": "*" } }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", - "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "5.62.0", - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "@types/node": "*" } }, "node_modules/@webassemblyjs/ast": { @@ -1398,53 +1462,6 @@ "@xtuc/long": "4.2.2" } }, - "node_modules/@webpack-cli/configtest": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", - "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.15.0" - }, - "peerDependencies": { - "webpack": "5.x.x", - "webpack-cli": "5.x.x" - } - }, - "node_modules/@webpack-cli/info": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", - "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.15.0" - }, - "peerDependencies": { - "webpack": "5.x.x", - "webpack-cli": "5.x.x" - } - }, - "node_modules/@webpack-cli/serve": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", - "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.15.0" - }, - "peerDependencies": { - "webpack": "5.x.x", - "webpack-cli": "5.x.x" - }, - "peerDependenciesMeta": { - "webpack-dev-server": { - "optional": true - } - } - }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -1459,75 +1476,68 @@ "dev": true, "license": "Apache-2.0" }, - "node_modules/abab": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", - "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", - "deprecated": "Use your platform's native atob() and btoa() methods instead", - "dev": true, - "license": "BSD-3-Clause" - }, "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", "dev": true, "license": "MIT", "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" }, "engines": { "node": ">= 0.6" } }, - "node_modules/accepts/node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "node_modules/accepts/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "node_modules/accepts/node_modules/negotiator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz", + "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==", "dev": true, "license": "MIT", - "bin": { - "acorn": "bin/acorn" + "dependencies": { + "content-type": "^2.1.0" }, "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", "dev": true, "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, "engines": { - "node": ">= 14" + "node": ">=0.4.0" } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "dependencies": { @@ -1560,9 +1570,9 @@ } }, "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", "dependencies": { @@ -1593,156 +1603,55 @@ "ajv": "^6.9.1" } }, - "node_modules/ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/ansi-html-community": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", - "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", - "dev": true, - "engines": [ - "node >= 0.8.0" - ], - "license": "Apache-2.0", - "bin": { - "ansi-html": "bin/ansi-html" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/are-docs-informative": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.0.2.tgz", - "integrity": "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - } - }, - "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/astral-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "node_modules/ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" + "engines": [ + "node >= 0.8.0" + ], + "license": "Apache-2.0", + "bin": { + "ansi-html": "bin/ansi-html" } }, - "node_modules/babel-eslint": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz", - "integrity": "sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==", - "deprecated": "babel-eslint is now @babel/eslint-parser. This package will no longer receive updates.", + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.7.0", - "@babel/traverse": "^7.7.0", - "@babel/types": "^7.7.0", - "eslint-visitor-keys": "^1.0.0", - "resolve": "^1.12.0" + "color-convert": "^2.0.1" }, "engines": { - "node": ">=6" + "node": ">=8" }, - "peerDependencies": { - "eslint": ">= 4.12.1" + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/babel-eslint/node_modules/eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true, - "license": "Apache-2.0", + "license": "Python-2.0" + }, + "node_modules/asn1js": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz", + "integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.5", + "tslib": "^2.8.1" + }, "engines": { - "node": ">=4" + "node": ">=12.0.0" } }, "node_modules/balanced-match": { @@ -1753,13 +1662,16 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.3.tgz", - "integrity": "sha512-mcE+Wr2CAhHNWxXN/DdTI+n4gsPc5QpXpWnyCQWiQYIYZX+ZMJ8juXZgjRa/0/YPJo/NSsgW15/YgmI4nbysYw==", + "version": "2.11.12", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.12.tgz", + "integrity": "sha512-r7WnVImvVCeFpf2DOXfy41aPWzeNg3H/A2X4dKmy1QL0MSyyk/e7z8ihJ3N6Nn2PsdhkVlqnEfnUE4a05P2aTA==", "dev": true, "license": "Apache-2.0", "bin": { - "baseline-browser-mapping": "dist/cli.js" + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/batch": { @@ -1769,101 +1681,90 @@ "dev": true, "license": "MIT" }, - "node_modules/big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", "dev": true, "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peer": true, + "dependencies": { + "require-from-string": "^2.0.2" } }, "node_modules/blockly": { - "version": "12.5.1", - "resolved": "https://registry.npmjs.org/blockly/-/blockly-12.5.1.tgz", - "integrity": "sha512-etXLpUtEkcRibHGwIJ4BsvnIzMJJs0C0yPIjE/W0NCtj8ACha/a7Q9n7Ib6+j7N4EzQ0p28YPZMnypi5pNIj1g==", + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/blockly/-/blockly-13.3.0.tgz", + "integrity": "sha512-qAHFGMGTSF67Lupr3pjZz9LQu4JCFdN76CMZxegBKiveZreK08yXhaCz0g9q9/xe5GEdoeFT3POAlQavPIRPEw==", "dev": true, "license": "Apache-2.0", - "dependencies": { - "jsdom": "26.1.0" - }, "engines": { - "node": ">=18" + "node": ">=22" + }, + "peerDependencies": { + "jsdom": ">=27.4.0 <30.0.0" } }, "node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", "dev": true, "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" }, "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/body-parser/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/body-parser/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", "dev": true, "license": "MIT", - "dependencies": { - "ms": "2.0.0" + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/body-parser/node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", "dev": true, "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, "node_modules/bonjour-service": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", - "integrity": "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==", + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.4.4.tgz", + "integrity": "sha512-jCZcVv7eoc4QesRscwEZtSROBen+6LpKAmBIsQYQrsAeVHLyMXWX/t6eIV5KiRZYNUBl8eVqImEEMQ8L5+c/Kw==", "dev": true, "license": "MIT", "dependencies": { @@ -1872,9 +1773,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { @@ -1903,9 +1804,9 @@ "license": "ISC" }, "node_modules/browserslist": { - "version": "4.26.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.0.tgz", - "integrity": "sha512-P9go2WrP9FiPwLv3zqRD/Uoxo0RSHjzFCiQz7d4vbmwNqQFo9T9WCeP/Qn5EbcKQY6DBbkxEXNcpJOmncNrb7A==", + "version": "4.28.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", + "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==", "dev": true, "funding": [ { @@ -1923,11 +1824,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.8.2", - "caniuse-lite": "^1.0.30001741", - "electron-to-chromium": "^1.5.218", - "node-releases": "^2.0.21", - "update-browserslist-db": "^1.1.3" + "baseline-browser-mapping": "^2.10.44", + "caniuse-lite": "^1.0.30001806", + "electron-to-chromium": "^1.5.393", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" @@ -1943,19 +1844,6 @@ "dev": true, "license": "MIT" }, - "node_modules/builtin-modules": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", - "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/bundle-name": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", @@ -1982,6 +1870,16 @@ "node": ">= 0.8" } }, + "node_modules/bytestreamjs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz", + "integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -2023,23 +1921,10 @@ "node": ">=6" } }, - "node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/caniuse-lite": { - "version": "1.0.30001741", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001741.tgz", - "integrity": "sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw==", + "version": "1.0.30001806", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", + "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", "dev": true, "funding": [ { @@ -2058,22 +1943,13 @@ "license": "CC-BY-4.0" }, "node_modules/chai": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", - "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, "license": "MIT", - "dependencies": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.3", - "deep-eql": "^4.1.3", - "get-func-name": "^2.0.2", - "loupe": "^2.3.6", - "pathval": "^1.1.1", - "type-detect": "^4.1.0" - }, "engines": { - "node": ">=4" + "node": ">=18" } }, "node_modules/chalk": { @@ -2093,42 +1969,20 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/check-error": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", - "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-func-name": "^2.0.2" - }, - "engines": { - "node": "*" - } - }, "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, "license": "MIT", "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "readdirp": "^4.0.1" }, "engines": { - "node": ">= 8.10.0" + "node": ">= 14.16.0" }, "funding": { "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" } }, "node_modules/chrome-trace-event": { @@ -2141,33 +1995,6 @@ "node": ">=6.0" } }, - "node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2188,13 +2015,6 @@ "dev": true, "license": "MIT" }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true, - "license": "MIT" - }, "node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -2202,16 +2022,6 @@ "dev": true, "license": "MIT" }, - "node_modules/comment-parser": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.1.tgz", - "integrity": "sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 12.0.0" - } - }, "node_modules/compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -2226,15 +2036,16 @@ } }, "node_modules/compression": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", - "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.2.tgz", + "integrity": "sha512-o8vI5RE5A6EVVOd9o41jKp41aJom+QTEO/Bx8MYNjexMo/Bv2WOjUfZr+aL0WnYSgymUy6zeguqLTsIhV0gMvQ==", "dev": true, "license": "MIT", "dependencies": { "bytes": "3.1.2", "compressible": "~2.0.18", "debug": "2.6.9", + "destroy": "1.2.0", "negotiator": "~0.6.4", "on-headers": "~1.1.0", "safe-buffer": "5.2.1", @@ -2242,6 +2053,10 @@ }, "engines": { "node": ">= 0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/compression/node_modules/debug": { @@ -2279,16 +2094,17 @@ } }, "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", "dev": true, "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/content-type": { @@ -2302,9 +2118,9 @@ } }, "node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "dev": true, "license": "MIT", "engines": { @@ -2312,63 +2128,55 @@ } }, "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "dev": true, - "license": "MIT" - }, - "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==", - "dev": true, - "license": "MIT" - }, - "node_modules/cosmiconfig": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", - "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", "dev": true, "license": "MIT", - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - }, "engines": { - "node": ">=10" + "node": ">=6.6.0" } }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", "dev": true, "license": "MIT", "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" }, "engines": { - "node": ">= 8" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/cssstyle": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", - "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@asamuzakjp/css-color": "^3.2.0", - "rrweb-cssom": "^0.8.0" + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" }, "engines": { - "node": ">=18" + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" } }, "node_modules/dat.gui": { @@ -2379,23 +2187,24 @@ "license": "Apache-2.0" }, "node_modules/data-urls": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", - "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^14.0.0" + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" }, "engines": { - "node": ">=18" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { @@ -2410,45 +2219,13 @@ } } }, - "node_modules/decamelize": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", - "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/decimal.js": { "version": "10.6.0", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", "dev": true, - "license": "MIT" - }, - "node_modules/deep-eql": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", - "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", - "dev": true, "license": "MIT", - "dependencies": { - "type-detect": "^4.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" + "peer": true }, "node_modules/deepmerge": { "version": "4.3.1", @@ -2461,9 +2238,9 @@ } }, "node_modules/default-browser": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", - "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.1.tgz", + "integrity": "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==", "dev": true, "license": "MIT", "dependencies": { @@ -2478,9 +2255,9 @@ } }, "node_modules/default-browser-id": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", - "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", "dev": true, "license": "MIT", "engines": { @@ -2523,35 +2300,15 @@ "node": ">= 0.8", "npm": "1.2.8000 || >= 1.4.16" } - }, - "node_modules/detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "dev": true, - "license": "MIT" - }, - "node_modules/diff": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, + }, + "node_modules/diff": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-9.0.0.tgz", + "integrity": "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==", + "dev": true, + "license": "BSD-3-Clause", "engines": { - "node": ">=8" + "node": ">=0.3.1" } }, "node_modules/dns-packet": { @@ -2567,17 +2324,14 @@ "node": ">=6" } }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "node_modules/dompurify": { + "version": "3.4.8", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.8.tgz", + "integrity": "sha512-yb1cEmaOum7wFvOCSQxyfgVlv5D47Rc30iZWoMpbDIWTnJ6grDDQyu2KFJzB2k7u0pMuJcQ1zphH//fFnw2tjQ==", "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" } }, "node_modules/dragselect": { @@ -2609,29 +2363,12 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.218", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.218.tgz", - "integrity": "sha512-uwwdN0TUHs8u6iRgN8vKeWZMRll4gBkz+QMqdS7DDe49uiK68/UX92lFb61oiFPrpYZNeZIqa4bA7O6Aiasnzg==", + "version": "1.5.399", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.399.tgz", + "integrity": "sha512-lEcqhErbHjXRvd41rnWLpzbyU/IXfIYo7QwaFWmxGeLiLyY2TBCdHnWY88vB+p3ubnihRypDm66panXl7TylLA==", "dev": true, "license": "ISC" }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -2643,63 +2380,37 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.18.3", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", - "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "version": "5.24.5", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", + "integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==", "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" + "tapable": "^2.3.3" }, "engines": { "node": ">=10.13.0" } }, - "node_modules/enquirer": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", - "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-colors": "^4.1.1", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8.6" - } - }, "node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "engines": { - "node": ">=0.12" + "node": ">=20.19.0" }, "funding": { "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/envinfo": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.14.0.tgz", - "integrity": "sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg==", - "dev": true, - "license": "MIT", - "bin": { - "envinfo": "dist/cli.js" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2727,16 +2438,16 @@ } }, "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", "dev": true, "license": "MIT" }, "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", "dev": true, "license": "MIT", "dependencies": { @@ -2763,115 +2474,6 @@ "dev": true, "license": "MIT" }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "7.32.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", - "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", - "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "7.12.11", - "@eslint/eslintrc": "^0.4.3", - "@humanwhocodes/config-array": "^0.5.0", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.0.1", - "doctrine": "^3.0.0", - "enquirer": "^2.3.5", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^2.1.0", - "eslint-visitor-keys": "^2.0.0", - "espree": "^7.3.1", - "esquery": "^1.4.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.1.2", - "globals": "^13.6.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "js-yaml": "^3.13.1", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.0.4", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "progress": "^2.0.0", - "regexpp": "^3.1.0", - "semver": "^7.2.1", - "strip-ansi": "^6.0.0", - "strip-json-comments": "^3.1.0", - "table": "^6.0.9", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-config-google": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/eslint-config-google/-/eslint-config-google-0.14.0.tgz", - "integrity": "sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=0.10.0" - }, - "peerDependencies": { - "eslint": ">=5.16.0" - } - }, - "node_modules/eslint-plugin-jsdoc": { - "version": "46.10.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-46.10.1.tgz", - "integrity": "sha512-x8wxIpv00Y50NyweDUpa+58ffgSAI5sqe+zcZh33xphD0AVh+1kqr1ombaTRb7Fhpove1zfUuujlX9DWWBP5ag==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@es-joy/jsdoccomment": "~0.41.0", - "are-docs-informative": "^0.0.2", - "comment-parser": "1.4.1", - "debug": "^4.3.4", - "escape-string-regexp": "^4.0.0", - "esquery": "^1.5.0", - "is-builtin-module": "^3.2.1", - "semver": "^7.5.4", - "spdx-expression-parse": "^4.0.0" - }, - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" - } - }, "node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -2886,162 +2488,6 @@ "node": ">=8.0.0" } }, - "node_modules/eslint-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^1.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - } - }, - "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-webpack-plugin": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/eslint-webpack-plugin/-/eslint-webpack-plugin-3.2.0.tgz", - "integrity": "sha512-avrKcGncpPbPSUHX6B3stNGzkKFto3eL+DKM4+VyMrVnhPc3vRczVlCq3uhuFOdRvDHTVXuzwk1ZKUrqDQHQ9w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/eslint": "^7.29.0 || ^8.4.1", - "jest-worker": "^28.0.2", - "micromatch": "^4.0.5", - "normalize-path": "^3.0.0", - "schema-utils": "^4.0.0" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0", - "webpack": "^5.0.0" - } - }, - "node_modules/eslint/node_modules/@babel/code-frame": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", - "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/highlight": "^7.10.4" - } - }, - "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10" - } - }, - "node_modules/eslint/node_modules/ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/espree": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", - "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^7.4.0", - "acorn-jsx": "^5.3.1", - "eslint-visitor-keys": "^1.3.0" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=4" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esquery/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, "node_modules/esrecurse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", @@ -3075,16 +2521,6 @@ "node": ">=4.0" } }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -3095,13 +2531,6 @@ "node": ">= 0.6" } }, - "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "dev": true, - "license": "MIT" - }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -3113,76 +2542,49 @@ } }, "node_modules/express": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", - "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", - "proxy-addr": "~2.0.7", - "qs": "6.13.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" }, "engines": { - "node": ">= 0.10.0" + "node": ">= 18" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/express" } }, - "node_modules/express/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/express/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, - "node_modules/express/node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", - "dev": true, - "license": "MIT" - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3190,23 +2592,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -3214,74 +2599,39 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", + "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", "dev": true, "funding": [ { "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/fastest-levenshtein": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", - "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4.9.1" - } - }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/faye-websocket": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", - "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "websocket-driver": ">=0.5.1" - }, - "engines": { - "node": ">=0.8.0" - } + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", - "dependencies": { - "flat-cache": "^3.0.4" - }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } } }, "node_modules/fill-range": { @@ -3298,122 +2648,55 @@ } }, "node_modules/finalhandler": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", - "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", "dev": true, "license": "MIT", "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" }, "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-8.0.0.tgz", + "integrity": "sha512-JGG8pvDi2C+JxidYdIwQDyS/CgcrIdh18cvgxcBge3wSHRQOrooMD3GlFBcmMJAN9M42SAZjDp5zv1dglJjwww==", "dev": true, "license": "MIT", "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" + "locate-path": "^8.0.0", + "unicorn-magic": "^0.3.0" }, "engines": { - "node": ">=10" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", - "dev": true, - "license": "BSD-3-Clause", - "bin": { - "flat": "cli.js" - } - }, - "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true, - "license": "ISC" - }, - "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, "node_modules/fork-ts-checker-webpack-plugin": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-8.0.0.tgz", - "integrity": "sha512-mX3qW3idpueT2klaQXBzrIM/pHw+T0B/V9KHEvNrqijTq9NFnMZU6oreVxDYcf33P8a5cW+67PjodNHthGnNVg==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.1.0.tgz", + "integrity": "sha512-mpafl89VFPJmhnJ1ssH+8wmM2b50n+Rew5x42NeI2U78aRWgtkEtGmctp7iT16UjquJTjorEmIfESj3DxdW84Q==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.16.7", "chalk": "^4.1.2", - "chokidar": "^3.5.3", - "cosmiconfig": "^7.0.1", + "chokidar": "^4.0.1", + "cosmiconfig": "^8.2.0", "deepmerge": "^4.2.2", "fs-extra": "^10.0.0", "memfs": "^3.4.1", @@ -3424,33 +2707,45 @@ "tapable": "^2.2.1" }, "engines": { - "node": ">=12.13.0", - "yarn": ">=1.0.0" + "node": ">=14.21.3" }, "peerDependencies": { "typescript": ">3.6.0", "webpack": "^5.11.0" } }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "node_modules/fork-ts-checker-webpack-plugin/node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dev": true, "license": "MIT", "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" }, "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "node": ">=6.9.0" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" } }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -3462,13 +2757,13 @@ } }, "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">= 0.8" } }, "node_modules/fs-extra": { @@ -3493,28 +2788,6 @@ "dev": true, "license": "Unlicense" }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, - "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": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -3525,33 +2798,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", - "dev": true, - "license": "MIT" - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-func-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", - "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -3592,104 +2838,77 @@ } }, "node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" }, "engines": { - "node": ">=12" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "node_modules/glob-to-regex.js": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.3.1.tgz", + "integrity": "sha512-zhWhMRsgnOym+1txhUmYJGD6eUsbUdrZv55gCYKuJX8t2p3+M8My23PD09bEIsreGUs5pnSa/idmcfp2/IlqGg==", "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, + "license": "Apache-2.0", "engines": { - "node": ">= 6" + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "node_modules/glob/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, "engines": { - "node": ">=10" + "node": "18 || 20 || >=22" } }, - "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "node_modules/glob/node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "dev": true, "license": "MIT", "dependencies": { - "type-fest": "^0.20.2" + "balanced-match": "^4.0.2" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "20 || >=22" } }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "node_modules/glob/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" + "brace-expansion": "^5.0.8" }, "engines": { - "node": ">=10" + "node": "18 || 20 || >=22" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/gopd": { @@ -3712,20 +2931,6 @@ "dev": true, "license": "ISC" }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, - "node_modules/handle-thing": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", - "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", - "dev": true, - "license": "MIT" - }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -3743,200 +2948,84 @@ "dev": true, "license": "MIT", "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true, - "license": "MIT", - "bin": { - "he": "bin/he" - } - }, - "node_modules/hpack.js": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", - "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.1", - "obuf": "^1.0.0", - "readable-stream": "^2.0.1", - "wbuf": "^1.1.0" - } - }, - "node_modules/hpack.js/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/hpack.js/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==", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/hpack.js/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==", - "dev": true, - "license": "MIT" - }, - "node_modules/hpack.js/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==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/html-encoding-sniffer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", - "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "whatwg-encoding": "^3.1.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/http-deceiver": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", - "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", - "dev": true, - "license": "MIT" - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/http-parser-js": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", - "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", - "dev": true, - "license": "MIT" - }, - "node_modules/http-proxy": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", - "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "dev": true, "license": "MIT", "dependencies": { - "eventemitter3": "^4.0.0", - "follow-redirects": "^1.0.0", - "requires-port": "^1.0.0" + "function-bind": "^1.1.2" }, "engines": { - "node": ">=8.0.0" + "node": ">= 0.4" } }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" + "@exodus/bytes": "^1.6.0" }, "engines": { - "node": ">= 14" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, - "node_modules/http-proxy-middleware": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", - "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "dev": true, "license": "MIT", "dependencies": { - "@types/http-proxy": "^1.17.8", - "http-proxy": "^1.18.1", - "is-glob": "^4.0.1", - "is-plain-obj": "^3.0.0", - "micromatch": "^4.0.2" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "@types/express": "^4.17.13" + "node": ">= 0.8" }, - "peerDependenciesMeta": { - "@types/express": { - "optional": true - } + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "node_modules/http-proxy-middleware": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-4.2.0.tgz", + "integrity": "sha512-ZA+oNOoM+GLoFTIzhkJptVQov73Srep2LBqhF8hG8CIPKO3nam1jonXVQ/QUH8RbwsmaaVz2SOJdzBNBHNtKbw==", "dev": true, "license": "MIT", "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" + "debug": "^4.4.3", + "httpxy": "^0.5.4", + "is-glob": "^4.0.3", + "is-plain-obj": "^4.1.0", + "micromatch": "^4.0.8" }, "engines": { - "node": ">= 14" + "node": "^22.15.0 || ^24.0.0 || >=26.0.0" } }, + "node_modules/httpxy": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/httpxy/-/httpxy-0.5.5.tgz", + "integrity": "sha512-uDjmnPyp1q4Sgzf3w+J/Fc6UqcCEj0x4Wjp7OqK5dGhNeDgpyrAmnS6ey8QWrX3SWDon2DMKf9sBa5X9+CVyMA==", + "dev": true, + "license": "MIT" + }, "node_modules/hyperdyperid": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", @@ -3960,16 +3049,6 @@ "node": ">=0.10.0" } }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -3987,48 +3066,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/import-local": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", - "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -4036,20 +3073,10 @@ "dev": true, "license": "ISC" }, - "node_modules/interpret": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", - "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/ipaddr.js": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", - "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.5.0.tgz", + "integrity": "sha512-aq+t5NAc+cS6rZQQVWC2x98CPqGtKKTMDd4Gaodv0wShnItdKg/51djkGJ1hqH+Oy0ivDftCbSLCQob8zso01w==", "dev": true, "license": "MIT", "engines": { @@ -4063,51 +3090,6 @@ "dev": true, "license": "MIT" }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-builtin-module": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", - "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", - "dev": true, - "license": "MIT", - "dependencies": { - "builtin-modules": "^3.3.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-docker": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", @@ -4134,16 +3116,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -4157,6 +3129,19 @@ "node": ">=0.10.0" } }, + "node_modules/is-in-ssh": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz", + "integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-inside-container": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", @@ -4177,9 +3162,9 @@ } }, "node_modules/is-network-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.1.0.tgz", - "integrity": "sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.2.tgz", + "integrity": "sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA==", "dev": true, "license": "MIT", "engines": { @@ -4199,30 +3184,30 @@ "node": ">=0.12.0" } }, - "node_modules/is-plain-obj": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", - "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "node_modules/is-path-inside": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", + "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", "dev": true, "license": "MIT", "engines": { - "node": ">=10" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", "dev": true, "license": "MIT", - "dependencies": { - "isobject": "^3.0.1" - }, "engines": { - "node": ">=0.10.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/is-potential-custom-element-name": { @@ -4230,6 +3215,14 @@ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true, "license": "MIT" }, "node_modules/is-unicode-supported": { @@ -4246,9 +3239,9 @@ } }, "node_modules/is-wsl": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", - "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", "dev": true, "license": "MIT", "dependencies": { @@ -4261,34 +3254,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/jest-worker": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.3.tgz", - "integrity": "sha512-CqRA220YV/6jCo8VWvAt1KKx6eek1VIHMPeLEbpcfSfkEeWyBNppynM/o6q+Wmw+sOhos2ml34wZbSX3G13//g==", + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", "dev": true, "license": "MIT", "dependencies": { @@ -4297,7 +3266,7 @@ "supports-color": "^8.0.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": ">= 10.13.0" } }, "node_modules/jest-worker/node_modules/supports-color": { @@ -4317,66 +3286,67 @@ } }, "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", "dev": true, "license": "MIT" }, "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsdoc-type-pratt-parser": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.0.0.tgz", - "integrity": "sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/jsdom": { - "version": "26.1.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", - "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "cssstyle": "^4.2.1", - "data-urls": "^5.0.0", - "decimal.js": "^10.5.0", - "html-encoding-sniffer": "^4.0.0", - "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.6", + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.16", - "parse5": "^7.2.1", - "rrweb-cssom": "^0.8.0", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", - "tough-cookie": "^5.1.1", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", "w3c-xmlserializer": "^5.0.0", - "webidl-conversions": "^7.0.0", - "whatwg-encoding": "^3.1.1", - "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^14.1.1", - "ws": "^8.18.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", "xml-name-validator": "^5.0.0" }, "engines": { - "node": ">=18" + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" }, "peerDependencies": { "canvas": "^3.0.0" @@ -4387,26 +3357,6 @@ } } }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -4421,30 +3371,10 @@ "dev": true, "license": "MIT" }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -4454,56 +3384,15 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/just-extend": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", - "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", - "dev": true, - "license": "MIT" - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/launch-editor": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.11.1.tgz", - "integrity": "sha512-SEET7oNfgSaB6Ym0jufAdCeo3meJVeCaaDyzRygy0xsp2BFKCprcfHljTq4QkzTLUxEKkFK6OK4811YM2oSrRg==", + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.14.1.tgz", + "integrity": "sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA==", "dev": true, "license": "MIT", "dependencies": { "picocolors": "^1.1.1", - "shell-quote": "^1.8.3" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" + "shell-quote": "^1.8.4" } }, "node_modules/lines-and-columns": { @@ -4513,42 +3402,17 @@ "dev": true, "license": "MIT" }, - "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.11.5" - } - }, - "node_modules/loader-utils": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", - "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - }, - "engines": { - "node": ">=8.9.0" - } - }, "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-8.0.0.tgz", + "integrity": "sha512-XT9ewWAC43tiAV7xDAPflMkG0qOPn2QjHqlgX8FOqmWa/rxnyYDulF9T0F7tRy1u+TVTmK/M//6VIOye+2zDXg==", "dev": true, "license": "MIT", "dependencies": { - "p-locate": "^5.0.0" + "p-locate": "^6.0.0" }, "engines": { - "node": ">=10" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -4561,14 +3425,6 @@ "dev": true, "license": "MIT" }, - "node_modules/lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", - "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", - "dev": true, - "license": "MIT" - }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -4576,47 +3432,29 @@ "dev": true, "license": "MIT" }, - "node_modules/lodash.truncate": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", - "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", - "dev": true, - "license": "MIT" - }, - "node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, + "license": "BlueOak-1.0.0", "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "20 || >=22" } }, - "node_modules/loupe": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", - "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "node_modules/marked": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", + "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", "dev": true, "license": "MIT", - "dependencies": { - "get-func-name": "^2.0.1" + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" } }, - "node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -4627,14 +3465,26 @@ "node": ">= 0.4" } }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0", + "peer": true + }, "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/memfs": { @@ -4651,11 +3501,14 @@ } }, "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", "dev": true, "license": "MIT", + "engines": { + "node": ">=18" + }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } @@ -4667,26 +3520,6 @@ "dev": true, "license": "MIT" }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -4701,23 +3534,23 @@ "node": ">=8.6" } }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", - "bin": { - "mime": "cli.js" - }, "engines": { - "node": ">=4" + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "dev": true, "license": "MIT", "engines": { @@ -4725,29 +3558,26 @@ } }, "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "dev": true, "license": "MIT", "dependencies": { - "mime-db": "1.52.0" + "mime-db": "^1.54.0" }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true, - "license": "ISC" - }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -4757,83 +3587,255 @@ "node": "*" } }, + "node_modules/minimizer-webpack-plugin": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/minimizer-webpack-plugin/-/minimizer-webpack-plugin-5.6.1.tgz", + "integrity": "sha512-DoeAZz8Q1C1znwsUzej1fdoi4jCf7/+Em27ouLqfK/+3m8G+D7yDhUwrc3CNhjSzGUN1kn7Iv4sWmjflQHenpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "html-minifier-terser": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/minimizer-webpack-plugin/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/minimizer-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/minimizer-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/minimizer-webpack-plugin/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/mocha": { - "version": "10.8.2", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", - "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-12.0.1.tgz", + "integrity": "sha512-/ILgtFHV+R7EJLwN0OkrPPOu6wbJyrtuRtTJ5szdFHv2A3mPmkbu3r1H2q6FhaAfywDJCulcQQ2S9qDGfZW6dw==", "dev": true, "license": "MIT", "dependencies": { - "ansi-colors": "^4.1.3", "browser-stdout": "^1.3.1", - "chokidar": "^3.5.3", + "chokidar": "^5.0.0", "debug": "^4.3.5", - "diff": "^5.2.0", - "escape-string-regexp": "^4.0.0", - "find-up": "^5.0.0", - "glob": "^8.1.0", - "he": "^1.2.0", - "js-yaml": "^4.1.0", - "log-symbols": "^4.1.0", - "minimatch": "^5.1.6", + "diff": "^9.0.0", + "find-up": "^8.0.0", + "glob": "^13.0.0", + "is-path-inside": "^4.0.0", + "is-unicode-supported": "^0.1.0", + "js-yaml": "^5.0.0", + "minimatch": "^10.2.2", "ms": "^2.1.3", - "serialize-javascript": "^6.0.2", - "strip-json-comments": "^3.1.1", + "picocolors": "^1.1.1", + "serialize-javascript": "^7.1.1", + "strip-json-comments": "^5.0.3", "supports-color": "^8.1.1", - "workerpool": "^6.5.1", - "yargs": "^16.2.0", - "yargs-parser": "^20.2.9", - "yargs-unparser": "^2.0.0" + "workerpool": "^10.0.0" }, "bin": { - "_mocha": "bin/_mocha", "mocha": "bin/mocha.js" }, "engines": { - "node": ">= 14.0.0" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/mocha/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "node_modules/mocha/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, - "license": "Python-2.0" + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } }, "node_modules/mocha/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/mocha/node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, "node_modules/mocha/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.4.2.tgz", + "integrity": "sha512-m+aqu+LwO1O6sIopafj8HUVl5aawITwZQe/yHpMCKjaWBaA/d07B/QdMb3529REftiU+RMMHL3Vlsw3hON7vWg==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, "bin": { - "js-yaml": "bin/js-yaml.js" + "js-yaml": "bin/js-yaml.mjs" } }, "node_modules/mocha/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^5.0.8" }, "engines": { - "node": ">=10" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/readdirp": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", + "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, "node_modules/mocha/node_modules/supports-color": { @@ -4853,11 +3855,15 @@ } }, "node_modules/monaco-editor": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.20.0.tgz", - "integrity": "sha512-hkvf4EtPJRMQlPC3UbMoRs0vTAFAYdzFQ+gpMb8A+9znae1c43q8Mab9iVsgTcg/4PNiLGGn3SlDIa8uvK1FIQ==", + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.56.0.tgz", + "integrity": "sha512-sXboRm3BeBeLm938eaiyLMe0OxzfXIlZvbv4ir/jVgQy1zDhWjgmny0WoN45fuDKhCCQsYMbBJrv/A6jd8aCUg==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "dompurify": "3.4.8", + "marked": "14.0.0" + } }, "node_modules/ms": { "version": "2.1.3", @@ -4880,20 +3886,6 @@ "multicast-dns": "cli.js" } }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/natural-compare-lite": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", - "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", - "dev": true, - "license": "MIT" - }, "node_modules/negotiator": { "version": "0.6.4", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", @@ -4911,61 +3903,23 @@ "dev": true, "license": "MIT" }, - "node_modules/nise": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/nise/-/nise-4.1.0.tgz", - "integrity": "sha512-eQMEmGN/8arp0xsvGoQ+B1qvSkR73B1nWSCh7nOt5neMCtwcQVYQGdzQMhcNscktTsWB54xnlSQFzOAPJD8nXA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^1.7.0", - "@sinonjs/fake-timers": "^6.0.0", - "@sinonjs/text-encoding": "^0.7.1", - "just-extend": "^4.0.2", - "path-to-regexp": "^1.7.0" - } - }, "node_modules/node-abort-controller": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", - "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", - "dev": true, - "license": "(BSD-3-Clause OR GPL-2.0)", - "engines": { - "node": ">= 6.13.0" - } - }, - "node_modules/node-releases": { - "version": "2.0.21", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz", - "integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", "dev": true, "license": "MIT" }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "node_modules/node-releases": { + "version": "2.0.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=18" } }, - "node_modules/nwsapi": { - "version": "2.2.23", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", - "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", - "dev": true, - "license": "MIT" - }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -4979,13 +3933,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/obuf": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", - "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", - "dev": true, - "license": "MIT" - }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -5020,101 +3967,80 @@ } }, "node_modules/open": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", - "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/open/-/open-11.0.4.tgz", + "integrity": "sha512-++Zlftm0kVLPmzC06t6epuWmcRMDbI4z5P3NNX979WA/k23+NtSOynEGzsVfZwguKw2mi5umVgnBlJQMwRz4Pg==", "dev": true, "license": "MIT", "dependencies": { - "default-browser": "^5.2.1", + "default-browser": "^5.5.1", "define-lazy-prop": "^3.0.0", + "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", - "wsl-utils": "^0.1.0" + "powershell-utils": "^0.2.1", + "wsl-utils": "^1.0.0" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", "dev": true, "license": "MIT", "dependencies": { - "yocto-queue": "^0.1.0" + "yocto-queue": "^1.0.0" }, "engines": { - "node": ">=10" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", "dev": true, "license": "MIT", "dependencies": { - "p-limit": "^3.0.2" + "p-limit": "^4.0.0" }, "engines": { - "node": ">=10" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/p-retry": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz", - "integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-8.0.1.tgz", + "integrity": "sha512-NAigyu8hfvJe7MsS18mnc4is0MZtau3QMdBklaJKK23oBQb6occO3UPM3vHmTckW8KhfyjMZaUOqdTFEYliHgg==", "dev": true, "license": "MIT", "dependencies": { - "@types/retry": "0.12.2", - "is-network-error": "^1.0.0", - "retry": "^0.13.1" + "is-network-error": "^1.3.0" }, "engines": { - "node": ">=16.17" + "node": ">=22" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } + "license": "BlueOak-1.0.0" }, "node_modules/parent-module": { "version": "1.0.1", @@ -5148,74 +4074,88 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/parse5": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", - "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "node_modules/parse-json/node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dev": true, "license": "MIT", "dependencies": { - "entities": "^6.0.0" + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" + "engines": { + "node": ">=6.9.0" } }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "node_modules/parse-json/node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.8" + "node": ">=6.9.0" } }, - "node_modules/path-exists": { + "node_modules/parse-json/node_modules/js-tokens": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } + "license": "MIT" }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", "dev": true, "license": "MIT", - "engines": { - "node": ">=0.10.0" + "peer": true, + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">= 0.8" } }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "dev": true, - "license": "MIT" + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, "node_modules/path-to-regexp": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", - "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", "dev": true, "license": "MIT", - "dependencies": { - "isarray": "0.0.1" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/path-type": { @@ -5228,16 +4168,6 @@ "node": ">=8" } }, - "node_modules/pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5246,85 +4176,47 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "node_modules/pkijs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.4.0.tgz", + "integrity": "sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", "dependencies": { - "p-locate": "^4.1.0" + "@noble/hashes": "1.4.0", + "asn1js": "^3.0.6", + "bytestreamjs": "^2.0.1", + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.3", + "tslib": "^2.8.1" }, "engines": { - "node": ">=8" + "node": ">=16.0.0" } }, - "node_modules/pkg-dir/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "node_modules/pkijs/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", "dev": true, "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, "engines": { - "node": ">=6" + "node": ">= 16" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-dir/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" + "url": "https://paulmillr.com/funding/" } }, "node_modules/playwright": { @@ -5374,37 +4266,23 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "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==", - "dev": true, - "license": "MIT" - }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "node_modules/powershell-utils": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.2.1.tgz", + "integrity": "sha512-C+y9x90UElAddDZmV4qOx9W53B61PO7cIqWz2dQsWlwswuq4mr8NEwytdGKboYbQlGZ3awrkTeNvcZiZNHnQ8A==", "dev": true, "license": "MIT", "engines": { - "node": ">=0.4.0" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz", + "integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5413,6 +4291,10 @@ }, "engines": { "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/proxy-addr/node_modules/ipaddr.js": { @@ -5435,215 +4317,119 @@ "node": ">=6" } }, - "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "node_modules/pvtsutils": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", "dev": true, "license": "MIT", "dependencies": { - "safe-buffer": "^5.1.0" + "tslib": "^2.8.1" } }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "node_modules/pvutils": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.2.0.tgz", + "integrity": "sha512-BbubeCEyTuQjVMakvJQ/Sxbc93F2pwmbsxONT/ZRrwU7Ua38d8unYTwXpTVLAKJ4BDuH9IGztCjQcd/N/39Dvg==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=16.0.0" } }, - "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "node_modules/qs": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", + "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" }, "engines": { - "node": ">= 0.8" - } - }, - "node_modules/raw-body/node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" + "node": ">=0.6" }, - "engines": { - "node": ">=0.10.0" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", "dev": true, "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, "engines": { - "node": ">= 6" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" + "node": ">= 0.6" }, - "engines": { - "node": ">=8.10.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/rechoir": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", - "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", "dev": true, "license": "MIT", "dependencies": { - "resolve": "^1.20.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, - "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">= 0.10" } }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", "dev": true, "license": "MIT", "dependencies": { - "is-core-module": "^2.16.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=0.10.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "node_modules/readdirp": { + "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", - "dependencies": { - "resolve-from": "^5.0.0" - }, "engines": { - "node": ">=8" + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, - "node_modules/resolve-cwd/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, "node_modules/resolve-from": { @@ -5656,73 +4442,43 @@ "node": ">=4" } }, - "node_modules/retry": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.3.tgz", + "integrity": "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "glob": "^7.1.3" + "glob": "^13.0.3", + "package-json-from-dist": "^1.0.1" }, "bin": { - "rimraf": "bin.js" + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/rimraf/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" }, "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">= 18" } }, - "node_modules/rrweb-cssom": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", - "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", - "dev": true, - "license": "MIT" - }, "node_modules/run-applescript": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", @@ -5736,30 +4492,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -5794,6 +4526,7 @@ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "xmlchars": "^2.2.0" }, @@ -5802,16 +4535,15 @@ } }, "node_modules/schema-utils": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", - "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", "dev": true, "license": "MIT", "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" }, "engines": { "node": ">= 10.13.0" @@ -5821,68 +4553,24 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/schema-utils/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/schema-utils/node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/schema-utils/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, - "node_modules/select-hose": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", - "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", - "dev": true, - "license": "MIT" - }, "node_modules/selfsigned": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", - "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-5.5.0.tgz", + "integrity": "sha512-ftnu3TW4+3eBfLRFnDEkzGxSF/10BJBkaLJuBHZX0kiPS7bRdlpZGu6YGt4KngMkdTwJE6MbjavFpqHvqVt+Ew==", "dev": true, "license": "MIT", "dependencies": { - "@types/node-forge": "^1.3.0", - "node-forge": "^1" + "@peculiar/x509": "^1.14.2", + "pkijs": "^3.3.3" }, "engines": { - "node": ">=10" + "node": ">=18" } }, "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "dev": true, "license": "ISC", "bin": { @@ -5893,84 +4581,77 @@ } }, "node_modules/send": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", "dev": true, "license": "MIT", "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" }, "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, - "node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.1.1.tgz", + "integrity": "sha512-k3CMsaIvvdSwm8oLB4MXSl0wH2/cwlH7xGcnRd2DaeRmBkbzYmyT8j0tsX60DwD1eRwHTpNpH8ljKu9oUT1MeQ==", "dev": true, "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" + "engines": { + "node": ">=20.0.0" } }, "node_modules/serve-index": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", - "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.2.tgz", + "integrity": "sha512-KDj11HScOaLmrPxl70KYNW1PksP4Nb/CLL2yvC+Qd2kHMPEEpfc4Re2e4FOay+bC/+XQl/7zAcWON3JVo5v3KQ==", "dev": true, "license": "MIT", "dependencies": { - "accepts": "~1.3.4", + "accepts": "~1.3.8", "batch": "0.6.1", "debug": "2.6.9", "escape-html": "~1.0.3", - "http-errors": "~1.6.2", - "mime-types": "~2.1.17", - "parseurl": "~1.3.2" + "http-errors": "~1.8.0", + "mime-types": "~2.1.35", + "parseurl": "~1.3.3" }, "engines": { "node": ">= 0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-index/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" } }, "node_modules/serve-index/node_modules/debug": { @@ -5994,27 +4675,44 @@ } }, "node_modules/serve-index/node_modules/http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", "dev": true, "license": "MIT", "dependencies": { "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" }, "engines": { "node": ">= 0.6" } }, - "node_modules/serve-index/node_modules/inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "node_modules/serve-index/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, - "license": "ISC" + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } }, "node_modules/serve-index/node_modules/ms": { "version": "2.0.0", @@ -6023,12 +4721,15 @@ "dev": true, "license": "MIT" }, - "node_modules/serve-index/node_modules/setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "node_modules/serve-index/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", "dev": true, - "license": "ISC" + "license": "MIT", + "engines": { + "node": ">= 0.6" + } }, "node_modules/serve-index/node_modules/statuses": { "version": "1.5.0", @@ -6041,68 +4742,36 @@ } }, "node_modules/serve-static": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", "dev": true, "license": "MIT", "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.19.0" + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" }, "engines": { - "node": ">= 0.8.0" + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true, - "license": "ISC" - }, - "node_modules/shallow-clone": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", - "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" }, "node_modules/shell-quote": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", - "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.10.0.tgz", + "integrity": "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==", "dev": true, "license": "MIT", "engines": { @@ -6113,15 +4782,15 @@ } }, "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" }, @@ -6133,14 +4802,14 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -6189,79 +4858,36 @@ } }, "node_modules/sinon": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.2.4.tgz", - "integrity": "sha512-zljcULZQsJxVra28qIAL6ow1Z9tpattkCTEJR4RBP3TGc00FcttsP5pK284Nas5WjMZU5Yzy3kAIp3B3KRf5Yg==", - "deprecated": "16.1.1", + "version": "22.1.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-22.1.0.tgz", + "integrity": "sha512-n1ajF2rBWMTtEwbKcw4UdFg4nCnDdq/U6RDoxtOd7oapOlRoJ5ynwFx60owROyhDpA9QhMZi0pCO/xtmwFjG7w==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "@sinonjs/commons": "^1.8.1", - "@sinonjs/fake-timers": "^6.0.1", - "@sinonjs/samsam": "^5.3.1", - "diff": "^4.0.2", - "nise": "^4.0.4", - "supports-color": "^7.1.0" + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^15.4.0", + "@sinonjs/samsam": "^10.0.2", + "diff": "^9.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/sinon" } }, - "node_modules/sinon/node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", "dev": true, "license": "BSD-3-Clause", "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/slice-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/sockjs": { - "version": "0.3.24", - "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", - "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "faye-websocket": "^0.11.3", - "uuid": "^8.3.2", - "websocket-driver": "^0.7.4" + "node": ">= 12" } }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -6269,56 +4895,26 @@ } }, "node_modules/source-map-loader": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-1.1.3.tgz", - "integrity": "sha512-6YHeF+XzDOrT/ycFJNI53cgEsp/tHTMl37hi7uVyqFAlTXW109JazaQCkbc+jjoL2637qkH1amLi+JzrIpt5lA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-5.0.0.tgz", + "integrity": "sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA==", "dev": true, "license": "MIT", "dependencies": { - "abab": "^2.0.5", - "iconv-lite": "^0.6.2", - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0", - "source-map": "^0.6.1", - "whatwg-mimetype": "^2.3.0" + "iconv-lite": "^0.6.3", + "source-map-js": "^1.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" - } - }, - "node_modules/source-map-loader/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "webpack": "^5.72.1" } }, - "node_modules/source-map-loader/node_modules/whatwg-mimetype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", - "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", - "dev": true, - "license": "MIT" - }, "node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", @@ -6330,126 +4926,34 @@ "source-map": "^0.6.0" } }, - "node_modules/spdx-exceptions": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", - "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", - "dev": true, - "license": "CC-BY-3.0" - }, - "node_modules/spdx-expression-parse": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", - "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-license-ids": { - "version": "3.0.22", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz", - "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/spdy": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", - "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.0", - "handle-thing": "^2.0.0", - "http-deceiver": "^1.2.7", - "select-hose": "^2.0.0", - "spdy-transport": "^3.0.0" - }, + "license": "BSD-3-Clause", "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/spdy-transport": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", - "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.0", - "detect-node": "^2.0.4", - "hpack.js": "^2.1.6", - "obuf": "^1.1.2", - "readable-stream": "^3.0.6", - "wbuf": "^1.7.3" + "node": ">=0.10.0" } }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true, - "license": "BSD-3-Clause" - }, "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" } }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", + "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=14.16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -6468,71 +4972,18 @@ "node": ">=8" } }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true, - "license": "MIT" - }, - "node_modules/table": { - "version": "6.9.0", - "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", - "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "ajv": "^8.0.1", - "lodash.truncate": "^4.4.2", - "slice-ansi": "^4.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/table/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/table/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" + "peer": true }, "node_modules/tapable": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz", - "integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", "dev": true, "license": "MIT", "engines": { @@ -6544,9 +4995,9 @@ } }, "node_modules/terser": { - "version": "5.44.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz", - "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==", + "version": "5.49.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.49.0.tgz", + "integrity": "sha512-SNiDnXyHSrxVcIOtVbULzcTmniUiwcV7Nwdyj1twVubeTmbjoa8p69KKDpfkdoOavuM4/GRm1+ykI8qqnavHoA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -6562,118 +5013,68 @@ "node": ">=10" } }, - "node_modules/terser-webpack-plugin": { - "version": "5.3.14", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", - "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", + "node_modules/thingies": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/thingies/-/thingies-2.6.1.tgz", + "integrity": "sha512-cV/CMGTK3M4MlnJ/0At6ismOw/A0EEniDNScajjz/Br3c1sqE72YD01rGpPTKwd27wAxI5Pr+6+0w8yofzFRYw==", "dev": true, "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "jest-worker": "^27.4.5", - "schema-utils": "^4.3.0", - "serialize-javascript": "^6.0.2", - "terser": "^5.31.1" - }, "engines": { - "node": ">= 10.13.0" + "node": ">=10.18" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "type": "github", + "url": "https://github.com/sponsors/streamich" }, "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } + "tslib": "^2" } }, - "node_modules/terser-webpack-plugin/node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } + "license": "MIT" }, - "node_modules/terser-webpack-plugin/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { - "has-flag": "^4.0.0" + "fdir": "^6.5.0", + "picomatch": "^4.0.4" }, "engines": { - "node": ">=10" + "node": ">=12.0.0" }, "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/terser/node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" + "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true, - "license": "MIT" - }, - "node_modules/thunky": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", - "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", - "dev": true, - "license": "MIT" - }, "node_modules/tldts": { - "version": "6.1.86", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", - "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "version": "7.4.10", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.10.tgz", + "integrity": "sha512-GgouD1B+sWwvkaEq8vXC15DjQitxbvs12oIXELpconwm+Tg3zfcEv4jgzq3vtKverDXsg3VI8aRgNL2Nra0Iog==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "tldts-core": "^6.1.86" + "tldts-core": "^7.4.10" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "6.1.86", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", - "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "version": "7.4.10", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.10.tgz", + "integrity": "sha512-KnQjp53ZekKgm/r3l+u8kJGGzYgrWdP8+Mql7a4vijh2WE0IrZWspQj/TpTxDho/YxO+AnOZnIjQcCD+q6iJsw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/to-regex-range": { "version": "5.0.1", @@ -6699,139 +5100,149 @@ } }, "node_modules/tough-cookie": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", - "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", + "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", "dev": true, "license": "BSD-3-Clause", + "peer": true, "dependencies": { - "tldts": "^6.1.32" + "tldts": "^7.0.5" }, "engines": { "node": ">=16" } }, "node_modules/tr46": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", - "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "punycode": "^2.3.1" }, "engines": { - "node": ">=18" + "node": ">=20" + } + }, + "node_modules/tree-dump": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.1.0.tgz", + "integrity": "sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, "node_modules/ts-loader": { - "version": "9.5.4", - "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.4.tgz", - "integrity": "sha512-nCz0rEwunlTZiy6rXFByQU1kVVpCIgUpc/psFiKVrUwrizdnIbRFu8w7bxhUF0X613DYwT4XzrZHpVyMe758hQ==", + "version": "9.6.2", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.6.2.tgz", + "integrity": "sha512-R4iuczmtgxvtuI556s+hTZ6/7Ee03VCAk/l/M8LY1OAsUgB7YydsCxkgq9D9pKRaD7GJqUi2u8fp9zZP/ufjKA==", "dev": true, "license": "MIT", "dependencies": { "chalk": "^4.1.0", - "enhanced-resolve": "^5.0.0", - "micromatch": "^4.0.0", - "semver": "^7.3.4", + "picomatch": "^4.0.0", "source-map": "^0.7.4" }, "engines": { "node": ">=12.0.0" }, "peerDependencies": { + "loader-utils": "*", "typescript": "*", - "webpack": "^5.0.0" - } - }, - "node_modules/ts-loader/node_modules/source-map": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", - "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">= 12" + "webpack": "^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "loader-utils": { + "optional": true + } } }, "node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, "license": "0BSD" }, - "node_modules/tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "node_modules/tsyringe": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz", + "integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==", "dev": true, "license": "MIT", "dependencies": { - "tslib": "^1.8.1" + "tslib": "^1.9.3" }, "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + "node": ">= 6.0.0" } }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "node_modules/tsyringe/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } + "license": "0BSD" }, "node_modules/type-detect": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", - "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", "dev": true, "license": "MIT", "engines": { "node": ">=4" } }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", "dev": true, - "license": "(MIT OR CC0-1.0)", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, "engines": { - "node": ">=10" + "node": ">= 18" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "node_modules/type-is/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", "dev": true, "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/typescript": { - "version": "5.9.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", - "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", "peer": true, @@ -6843,6 +5254,17 @@ "node": ">=14.17" } }, + "node_modules/undici": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", + "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", @@ -6850,6 +5272,19 @@ "dev": true, "license": "MIT" }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -6871,9 +5306,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, "funding": [ { @@ -6911,40 +5346,6 @@ "punycode": "^2.1.0" } }, - "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==", - "dev": true, - "license": "MIT" - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/v8-compile-cache": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz", - "integrity": "sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==", - "dev": true, - "license": "MIT" - }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -6961,6 +5362,7 @@ "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "xml-name-validator": "^5.0.0" }, @@ -6969,71 +5371,56 @@ } }, "node_modules/watchpack": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", - "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz", + "integrity": "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==", "dev": true, "license": "MIT", "dependencies": { - "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" }, "engines": { "node": ">=10.13.0" } }, - "node_modules/wbuf": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", - "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimalistic-assert": "^1.0.0" - } - }, "node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "engines": { - "node": ">=12" + "node": ">=20" } }, "node_modules/webpack": { - "version": "5.101.3", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.3.tgz", - "integrity": "sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A==", + "version": "5.109.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.109.2.tgz", + "integrity": "sha512-U9/cvLzxObKNEZ9+TtdqrHM5/9z3lgl2c+c4BzbqGxFQvQvBAq87yql5A8pQ+rrMbS496MZJeF5enVBndIy2hw==", "dev": true, "license": "MIT", "dependencies": { - "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", - "acorn": "^8.15.0", - "acorn-import-phases": "^1.0.3", - "browserslist": "^4.24.0", + "acorn": "^8.16.0", + "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.3", - "es-module-lexer": "^1.2.1", + "enhanced-resolve": "^5.24.4", + "es-module-lexer": "^2.1.0", "eslint-scope": "5.1.1", "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", + "mime-db": "^1.54.0", + "minimizer-webpack-plugin": "^5.6.1", "neo-async": "^2.6.2", - "schema-utils": "^4.3.2", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.11", - "watchpack": "^2.4.1", - "webpack-sources": "^3.3.3" + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "watchpack": "^2.5.2", + "webpack-sources": "^3.5.1" }, "bin": { "webpack": "bin/webpack.js" @@ -7051,85 +5438,28 @@ } } }, - "node_modules/webpack-cli": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", - "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@discoveryjs/json-ext": "^0.5.0", - "@webpack-cli/configtest": "^2.1.1", - "@webpack-cli/info": "^2.0.2", - "@webpack-cli/serve": "^2.0.5", - "colorette": "^2.0.14", - "commander": "^10.0.1", - "cross-spawn": "^7.0.3", - "envinfo": "^7.7.3", - "fastest-levenshtein": "^1.0.12", - "import-local": "^3.0.2", - "interpret": "^3.1.1", - "rechoir": "^0.8.0", - "webpack-merge": "^5.7.3" - }, - "bin": { - "webpack-cli": "bin/cli.js" - }, - "engines": { - "node": ">=14.15.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "5.x.x" - }, - "peerDependenciesMeta": { - "@webpack-cli/generators": { - "optional": true - }, - "webpack-bundle-analyzer": { - "optional": true - }, - "webpack-dev-server": { - "optional": true - } - } - }, - "node_modules/webpack-cli/node_modules/commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - } - }, "node_modules/webpack-dev-middleware": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.3.tgz", - "integrity": "sha512-5kA/PzpZzDz5mNOkcNLmU1UdjGeSSxd7rt1akWpI70jMNHLASiBPRaQZn0hgyhvhawfIwSnnLfDABIxL3ueyFg==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-8.3.0.tgz", + "integrity": "sha512-jFNNMB29ugmkEUFW9cpWNqcWFQNwuMTMVoRpKVAiz/5nh3gA4x9x6DRPhNHGc/SLre7pEP9yrAPDAmCvdCblKg==", "dev": true, "license": "MIT", "dependencies": { - "colorette": "^2.0.10", - "memfs": "^4.6.0", - "mime-types": "^3.0.1", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "schema-utils": "^4.0.0" + "ansi-html-community": "^0.0.8", + "memfs": "^4.68.2", + "mime-types": "^3.0.2", + "range-parser": "^1.3.0", + "schema-utils": "^4.3.3" }, "engines": { - "node": ">= 18.12.0" + "node": ">= 20.9.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "webpack": "^5.0.0" + "webpack": "^5.101.0" }, "peerDependenciesMeta": { "webpack": { @@ -7137,276 +5467,153 @@ } } }, - "node_modules/webpack-dev-middleware/node_modules/@jsonjoy.com/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/webpack-dev-middleware/node_modules/@jsonjoy.com/buffers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.0.0.tgz", - "integrity": "sha512-NDigYR3PHqCnQLXYyoLbnEdzMMvzeiCWo1KOut7Q0CoIqg9tUAPKJ1iq/2nFhc5kZtexzutNY0LFjdwWL3Dw3Q==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/webpack-dev-middleware/node_modules/@jsonjoy.com/codegen": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz", - "integrity": "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/webpack-dev-middleware/node_modules/@jsonjoy.com/json-pack": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.11.0.tgz", - "integrity": "sha512-nLqSTAYwpk+5ZQIoVp7pfd/oSKNWlEdvTq2LzVA4r2wtWZg6v+5u0VgBOaDJuUfNOuw/4Ysq6glN5QKSrOCgrA==", + "node_modules/webpack-dev-middleware/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@jsonjoy.com/base64": "^1.1.2", - "@jsonjoy.com/buffers": "^1.0.0", - "@jsonjoy.com/codegen": "^1.0.0", - "@jsonjoy.com/json-pointer": "^1.0.1", - "@jsonjoy.com/util": "^1.9.0", - "hyperdyperid": "^1.2.0", - "thingies": "^2.5.0" - }, - "engines": { - "node": ">=10.0" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" }, "funding": { "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/webpack-dev-middleware/node_modules/@jsonjoy.com/json-pointer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz", - "integrity": "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==", + "node_modules/webpack-dev-middleware/node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@jsonjoy.com/codegen": "^1.0.0", - "@jsonjoy.com/util": "^1.9.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" + "ajv": "^8.0.0" }, "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/webpack-dev-middleware/node_modules/@jsonjoy.com/util": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.9.0.tgz", - "integrity": "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/buffers": "^1.0.0", - "@jsonjoy.com/codegen": "^1.0.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" + "ajv": "^8.0.0" }, - "peerDependencies": { - "tslib": "2" + "peerDependenciesMeta": { + "ajv": { + "optional": true + } } }, - "node_modules/webpack-dev-middleware/node_modules/glob-to-regex.js": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.0.1.tgz", - "integrity": "sha512-CG/iEvgQqfzoVsMUbxSJcwbG2JwyZ3naEqPkeltwl0BSS8Bp83k3xlGms+0QdWFUAwV+uvo80wNswKF6FWEkKg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" + "node_modules/webpack-dev-middleware/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" }, "peerDependencies": { - "tslib": "2" + "ajv": "^8.8.2" } }, + "node_modules/webpack-dev-middleware/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, "node_modules/webpack-dev-middleware/node_modules/memfs": { - "version": "4.39.0", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.39.0.tgz", - "integrity": "sha512-tFRr2IkSXl2B6IAJsxjHIMTOsfLt9W+8+t2uNxCeQcz4tFqgQR8DYk8hlLH2HsucTctLuoHq3U0G08atyBE3yw==", + "version": "4.78.0", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.78.0.tgz", + "integrity": "sha512-gX14RGPP+fyNodL8g9jc5DfKtWNCronEMItOKDyZatTI3CPpMZDPtm7rAbftDqQAFovP4nzR0RO80sESSEnI4g==", "dev": true, "license": "Apache-2.0", "dependencies": { + "@jsonjoy.com/fs-core": "4.78.0", + "@jsonjoy.com/fs-fsa": "4.78.0", + "@jsonjoy.com/fs-node": "4.78.0", + "@jsonjoy.com/fs-node-builtins": "4.78.0", + "@jsonjoy.com/fs-node-to-fsa": "4.78.0", + "@jsonjoy.com/fs-node-utils": "4.78.0", + "@jsonjoy.com/fs-print": "4.78.0", + "@jsonjoy.com/fs-snapshot": "4.78.0", "@jsonjoy.com/json-pack": "^1.11.0", "@jsonjoy.com/util": "^1.9.0", - "glob-to-regex.js": "^1.0.1", + "glob-to-regex.js": "^1.3.1", "thingies": "^2.5.0", "tree-dump": "^1.0.3", "tslib": "^2.0.0" }, - "engines": { - "node": ">= 4.0.0" - }, "funding": { "type": "github", "url": "https://github.com/sponsors/streamich" } }, - "node_modules/webpack-dev-middleware/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/webpack-dev-middleware/node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "node_modules/webpack-dev-middleware/node_modules/schema-utils": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.5.0.tgz", + "integrity": "sha512-zJlMCZ0cAR5p/Y4oVpRoqioDMJcGxaXRrQ/4rP4WyR84vc5z/DolXdbvXeDpTwbtocDFr2rhPqHPErDCUtz2kA==", "dev": true, "license": "MIT", "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/webpack-dev-middleware/node_modules/thingies": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/thingies/-/thingies-2.5.0.tgz", - "integrity": "sha512-s+2Bwztg6PhWUD7XMfeYm5qliDdSiZm7M7n8KjTkIsm3l/2lgVRc2/Gx/v+ZX8lT4FMA+i8aQvhcWylldc+ZNw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" + "@types/json-schema": "^7.0.15", + "ajv": "^8.20.0", + "ajv-formats": "^3.0.1", + "ajv-keywords": "^5.1.0" }, - "peerDependencies": { - "tslib": "^2" - } - }, - "node_modules/webpack-dev-middleware/node_modules/tree-dump": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.1.0.tgz", - "integrity": "sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==", - "dev": true, - "license": "Apache-2.0", "engines": { - "node": ">=10.0" + "node": ">= 10.13.0" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, - "node_modules/webpack-dev-middleware/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, "node_modules/webpack-dev-server": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.2.tgz", - "integrity": "sha512-QcQ72gh8a+7JO63TAx/6XZf/CWhgMzu5m0QirvPfGvptOusAxG12w2+aua1Jkjr7hzaWDnJ2n6JFeexMHI+Zjg==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-6.0.0.tgz", + "integrity": "sha512-q9SD4ItOGhZLeU6EGT10caDZdHjF50Pz1DtkRZZOPsfluMXOkacWKKOtSBSLVkPqKiF67eFUC0rI88U/tSFPEw==", "dev": true, "license": "MIT", "dependencies": { "@types/bonjour": "^3.5.13", "@types/connect-history-api-fallback": "^1.5.4", - "@types/express": "^4.17.21", - "@types/express-serve-static-core": "^4.17.21", + "@types/express": "^5.0.6", + "@types/express-serve-static-core": "^5.1.1", "@types/serve-index": "^1.9.4", - "@types/serve-static": "^1.15.5", - "@types/sockjs": "^0.3.36", - "@types/ws": "^8.5.10", + "@types/serve-static": "^2.2.0", + "@types/ws": "^8.18.1", "ansi-html-community": "^0.0.8", - "bonjour-service": "^1.2.1", - "chokidar": "^3.6.0", - "colorette": "^2.0.10", - "compression": "^1.7.4", + "bonjour-service": "^1.3.0", + "chokidar": "^5.0.0", + "compression": "^1.8.1", "connect-history-api-fallback": "^2.0.0", - "express": "^4.21.2", - "graceful-fs": "^4.2.6", - "http-proxy-middleware": "^2.0.9", - "ipaddr.js": "^2.1.0", - "launch-editor": "^2.6.1", - "open": "^10.0.3", - "p-retry": "^6.2.0", - "schema-utils": "^4.2.0", - "selfsigned": "^2.4.1", - "serve-index": "^1.9.1", - "sockjs": "^0.3.24", - "spdy": "^4.0.2", - "webpack-dev-middleware": "^7.4.2", - "ws": "^8.18.0" + "express": "^5.2.1", + "graceful-fs": "^4.2.11", + "http-proxy-middleware": "^4.1.1", + "ipaddr.js": "^2.3.0", + "launch-editor": "^2.14.1", + "open": "^11.0.0", + "p-retry": "^8.0.0", + "schema-utils": "^4.3.3", + "selfsigned": "^5.5.0", + "serve-index": "^1.9.2", + "tinyglobby": "^0.2.15", + "webpack-dev-middleware": "^8.0.3", + "ws": "^8.20.0" }, "bin": { "webpack-dev-server": "bin/webpack-dev-server.js" }, "engines": { - "node": ">= 18.12.0" + "node": ">= 22.15.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "webpack": "^5.0.0" + "webpack": "^5.101.0" }, "peerDependenciesMeta": { "webpack": { @@ -7417,178 +5624,212 @@ } } }, - "node_modules/webpack-merge": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", - "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "node_modules/webpack-dev-server/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", "dependencies": { - "clone-deep": "^4.0.1", - "flat": "^5.0.2", - "wildcard": "^2.0.0" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/webpack-sources": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", - "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.13.0" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/webpack/node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "node_modules/webpack-dev-server/node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", "dev": true, "license": "MIT", - "bin": { - "acorn": "bin/acorn" + "dependencies": { + "ajv": "^8.0.0" }, - "engines": { - "node": ">=0.4.0" + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } } }, - "node_modules/webpack/node_modules/acorn-import-phases": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", - "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "node_modules/webpack-dev-server/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "dev": true, "license": "MIT", - "engines": { - "node": ">=10.13.0" + "dependencies": { + "fast-deep-equal": "^3.1.3" }, "peerDependencies": { - "acorn": "^8.14.0" + "ajv": "^8.8.2" } }, - "node_modules/websocket-driver": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", - "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "node_modules/webpack-dev-server/node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "http-parser-js": ">=0.5.1", - "safe-buffer": ">=5.1.0", - "websocket-extensions": ">=0.1.1" + "readdirp": "^5.0.0" }, "engines": { - "node": ">=0.8.0" + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/websocket-extensions": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", - "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "node_modules/webpack-dev-server/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/webpack-dev-server/node_modules/readdirp": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", + "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "engines": { - "node": ">=0.8.0" + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, - "node_modules/whatwg-encoding": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", - "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", - "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "node_modules/webpack-dev-server/node_modules/schema-utils": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.5.0.tgz", + "integrity": "sha512-zJlMCZ0cAR5p/Y4oVpRoqioDMJcGxaXRrQ/4rP4WyR84vc5z/DolXdbvXeDpTwbtocDFr2rhPqHPErDCUtz2kA==", "dev": true, "license": "MIT", "dependencies": { - "iconv-lite": "0.6.3" + "@types/json-schema": "^7.0.15", + "ajv": "^8.20.0", + "ajv-formats": "^3.0.1", + "ajv-keywords": "^5.1.0" }, "engines": { - "node": ">=18" + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, - "node_modules/whatwg-mimetype": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", - "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "node_modules/webpack-sources": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.1.tgz", + "integrity": "sha512-jyuiGJdtvY434z5bUZrjz67v76/ePNvFZTp9Mdz29IlH4+GPsgyGjiv0fKI+M7BdkU6ADjulUcKAd3tUK3WlEw==", "dev": true, "license": "MIT", "engines": { - "node": ">=18" + "node": ">=10.13.0" } }, - "node_modules/whatwg-url": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", - "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "node_modules/webpack/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", "dependencies": { - "tr46": "^5.1.0", - "webidl-conversions": "^7.0.0" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" }, - "engines": { - "node": ">=18" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "node_modules/webpack/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" + "fast-deep-equal": "^3.1.3" }, - "engines": { - "node": ">= 8" + "peerDependencies": { + "ajv": "^8.8.2" } }, - "node_modules/wildcard": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", - "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "node_modules/webpack/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, "license": "MIT" }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "node_modules/webpack/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dev": true, "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, "engines": { - "node": ">=0.10.0" + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, - "node_modules/workerpool": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", - "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", "dev": true, - "license": "Apache-2.0" + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20" + } }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, + "node_modules/workerpool": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-10.0.3.tgz", + "integrity": "sha512-6z2Iis68Wqth93/G/wJP9u+R3O+d2XTlgWChGCwuT1qLbBsOYueGRZuJ++v3mtDP5KjYdy+WzvWC+VWETSVXJA==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -7597,9 +5838,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", "dev": true, "license": "MIT", "engines": { @@ -7619,16 +5860,30 @@ } }, "node_modules/wsl-utils": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", - "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-1.0.0.tgz", + "integrity": "sha512-Hl0ZOAs672vg+06kfujwRhoS6/jehvULrlFkuF2dRu6pHgA8U06h3xqNIqNNU1LTXPcedxByAR4GS6pwQK0mgA==", "dev": true, "license": "MIT", "dependencies": { - "is-wsl": "^3.1.0" + "is-wsl": "^3.1.0", + "powershell-utils": "^0.1.0" }, "engines": { - "node": ">=18" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wsl-utils/node_modules/powershell-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz", + "integrity": "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -7640,6 +5895,7 @@ "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", "dev": true, "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=18" } @@ -7649,91 +5905,17 @@ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true, - "license": "MIT" - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 6" - } - }, - "node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs-unparser": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", - "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", - "dev": true, - "license": "MIT", - "dependencies": { - "camelcase": "^6.0.0", - "decamelize": "^4.0.0", - "flat": "^5.0.2", - "is-plain-obj": "^2.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs-unparser/node_modules/is-plain-obj": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", - "dev": true, "license": "MIT", - "engines": { - "node": ">=8" - } + "peer": true }, "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=10" + "node": ">=12.20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" diff --git a/package.json b/package.json index d392106..93b212e 100644 --- a/package.json +++ b/package.json @@ -43,15 +43,15 @@ "dragselect": "^2.7.4" }, "devDependencies": { - "@blockly/dev-scripts": "^4.0.11", - "@blockly/dev-tools": "^9.0.8", - "@blockly/workspace-backpack": "^7.0.10", + "@blockly/dev-scripts": "^13.3.0", + "@blockly/dev-tools": "^13.3.0", + "@blockly/workspace-backpack": "^13.3.0", "@playwright/test": "^1.58.2", "@types/node": "^24.12.2", - "blockly": "^12.5.1" + "blockly": "^13.3.0" }, "peerDependencies": { - "blockly": "^12.5.1" + "blockly": "^13.3.0" }, "publishConfig": {}, "eslintConfig": { From 5b1a11d952346be1ec59baa0561aa3dfb281604e Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Mon, 3 Aug 2026 15:26:30 -0700 Subject: [PATCH 46/67] Blockly 13 omits keyboard nav plugin's clipboard context menu items --- src/multiselect_contextmenu.js | 18 +---- .../multiselect/workspace/block.spec.ts | 72 +++---------------- .../multiselect/workspace/comment.spec.ts | 62 ++-------------- .../actions/select/workspace/block.spec.ts | 72 +++---------------- .../actions/select/workspace/comment.spec.ts | 62 ++-------------- test-e2e/keyboard/multiselect/block.spec.ts | 30 +++----- test-e2e/keyboard/multiselect/comment.spec.ts | 20 ++---- test-e2e/keyboard/select/block.spec.ts | 34 ++++----- test-e2e/keyboard/select/comment.spec.ts | 22 ++---- test/index.js | 2 +- 10 files changed, 67 insertions(+), 327 deletions(-) diff --git a/src/multiselect_contextmenu.js b/src/multiselect_contextmenu.js index 3b992ec..34c3ea3 100644 --- a/src/multiselect_contextmenu.js +++ b/src/multiselect_contextmenu.js @@ -131,6 +131,7 @@ const registerCopy = function(useCopyPasteCrossTab) { scopeType: Blockly.ContextMenuRegistry.ScopeType.BLOCK, id, weight: 0, + associatedKeyboardShortcut: Blockly.ShortcutItems.names.COPY, }; if (Blockly.ContextMenuRegistry.registry.getItem(id) !== null) { Blockly.ContextMenuRegistry.registry.unregister(id); @@ -858,6 +859,7 @@ const registerPaste = function(useCopyPasteCrossTab) { scopeType: Blockly.ContextMenuRegistry.ScopeType.WORKSPACE, id, weight: 0, + associatedKeyboardShortcut: Blockly.ShortcutItems.names.PASTE, }; if (Blockly.ContextMenuRegistry.registry.getItem(id) !== null) { Blockly.ContextMenuRegistry.registry.unregister(id); @@ -1326,6 +1328,7 @@ const registerCommentCopy = function(useCopyPasteCrossTab) { scopeType: Blockly.ContextMenuRegistry.ScopeType.COMMENT, id, weight: 0, + associatedKeyboardShortcut: Blockly.ShortcutItems.names.COPY, }; if (Blockly.ContextMenuRegistry.registry.getItem(id) !== null) { Blockly.ContextMenuRegistry.registry.unregister(id); @@ -1438,24 +1441,9 @@ export const registerOurKeyboardNavigationMenuItems = function(useCopyPasteCross }); }; - addMultiselectCount( - Blockly.ContextMenuRegistry.registry.getItem('blockCopyFromContextMenu'), - (scope) => { - if (scope.block) return blockCopyDisplayText(scope); - if (scope.comment) return commentCopyDisplayText(scope); - }); - addMultiselectCount( - Blockly.ContextMenuRegistry.registry.getItem('blockCutFromContextMenu'), - (scope) => { - if (scope.block) return blockCutDisplayText(scope); - if (scope.comment) return commentCutDisplayText(scope); - }); addMultiselectCount( Blockly.ContextMenuRegistry.registry.getItem('blockDuplicate'), multiselectDisplayText['blockDuplicate']); - addMultiselectCount( - Blockly.ContextMenuRegistry.registry.getItem('blockPasteFromContextMenu'), - () => pasteDisplayText(useCopyPasteCrossTab)); addMultiselectCount( Blockly.ContextMenuRegistry.registry.getItem('commentDuplicate'), multiselectDisplayText['commentDuplicate']); diff --git a/test-e2e/actions/multiselect/workspace/block.spec.ts b/test-e2e/actions/multiselect/workspace/block.spec.ts index 2731802..56d3378 100644 --- a/test-e2e/actions/multiselect/workspace/block.spec.ts +++ b/test-e2e/actions/multiselect/workspace/block.spec.ts @@ -65,29 +65,19 @@ test("open context menu", async ({ page, act }) => { ); await expect(page.getByRole("menu")).toBeVisible(); - const expectedMenuItems: [string, boolean][] = [ - ["Duplicate (3) D", true], - ["Add Comment (4)", true], - ["Collapse Block (3)", true], - ["Disable Block (3)", true], - ["Delete 5 Blocks Delete", true], - ["Help", true], - [`Cut (3) ${cmdOrCtrlLabel("X")}`, true], - [`Copy (3) ${cmdOrCtrlLabel("C")}`, true], - [`Paste ${cmdOrCtrlLabel("V")}`, true], - ["Copy to Backpack (3)", true], + const expectedMenuItems = [ + `Copy (3) ${cmdOrCtrlLabel("C")}`, + "Duplicate (3) D", + "Add Comment (4)", + "Collapse Block (3)", + "Disable Block (3)", + "Delete 5 Blocks Delete", + "Help", + "Copy to Backpack (3)", ]; expect(await page.getByRole("menuitem").allTextContents()).toEqual( - expectedMenuItems.map(([name]) => name), + expectedMenuItems, ); - for (const [name, enabled] of expectedMenuItems) { - const menuItem = page.getByRole("menuitem", { exact: true, name }); - if (enabled) { - await expect(menuItem).toBeEnabled(); - } else { - await expect(menuItem).toBeDisabled(); - } - } expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); expect(await isEphemeralFocusTaken(page)).toBe(true); @@ -295,48 +285,6 @@ test("cut and paste blocks via keyboard", async ({ page, act }) => { expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); }); -test("cut and paste blocks via context menu", async ({ page, act }) => { - await act( - page.mouse.click(...(await getBlock(page, { id: "block1" })).centerTop, { - button: "right", - }), - ); - expect(await getHighlightedBlockIds(page)).toEqual([]); - expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); - expect(await isEphemeralFocusTaken(page)).toBe(true); - await act( - page - .getByRole("menuitem", { - exact: true, - name: `Cut (3) ${cmdOrCtrlLabel("X")}`, - }) - .click(), - ); - expect(await getAllBlockIds(page)).toEqual(["block4"]); - expect(await getHighlightedBlockIds(page)).toEqual(["block4"]); - expect(await getSelectedId(page)).toBe("block4"); - - await act( - page.mouse.click(...(await getEmptySpace(page)), { - button: "right", - }), - ); - await act( - page - .getByRole("menuitem", { - exact: true, - name: `Paste (3) ${cmdOrCtrlLabel("V")}`, - }) - .click(), - ); - expect(await getAllBlockIds(page)).toHaveLength(6); - const highlightedBlockIds = await getHighlightedBlockIds(page); - expect(highlightedBlockIds).toHaveLength(3); - expect(highlightedBlockIds).not.toContain("block4"); - expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); - expect(await isEphemeralFocusTaken(page)).toBe(false); -}); - test("delete blocks via keyboard", async ({ page, act }) => { await act(page.keyboard.press("Delete")); diff --git a/test-e2e/actions/multiselect/workspace/comment.spec.ts b/test-e2e/actions/multiselect/workspace/comment.spec.ts index 316322b..5d15443 100644 --- a/test-e2e/actions/multiselect/workspace/comment.spec.ts +++ b/test-e2e/actions/multiselect/workspace/comment.spec.ts @@ -41,24 +41,14 @@ test("open context menu", async ({ page, act }) => { ); await expect(page.getByRole("menu")).toBeVisible(); - const expectedMenuItems: [string, boolean][] = [ - ["Duplicate Comment (2) D", true], - ["Remove Comment (2)", true], - [`Cut (2) ${cmdOrCtrlLabel("X")}`, true], - [`Copy (2) ${cmdOrCtrlLabel("C")}`, true], - [`Paste ${cmdOrCtrlLabel("V")}`, true], + const expectedMenuItems = [ + `Copy (2) ${cmdOrCtrlLabel("C")}`, + "Duplicate Comment (2) D", + "Remove Comment (2)", ]; expect(await page.getByRole("menuitem").allTextContents()).toEqual( - expectedMenuItems.map(([name]) => name), + expectedMenuItems, ); - for (const [name, enabled] of expectedMenuItems) { - const menuItem = page.getByRole("menuitem", { exact: true, name }); - if (enabled) { - await expect(menuItem).toBeEnabled(); - } else { - await expect(menuItem).toBeDisabled(); - } - } expect(await getHighlightedCommentIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); expect(await isEphemeralFocusTaken(page)).toBe(true); @@ -203,48 +193,6 @@ test("cut and paste comments via keyboard", async ({ page, act }) => { expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); }); -test("cut and paste comments via context menu", async ({ page, act }) => { - await act( - page.mouse.click(...(await getComment(page, "comment1")).centerTop, { - button: "right", - }), - ); - expect(await getHighlightedCommentIds(page)).toEqual([]); - expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); - expect(await isEphemeralFocusTaken(page)).toBe(true); - await act( - page - .getByRole("menuitem", { - exact: true, - name: `Cut (2) ${cmdOrCtrlLabel("X")}`, - }) - .click(), - ); - expect(await getAllCommentIds(page)).toEqual(["comment3"]); - expect(await getHighlightedCommentIds(page)).toEqual([]); - expect(await getSelectedId(page)).toBeNull(); - - await act( - page.mouse.click(...(await getEmptySpace(page)), { - button: "right", - }), - ); - await act( - page - .getByRole("menuitem", { - exact: true, - name: `Paste (2) ${cmdOrCtrlLabel("V")}`, - }) - .click(), - ); - expect(await getAllCommentIds(page)).toHaveLength(3); - const highlightedCommentIds = await getHighlightedCommentIds(page); - expect(highlightedCommentIds).toHaveLength(2); - expect(highlightedCommentIds).not.toContain("comment3"); - expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); - expect(await isEphemeralFocusTaken(page)).toBe(false); -}); - test("delete comments via keyboard", async ({ page, act }) => { await act(page.keyboard.press("Delete")); diff --git a/test-e2e/actions/select/workspace/block.spec.ts b/test-e2e/actions/select/workspace/block.spec.ts index a2cb86c..3dda8f5 100644 --- a/test-e2e/actions/select/workspace/block.spec.ts +++ b/test-e2e/actions/select/workspace/block.spec.ts @@ -37,29 +37,19 @@ test("open context menu", async ({ page, act }) => { ); await expect(page.getByRole("menu")).toBeVisible(); - const expectedMenuItems: [string, boolean][] = [ - ["Duplicate D", true], - ["Add Comment", true], - ["Collapse Block", true], - ["Disable Block", true], - ["Delete Block Delete", true], - ["Help", true], - [`Cut ${cmdOrCtrlLabel("X")}`, true], - [`Copy ${cmdOrCtrlLabel("C")}`, true], - [`Paste ${cmdOrCtrlLabel("V")}`, true], - ["Copy to Backpack", true], + const expectedMenuItems = [ + `Copy ${cmdOrCtrlLabel("C")}`, + "Duplicate D", + "Add Comment", + "Collapse Block", + "Disable Block", + "Delete Block Delete", + "Help", + "Copy to Backpack", ]; expect(await page.getByRole("menuitem").allTextContents()).toEqual( - expectedMenuItems.map(([name]) => name), + expectedMenuItems, ); - for (const [name, enabled] of expectedMenuItems) { - const menuItem = page.getByRole("menuitem", { exact: true, name }); - if (enabled) { - await expect(menuItem).toBeEnabled(); - } else { - await expect(menuItem).toBeDisabled(); - } - } expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe("block1"); expect(await isEphemeralFocusTaken(page)).toBe(true); @@ -178,48 +168,6 @@ test("cut and paste block via keyboard", async ({ page, act }) => { expect(await getSelectedId(page)).toBe(highlightedBlockIds[0]); }); -test("cut and paste block via context menu", async ({ page, act }) => { - await act( - page.mouse.click(...(await getBlock(page, { id: "block1" })).centerTop, { - button: "right", - }), - ); - expect(await getHighlightedBlockIds(page)).toEqual([]); - expect(await getSelectedId(page)).toBe("block1"); - expect(await isEphemeralFocusTaken(page)).toBe(true); - await act( - page - .getByRole("menuitem", { - exact: true, - name: `Cut ${cmdOrCtrlLabel("X")}`, - }) - .click(), - ); - expect(await getAllBlockIds(page)).toEqual(["block2"]); - expect(await getHighlightedBlockIds(page)).toEqual(["block2"]); - expect(await getSelectedId(page)).toBe("block2"); - - await act( - page.mouse.click(...(await getEmptySpace(page)), { - button: "right", - }), - ); - await act( - page - .getByRole("menuitem", { - exact: true, - name: `Paste ${cmdOrCtrlLabel("V")}`, - }) - .click(), - ); - expect(await getAllBlockIds(page)).toHaveLength(2); - const highlightedBlockIds = await getHighlightedBlockIds(page); - expect(highlightedBlockIds).toHaveLength(1); - expect(highlightedBlockIds).not.toContain("block2"); - expect(await getSelectedId(page)).toBe(highlightedBlockIds[0]); - expect(await isEphemeralFocusTaken(page)).toBe(false); -}); - test("delete block via keyboard", async ({ page, act }) => { await act(page.keyboard.press("Delete")); diff --git a/test-e2e/actions/select/workspace/comment.spec.ts b/test-e2e/actions/select/workspace/comment.spec.ts index 4f9db72..50aead1 100644 --- a/test-e2e/actions/select/workspace/comment.spec.ts +++ b/test-e2e/actions/select/workspace/comment.spec.ts @@ -29,24 +29,14 @@ test("open context menu", async ({ page, act }) => { ); await expect(page.getByRole("menu")).toBeVisible(); - const expectedMenuItems: [string, boolean][] = [ - ["Duplicate Comment D", true], - ["Remove Comment", true], - [`Cut ${cmdOrCtrlLabel("X")}`, true], - [`Copy ${cmdOrCtrlLabel("C")}`, true], - [`Paste ${cmdOrCtrlLabel("V")}`, true], + const expectedMenuItems = [ + `Copy ${cmdOrCtrlLabel("C")}`, + "Duplicate Comment D", + "Remove Comment", ]; expect(await page.getByRole("menuitem").allTextContents()).toEqual( - expectedMenuItems.map(([name]) => name), + expectedMenuItems, ); - for (const [name, enabled] of expectedMenuItems) { - const menuItem = page.getByRole("menuitem", { exact: true, name }); - if (enabled) { - await expect(menuItem).toBeEnabled(); - } else { - await expect(menuItem).toBeDisabled(); - } - } expect(await getHighlightedCommentIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe("comment1"); expect(await isEphemeralFocusTaken(page)).toBe(true); @@ -167,48 +157,6 @@ test("cut and paste comment via keyboard", async ({ page, act }) => { expect(await getSelectedId(page)).toBe(highlightedCommentIds[0]); }); -test("cut and paste comment via context menu", async ({ page, act }) => { - await act( - page.mouse.click(...(await getComment(page, "comment1")).centerTop, { - button: "right", - }), - ); - expect(await getHighlightedCommentIds(page)).toEqual([]); - expect(await getSelectedId(page)).toBe("comment1"); - expect(await isEphemeralFocusTaken(page)).toBe(true); - await act( - page - .getByRole("menuitem", { - exact: true, - name: `Cut ${cmdOrCtrlLabel("X")}`, - }) - .click(), - ); - expect(await getAllCommentIds(page)).toEqual(["comment2"]); - expect(await getHighlightedCommentIds(page)).toEqual([]); - expect(await getSelectedId(page)).toBeNull(); - - await act( - page.mouse.click(...(await getEmptySpace(page)), { - button: "right", - }), - ); - await act( - page - .getByRole("menuitem", { - exact: true, - name: `Paste ${cmdOrCtrlLabel("V")}`, - }) - .click(), - ); - expect(await getAllCommentIds(page)).toHaveLength(2); - const highlightedCommentIds = await getHighlightedCommentIds(page); - expect(highlightedCommentIds).toHaveLength(1); - expect(highlightedCommentIds).not.toContain("comment2"); - expect(await getSelectedId(page)).toBe(highlightedCommentIds[0]); - expect(await isEphemeralFocusTaken(page)).toBe(false); -}); - test("delete comment via keyboard", async ({ page, act }) => { await act(page.keyboard.press("Delete")); diff --git a/test-e2e/keyboard/multiselect/block.spec.ts b/test-e2e/keyboard/multiselect/block.spec.ts index c04bf23..436f685 100644 --- a/test-e2e/keyboard/multiselect/block.spec.ts +++ b/test-e2e/keyboard/multiselect/block.spec.ts @@ -66,29 +66,19 @@ test("open context menu", async ({ page, act }) => { await act(page.keyboard.press(cmdOrCtrl("Enter"))); await expect(page.getByRole("menu")).toBeVisible(); - const expectedMenuItems: [string, boolean][] = [ - ["Duplicate (2) D", true], - ["Add Comment (2)", true], - ["Collapse Block (2)", true], - ["Disable Block (2)", true], - ["Delete 2 Blocks Delete", true], - ["Help", true], - [`Cut (2) ${cmdOrCtrlLabel("X")}`, true], - [`Copy (2) ${cmdOrCtrlLabel("C")}`, true], - [`Paste ${cmdOrCtrlLabel("V")}`, true], - ["Copy to Backpack (2)", true], + const expectedMenuItems = [ + `Copy (2) ${cmdOrCtrlLabel("C")}`, + "Duplicate (2) D", + "Add Comment (2)", + "Collapse Block (2)", + "Disable Block (2)", + "Delete 2 Blocks Delete", + "Help", + "Copy to Backpack (2)", ]; expect(await page.getByRole("menuitem").allTextContents()).toEqual( - expectedMenuItems.map(([name]) => name), + expectedMenuItems, ); - for (const [name, enabled] of expectedMenuItems) { - const menuItem = page.getByRole("menuitem", { exact: true, name }); - if (enabled) { - await expect(menuItem).toBeEnabled(); - } else { - await expect(menuItem).toBeDisabled(); - } - } expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); expect(await isEphemeralFocusTaken(page)).toBe(true); diff --git a/test-e2e/keyboard/multiselect/comment.spec.ts b/test-e2e/keyboard/multiselect/comment.spec.ts index 3fffb3e..84c25e6 100644 --- a/test-e2e/keyboard/multiselect/comment.spec.ts +++ b/test-e2e/keyboard/multiselect/comment.spec.ts @@ -66,24 +66,14 @@ test("open context menu", async ({ page, act }) => { await act(page.keyboard.press(cmdOrCtrl("Enter"))); await expect(page.getByRole("menu")).toBeVisible(); - const expectedMenuItems: [string, boolean][] = [ - ["Duplicate Comment (2) D", true], - ["Remove Comment (2)", true], - [`Cut (2) ${cmdOrCtrlLabel("X")}`, true], - [`Copy (2) ${cmdOrCtrlLabel("C")}`, true], - [`Paste ${cmdOrCtrlLabel("V")}`, true], + const expectedMenuItems = [ + `Copy (2) ${cmdOrCtrlLabel("C")}`, + "Duplicate Comment (2) D", + "Remove Comment (2)", ]; expect(await page.getByRole("menuitem").allTextContents()).toEqual( - expectedMenuItems.map(([name]) => name), + expectedMenuItems, ); - for (const [name, enabled] of expectedMenuItems) { - const menuItem = page.getByRole("menuitem", { exact: true, name }); - if (enabled) { - await expect(menuItem).toBeEnabled(); - } else { - await expect(menuItem).toBeDisabled(); - } - } expect(await getHighlightedCommentIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); expect(await isEphemeralFocusTaken(page)).toBe(true); diff --git a/test-e2e/keyboard/select/block.spec.ts b/test-e2e/keyboard/select/block.spec.ts index e095878..63e7d41 100644 --- a/test-e2e/keyboard/select/block.spec.ts +++ b/test-e2e/keyboard/select/block.spec.ts @@ -159,31 +159,21 @@ test("open context menu", async ({ page, act }) => { await act(page.keyboard.press(cmdOrCtrl("Enter"))); await expect(page.getByRole("menu")).toBeVisible(); - const expectedMenuItems: [string, boolean][] = [ - ["Duplicate D", true], - ["Add Comment", true], - ["Collapse Block", true], - ["Disable Block", true], - ["Delete Block Delete", true], - ["Help", true], - ["Move Block M", true], - ["Edit Block contents Right", true], - [`Cut ${cmdOrCtrlLabel("X")}`, true], - [`Copy ${cmdOrCtrlLabel("C")}`, true], - [`Paste ${cmdOrCtrlLabel("V")}`, true], - ["Copy to Backpack", true], + const expectedMenuItems = [ + `Copy ${cmdOrCtrlLabel("C")}`, + "Duplicate D", + "Add Comment", + "Collapse Block", + "Disable Block", + "Delete Block Delete", + "Help", + "Move Block M", + "Edit Block contents Right", + "Copy to Backpack", ]; expect(await page.getByRole("menuitem").allTextContents()).toEqual( - expectedMenuItems.map(([name]) => name), + expectedMenuItems, ); - for (const [name, enabled] of expectedMenuItems) { - const menuItem = page.getByRole("menuitem", { exact: true, name }); - if (enabled) { - await expect(menuItem).toBeEnabled(); - } else { - await expect(menuItem).toBeDisabled(); - } - } expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe("block2"); expect(await isEphemeralFocusTaken(page)).toBe(true); diff --git a/test-e2e/keyboard/select/comment.spec.ts b/test-e2e/keyboard/select/comment.spec.ts index b0803c5..71be118 100644 --- a/test-e2e/keyboard/select/comment.spec.ts +++ b/test-e2e/keyboard/select/comment.spec.ts @@ -255,25 +255,15 @@ test("open context menu", async ({ page, act }) => { await act(page.keyboard.press(cmdOrCtrl("Enter"))); await expect(page.getByRole("menu")).toBeVisible(); - const expectedMenuItems: [string, boolean][] = [ - ["Duplicate Comment D", true], - ["Remove Comment", true], - ["Move Comment M", true], - [`Cut ${cmdOrCtrlLabel("X")}`, true], - [`Copy ${cmdOrCtrlLabel("C")}`, true], - [`Paste ${cmdOrCtrlLabel("V")}`, true], + const expectedMenuItems = [ + `Copy ${cmdOrCtrlLabel("C")}`, + "Duplicate Comment D", + "Remove Comment", + "Move Comment M", ]; expect(await page.getByRole("menuitem").allTextContents()).toEqual( - expectedMenuItems.map(([name]) => name), + expectedMenuItems, ); - for (const [name, enabled] of expectedMenuItems) { - const menuItem = page.getByRole("menuitem", { exact: true, name }); - if (enabled) { - await expect(menuItem).toBeEnabled(); - } else { - await expect(menuItem).toBeDisabled(); - } - } expect(await getHighlightedCommentIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe("comment2"); expect(await isEphemeralFocusTaken(page)).toBe(true); diff --git a/test/index.js b/test/index.js index 95dd59a..9635772 100644 --- a/test/index.js +++ b/test/index.js @@ -105,7 +105,7 @@ document.addEventListener('DOMContentLoaded', function() { multiSelectKeys: ['Shift'], multiselectCopyPaste: { crossTab: true, - menu: false, + menu: true, }, grid: { spacing: 25, From 05716756e8362f30925ac5b1bdba6c76d09a7c83 Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Tue, 4 Aug 2026 16:32:39 -0700 Subject: [PATCH 47/67] Blockly 13 omits keyboard nav plugin's move/edit context menu items --- src/multiselect_contextmenu.js | 23 ----- test-e2e/keyboard/select/block.spec.ts | 58 +----------- test-e2e/keyboard/select/comment.spec.ts | 113 +---------------------- test-e2e/keyboard/select/stack.spec.ts | 109 +--------------------- 4 files changed, 10 insertions(+), 293 deletions(-) diff --git a/src/multiselect_contextmenu.js b/src/multiselect_contextmenu.js index 34c3ea3..973c819 100644 --- a/src/multiselect_contextmenu.js +++ b/src/multiselect_contextmenu.js @@ -1448,29 +1448,6 @@ export const registerOurKeyboardNavigationMenuItems = function(useCopyPasteCross Blockly.ContextMenuRegistry.registry.getItem('commentDuplicate'), multiselectDisplayText['commentDuplicate']); - const hideForMultiselect = (keyboardNavMenuItem) => { - const originalPrecondition = keyboardNavMenuItem.preconditionFn; - origKeyboardNavigationMenuItems.push(keyboardNavMenuItem); - Blockly.ContextMenuRegistry.registry.unregister(keyboardNavMenuItem.id); - Blockly.ContextMenuRegistry.registry.register({ - ...keyboardNavMenuItem, - preconditionFn: (scope, menuOpenEvent) => { - if (Blockly.getSelected() instanceof MultiselectDraggable) return 'hidden'; - // MOVE_COMMENT is missing the PointerEvent check that MOVE_BLOCK - // has, so it incorrectly appears in right-click menus. - // https://github.com/RaspberryPiFoundation/blockly-keyboard-experimentation/blob/%40blockly/keyboard-navigation%403.0.5/src/actions/move.ts#L187-L194 - if (menuOpenEvent instanceof PointerEvent) return 'hidden'; - return originalPrecondition(scope, menuOpenEvent); - }, - }); - }; - - if (Blockly.ContextMenuRegistry.registry.getItem('move')) { - hideForMultiselect(Blockly.ContextMenuRegistry.registry.getItem('move')); - } - if (Blockly.ContextMenuRegistry.registry.getItem('move_comment')) { - hideForMultiselect(Blockly.ContextMenuRegistry.registry.getItem('move_comment')); - } }; export const registerOrigKeyboardNavigationMenuItems = function() { diff --git a/test-e2e/keyboard/select/block.spec.ts b/test-e2e/keyboard/select/block.spec.ts index 63e7d41..1c8189a 100644 --- a/test-e2e/keyboard/select/block.spec.ts +++ b/test-e2e/keyboard/select/block.spec.ts @@ -59,7 +59,7 @@ test("navigate right", async ({ page, act }) => { }); }); -test("unconstrained move via shortcut", async ({ page, act }) => { +test("unconstrained move", async ({ page, act }) => { const gridSpacing = await getGridSpacing(page); if (gridSpacing === null) throw new Error("Workspace has no grid"); const block1BoundsStart = (await getBlock(page, { id: "block1" })).bounds; @@ -87,7 +87,7 @@ test("unconstrained move via shortcut", async ({ page, act }) => { expect(await getSelectedId(page)).toBe("block2"); }); -test("abort unconstrained move via shortcut", async ({ page, act }) => { +test("abort unconstrained move", async ({ page, act }) => { const block2BoundsStart = (await getBlock(page, { id: "block2" })).bounds; await act(page.keyboard.press("M")); @@ -103,58 +103,6 @@ test("abort unconstrained move via shortcut", async ({ page, act }) => { expect(await getSelectedId(page)).toBe("block2"); }); -test("unconstrained move via context menu", async ({ page, act }) => { - const gridSpacing = await getGridSpacing(page); - if (gridSpacing === null) throw new Error("Workspace has no grid"); - const block1BoundsStart = (await getBlock(page, { id: "block1" })).bounds; - const block2BoundsStart = (await getBlock(page, { id: "block2" })).bounds; - const block3BoundsStart = (await getBlock(page, { id: "block3" })).bounds; - - await act(page.keyboard.press(cmdOrCtrl("Enter"))); - for (let i = 0; i < 6; i++) { - await act(page.keyboard.press("ArrowDown")); - } - await act(page.keyboard.press("Enter")); - await expect(page.locator(".blocklyMoveIndicatorBubble")).toBeVisible(); - await act(page.keyboard.press("Alt+ArrowRight")); - await act(page.keyboard.press("Enter")); - await expect(page.locator(".blocklyMoveIndicatorBubble")).not.toBeVisible(); - - const block1BoundsEnd = (await getBlock(page, { id: "block1" })).bounds; - const block2BoundsEnd = (await getBlock(page, { id: "block2" })).bounds; - const block3BoundsEnd = (await getBlock(page, { id: "block3" })).bounds; - expect(block2BoundsEnd.left).toBeCloseTo( - block2BoundsStart.left + gridSpacing, - ); - expect(block2BoundsEnd.top).toBeCloseTo(block2BoundsStart.top); - expect(block1BoundsEnd.left).toBeCloseTo(block1BoundsStart.left); - expect(block1BoundsEnd.top).toBeCloseTo(block1BoundsStart.top); - expect(block3BoundsEnd.left).toBeCloseTo(block3BoundsStart.left); - expect(block3BoundsEnd.top).toBeCloseTo(block3BoundsStart.top); - expect(await getHighlightedBlockIds(page)).toEqual(["block2"]); - expect(await getSelectedId(page)).toBe("block2"); -}); - -test("abort unconstrained move via context menu", async ({ page, act }) => { - const block2BoundsStart = (await getBlock(page, { id: "block2" })).bounds; - - await act(page.keyboard.press(cmdOrCtrl("Enter"))); - for (let i = 0; i < 6; i++) { - await act(page.keyboard.press("ArrowDown")); - } - await act(page.keyboard.press("Enter")); - await expect(page.locator(".blocklyMoveIndicatorBubble")).toBeVisible(); - await act(page.keyboard.press("Alt+ArrowRight")); - await act(page.keyboard.press("Escape")); - await expect(page.locator(".blocklyMoveIndicatorBubble")).not.toBeVisible(); - - const block2BoundsEnd = (await getBlock(page, { id: "block2" })).bounds; - expect(block2BoundsEnd.left).toBeCloseTo(block2BoundsStart.left); - expect(block2BoundsEnd.top).toBeCloseTo(block2BoundsStart.top); - expect(await getHighlightedBlockIds(page)).toEqual(["block2"]); - expect(await getSelectedId(page)).toBe("block2"); -}); - test("open context menu", async ({ page, act }) => { await act(page.keyboard.press(cmdOrCtrl("Enter"))); @@ -167,8 +115,6 @@ test("open context menu", async ({ page, act }) => { "Disable Block", "Delete Block Delete", "Help", - "Move Block M", - "Edit Block contents Right", "Copy to Backpack", ]; expect(await page.getByRole("menuitem").allTextContents()).toEqual( diff --git a/test-e2e/keyboard/select/comment.spec.ts b/test-e2e/keyboard/select/comment.spec.ts index 71be118..150ea42 100644 --- a/test-e2e/keyboard/select/comment.spec.ts +++ b/test-e2e/keyboard/select/comment.spec.ts @@ -59,7 +59,7 @@ test("navigate right", async ({ page, act }) => { }); }); -test("constrained move via shortcut", async ({ page, act }) => { +test("constrained move", async ({ page, act }) => { const gridSpacing = await getGridSpacing(page); if (gridSpacing === null) throw new Error("Workspace has no grid"); const comment1BoundsStart = (await getComment(page, "comment1")).bounds; @@ -87,7 +87,7 @@ test("constrained move via shortcut", async ({ page, act }) => { expect(await getSelectedId(page)).toBe("comment2"); }); -test("abort constrained move via shortcut", async ({ page, act }) => { +test("abort constrained move", async ({ page, act }) => { const comment2BoundsStart = (await getComment(page, "comment2")).bounds; await act(page.keyboard.press("M")); @@ -103,59 +103,7 @@ test("abort constrained move via shortcut", async ({ page, act }) => { expect(await getSelectedId(page)).toBe("comment2"); }); -test("constrained move via context menu", async ({ page, act }) => { - const gridSpacing = await getGridSpacing(page); - if (gridSpacing === null) throw new Error("Workspace has no grid"); - const comment1BoundsStart = (await getComment(page, "comment1")).bounds; - const comment2BoundsStart = (await getComment(page, "comment2")).bounds; - const comment3BoundsStart = (await getComment(page, "comment3")).bounds; - - await act(page.keyboard.press(cmdOrCtrl("Enter"))); - for (let i = 0; i < 2; i++) { - await act(page.keyboard.press("ArrowDown")); - } - await act(page.keyboard.press("Enter")); - await expect(page.locator(".blocklyMoveIndicatorBubble")).toBeVisible(); - await act(page.keyboard.press("ArrowRight")); - await act(page.keyboard.press("Enter")); - await expect(page.locator(".blocklyMoveIndicatorBubble")).not.toBeVisible(); - - const comment1BoundsEnd = (await getComment(page, "comment1")).bounds; - const comment2BoundsEnd = (await getComment(page, "comment2")).bounds; - const comment3BoundsEnd = (await getComment(page, "comment3")).bounds; - expect(comment2BoundsEnd.left).toBeCloseTo( - comment2BoundsStart.left + gridSpacing, - ); - expect(comment2BoundsEnd.top).toBeCloseTo(comment2BoundsStart.top); - expect(comment1BoundsEnd.left).toBeCloseTo(comment1BoundsStart.left); - expect(comment1BoundsEnd.top).toBeCloseTo(comment1BoundsStart.top); - expect(comment3BoundsEnd.left).toBeCloseTo(comment3BoundsStart.left); - expect(comment3BoundsEnd.top).toBeCloseTo(comment3BoundsStart.top); - expect(await getHighlightedCommentIds(page)).toEqual(["comment2"]); - expect(await getSelectedId(page)).toBe("comment2"); -}); - -test("abort constrained move via context menu", async ({ page, act }) => { - const comment2BoundsStart = (await getComment(page, "comment2")).bounds; - - await act(page.keyboard.press(cmdOrCtrl("Enter"))); - for (let i = 0; i < 2; i++) { - await act(page.keyboard.press("ArrowDown")); - } - await act(page.keyboard.press("Enter")); - await expect(page.locator(".blocklyMoveIndicatorBubble")).toBeVisible(); - await act(page.keyboard.press("ArrowRight")); - await act(page.keyboard.press("Escape")); - await expect(page.locator(".blocklyMoveIndicatorBubble")).not.toBeVisible(); - - const comment2BoundsEnd = (await getComment(page, "comment2")).bounds; - expect(comment2BoundsEnd.top).toBeCloseTo(comment2BoundsStart.top); - expect(comment2BoundsEnd.left).toBeCloseTo(comment2BoundsStart.left); - expect(await getHighlightedCommentIds(page)).toEqual(["comment2"]); - expect(await getSelectedId(page)).toBe("comment2"); -}); - -test("unconstrained move via shortcut", async ({ page, act }) => { +test("unconstrained move", async ({ page, act }) => { const gridSpacing = await getGridSpacing(page); if (gridSpacing === null) throw new Error("Workspace has no grid"); const comment1BoundsStart = (await getComment(page, "comment1")).bounds; @@ -183,7 +131,7 @@ test("unconstrained move via shortcut", async ({ page, act }) => { expect(await getSelectedId(page)).toBe("comment2"); }); -test("abort unconstrained move via shortcut", async ({ page, act }) => { +test("abort unconstrained move", async ({ page, act }) => { const comment2BoundsStart = (await getComment(page, "comment2")).bounds; await act(page.keyboard.press("M")); @@ -199,58 +147,6 @@ test("abort unconstrained move via shortcut", async ({ page, act }) => { expect(await getSelectedId(page)).toBe("comment2"); }); -test("unconstrained move via context menu", async ({ page, act }) => { - const gridSpacing = await getGridSpacing(page); - if (gridSpacing === null) throw new Error("Workspace has no grid"); - const comment1BoundsStart = (await getComment(page, "comment1")).bounds; - const comment2BoundsStart = (await getComment(page, "comment2")).bounds; - const comment3BoundsStart = (await getComment(page, "comment3")).bounds; - - await act(page.keyboard.press(cmdOrCtrl("Enter"))); - for (let i = 0; i < 2; i++) { - await act(page.keyboard.press("ArrowDown")); - } - await act(page.keyboard.press("Enter")); - await expect(page.locator(".blocklyMoveIndicatorBubble")).toBeVisible(); - await act(page.keyboard.press("Alt+ArrowRight")); - await act(page.keyboard.press("Enter")); - await expect(page.locator(".blocklyMoveIndicatorBubble")).not.toBeVisible(); - - const comment1BoundsEnd = (await getComment(page, "comment1")).bounds; - const comment2BoundsEnd = (await getComment(page, "comment2")).bounds; - const comment3BoundsEnd = (await getComment(page, "comment3")).bounds; - expect(comment2BoundsEnd.left).toBeCloseTo( - comment2BoundsStart.left + gridSpacing, - ); - expect(comment2BoundsEnd.top).toBeCloseTo(comment2BoundsStart.top); - expect(comment1BoundsEnd.left).toBeCloseTo(comment1BoundsStart.left); - expect(comment1BoundsEnd.top).toBeCloseTo(comment1BoundsStart.top); - expect(comment3BoundsEnd.left).toBeCloseTo(comment3BoundsStart.left); - expect(comment3BoundsEnd.top).toBeCloseTo(comment3BoundsStart.top); - expect(await getHighlightedCommentIds(page)).toEqual(["comment2"]); - expect(await getSelectedId(page)).toBe("comment2"); -}); - -test("abort unconstrained move via context menu", async ({ page, act }) => { - const comment2BoundsStart = (await getComment(page, "comment2")).bounds; - - await act(page.keyboard.press(cmdOrCtrl("Enter"))); - for (let i = 0; i < 2; i++) { - await act(page.keyboard.press("ArrowDown")); - } - await act(page.keyboard.press("Enter")); - await expect(page.locator(".blocklyMoveIndicatorBubble")).toBeVisible(); - await act(page.keyboard.press("Alt+ArrowRight")); - await act(page.keyboard.press("Escape")); - await expect(page.locator(".blocklyMoveIndicatorBubble")).not.toBeVisible(); - - const comment2BoundsEnd = (await getComment(page, "comment2")).bounds; - expect(comment2BoundsEnd.top).toBeCloseTo(comment2BoundsStart.top); - expect(comment2BoundsEnd.left).toBeCloseTo(comment2BoundsStart.left); - expect(await getHighlightedCommentIds(page)).toEqual(["comment2"]); - expect(await getSelectedId(page)).toBe("comment2"); -}); - test("open context menu", async ({ page, act }) => { await act(page.keyboard.press(cmdOrCtrl("Enter"))); @@ -259,7 +155,6 @@ test("open context menu", async ({ page, act }) => { `Copy ${cmdOrCtrlLabel("C")}`, "Duplicate Comment D", "Remove Comment", - "Move Comment M", ]; expect(await page.getByRole("menuitem").allTextContents()).toEqual( expectedMenuItems, diff --git a/test-e2e/keyboard/select/stack.spec.ts b/test-e2e/keyboard/select/stack.spec.ts index b6dae9d..46a9b5b 100644 --- a/test-e2e/keyboard/select/stack.spec.ts +++ b/test-e2e/keyboard/select/stack.spec.ts @@ -65,7 +65,7 @@ test("navigate to next stack", async ({ page, act }) => { expect(await getSelectedId(page)).toBe("stack3"); }); -test("constrained move via shortcut", async ({ page, act }) => { +test("constrained move", async ({ page, act }) => { await act(page.keyboard.press("M")); await expect(page.locator(".blocklyMoveIndicatorBubble")).toBeVisible(); await act(page.keyboard.press("ArrowDown")); @@ -79,7 +79,7 @@ test("constrained move via shortcut", async ({ page, act }) => { ]); }); -test("abort constrained move via shortcut", async ({ page, act }) => { +test("abort constrained move", async ({ page, act }) => { await act(page.keyboard.press("M")); await expect(page.locator(".blocklyMoveIndicatorBubble")).toBeVisible(); await act(page.keyboard.press("ArrowDown")); @@ -93,43 +93,7 @@ test("abort constrained move via shortcut", async ({ page, act }) => { ]); }); -test("constrained move via context menu", async ({ page, act }) => { - await act(page.keyboard.press(cmdOrCtrl("Enter"))); - for (let i = 0; i < 6; i++) { - await act(page.keyboard.press("ArrowDown")); - } - await act(page.keyboard.press("Enter")); - await expect(page.locator(".blocklyMoveIndicatorBubble")).toBeVisible(); - await act(page.keyboard.press("ArrowDown")); - await act(page.keyboard.press("Enter")); - await expect(page.locator(".blocklyMoveIndicatorBubble")).not.toBeVisible(); - - expect(await getStackBlockIds(page, "stack2_top")).toEqual([ - "stack2_top", - "stack2_bottom", - "stack2_middle", - ]); -}); - -test("abort constrained move via context menu", async ({ page, act }) => { - await act(page.keyboard.press(cmdOrCtrl("Enter"))); - for (let i = 0; i < 6; i++) { - await act(page.keyboard.press("ArrowDown")); - } - await act(page.keyboard.press("Enter")); - await expect(page.locator(".blocklyMoveIndicatorBubble")).toBeVisible(); - await act(page.keyboard.press("ArrowDown")); - await act(page.keyboard.press("Escape")); - await expect(page.locator(".blocklyMoveIndicatorBubble")).not.toBeVisible(); - - expect(await getStackBlockIds(page, "stack2_top")).toEqual([ - "stack2_top", - "stack2_middle", - "stack2_bottom", - ]); -}); - -test("unconstrained move via shortcut", async ({ page, act }) => { +test("unconstrained move", async ({ page, act }) => { const gridSpacing = await getGridSpacing(page); if (gridSpacing === null) throw new Error("Workspace has no grid"); const stack1BoundsStart = (await getBlock(page, { id: "stack1" })).bounds; @@ -170,7 +134,7 @@ test("unconstrained move via shortcut", async ({ page, act }) => { expect(await getSelectedId(page)).toBe("stack2_middle"); }); -test("abort unconstrained move via shortcut", async ({ page, act }) => { +test("abort unconstrained move", async ({ page, act }) => { await act(page.keyboard.press("M")); await expect(page.locator(".blocklyMoveIndicatorBubble")).toBeVisible(); for (let i = 0; i < 3; i++) { @@ -185,68 +149,3 @@ test("abort unconstrained move via shortcut", async ({ page, act }) => { "stack2_bottom", ]); }); - -test("unconstrained move via context menu", async ({ page, act }) => { - const gridSpacing = await getGridSpacing(page); - if (gridSpacing === null) throw new Error("Workspace has no grid"); - const stack1BoundsStart = (await getBlock(page, { id: "stack1" })).bounds; - const stack2TopBoundsStart = (await getBlock(page, { id: "stack2_top" })) - .bounds; - const stack3BoundsStart = (await getBlock(page, { id: "stack3" })).bounds; - - await act(page.keyboard.press(cmdOrCtrl("Enter"))); - for (let i = 0; i < 6; i++) { - await act(page.keyboard.press("ArrowDown")); - } - await act(page.keyboard.press("Enter")); - await expect(page.locator(".blocklyMoveIndicatorBubble")).toBeVisible(); - for (let i = 0; i < 3; i++) { - await act(page.keyboard.press("Alt+ArrowRight")); - } - await act(page.keyboard.press("Enter")); - await expect(page.locator(".blocklyMoveIndicatorBubble")).not.toBeVisible(); - - expect(await getStackBlockIds(page, "stack2_top")).toEqual([ - "stack2_top", - "stack2_bottom", - ]); - expect(await getStackBlockIds(page, "stack2_middle")).toEqual([ - "stack2_middle", - ]); - const stack2MiddleBoundsEnd = (await getBlock(page, { id: "stack2_middle" })) - .bounds; - expect(stack2MiddleBoundsEnd.left - stack2TopBoundsStart.left).toBeCloseTo( - 3 * gridSpacing, - ); - expect(stack2MiddleBoundsEnd.top - stack2TopBoundsStart.top).toBeCloseTo( - gridSpacing, - ); - const stack1BoundsEnd = (await getBlock(page, { id: "stack1" })).bounds; - expect(stack1BoundsEnd.left).toBeCloseTo(stack1BoundsStart.left); - expect(stack1BoundsEnd.top).toBeCloseTo(stack1BoundsStart.top); - const stack3BoundsEnd = (await getBlock(page, { id: "stack3" })).bounds; - expect(stack3BoundsEnd.left).toBeCloseTo(stack3BoundsStart.left); - expect(stack3BoundsEnd.top).toBeCloseTo(stack3BoundsStart.top); - expect(await getHighlightedBlockIds(page)).toEqual(["stack2_middle"]); - expect(await getSelectedId(page)).toBe("stack2_middle"); -}); - -test("abort unconstrained move via context menu", async ({ page, act }) => { - await act(page.keyboard.press(cmdOrCtrl("Enter"))); - for (let i = 0; i < 6; i++) { - await act(page.keyboard.press("ArrowDown")); - } - await act(page.keyboard.press("Enter")); - await expect(page.locator(".blocklyMoveIndicatorBubble")).toBeVisible(); - for (let i = 0; i < 3; i++) { - await act(page.keyboard.press("Alt+ArrowRight")); - } - await act(page.keyboard.press("Escape")); - await expect(page.locator(".blocklyMoveIndicatorBubble")).not.toBeVisible(); - - expect(await getStackBlockIds(page, "stack2_top")).toEqual([ - "stack2_top", - "stack2_middle", - "stack2_bottom", - ]); -}); From 68303fea3aee32dbf1a2d5f931f30a9c237a3df7 Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Tue, 4 Aug 2026 09:41:40 -0700 Subject: [PATCH 48/67] Blockly 13 separates context menu shortcut text from displayText https://github.com/RaspberryPiFoundation/blockly/commit/b5a54b9e54d0cddf39cd72a560b369d2b3742e5f --- src/multiselect.js | 2 -- src/multiselect_contextmenu.js | 38 +++------------------------------- 2 files changed, 3 insertions(+), 37 deletions(-) diff --git a/src/multiselect.js b/src/multiselect.js index 6fbb725..6881518 100644 --- a/src/multiselect.js +++ b/src/multiselect.js @@ -140,7 +140,6 @@ export class Multiselect { Blockly.BlockSvg.prototype.bumpNeighbours = function() {}; } - ContextMenu.registerOurKeyboardNavigationMenuItems(this.useCopyPasteCrossTab_); Shortcut.registerDuplicateShortcut(); } @@ -207,7 +206,6 @@ export class Multiselect { Blockly.ContextMenuRegistry.registry.unregister('workspaceSelectAll'); Blockly.ContextMenuRegistry.registry.unregister('copy_to_backpack'); ContextMenu.registerOrigContextMenu(); - ContextMenu.registerOrigKeyboardNavigationMenuItems(); Shortcut.unregisterDuplicateShortcut(); Shortcut.unregisterOrigShortcut(); diff --git a/src/multiselect_contextmenu.js b/src/multiselect_contextmenu.js index 973c819..eb596ec 100644 --- a/src/multiselect_contextmenu.js +++ b/src/multiselect_contextmenu.js @@ -278,6 +278,7 @@ const registerDuplicate = function() { scopeType: Blockly.ContextMenuRegistry.ScopeType.BLOCK, id, weight: 1, + associatedKeyboardShortcut: Blockly.ShortcutItems.names.DUPLICATE, }; if (Blockly.ContextMenuRegistry.registry.getItem(id) !== null) { Blockly.ContextMenuRegistry.registry.unregister(id); @@ -744,6 +745,7 @@ const registerDelete = function() { scopeType: Blockly.ContextMenuRegistry.ScopeType.BLOCK, id, weight: 6, + associatedKeyboardShortcut: Blockly.ShortcutItems.names.DELETE, }; if (Blockly.ContextMenuRegistry.registry.getItem(id) !== null) { Blockly.ContextMenuRegistry.registry.unregister(id); @@ -1214,6 +1216,7 @@ const registerCommentDuplicate = function() { scopeType: Blockly.ContextMenuRegistry.ScopeType.COMMENT, id, weight: 1, + associatedKeyboardShortcut: Blockly.ShortcutItems.names.DUPLICATE, }; if (Blockly.ContextMenuRegistry.registry.getItem(id) !== null) { Blockly.ContextMenuRegistry.registry.unregister(id); @@ -1424,38 +1427,3 @@ export const registerOurContextMenu = function(useCopyPasteMenu, useCopyPasteCro registerSelectAll(); updateToMultiCopyToBackpack(); }; - -const origKeyboardNavigationMenuItems = []; - -export const registerOurKeyboardNavigationMenuItems = function(useCopyPasteCrossTab) { - const addMultiselectCount = (keyboardNavMenuItem, multiselectDisplayText) => { - origKeyboardNavigationMenuItems.push(keyboardNavMenuItem); - Blockly.ContextMenuRegistry.registry.unregister(keyboardNavMenuItem.id); - Blockly.ContextMenuRegistry.registry.register({ - ...keyboardNavMenuItem, - displayText: (scope) => { - const element = keyboardNavMenuItem.displayText(scope); - element.firstChild.textContent = multiselectDisplayText(scope); - return element; - }, - }); - }; - - addMultiselectCount( - Blockly.ContextMenuRegistry.registry.getItem('blockDuplicate'), - multiselectDisplayText['blockDuplicate']); - addMultiselectCount( - Blockly.ContextMenuRegistry.registry.getItem('commentDuplicate'), - multiselectDisplayText['commentDuplicate']); - -}; - -export const registerOrigKeyboardNavigationMenuItems = function() { - for (const item of origKeyboardNavigationMenuItems) { - if (Blockly.ContextMenuRegistry.registry.getItem(item.id) !== null) { - Blockly.ContextMenuRegistry.registry.unregister(item.id); - } - Blockly.ContextMenuRegistry.registry.register(item); - } - origKeyboardNavigationMenuItems.length = 0; -}; From 7a3d6ae36296bbfe34486466bf7619f95447577f Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Wed, 5 Aug 2026 15:09:52 -0700 Subject: [PATCH 49/67] Blockly 13 adds shortcut text to Remove Comment context menu item https://github.com/RaspberryPiFoundation/blockly/commit/b5a54b9e54d0cddf39cd72a560b369d2b3742e5f --- src/multiselect_contextmenu.js | 1 + test-e2e/actions/multiselect/workspace/comment.spec.ts | 4 ++-- test-e2e/actions/select/workspace/comment.spec.ts | 4 ++-- test-e2e/keyboard/multiselect/comment.spec.ts | 2 +- test-e2e/keyboard/select/comment.spec.ts | 2 +- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/multiselect_contextmenu.js b/src/multiselect_contextmenu.js index eb596ec..bab4971 100644 --- a/src/multiselect_contextmenu.js +++ b/src/multiselect_contextmenu.js @@ -1115,6 +1115,7 @@ const registerCommentDelete = function() { scopeType: Blockly.ContextMenuRegistry.ScopeType.COMMENT, id, weight: 6, + associatedKeyboardShortcut: Blockly.ShortcutItems.names.DELETE, }; if (Blockly.ContextMenuRegistry.registry.getItem(id) !== null) { Blockly.ContextMenuRegistry.registry.unregister(id); diff --git a/test-e2e/actions/multiselect/workspace/comment.spec.ts b/test-e2e/actions/multiselect/workspace/comment.spec.ts index 5d15443..e2bb380 100644 --- a/test-e2e/actions/multiselect/workspace/comment.spec.ts +++ b/test-e2e/actions/multiselect/workspace/comment.spec.ts @@ -44,7 +44,7 @@ test("open context menu", async ({ page, act }) => { const expectedMenuItems = [ `Copy (2) ${cmdOrCtrlLabel("C")}`, "Duplicate Comment (2) D", - "Remove Comment (2)", + "Remove Comment (2) Delete", ]; expect(await page.getByRole("menuitem").allTextContents()).toEqual( expectedMenuItems, @@ -214,7 +214,7 @@ test("delete comments via context menu", async ({ page, act }) => { page .getByRole("menuitem", { exact: true, - name: "Remove Comment (2)", + name: "Remove Comment (2) Delete", }) .click(), ); diff --git a/test-e2e/actions/select/workspace/comment.spec.ts b/test-e2e/actions/select/workspace/comment.spec.ts index 50aead1..8bb31f8 100644 --- a/test-e2e/actions/select/workspace/comment.spec.ts +++ b/test-e2e/actions/select/workspace/comment.spec.ts @@ -32,7 +32,7 @@ test("open context menu", async ({ page, act }) => { const expectedMenuItems = [ `Copy ${cmdOrCtrlLabel("C")}`, "Duplicate Comment D", - "Remove Comment", + "Remove Comment Delete", ]; expect(await page.getByRole("menuitem").allTextContents()).toEqual( expectedMenuItems, @@ -175,7 +175,7 @@ test("delete comment via context menu", async ({ page, act }) => { expect(await getSelectedId(page)).toBe("comment1"); expect(await isEphemeralFocusTaken(page)).toBe(true); await act( - page.getByRole("menuitem", { exact: true, name: "Remove Comment" }).click(), + page.getByRole("menuitem", { exact: true, name: "Remove Comment Delete" }).click(), ); expect(await getAllCommentIds(page)).toEqual(["comment2"]); diff --git a/test-e2e/keyboard/multiselect/comment.spec.ts b/test-e2e/keyboard/multiselect/comment.spec.ts index 84c25e6..34e1aa4 100644 --- a/test-e2e/keyboard/multiselect/comment.spec.ts +++ b/test-e2e/keyboard/multiselect/comment.spec.ts @@ -69,7 +69,7 @@ test("open context menu", async ({ page, act }) => { const expectedMenuItems = [ `Copy (2) ${cmdOrCtrlLabel("C")}`, "Duplicate Comment (2) D", - "Remove Comment (2)", + "Remove Comment (2) Delete", ]; expect(await page.getByRole("menuitem").allTextContents()).toEqual( expectedMenuItems, diff --git a/test-e2e/keyboard/select/comment.spec.ts b/test-e2e/keyboard/select/comment.spec.ts index 150ea42..4e31e60 100644 --- a/test-e2e/keyboard/select/comment.spec.ts +++ b/test-e2e/keyboard/select/comment.spec.ts @@ -154,7 +154,7 @@ test("open context menu", async ({ page, act }) => { const expectedMenuItems = [ `Copy ${cmdOrCtrlLabel("C")}`, "Duplicate Comment D", - "Remove Comment", + "Remove Comment Delete", ]; expect(await page.getByRole("menuitem").allTextContents()).toEqual( expectedMenuItems, From 2abc136824b711e7cd9a0ec4d0cf9d73f3bf9450 Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Wed, 5 Aug 2026 14:35:19 -0700 Subject: [PATCH 50/67] Blockly 13 adds shortcut text to Undo context menu item https://github.com/RaspberryPiFoundation/blockly/commit/b5a54b9e54d0cddf39cd72a560b369d2b3742e5f --- test-e2e/actions/multiselect/workspace/block.spec.ts | 2 +- test-e2e/actions/multiselect/workspace/comment.spec.ts | 2 +- test-e2e/actions/select/workspace/block.spec.ts | 2 +- test-e2e/actions/select/workspace/comment.spec.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test-e2e/actions/multiselect/workspace/block.spec.ts b/test-e2e/actions/multiselect/workspace/block.spec.ts index 56d3378..5c1db3d 100644 --- a/test-e2e/actions/multiselect/workspace/block.spec.ts +++ b/test-e2e/actions/multiselect/workspace/block.spec.ts @@ -402,7 +402,7 @@ test("undo via context menu", async ({ page, act }) => { button: "right", }), ); - await act(page.getByRole("menuitem", { exact: true, name: "Undo" }).click()); + await act(page.getByRole("menuitem", { exact: true, name: `Undo ${cmdOrCtrlLabel("Z")}` }).click()); expect(await getAllBlockIds(page)).toEqual([ "block1", diff --git a/test-e2e/actions/multiselect/workspace/comment.spec.ts b/test-e2e/actions/multiselect/workspace/comment.spec.ts index e2bb380..7ace391 100644 --- a/test-e2e/actions/multiselect/workspace/comment.spec.ts +++ b/test-e2e/actions/multiselect/workspace/comment.spec.ts @@ -258,7 +258,7 @@ test("undo via context menu", async ({ page, act }) => { button: "right", }), ); - await act(page.getByRole("menuitem", { exact: true, name: "Undo" }).click()); + await act(page.getByRole("menuitem", { exact: true, name: `Undo ${cmdOrCtrlLabel("Z")}` }).click()); expect(await getAllCommentIds(page)).toEqual([ "comment1", diff --git a/test-e2e/actions/select/workspace/block.spec.ts b/test-e2e/actions/select/workspace/block.spec.ts index 3dda8f5..1df7066 100644 --- a/test-e2e/actions/select/workspace/block.spec.ts +++ b/test-e2e/actions/select/workspace/block.spec.ts @@ -305,7 +305,7 @@ test("undo via context menu", async ({ page, act }) => { button: "right", }), ); - await act(page.getByRole("menuitem", { exact: true, name: "Undo" }).click()); + await act(page.getByRole("menuitem", { exact: true, name: `Undo ${cmdOrCtrlLabel("Z")}` }).click()); expect(await getAllBlockIds(page)).toEqual(["block1", "block2"]); }); diff --git a/test-e2e/actions/select/workspace/comment.spec.ts b/test-e2e/actions/select/workspace/comment.spec.ts index 8bb31f8..6db5271 100644 --- a/test-e2e/actions/select/workspace/comment.spec.ts +++ b/test-e2e/actions/select/workspace/comment.spec.ts @@ -213,7 +213,7 @@ test("undo via context menu", async ({ page, act }) => { button: "right", }), ); - await act(page.getByRole("menuitem", { exact: true, name: "Undo" }).click()); + await act(page.getByRole("menuitem", { exact: true, name: `Undo ${cmdOrCtrlLabel("Z")}` }).click()); expect(await getAllCommentIds(page)).toEqual(["comment1", "comment2"]); }); From d04b2e52bdc926117a33f9c3eb36605342337642 Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Wed, 5 Aug 2026 13:04:30 -0700 Subject: [PATCH 51/67] Blockly 13 adds a trailing space to context menu items without shortcuts https://github.com/RaspberryPiFoundation/blockly/commit/b5a54b9e54d0cddf39cd72a560b369d2b3742e5f --- package-lock.json | 30 +++++++++---------- package.json | 2 +- playwright.config.ts | 5 ++++ .../multiselect/workspace/block.spec.ts | 24 +++++++-------- .../multiselect/workspace/comment.spec.ts | 14 ++++----- .../actions/select/workspace/block.spec.ts | 24 +++++++-------- .../actions/select/workspace/comment.spec.ts | 14 ++++----- test-e2e/keyboard/multiselect/block.spec.ts | 24 +++++++-------- test-e2e/keyboard/multiselect/comment.spec.ts | 14 ++++----- test-e2e/keyboard/select/block.spec.ts | 24 +++++++-------- test-e2e/keyboard/select/comment.spec.ts | 14 ++++----- 11 files changed, 89 insertions(+), 100 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9d1488c..a3f6ac5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@blockly/dev-scripts": "^13.3.0", "@blockly/dev-tools": "^13.3.0", "@blockly/workspace-backpack": "^13.3.0", - "@playwright/test": "^1.58.2", + "@playwright/test": "^1.62.1", "@types/node": "^24.12.2", "blockly": "^13.3.0" }, @@ -1084,19 +1084,19 @@ } }, "node_modules/@playwright/test": { - "version": "1.58.2", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", - "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.1.tgz", + "integrity": "sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.58.2" + "playwright": "1.62.1" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/@sinonjs/commons": { @@ -4220,35 +4220,35 @@ } }, "node_modules/playwright": { - "version": "1.58.2", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", - "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz", + "integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.58.2" + "playwright-core": "1.62.1" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=18" + "node": ">=20" }, "optionalDependencies": { "fsevents": "2.3.2" } }, "node_modules/playwright-core": { - "version": "1.58.2", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", - "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz", + "integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==", "dev": true, "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" }, "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/playwright/node_modules/fsevents": { diff --git a/package.json b/package.json index 93b212e..e23c2c3 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "@blockly/dev-scripts": "^13.3.0", "@blockly/dev-tools": "^13.3.0", "@blockly/workspace-backpack": "^13.3.0", - "@playwright/test": "^1.58.2", + "@playwright/test": "^1.62.1", "@types/node": "^24.12.2", "blockly": "^13.3.0" }, diff --git a/playwright.config.ts b/playwright.config.ts index 2d9b693..c962f54 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -6,6 +6,11 @@ export default defineConfig({ testDir: "./test-e2e", fullyParallel: true, reporter: [["list"], ["html"]], + expect: { + toMatchAriaSnapshot: { + children: "deep-equal", + }, + }, use: { baseURL, }, diff --git a/test-e2e/actions/multiselect/workspace/block.spec.ts b/test-e2e/actions/multiselect/workspace/block.spec.ts index 5c1db3d..780943d 100644 --- a/test-e2e/actions/multiselect/workspace/block.spec.ts +++ b/test-e2e/actions/multiselect/workspace/block.spec.ts @@ -65,19 +65,17 @@ test("open context menu", async ({ page, act }) => { ); await expect(page.getByRole("menu")).toBeVisible(); - const expectedMenuItems = [ - `Copy (3) ${cmdOrCtrlLabel("C")}`, - "Duplicate (3) D", - "Add Comment (4)", - "Collapse Block (3)", - "Disable Block (3)", - "Delete 5 Blocks Delete", - "Help", - "Copy to Backpack (3)", - ]; - expect(await page.getByRole("menuitem").allTextContents()).toEqual( - expectedMenuItems, - ); + await expect(page.getByRole("menu")).toMatchAriaSnapshot(` + - menu: + - menuitem "Copy (3) ${cmdOrCtrlLabel("C")}" + - menuitem "Duplicate (3) D" + - menuitem "Add Comment (4)" + - menuitem "Collapse Block (3)" + - menuitem "Disable Block (3)" + - menuitem "Delete 5 Blocks Delete" + - menuitem "Help" + - menuitem "Copy to Backpack (3)" + `); expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); expect(await isEphemeralFocusTaken(page)).toBe(true); diff --git a/test-e2e/actions/multiselect/workspace/comment.spec.ts b/test-e2e/actions/multiselect/workspace/comment.spec.ts index 7ace391..61aa3ad 100644 --- a/test-e2e/actions/multiselect/workspace/comment.spec.ts +++ b/test-e2e/actions/multiselect/workspace/comment.spec.ts @@ -41,14 +41,12 @@ test("open context menu", async ({ page, act }) => { ); await expect(page.getByRole("menu")).toBeVisible(); - const expectedMenuItems = [ - `Copy (2) ${cmdOrCtrlLabel("C")}`, - "Duplicate Comment (2) D", - "Remove Comment (2) Delete", - ]; - expect(await page.getByRole("menuitem").allTextContents()).toEqual( - expectedMenuItems, - ); + await expect(page.getByRole("menu")).toMatchAriaSnapshot(` + - menu: + - menuitem "Copy (2) ${cmdOrCtrlLabel("C")}" + - menuitem "Duplicate Comment (2) D" + - menuitem "Remove Comment (2) Delete" + `); expect(await getHighlightedCommentIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); expect(await isEphemeralFocusTaken(page)).toBe(true); diff --git a/test-e2e/actions/select/workspace/block.spec.ts b/test-e2e/actions/select/workspace/block.spec.ts index 1df7066..cf3585e 100644 --- a/test-e2e/actions/select/workspace/block.spec.ts +++ b/test-e2e/actions/select/workspace/block.spec.ts @@ -37,19 +37,17 @@ test("open context menu", async ({ page, act }) => { ); await expect(page.getByRole("menu")).toBeVisible(); - const expectedMenuItems = [ - `Copy ${cmdOrCtrlLabel("C")}`, - "Duplicate D", - "Add Comment", - "Collapse Block", - "Disable Block", - "Delete Block Delete", - "Help", - "Copy to Backpack", - ]; - expect(await page.getByRole("menuitem").allTextContents()).toEqual( - expectedMenuItems, - ); + await expect(page.getByRole("menu")).toMatchAriaSnapshot(` + - menu: + - menuitem "Copy ${cmdOrCtrlLabel("C")}" + - menuitem "Duplicate D" + - menuitem "Add Comment" + - menuitem "Collapse Block" + - menuitem "Disable Block" + - menuitem "Delete Block Delete" + - menuitem "Help" + - menuitem "Copy to Backpack" + `); expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe("block1"); expect(await isEphemeralFocusTaken(page)).toBe(true); diff --git a/test-e2e/actions/select/workspace/comment.spec.ts b/test-e2e/actions/select/workspace/comment.spec.ts index 6db5271..f08d850 100644 --- a/test-e2e/actions/select/workspace/comment.spec.ts +++ b/test-e2e/actions/select/workspace/comment.spec.ts @@ -29,14 +29,12 @@ test("open context menu", async ({ page, act }) => { ); await expect(page.getByRole("menu")).toBeVisible(); - const expectedMenuItems = [ - `Copy ${cmdOrCtrlLabel("C")}`, - "Duplicate Comment D", - "Remove Comment Delete", - ]; - expect(await page.getByRole("menuitem").allTextContents()).toEqual( - expectedMenuItems, - ); + await expect(page.getByRole("menu")).toMatchAriaSnapshot(` + - menu: + - menuitem "Copy ${cmdOrCtrlLabel("C")}" + - menuitem "Duplicate Comment D" + - menuitem "Remove Comment Delete" + `); expect(await getHighlightedCommentIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe("comment1"); expect(await isEphemeralFocusTaken(page)).toBe(true); diff --git a/test-e2e/keyboard/multiselect/block.spec.ts b/test-e2e/keyboard/multiselect/block.spec.ts index 436f685..f4491aa 100644 --- a/test-e2e/keyboard/multiselect/block.spec.ts +++ b/test-e2e/keyboard/multiselect/block.spec.ts @@ -66,19 +66,17 @@ test("open context menu", async ({ page, act }) => { await act(page.keyboard.press(cmdOrCtrl("Enter"))); await expect(page.getByRole("menu")).toBeVisible(); - const expectedMenuItems = [ - `Copy (2) ${cmdOrCtrlLabel("C")}`, - "Duplicate (2) D", - "Add Comment (2)", - "Collapse Block (2)", - "Disable Block (2)", - "Delete 2 Blocks Delete", - "Help", - "Copy to Backpack (2)", - ]; - expect(await page.getByRole("menuitem").allTextContents()).toEqual( - expectedMenuItems, - ); + await expect(page.getByRole("menu")).toMatchAriaSnapshot(` + - menu: + - menuitem "Copy (2) ${cmdOrCtrlLabel("C")}" + - menuitem "Duplicate (2) D" + - menuitem "Add Comment (2)" + - menuitem "Collapse Block (2)" + - menuitem "Disable Block (2)" + - menuitem "Delete 2 Blocks Delete" + - menuitem "Help" + - menuitem "Copy to Backpack (2)" + `); expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); expect(await isEphemeralFocusTaken(page)).toBe(true); diff --git a/test-e2e/keyboard/multiselect/comment.spec.ts b/test-e2e/keyboard/multiselect/comment.spec.ts index 34e1aa4..4f5950d 100644 --- a/test-e2e/keyboard/multiselect/comment.spec.ts +++ b/test-e2e/keyboard/multiselect/comment.spec.ts @@ -66,14 +66,12 @@ test("open context menu", async ({ page, act }) => { await act(page.keyboard.press(cmdOrCtrl("Enter"))); await expect(page.getByRole("menu")).toBeVisible(); - const expectedMenuItems = [ - `Copy (2) ${cmdOrCtrlLabel("C")}`, - "Duplicate Comment (2) D", - "Remove Comment (2) Delete", - ]; - expect(await page.getByRole("menuitem").allTextContents()).toEqual( - expectedMenuItems, - ); + await expect(page.getByRole("menu")).toMatchAriaSnapshot(` + - menu: + - menuitem "Copy (2) ${cmdOrCtrlLabel("C")}" + - menuitem "Duplicate Comment (2) D" + - menuitem "Remove Comment (2) Delete" + `); expect(await getHighlightedCommentIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); expect(await isEphemeralFocusTaken(page)).toBe(true); diff --git a/test-e2e/keyboard/select/block.spec.ts b/test-e2e/keyboard/select/block.spec.ts index 1c8189a..6ec5bd6 100644 --- a/test-e2e/keyboard/select/block.spec.ts +++ b/test-e2e/keyboard/select/block.spec.ts @@ -107,19 +107,17 @@ test("open context menu", async ({ page, act }) => { await act(page.keyboard.press(cmdOrCtrl("Enter"))); await expect(page.getByRole("menu")).toBeVisible(); - const expectedMenuItems = [ - `Copy ${cmdOrCtrlLabel("C")}`, - "Duplicate D", - "Add Comment", - "Collapse Block", - "Disable Block", - "Delete Block Delete", - "Help", - "Copy to Backpack", - ]; - expect(await page.getByRole("menuitem").allTextContents()).toEqual( - expectedMenuItems, - ); + await expect(page.getByRole("menu")).toMatchAriaSnapshot(` + - menu: + - menuitem "Copy ${cmdOrCtrlLabel("C")}" + - menuitem "Duplicate D" + - menuitem "Add Comment" + - menuitem "Collapse Block" + - menuitem "Disable Block" + - menuitem "Delete Block Delete" + - menuitem "Help" + - menuitem "Copy to Backpack" + `); expect(await getHighlightedBlockIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe("block2"); expect(await isEphemeralFocusTaken(page)).toBe(true); diff --git a/test-e2e/keyboard/select/comment.spec.ts b/test-e2e/keyboard/select/comment.spec.ts index 4e31e60..19bdf12 100644 --- a/test-e2e/keyboard/select/comment.spec.ts +++ b/test-e2e/keyboard/select/comment.spec.ts @@ -151,14 +151,12 @@ test("open context menu", async ({ page, act }) => { await act(page.keyboard.press(cmdOrCtrl("Enter"))); await expect(page.getByRole("menu")).toBeVisible(); - const expectedMenuItems = [ - `Copy ${cmdOrCtrlLabel("C")}`, - "Duplicate Comment D", - "Remove Comment Delete", - ]; - expect(await page.getByRole("menuitem").allTextContents()).toEqual( - expectedMenuItems, - ); + await expect(page.getByRole("menu")).toMatchAriaSnapshot(` + - menu: + - menuitem "Copy ${cmdOrCtrlLabel("C")}" + - menuitem "Duplicate Comment D" + - menuitem "Remove Comment Delete" + `); expect(await getHighlightedCommentIds(page)).toEqual([]); expect(await getSelectedId(page)).toBe("comment2"); expect(await isEphemeralFocusTaken(page)).toBe(true); From 973c51b82936b1574c8c2601408d85e505f61efa Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Tue, 4 Aug 2026 13:37:57 -0700 Subject: [PATCH 52/67] Remove fixed workspace context menu workaround https://github.com/RaspberryPiFoundation/blockly-samples/commit/8fe56c62f47118b906164874c876de5cd4f855b3 --- test-e2e/keyboard/workspace.spec.ts | 24 ------------------------ test/index.js | 9 --------- 2 files changed, 33 deletions(-) delete mode 100644 test-e2e/keyboard/workspace.spec.ts diff --git a/test-e2e/keyboard/workspace.spec.ts b/test-e2e/keyboard/workspace.spec.ts deleted file mode 100644 index 99f2154..0000000 --- a/test-e2e/keyboard/workspace.spec.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { expect } from "@playwright/test"; -import { cmdOrCtrl, getEmptySpace, loadBlocks, test } from "../test"; - -test.beforeEach(async ({ page, act }) => { - await act(loadBlocks(page, [{ type: "logic_boolean", id: "block1" }])); -}); - -test("workspace context menu", async ({ page, act }) => { - await act( - page.mouse.click(...(await getEmptySpace(page)), { button: "right" }), - ); - await expect(page.getByRole("menu")).toBeVisible(); - const rightClickMenuItems = await page - .getByRole("menuitem") - .allTextContents(); - await act(page.keyboard.press("Escape")); - - await act(page.keyboard.press("W")); - await act(page.keyboard.press(cmdOrCtrl("Enter"))); - await expect(page.getByRole("menu")).toBeVisible(); - const keyboardMenuItems = await page.getByRole("menuitem").allTextContents(); - - expect(keyboardMenuItems).toEqual(rightClickMenuItems); -}); diff --git a/test/index.js b/test/index.js index 9635772..8f29d4a 100644 --- a/test/index.js +++ b/test/index.js @@ -28,15 +28,6 @@ function createWorkspace(blocklyDiv, options) { const backpack = new Backpack(workspace); backpack.init(); - // Work around backpack plugin bug where its configureContextMenu clears - // all menu items for non-PointerEvent events (e.g. keyboard nav). - const origConfigureContextMenu = workspace.configureContextMenu; - workspace.configureContextMenu = (menuOptions, event) => { - if (event instanceof PointerEvent) { - origConfigureContextMenu.call(null, menuOptions, event); - } - }; - // Initialize multiselect plugin. const multiselectPlugin = new Multiselect(workspace); multiselectPlugin.init(options); From 5af8de6905d099818f0dc0a5b7083a7e66c7101c Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Tue, 4 Aug 2026 16:03:47 -0700 Subject: [PATCH 53/67] Blockly 13 renamed keyboard move indicator CSS class https://github.com/RaspberryPiFoundation/blockly/commit/da1db45dd257b2e033ef5fb8e4bae2f48d0da83b --- test-e2e/keyboard/select/block.spec.ts | 8 ++++---- test-e2e/keyboard/select/comment.spec.ts | 16 ++++++++-------- test-e2e/keyboard/select/stack.spec.ts | 16 ++++++++-------- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/test-e2e/keyboard/select/block.spec.ts b/test-e2e/keyboard/select/block.spec.ts index 6ec5bd6..101d552 100644 --- a/test-e2e/keyboard/select/block.spec.ts +++ b/test-e2e/keyboard/select/block.spec.ts @@ -67,10 +67,10 @@ test("unconstrained move", async ({ page, act }) => { const block3BoundsStart = (await getBlock(page, { id: "block3" })).bounds; await act(page.keyboard.press("M")); - await expect(page.locator(".blocklyMoveIndicatorBubble")).toBeVisible(); + await expect(page.locator(".blocklyMoveIndicator")).toBeVisible(); await act(page.keyboard.press("Alt+ArrowRight")); await act(page.keyboard.press("Enter")); - await expect(page.locator(".blocklyMoveIndicatorBubble")).not.toBeVisible(); + await expect(page.locator(".blocklyMoveIndicator")).not.toBeVisible(); const block1BoundsEnd = (await getBlock(page, { id: "block1" })).bounds; const block2BoundsEnd = (await getBlock(page, { id: "block2" })).bounds; @@ -91,10 +91,10 @@ test("abort unconstrained move", async ({ page, act }) => { const block2BoundsStart = (await getBlock(page, { id: "block2" })).bounds; await act(page.keyboard.press("M")); - await expect(page.locator(".blocklyMoveIndicatorBubble")).toBeVisible(); + await expect(page.locator(".blocklyMoveIndicator")).toBeVisible(); await act(page.keyboard.press("Alt+ArrowRight")); await act(page.keyboard.press("Escape")); - await expect(page.locator(".blocklyMoveIndicatorBubble")).not.toBeVisible(); + await expect(page.locator(".blocklyMoveIndicator")).not.toBeVisible(); const block2BoundsEnd = (await getBlock(page, { id: "block2" })).bounds; expect(block2BoundsEnd.left).toBeCloseTo(block2BoundsStart.left); diff --git a/test-e2e/keyboard/select/comment.spec.ts b/test-e2e/keyboard/select/comment.spec.ts index 19bdf12..71b7d82 100644 --- a/test-e2e/keyboard/select/comment.spec.ts +++ b/test-e2e/keyboard/select/comment.spec.ts @@ -67,10 +67,10 @@ test("constrained move", async ({ page, act }) => { const comment3BoundsStart = (await getComment(page, "comment3")).bounds; await act(page.keyboard.press("M")); - await expect(page.locator(".blocklyMoveIndicatorBubble")).toBeVisible(); + await expect(page.locator(".blocklyMoveIndicator")).toBeVisible(); await act(page.keyboard.press("ArrowRight")); await act(page.keyboard.press("Enter")); - await expect(page.locator(".blocklyMoveIndicatorBubble")).not.toBeVisible(); + await expect(page.locator(".blocklyMoveIndicator")).not.toBeVisible(); const comment1BoundsEnd = (await getComment(page, "comment1")).bounds; const comment2BoundsEnd = (await getComment(page, "comment2")).bounds; @@ -91,10 +91,10 @@ test("abort constrained move", async ({ page, act }) => { const comment2BoundsStart = (await getComment(page, "comment2")).bounds; await act(page.keyboard.press("M")); - await expect(page.locator(".blocklyMoveIndicatorBubble")).toBeVisible(); + await expect(page.locator(".blocklyMoveIndicator")).toBeVisible(); await act(page.keyboard.press("ArrowRight")); await act(page.keyboard.press("Escape")); - await expect(page.locator(".blocklyMoveIndicatorBubble")).not.toBeVisible(); + await expect(page.locator(".blocklyMoveIndicator")).not.toBeVisible(); const comment2BoundsEnd = (await getComment(page, "comment2")).bounds; expect(comment2BoundsEnd.top).toBeCloseTo(comment2BoundsStart.top); @@ -111,10 +111,10 @@ test("unconstrained move", async ({ page, act }) => { const comment3BoundsStart = (await getComment(page, "comment3")).bounds; await act(page.keyboard.press("M")); - await expect(page.locator(".blocklyMoveIndicatorBubble")).toBeVisible(); + await expect(page.locator(".blocklyMoveIndicator")).toBeVisible(); await act(page.keyboard.press("Alt+ArrowRight")); await act(page.keyboard.press("Enter")); - await expect(page.locator(".blocklyMoveIndicatorBubble")).not.toBeVisible(); + await expect(page.locator(".blocklyMoveIndicator")).not.toBeVisible(); const comment1BoundsEnd = (await getComment(page, "comment1")).bounds; const comment2BoundsEnd = (await getComment(page, "comment2")).bounds; @@ -135,10 +135,10 @@ test("abort unconstrained move", async ({ page, act }) => { const comment2BoundsStart = (await getComment(page, "comment2")).bounds; await act(page.keyboard.press("M")); - await expect(page.locator(".blocklyMoveIndicatorBubble")).toBeVisible(); + await expect(page.locator(".blocklyMoveIndicator")).toBeVisible(); await act(page.keyboard.press("Alt+ArrowRight")); await act(page.keyboard.press("Escape")); - await expect(page.locator(".blocklyMoveIndicatorBubble")).not.toBeVisible(); + await expect(page.locator(".blocklyMoveIndicator")).not.toBeVisible(); const comment2BoundsEnd = (await getComment(page, "comment2")).bounds; expect(comment2BoundsEnd.top).toBeCloseTo(comment2BoundsStart.top); diff --git a/test-e2e/keyboard/select/stack.spec.ts b/test-e2e/keyboard/select/stack.spec.ts index 46a9b5b..5ab6fdf 100644 --- a/test-e2e/keyboard/select/stack.spec.ts +++ b/test-e2e/keyboard/select/stack.spec.ts @@ -67,10 +67,10 @@ test("navigate to next stack", async ({ page, act }) => { test("constrained move", async ({ page, act }) => { await act(page.keyboard.press("M")); - await expect(page.locator(".blocklyMoveIndicatorBubble")).toBeVisible(); + await expect(page.locator(".blocklyMoveIndicator")).toBeVisible(); await act(page.keyboard.press("ArrowDown")); await act(page.keyboard.press("Enter")); - await expect(page.locator(".blocklyMoveIndicatorBubble")).not.toBeVisible(); + await expect(page.locator(".blocklyMoveIndicator")).not.toBeVisible(); expect(await getStackBlockIds(page, "stack2_top")).toEqual([ "stack2_top", @@ -81,10 +81,10 @@ test("constrained move", async ({ page, act }) => { test("abort constrained move", async ({ page, act }) => { await act(page.keyboard.press("M")); - await expect(page.locator(".blocklyMoveIndicatorBubble")).toBeVisible(); + await expect(page.locator(".blocklyMoveIndicator")).toBeVisible(); await act(page.keyboard.press("ArrowDown")); await act(page.keyboard.press("Escape")); - await expect(page.locator(".blocklyMoveIndicatorBubble")).not.toBeVisible(); + await expect(page.locator(".blocklyMoveIndicator")).not.toBeVisible(); expect(await getStackBlockIds(page, "stack2_top")).toEqual([ "stack2_top", @@ -102,12 +102,12 @@ test("unconstrained move", async ({ page, act }) => { const stack3BoundsStart = (await getBlock(page, { id: "stack3" })).bounds; await act(page.keyboard.press("M")); - await expect(page.locator(".blocklyMoveIndicatorBubble")).toBeVisible(); + await expect(page.locator(".blocklyMoveIndicator")).toBeVisible(); for (let i = 0; i < 3; i++) { await act(page.keyboard.press("Alt+ArrowRight")); } await act(page.keyboard.press("Enter")); - await expect(page.locator(".blocklyMoveIndicatorBubble")).not.toBeVisible(); + await expect(page.locator(".blocklyMoveIndicator")).not.toBeVisible(); expect(await getStackBlockIds(page, "stack2_top")).toEqual([ "stack2_top", @@ -136,12 +136,12 @@ test("unconstrained move", async ({ page, act }) => { test("abort unconstrained move", async ({ page, act }) => { await act(page.keyboard.press("M")); - await expect(page.locator(".blocklyMoveIndicatorBubble")).toBeVisible(); + await expect(page.locator(".blocklyMoveIndicator")).toBeVisible(); for (let i = 0; i < 3; i++) { await act(page.keyboard.press("Alt+ArrowRight")); } await act(page.keyboard.press("Escape")); - await expect(page.locator(".blocklyMoveIndicatorBubble")).not.toBeVisible(); + await expect(page.locator(".blocklyMoveIndicator")).not.toBeVisible(); expect(await getStackBlockIds(page, "stack2_top")).toEqual([ "stack2_top", From 5d7e5f0b2f26a7e9ce59589a6dd5b21be16c68fd Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Tue, 4 Aug 2026 16:20:54 -0700 Subject: [PATCH 54/67] Blockly 13 changed unconstrained move to KeyCodes.CTRL_CMD https://github.com/RaspberryPiFoundation/blockly/commit/da1db45dd257b2e033ef5fb8e4bae2f48d0da83b --- test-e2e/keyboard/select/block.spec.ts | 4 ++-- test-e2e/keyboard/select/comment.spec.ts | 4 ++-- test-e2e/keyboard/select/stack.spec.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/test-e2e/keyboard/select/block.spec.ts b/test-e2e/keyboard/select/block.spec.ts index 101d552..f7d152f 100644 --- a/test-e2e/keyboard/select/block.spec.ts +++ b/test-e2e/keyboard/select/block.spec.ts @@ -68,7 +68,7 @@ test("unconstrained move", async ({ page, act }) => { await act(page.keyboard.press("M")); await expect(page.locator(".blocklyMoveIndicator")).toBeVisible(); - await act(page.keyboard.press("Alt+ArrowRight")); + await act(page.keyboard.press(cmdOrCtrl("ArrowRight"))); await act(page.keyboard.press("Enter")); await expect(page.locator(".blocklyMoveIndicator")).not.toBeVisible(); @@ -92,7 +92,7 @@ test("abort unconstrained move", async ({ page, act }) => { await act(page.keyboard.press("M")); await expect(page.locator(".blocklyMoveIndicator")).toBeVisible(); - await act(page.keyboard.press("Alt+ArrowRight")); + await act(page.keyboard.press(cmdOrCtrl("ArrowRight"))); await act(page.keyboard.press("Escape")); await expect(page.locator(".blocklyMoveIndicator")).not.toBeVisible(); diff --git a/test-e2e/keyboard/select/comment.spec.ts b/test-e2e/keyboard/select/comment.spec.ts index 71b7d82..4e742e5 100644 --- a/test-e2e/keyboard/select/comment.spec.ts +++ b/test-e2e/keyboard/select/comment.spec.ts @@ -112,7 +112,7 @@ test("unconstrained move", async ({ page, act }) => { await act(page.keyboard.press("M")); await expect(page.locator(".blocklyMoveIndicator")).toBeVisible(); - await act(page.keyboard.press("Alt+ArrowRight")); + await act(page.keyboard.press(cmdOrCtrl("ArrowRight"))); await act(page.keyboard.press("Enter")); await expect(page.locator(".blocklyMoveIndicator")).not.toBeVisible(); @@ -136,7 +136,7 @@ test("abort unconstrained move", async ({ page, act }) => { await act(page.keyboard.press("M")); await expect(page.locator(".blocklyMoveIndicator")).toBeVisible(); - await act(page.keyboard.press("Alt+ArrowRight")); + await act(page.keyboard.press(cmdOrCtrl("ArrowRight"))); await act(page.keyboard.press("Escape")); await expect(page.locator(".blocklyMoveIndicator")).not.toBeVisible(); diff --git a/test-e2e/keyboard/select/stack.spec.ts b/test-e2e/keyboard/select/stack.spec.ts index 5ab6fdf..3ce8382 100644 --- a/test-e2e/keyboard/select/stack.spec.ts +++ b/test-e2e/keyboard/select/stack.spec.ts @@ -104,7 +104,7 @@ test("unconstrained move", async ({ page, act }) => { await act(page.keyboard.press("M")); await expect(page.locator(".blocklyMoveIndicator")).toBeVisible(); for (let i = 0; i < 3; i++) { - await act(page.keyboard.press("Alt+ArrowRight")); + await act(page.keyboard.press(cmdOrCtrl("ArrowRight"))); } await act(page.keyboard.press("Enter")); await expect(page.locator(".blocklyMoveIndicator")).not.toBeVisible(); @@ -138,7 +138,7 @@ test("abort unconstrained move", async ({ page, act }) => { await act(page.keyboard.press("M")); await expect(page.locator(".blocklyMoveIndicator")).toBeVisible(); for (let i = 0; i < 3; i++) { - await act(page.keyboard.press("Alt+ArrowRight")); + await act(page.keyboard.press(cmdOrCtrl("ArrowRight"))); } await act(page.keyboard.press("Escape")); await expect(page.locator(".blocklyMoveIndicator")).not.toBeVisible(); From da482679f6cefd40399dd11e00f7bce4ec5ac97c Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Wed, 5 Aug 2026 16:05:20 -0700 Subject: [PATCH 55/67] Blockly 13 changed startDrag return from void to IDraggable https://github.com/RaspberryPiFoundation/blockly/commit/da1db45dd257b2e033ef5fb8e4bae2f48d0da83b --- src/multiselect_draggable.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/multiselect_draggable.js b/src/multiselect_draggable.js index f666ec6..762c7ea 100644 --- a/src/multiselect_draggable.js +++ b/src/multiselect_draggable.js @@ -255,6 +255,7 @@ export class MultiselectDraggable { Blockly.utils.dom.addClass(subDraggable.getSvgRoot(), 'blocklySelected'); } Blockly.common.setSelected(this); + return this; } /** From 9718f9dd0445e0a342e6431c95489b214d58b43b Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Mon, 10 Aug 2026 13:41:58 -0700 Subject: [PATCH 56/67] Blockly 13 focuses workspace instead of first block https://github.com/RaspberryPiFoundation/blockly/commit/ed761c386755cfc6c49bb6dffcd3c2102193e57a --- src/multiselect_shortcut.js | 2 -- .../actions/multiselect/workspace/block.spec.ts | 13 +++++++------ .../actions/multiselect/workspace/comment.spec.ts | 5 +++++ test-e2e/test.ts | 6 ++++++ 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/multiselect_shortcut.js b/src/multiselect_shortcut.js index c8f6fc7..1de6cdd 100644 --- a/src/multiselect_shortcut.js +++ b/src/multiselect_shortcut.js @@ -99,7 +99,6 @@ const registerShortcutDelete = function() { apply(selected); } - Blockly.getFocusManager().focusTree(workspace); Blockly.Events.setGroup(false); return true; }, @@ -301,7 +300,6 @@ const registerCut = function(useCopyPasteCrossTab) { if (useCopyPasteCrossTab) { dataCopyToStorage(); } - Blockly.getFocusManager().focusTree(workspace); Blockly.Events.setGroup(false); return true; }, diff --git a/test-e2e/actions/multiselect/workspace/block.spec.ts b/test-e2e/actions/multiselect/workspace/block.spec.ts index 780943d..6983ada 100644 --- a/test-e2e/actions/multiselect/workspace/block.spec.ts +++ b/test-e2e/actions/multiselect/workspace/block.spec.ts @@ -12,6 +12,7 @@ import { getSelectedId, getTrash, isEphemeralFocusTaken, + isWorkspaceFocused, loadBlocks, openBackpack, openTrash, @@ -272,8 +273,8 @@ test("copy and paste blocks via context menu", async ({ page, act }) => { test("cut and paste blocks via keyboard", async ({ page, act }) => { await act(page.keyboard.press(cmdOrCtrl("X"))); expect(await getAllBlockIds(page)).toEqual(["block4"]); - expect(await getHighlightedBlockIds(page)).toEqual(["block4"]); - expect(await getSelectedId(page)).toBe("block4"); + expect(await getHighlightedBlockIds(page)).toEqual([]); + expect(await isWorkspaceFocused(page)).toBe(true); await act(page.keyboard.press(cmdOrCtrl("V"))); expect(await getAllBlockIds(page)).toHaveLength(6); @@ -287,8 +288,8 @@ test("delete blocks via keyboard", async ({ page, act }) => { await act(page.keyboard.press("Delete")); expect(await getAllBlockIds(page)).toEqual(["block4"]); - expect(await getHighlightedBlockIds(page)).toEqual(["block4"]); - expect(await getSelectedId(page)).toBe("block4"); + expect(await getHighlightedBlockIds(page)).toEqual([]); + expect(await isWorkspaceFocused(page)).toBe(true); }); test("delete blocks via context menu", async ({ page, act }) => { @@ -307,8 +308,8 @@ test("delete blocks via context menu", async ({ page, act }) => { ); expect(await getAllBlockIds(page)).toEqual(["block4"]); - expect(await getHighlightedBlockIds(page)).toEqual(["block4"]); - expect(await getSelectedId(page)).toBe("block4"); + expect(await getHighlightedBlockIds(page)).toEqual([]); + expect(await isWorkspaceFocused(page)).toBe(true); expect(await isEphemeralFocusTaken(page)).toBe(false); }); diff --git a/test-e2e/actions/multiselect/workspace/comment.spec.ts b/test-e2e/actions/multiselect/workspace/comment.spec.ts index 61aa3ad..8e9cb22 100644 --- a/test-e2e/actions/multiselect/workspace/comment.spec.ts +++ b/test-e2e/actions/multiselect/workspace/comment.spec.ts @@ -11,6 +11,7 @@ import { getSelectedId, getTrash, isEphemeralFocusTaken, + isWorkspaceFocused, loadComments, test, } from "../../../test"; @@ -182,6 +183,7 @@ test("cut and paste comments via keyboard", async ({ page, act }) => { expect(await getAllCommentIds(page)).toEqual(["comment3"]); expect(await getHighlightedCommentIds(page)).toEqual([]); expect(await getSelectedId(page)).toBeNull(); + expect(await isWorkspaceFocused(page)).toBe(true); await act(page.keyboard.press(cmdOrCtrl("V"))); expect(await getAllCommentIds(page)).toHaveLength(3); @@ -197,6 +199,7 @@ test("delete comments via keyboard", async ({ page, act }) => { expect(await getAllCommentIds(page)).toEqual(["comment3"]); expect(await getHighlightedCommentIds(page)).toEqual([]); expect(await getSelectedId(page)).toBeNull(); + expect(await isWorkspaceFocused(page)).toBe(true); }); test("delete comments via context menu", async ({ page, act }) => { @@ -220,6 +223,7 @@ test("delete comments via context menu", async ({ page, act }) => { expect(await getAllCommentIds(page)).toEqual(["comment3"]); expect(await getHighlightedCommentIds(page)).toEqual([]); expect(await getSelectedId(page)).toBeNull(); + expect(await isWorkspaceFocused(page)).toBe(true); expect(await isEphemeralFocusTaken(page)).toBe(false); }); @@ -232,6 +236,7 @@ test("drag comments to trash", async ({ page, act }) => { expect(await getAllCommentIds(page)).toEqual(["comment3"]); expect(await getHighlightedCommentIds(page)).toEqual([]); expect(await getSelectedId(page)).toBeNull(); + expect(await isWorkspaceFocused(page)).toBe(true); }); test("undo via keyboard", async ({ page, act }) => { diff --git a/test-e2e/test.ts b/test-e2e/test.ts index f7a5dc4..7a1f2a1 100644 --- a/test-e2e/test.ts +++ b/test-e2e/test.ts @@ -333,6 +333,12 @@ export const getFocusedCommentButton = ( return null; }); +export const isWorkspaceFocused = (page: Page): Promise => + page.evaluate(() => { + const workspace = Blockly.getMainWorkspace() as WorkspaceSvg; + return Blockly.getFocusManager().getFocusedNode() === workspace.getWorkspaceFocusTarget(); + }); + export const isEphemeralFocusTaken = (page: Page): Promise => page.evaluate(() => Blockly.getFocusManager().ephemeralFocusTaken()); From 4ed67b089347cf85adfa7ca26e31549f91444092 Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Mon, 10 Aug 2026 14:29:36 -0700 Subject: [PATCH 57/67] Blockly 13 focuses nearest block after deleting focused block https://github.com/RaspberryPiFoundation/blockly/commit/a718c7bb1954fdea3262509fc7ef00c165c52eb4 --- src/multiselect_draggable.js | 1 + test-e2e/actions/multiselect/workspace/block.spec.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/multiselect_draggable.js b/src/multiselect_draggable.js index 762c7ea..2d11584 100644 --- a/src/multiselect_draggable.js +++ b/src/multiselect_draggable.js @@ -393,6 +393,7 @@ export class MultiselectDraggable { * Disposes all subdraggables in the multiselectDraggable object. */ dispose() { + Blockly.getFocusManager().focusNode(this.workspace.getWorkspaceFocusTarget()); for (const draggable of this.subDraggables) { if (!draggable[0].isDeletable()) { continue; diff --git a/test-e2e/actions/multiselect/workspace/block.spec.ts b/test-e2e/actions/multiselect/workspace/block.spec.ts index 6983ada..3971294 100644 --- a/test-e2e/actions/multiselect/workspace/block.spec.ts +++ b/test-e2e/actions/multiselect/workspace/block.spec.ts @@ -323,7 +323,7 @@ test("drag blocks to trash", async ({ page, act }) => { expect(await getAllBlockIds(page)).toEqual(["block4"]); expect(await getHighlightedBlockIds(page)).toEqual([]); - expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); + expect(await isWorkspaceFocused(page)).toBe(true); await openTrash(page); await getBlock(page, { type: "logic_boolean", workspace: "trash" }); From f2ae3e2d26bc6996fd548e0a781b9866cfee65e2 Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Mon, 10 Aug 2026 14:42:57 -0700 Subject: [PATCH 58/67] Blockly 13 moved navigateStacks from Blockly to navigator https://github.com/RaspberryPiFoundation/blockly/commit/05af6b6711b25d6fe51d87586454262428bef7b8 --- src/multiselect_navigation_policy.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/multiselect_navigation_policy.js b/src/multiselect_navigation_policy.js index bcb8b6b..2e8fa44 100644 --- a/src/multiselect_navigation_policy.js +++ b/src/multiselect_navigation_policy.js @@ -28,21 +28,21 @@ export class MultiselectNavigationPolicy { const { bottomBlock } = MultiselectNavigationPolicy.getSelectionBounds(selection); if (bottomBlock instanceof Blockly.comments.RenderedWorkspaceComment) { - return Blockly.navigateStacks(bottomBlock, 1); + return selection.workspace.getNavigator().navigateStacks(bottomBlock, 1); } const anchor = MultiselectNavigationPolicy.findStatementAncestor(bottomBlock); - return anchor.getNextBlock() || Blockly.navigateStacks(anchor, 1); + return anchor.getNextBlock() || selection.workspace.getNavigator().navigateStacks(anchor, 1); } getPreviousSibling(selection) { const { topBlock } = MultiselectNavigationPolicy.getSelectionBounds(selection); if (topBlock instanceof Blockly.comments.RenderedWorkspaceComment) { - return Blockly.navigateStacks(topBlock, -1); + return selection.workspace.getNavigator().navigateStacks(topBlock, -1); } const anchor = MultiselectNavigationPolicy.findStatementAncestor(topBlock); - return anchor.getPreviousBlock() || Blockly.navigateStacks(anchor, -1); + return anchor.getPreviousBlock() || selection.workspace.getNavigator().navigateStacks(anchor, -1); } isNavigable(selection) { @@ -128,7 +128,7 @@ export class MultiselectNavigationPolicy { const stacks = MultiselectNavigationPolicy.getSelectedStacks( Blockly.getSelected(), ); - workspace.getCursor().setCurNode(Blockly.navigateStacks(stacks[0], -1)); + workspace.getCursor().setCurNode(workspace.getNavigator().navigateStacks(stacks[0], -1)); return true; }, keyCodes: [Blockly.utils.KeyCodes.B], @@ -145,7 +145,7 @@ export class MultiselectNavigationPolicy { ); workspace .getCursor() - .setCurNode(Blockly.navigateStacks(stacks[stacks.length - 1], 1)); + .setCurNode(workspace.getNavigator().navigateStacks(stacks[stacks.length - 1], 1)); return true; }, keyCodes: [Blockly.utils.KeyCodes.N], From 5e00a8dd9f0bfb25d445f499b2317b8ff542a8b2 Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Mon, 10 Aug 2026 15:23:19 -0700 Subject: [PATCH 59/67] Blockly 13 navigates horizontally within rows https://github.com/RaspberryPiFoundation/blockly/commit/05af6b6711b25d6fe51d87586454262428bef7b8 --- src/multiselect_navigation_policy.js | 4 ++++ test-e2e/keyboard/multiselect/block.spec.ts | 12 ++++-------- test-e2e/keyboard/multiselect/comment.spec.ts | 12 ++++-------- test-e2e/keyboard/select/block.spec.ts | 17 ++++++----------- test-e2e/keyboard/select/comment.spec.ts | 17 ++++++----------- 5 files changed, 24 insertions(+), 38 deletions(-) diff --git a/src/multiselect_navigation_policy.js b/src/multiselect_navigation_policy.js index 2e8fa44..536059b 100644 --- a/src/multiselect_navigation_policy.js +++ b/src/multiselect_navigation_policy.js @@ -45,6 +45,10 @@ export class MultiselectNavigationPolicy { return anchor.getPreviousBlock() || selection.workspace.getNavigator().navigateStacks(anchor, -1); } + getRowId(selection) { + return selection.id; + } + isNavigable(selection) { return selection.canBeFocused(); } diff --git a/test-e2e/keyboard/multiselect/block.spec.ts b/test-e2e/keyboard/multiselect/block.spec.ts index f4491aa..17ebde5 100644 --- a/test-e2e/keyboard/multiselect/block.spec.ts +++ b/test-e2e/keyboard/multiselect/block.spec.ts @@ -3,7 +3,6 @@ import { cmdOrCtrl, cmdOrCtrlLabel, getBlock, - getFocusedField, getHighlightedBlockIds, getMultiselectDraggableId, getSelectedId, @@ -48,18 +47,15 @@ test("navigate down", async ({ page, act }) => { test("navigate left", async ({ page, act }) => { await act(page.keyboard.press("ArrowLeft")); - expect(await getHighlightedBlockIds(page)).toEqual([]); - expect(await getFocusedField(page)).toEqual({ - blockId: "block1", - name: "BOOL", - }); + expect(await getHighlightedBlockIds(page)).toEqual(["block2", "block3"]); + expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); }); test("navigate right", async ({ page, act }) => { await act(page.keyboard.press("ArrowRight")); - expect(await getHighlightedBlockIds(page)).toEqual(["block4"]); - expect(await getSelectedId(page)).toBe("block4"); + expect(await getHighlightedBlockIds(page)).toEqual(["block2", "block3"]); + expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); }); test("open context menu", async ({ page, act }) => { diff --git a/test-e2e/keyboard/multiselect/comment.spec.ts b/test-e2e/keyboard/multiselect/comment.spec.ts index 4f5950d..a85d091 100644 --- a/test-e2e/keyboard/multiselect/comment.spec.ts +++ b/test-e2e/keyboard/multiselect/comment.spec.ts @@ -3,7 +3,6 @@ import { cmdOrCtrl, cmdOrCtrlLabel, getComment, - getFocusedCommentButton, getHighlightedCommentIds, getMultiselectDraggableId, getSelectedId, @@ -48,18 +47,15 @@ test("navigate down", async ({ page, act }) => { test("navigate left", async ({ page, act }) => { await act(page.keyboard.press("ArrowLeft")); - expect(await getHighlightedCommentIds(page)).toEqual([]); - expect(await getFocusedCommentButton(page)).toEqual({ - commentId: "comment1", - type: "delete", - }); + expect(await getHighlightedCommentIds(page)).toEqual(["comment2", "comment3"]); + expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); }); test("navigate right", async ({ page, act }) => { await act(page.keyboard.press("ArrowRight")); - expect(await getHighlightedCommentIds(page)).toEqual(["comment4"]); - expect(await getSelectedId(page)).toBe("comment4"); + expect(await getHighlightedCommentIds(page)).toEqual(["comment2", "comment3"]); + expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); }); test("open context menu", async ({ page, act }) => { diff --git a/test-e2e/keyboard/select/block.spec.ts b/test-e2e/keyboard/select/block.spec.ts index f7d152f..6a758fe 100644 --- a/test-e2e/keyboard/select/block.spec.ts +++ b/test-e2e/keyboard/select/block.spec.ts @@ -39,17 +39,7 @@ test("navigate down", async ({ page, act }) => { expect(await getSelectedId(page)).toBe("block3"); }); -test("navigate left", async ({ page, act }) => { - await act(page.keyboard.press("ArrowLeft")); - - expect(await getHighlightedBlockIds(page)).toEqual([]); - expect(await getFocusedField(page)).toEqual({ - blockId: "block1", - name: "BOOL", - }); -}); - -test("navigate right", async ({ page, act }) => { +test("navigate right and left", async ({ page, act }) => { await act(page.keyboard.press("ArrowRight")); expect(await getHighlightedBlockIds(page)).toEqual([]); @@ -57,6 +47,11 @@ test("navigate right", async ({ page, act }) => { blockId: "block2", name: "NUM", }); + + await act(page.keyboard.press("ArrowLeft")); + + expect(await getHighlightedBlockIds(page)).toEqual(["block2"]); + expect(await getSelectedId(page)).toBe("block2"); }); test("unconstrained move", async ({ page, act }) => { diff --git a/test-e2e/keyboard/select/comment.spec.ts b/test-e2e/keyboard/select/comment.spec.ts index 4e742e5..8927463 100644 --- a/test-e2e/keyboard/select/comment.spec.ts +++ b/test-e2e/keyboard/select/comment.spec.ts @@ -39,17 +39,7 @@ test("navigate down", async ({ page, act }) => { expect(await getSelectedId(page)).toBe("comment3"); }); -test("navigate left", async ({ page, act }) => { - await act(page.keyboard.press("ArrowLeft")); - - expect(await getHighlightedCommentIds(page)).toEqual([]); - expect(await getFocusedCommentButton(page)).toEqual({ - commentId: "comment1", - type: "delete", - }); -}); - -test("navigate right", async ({ page, act }) => { +test("navigate right and left", async ({ page, act }) => { await act(page.keyboard.press("ArrowRight")); expect(await getHighlightedCommentIds(page)).toEqual([]); @@ -57,6 +47,11 @@ test("navigate right", async ({ page, act }) => { commentId: "comment2", type: "collapse", }); + + await act(page.keyboard.press("ArrowLeft")); + + expect(await getHighlightedCommentIds(page)).toEqual(["comment2"]); + expect(await getSelectedId(page)).toBe("comment2"); }); test("constrained move", async ({ page, act }) => { From ea451e81ee96d290d6887d46e20141e39b25ea02 Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Tue, 11 Aug 2026 09:49:17 -0700 Subject: [PATCH 60/67] Blockly 13 replaced workspace cursor with focus navigation https://github.com/RaspberryPiFoundation/blockly/commit/05af6b6711b25d6fe51d87586454262428bef7b8 --- src/multiselect_navigation_policy.js | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/multiselect_navigation_policy.js b/src/multiselect_navigation_policy.js index 536059b..62f2145 100644 --- a/src/multiselect_navigation_policy.js +++ b/src/multiselect_navigation_policy.js @@ -123,6 +123,21 @@ export class MultiselectNavigationPolicy { } install() { + Blockly.ShortcutRegistry.registry.register({ + name: "multiselect_navigate_up", + allowCollision: true, + preconditionFn: () => + Blockly.getSelected() instanceof MultiselectDraggable && + !Blockly.KeyboardMover.mover.isMoving(), + callback: (workspace) => { + const { topBlock } = MultiselectNavigationPolicy.getSelectionBounds(Blockly.getSelected()); + const previousNode = workspace.getNavigator().getPreviousNode(topBlock); + if (previousNode) Blockly.getFocusManager().focusNode(previousNode); + return true; + }, + keyCodes: [Blockly.utils.KeyCodes.UP], + }); + Blockly.ShortcutRegistry.registry.register({ name: "multiselect_previous_stack", allowCollision: true, @@ -132,7 +147,8 @@ export class MultiselectNavigationPolicy { const stacks = MultiselectNavigationPolicy.getSelectedStacks( Blockly.getSelected(), ); - workspace.getCursor().setCurNode(workspace.getNavigator().navigateStacks(stacks[0], -1)); + const previousStack = workspace.getNavigator().navigateStacks(stacks[0], -1); + if (previousStack) Blockly.getFocusManager().focusNode(previousStack); return true; }, keyCodes: [Blockly.utils.KeyCodes.B], @@ -147,9 +163,8 @@ export class MultiselectNavigationPolicy { const stacks = MultiselectNavigationPolicy.getSelectedStacks( Blockly.getSelected(), ); - workspace - .getCursor() - .setCurNode(workspace.getNavigator().navigateStacks(stacks[stacks.length - 1], 1)); + const nextStack = workspace.getNavigator().navigateStacks(stacks[stacks.length - 1], 1); + if (nextStack) Blockly.getFocusManager().focusNode(nextStack); return true; }, keyCodes: [Blockly.utils.KeyCodes.N], @@ -157,6 +172,7 @@ export class MultiselectNavigationPolicy { } uninstall() { + Blockly.ShortcutRegistry.registry.unregister("multiselect_navigate_up"); Blockly.ShortcutRegistry.registry.unregister("multiselect_previous_stack"); Blockly.ShortcutRegistry.registry.unregister("multiselect_next_stack"); } From e36908d96bbaa795d0500766661052721b6ed587 Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Tue, 11 Aug 2026 10:40:03 -0700 Subject: [PATCH 61/67] Blockly 13 renamed Ctrl shortcut text to Control https://github.com/RaspberryPiFoundation/blockly/commit/41319baf5d8ec4714858f70b314ec62a934d4f93 --- test-e2e/test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-e2e/test.ts b/test-e2e/test.ts index 7a1f2a1..5183bd8 100644 --- a/test-e2e/test.ts +++ b/test-e2e/test.ts @@ -40,7 +40,7 @@ export const cmdOrCtrl = (key: string): string => `${process.platform === "darwin" ? "Meta" : "Control"}+${key}`; export const cmdOrCtrlLabel = (key: string): string => - `${process.platform === "darwin" ? "⌘" : "Ctrl +"} ${key}`; + `${process.platform === "darwin" ? "⌘" : "Control +"} ${key}`; export const test = base.extend<{ act: Act }>({ page: async ( From 24c3418a9086116018ecf386a23e340b73bb7fc6 Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Tue, 11 Aug 2026 14:58:12 -0700 Subject: [PATCH 62/67] Blockly 13 added workspace focus target https://github.com/RaspberryPiFoundation/blockly/commit/9734a633610b2f3a672595a2d694ff7aa97875ae --- src/multiselect_contextmenu.js | 10 +++++----- src/multiselect_controls.js | 6 +++--- src/multiselect_shortcut.js | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/multiselect_contextmenu.js b/src/multiselect_contextmenu.js index bab4971..5ff7204 100644 --- a/src/multiselect_contextmenu.js +++ b/src/multiselect_contextmenu.js @@ -242,7 +242,7 @@ const registerDuplicate = function() { }); dragSelection.clear(); multiDraggable.clearAll_(); - Blockly.getFocusManager().focusTree(workspace); + Blockly.getFocusManager().focusNode(workspace.getWorkspaceFocusTarget()); } else { apply(scope.block); } @@ -739,7 +739,7 @@ const registerDelete = function() { apply(selected); } - Blockly.getFocusManager().focusTree(workspace); + Blockly.getFocusManager().focusNode(workspace.getWorkspaceFocusTarget()); Blockly.Events.setGroup(false); }, scopeType: Blockly.ContextMenuRegistry.ScopeType.BLOCK, @@ -901,7 +901,7 @@ const registerSelectAll = function() { } else { Blockly.getSelected().unselect(); } - Blockly.getFocusManager().focusTree(scope.workspace); + Blockly.getFocusManager().focusNode(scope.workspace.getWorkspaceFocusTarget()); multiDraggable.clearAll_(); dragSelectionWeakMap.get(scope.workspace).clear(); } @@ -1109,7 +1109,7 @@ const registerCommentDelete = function() { apply(scope.comment); } - Blockly.getFocusManager().focusTree(workspace); + Blockly.getFocusManager().focusNode(workspace.getWorkspaceFocusTarget()); Blockly.Events.setGroup(false); }, scopeType: Blockly.ContextMenuRegistry.ScopeType.COMMENT, @@ -1193,7 +1193,7 @@ const registerCommentDuplicate = function() { }); dragSelection.clear(); multiDraggable.clearAll_(); - Blockly.getFocusManager().focusTree(workspace); + Blockly.getFocusManager().focusNode(workspace.getWorkspaceFocusTarget()); } else { apply(scope.comment); } diff --git a/src/multiselect_controls.js b/src/multiselect_controls.js index a8d8f79..87134c1 100644 --- a/src/multiselect_controls.js +++ b/src/multiselect_controls.js @@ -410,7 +410,7 @@ export class MultiselectControls { Blockly.common.setSelected(getByID(this.workspace_, this.dragSelection.values().next().value)); } else if (this.dragSelection.size === 0 && Blockly.getSelected() !== null) { - Blockly.getFocusManager().focusTree(this.workspace_); + Blockly.getFocusManager().focusNode(this.workspace_.getWorkspaceFocusTarget()); } else if (this.lastSelectedElement_ && !inPasteShortcut.get(this.workspace_)) { this.updateDraggables_(this.lastSelectedElement_); @@ -424,7 +424,7 @@ export class MultiselectControls { Blockly.common.setSelected(getByID(this.workspace_, this.dragSelection.values().next().value)); } else if (this.dragSelection.size === 0 && Blockly.getSelected() !== null) { - Blockly.getFocusManager().focusTree(this.workspace_); + Blockly.getFocusManager().focusNode(this.workspace_.getWorkspaceFocusTarget()); } } else if (!this.dragSelection.size && !(Blockly.getSelected() instanceof MultiselectDraggable)) { @@ -548,7 +548,7 @@ export class MultiselectControls { Blockly.common.setSelected(getByID(this.workspace_, this.dragSelection.values().next().value)); } else if (this.dragSelection.size === 0 && Blockly.getSelected() !== null) { - Blockly.getFocusManager().focusTree(this.workspace_); + Blockly.getFocusManager().focusNode(this.workspace_.getWorkspaceFocusTarget()); } if (this.hasDisableWorkspaceDrag_) { this.workspace_.options.moveOptions.drag = true; diff --git a/src/multiselect_shortcut.js b/src/multiselect_shortcut.js index 1de6cdd..1f9f32a 100644 --- a/src/multiselect_shortcut.js +++ b/src/multiselect_shortcut.js @@ -503,7 +503,7 @@ export const registerDuplicateShortcut = function() { }); dragSelection.clear(); multiDraggable.clearAll_(); - Blockly.getFocusManager().focusTree(workspace); + Blockly.getFocusManager().focusNode(workspace.getWorkspaceFocusTarget()); } else { apply(selected); } @@ -584,7 +584,7 @@ const registerSelectAll = function() { } else { Blockly.getSelected().unselect(); } - Blockly.getFocusManager().focusTree(workspace); + Blockly.getFocusManager().focusNode(workspace.getWorkspaceFocusTarget()); multiDraggable.clearAll_(); dragSelectionWeakMap.get(workspace).clear(); } From 58abfcb1461137633b01d1240eb31823033343ee Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Tue, 11 Aug 2026 14:20:29 -0700 Subject: [PATCH 63/67] Blockly 13 added stack move shortcut https://github.com/RaspberryPiFoundation/blockly/commit/3044298f990b24e3b4d41968aabd44e5e6e070bb --- src/multiselect_controls.js | 5 ++ test-e2e/keyboard/select/stack.spec.ts | 87 ++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/src/multiselect_controls.js b/src/multiselect_controls.js index 87134c1..2ce37e0 100644 --- a/src/multiselect_controls.js +++ b/src/multiselect_controls.js @@ -524,6 +524,11 @@ export class MultiselectControls { } this.updateMultiselectIcon(true); inMultipleSelectionModeWeakMap.set(this.workspace_, true); + const selected = Blockly.getSelected(); + if (selected && !(selected instanceof MultiselectDraggable) && !this.dragSelection.has(selected.id)) { + this.updateDraggables_(selected); + this.lastSelectedElement_ = null; + } } /** diff --git a/test-e2e/keyboard/select/stack.spec.ts b/test-e2e/keyboard/select/stack.spec.ts index 3ce8382..b7f0e4c 100644 --- a/test-e2e/keyboard/select/stack.spec.ts +++ b/test-e2e/keyboard/select/stack.spec.ts @@ -149,3 +149,90 @@ test("abort unconstrained move", async ({ page, act }) => { "stack2_bottom", ]); }); + +test("constrained stack move", async ({ page, act }) => { + await act(page.keyboard.press("Shift+M")); + await expect(page.locator(".blocklyMoveIndicator")).toBeVisible(); + await act(page.keyboard.press("ArrowDown")); + await act(page.keyboard.press("Enter")); + await expect(page.locator(".blocklyMoveIndicator")).not.toBeVisible(); + + expect(await getStackBlockIds(page, "stack2_top")).toEqual(["stack2_top"]); + expect(await getStackBlockIds(page, "stack2_middle")).toEqual([ + "stack2_middle", + "stack2_bottom", + ]); +}); + +test("abort constrained stack move", async ({ page, act }) => { + await act(page.keyboard.press("Shift+M")); + await expect(page.locator(".blocklyMoveIndicator")).toBeVisible(); + await act(page.keyboard.press("ArrowDown")); + await act(page.keyboard.press("Escape")); + await expect(page.locator(".blocklyMoveIndicator")).not.toBeVisible(); + + expect(await getStackBlockIds(page, "stack2_top")).toEqual([ + "stack2_top", + "stack2_middle", + "stack2_bottom", + ]); +}); + +test("unconstrained stack move", async ({ page, act }) => { + const gridSpacing = await getGridSpacing(page); + if (gridSpacing === null) throw new Error("Workspace has no grid"); + const stack1BoundsStart = (await getBlock(page, { id: "stack1" })).bounds; + const stack2TopBoundsStart = (await getBlock(page, { id: "stack2_top" })) + .bounds; + const stack3BoundsStart = (await getBlock(page, { id: "stack3" })).bounds; + + await act(page.keyboard.press("Shift+M")); + await expect(page.locator(".blocklyMoveIndicator")).toBeVisible(); + for (let i = 0; i < 3; i++) { + await act(page.keyboard.press(cmdOrCtrl("ArrowRight"))); + } + await act(page.keyboard.press("Enter")); + await expect(page.locator(".blocklyMoveIndicator")).not.toBeVisible(); + + expect(await getStackBlockIds(page, "stack2_top")).toEqual(["stack2_top"]); + expect(await getStackBlockIds(page, "stack2_middle")).toEqual([ + "stack2_middle", + "stack2_bottom", + ]); + const stack2MiddleBoundsEnd = (await getBlock(page, { id: "stack2_middle" })) + .bounds; + const stack2BottomBoundsEnd = (await getBlock(page, { id: "stack2_bottom" })) + .bounds; + expect(stack2MiddleBoundsEnd.left - stack2TopBoundsStart.left).toBeCloseTo( + 3 * gridSpacing, + ); + expect(stack2MiddleBoundsEnd.top - stack2TopBoundsStart.top).toBeCloseTo( + gridSpacing, + ); + expect(stack2BottomBoundsEnd.left).toBeCloseTo(stack2MiddleBoundsEnd.left); + expect(stack2BottomBoundsEnd.top).toBeGreaterThan(stack2MiddleBoundsEnd.top); + const stack1BoundsEnd = (await getBlock(page, { id: "stack1" })).bounds; + expect(stack1BoundsEnd.left).toBeCloseTo(stack1BoundsStart.left); + expect(stack1BoundsEnd.top).toBeCloseTo(stack1BoundsStart.top); + const stack3BoundsEnd = (await getBlock(page, { id: "stack3" })).bounds; + expect(stack3BoundsEnd.left).toBeCloseTo(stack3BoundsStart.left); + expect(stack3BoundsEnd.top).toBeCloseTo(stack3BoundsStart.top); + expect(await getHighlightedBlockIds(page)).toEqual(["stack2_middle"]); + expect(await getSelectedId(page)).toBe("stack2_middle"); +}); + +test("abort unconstrained stack move", async ({ page, act }) => { + await act(page.keyboard.press("Shift+M")); + await expect(page.locator(".blocklyMoveIndicator")).toBeVisible(); + for (let i = 0; i < 3; i++) { + await act(page.keyboard.press(cmdOrCtrl("ArrowRight"))); + } + await act(page.keyboard.press("Escape")); + await expect(page.locator(".blocklyMoveIndicator")).not.toBeVisible(); + + expect(await getStackBlockIds(page, "stack2_top")).toEqual([ + "stack2_top", + "stack2_middle", + "stack2_bottom", + ]); +}); From ec7edcde74f555426e5dc8999767f1cf97ebfed5 Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Wed, 12 Aug 2026 09:27:24 -0700 Subject: [PATCH 64/67] Blockly 13 added duplicate shortcut to core https://github.com/RaspberryPiFoundation/blockly/commit/3b9ed58f7103eaba5895f2e463ffa7fa6f2e832b --- src/multiselect.js | 2 -- src/multiselect_shortcut.js | 29 +++++++++-------------------- 2 files changed, 9 insertions(+), 22 deletions(-) diff --git a/src/multiselect.js b/src/multiselect.js index 6881518..256a90a 100644 --- a/src/multiselect.js +++ b/src/multiselect.js @@ -140,7 +140,6 @@ export class Multiselect { Blockly.BlockSvg.prototype.bumpNeighbours = function() {}; } - Shortcut.registerDuplicateShortcut(); } /** @@ -207,7 +206,6 @@ export class Multiselect { Blockly.ContextMenuRegistry.registry.unregister('copy_to_backpack'); ContextMenu.registerOrigContextMenu(); - Shortcut.unregisterDuplicateShortcut(); Shortcut.unregisterOrigShortcut(); Blockly.ShortcutRegistry.registry.unregister('selectall'); Shortcut.registerOrigShortcut(); diff --git a/src/multiselect_shortcut.js b/src/multiselect_shortcut.js index 1f9f32a..01e7612 100644 --- a/src/multiselect_shortcut.js +++ b/src/multiselect_shortcut.js @@ -16,8 +16,6 @@ import { } from './global'; import {MultiselectDraggable} from './multiselect_draggable'; -let originalDuplicateShortcut = null; - /** * Modification for keyboard shortcut 'Delete' to be available * for multiple blocks. @@ -429,10 +427,8 @@ const registerPaste = function(useCopyPasteCrossTab) { * Modification for keyboard shortcut 'Duplicate' to be available * for multiple blocks or comments. */ -export const registerDuplicateShortcut = function() { - const name = 'duplicate'; - originalDuplicateShortcut = - Blockly.ShortcutRegistry.registry.getRegistry()[name] || null; +const registerDuplicateShortcut = function() { + const name = Blockly.ShortcutItems.names.DUPLICATE; const duplicateShortcut = { name, preconditionFn: function(workspace) { @@ -630,7 +626,8 @@ export const unregisterOrigShortcut = function() { registeredShortcut.length = 0; for (const name of [Blockly.ShortcutItems.names.DELETE, Blockly.ShortcutItems.names.COPY, - Blockly.ShortcutItems.names.CUT, Blockly.ShortcutItems.names.PASTE]) { + Blockly.ShortcutItems.names.CUT, Blockly.ShortcutItems.names.PASTE, + Blockly.ShortcutItems.names.DUPLICATE]) { if (Object.entries(Blockly.ShortcutRegistry.registry.getRegistry()) .map(([_, value]) => value.name).includes(name)) { Blockly.ShortcutRegistry.registry.unregister(name); @@ -643,7 +640,8 @@ export const unregisterOurShortcut = function() { registeredShortcut.length = 0; for (const name of [Blockly.ShortcutItems.names.DELETE, Blockly.ShortcutItems.names.COPY, - Blockly.ShortcutItems.names.CUT, Blockly.ShortcutItems.names.PASTE]) { + Blockly.ShortcutItems.names.CUT, Blockly.ShortcutItems.names.PASTE, + Blockly.ShortcutItems.names.DUPLICATE]) { if (Object.entries(Blockly.ShortcutRegistry.registry.getRegistry()) .map(([_, value]) => value.name).includes(name)) { Blockly.ShortcutRegistry.registry.unregister(name); @@ -652,17 +650,6 @@ export const unregisterOurShortcut = function() { } }; -export const unregisterDuplicateShortcut = function() { - const name = 'duplicate'; - if (name in Blockly.ShortcutRegistry.registry.getRegistry()) { - Blockly.ShortcutRegistry.registry.unregister(name); - } - if (originalDuplicateShortcut) { - Blockly.ShortcutRegistry.registry.register(originalDuplicateShortcut); - originalDuplicateShortcut = null; - } -}; - /** * Register default keyboard shortcut item. */ @@ -672,6 +659,7 @@ export const registerOrigShortcut = function() { [Blockly.ShortcutItems.names.COPY]: Blockly.ShortcutItems.registerCopy, [Blockly.ShortcutItems.names.CUT]: Blockly.ShortcutItems.registerCut, [Blockly.ShortcutItems.names.PASTE]: Blockly.ShortcutItems.registerPaste, + [Blockly.ShortcutItems.names.DUPLICATE]: Blockly.ShortcutItems.registerDuplicate, }; for (const name of registeredShortcut) { map[name](); @@ -683,12 +671,13 @@ export const registerOrigShortcut = function() { * @param {boolean} useCopyPasteCrossTab Whether to use copy/paste cross tab. */ export const registerOurShortcut = function(useCopyPasteCrossTab) { - const ListNoParameter = [Blockly.ShortcutItems.names.DELETE]; + const ListNoParameter = [Blockly.ShortcutItems.names.DELETE, Blockly.ShortcutItems.names.DUPLICATE]; const map = { [Blockly.ShortcutItems.names.DELETE]: registerShortcutDelete, [Blockly.ShortcutItems.names.COPY]: registerCopy, [Blockly.ShortcutItems.names.CUT]: registerCut, [Blockly.ShortcutItems.names.PASTE]: registerPaste, + [Blockly.ShortcutItems.names.DUPLICATE]: registerDuplicateShortcut, }; for (const name of registeredShortcut) { if (ListNoParameter.includes(name)) { From f845667b96433cee0a5f4928d87eb2aa92ff0326 Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Wed, 12 Aug 2026 10:40:43 -0700 Subject: [PATCH 65/67] Blockly 13 moves draggables instead of blocks and comments https://github.com/RaspberryPiFoundation/blockly/commit/da1db45dd257b2e033ef5fb8e4bae2f48d0da83b --- src/multiselect_navigation_policy.js | 25 +++++++++++++++++++ test-e2e/keyboard/multiselect/block.spec.ts | 16 ++++++++++++ test-e2e/keyboard/multiselect/comment.spec.ts | 16 ++++++++++++ 3 files changed, 57 insertions(+) diff --git a/src/multiselect_navigation_policy.js b/src/multiselect_navigation_policy.js index 62f2145..2fbc2e0 100644 --- a/src/multiselect_navigation_policy.js +++ b/src/multiselect_navigation_policy.js @@ -169,11 +169,36 @@ export class MultiselectNavigationPolicy { }, keyCodes: [Blockly.utils.KeyCodes.N], }); + + Blockly.ShortcutRegistry.registry.register({ + name: "multiselect_move_disabled", + allowCollision: true, + preconditionFn: () => + Blockly.getSelected() instanceof MultiselectDraggable, + callback: () => true, + keyCodes: [Blockly.utils.KeyCodes.M], + }); + + Blockly.ShortcutRegistry.registry.register({ + name: "multiselect_stack_move_disabled", + allowCollision: true, + preconditionFn: () => + Blockly.getSelected() instanceof MultiselectDraggable, + callback: () => true, + keyCodes: [ + Blockly.ShortcutRegistry.registry.createSerializedKey( + Blockly.utils.KeyCodes.M, + [Blockly.utils.KeyCodes.SHIFT], + ), + ], + }); } uninstall() { Blockly.ShortcutRegistry.registry.unregister("multiselect_navigate_up"); Blockly.ShortcutRegistry.registry.unregister("multiselect_previous_stack"); Blockly.ShortcutRegistry.registry.unregister("multiselect_next_stack"); + Blockly.ShortcutRegistry.registry.unregister("multiselect_move_disabled"); + Blockly.ShortcutRegistry.registry.unregister("multiselect_stack_move_disabled"); } } diff --git a/test-e2e/keyboard/multiselect/block.spec.ts b/test-e2e/keyboard/multiselect/block.spec.ts index 17ebde5..164c238 100644 --- a/test-e2e/keyboard/multiselect/block.spec.ts +++ b/test-e2e/keyboard/multiselect/block.spec.ts @@ -58,6 +58,22 @@ test("navigate right", async ({ page, act }) => { expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); }); +test("move is disabled", async ({ page, act }) => { + await act(page.keyboard.press("M")); + + await expect(page.locator(".blocklyMoveIndicator")).not.toBeVisible(); + expect(await getHighlightedBlockIds(page)).toEqual(["block2", "block3"]); + expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); +}); + +test("stack move is disabled", async ({ page, act }) => { + await act(page.keyboard.press("Shift+M")); + + await expect(page.locator(".blocklyMoveIndicator")).not.toBeVisible(); + expect(await getHighlightedBlockIds(page)).toEqual(["block2", "block3"]); + expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); +}); + test("open context menu", async ({ page, act }) => { await act(page.keyboard.press(cmdOrCtrl("Enter"))); diff --git a/test-e2e/keyboard/multiselect/comment.spec.ts b/test-e2e/keyboard/multiselect/comment.spec.ts index a85d091..2efa991 100644 --- a/test-e2e/keyboard/multiselect/comment.spec.ts +++ b/test-e2e/keyboard/multiselect/comment.spec.ts @@ -58,6 +58,22 @@ test("navigate right", async ({ page, act }) => { expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); }); +test("move is disabled", async ({ page, act }) => { + await act(page.keyboard.press("M")); + + await expect(page.locator(".blocklyMoveIndicator")).not.toBeVisible(); + expect(await getHighlightedCommentIds(page)).toEqual(["comment2", "comment3"]); + expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); +}); + +test("stack move is disabled", async ({ page, act }) => { + await act(page.keyboard.press("Shift+M")); + + await expect(page.locator(".blocklyMoveIndicator")).not.toBeVisible(); + expect(await getHighlightedCommentIds(page)).toEqual(["comment2", "comment3"]); + expect(await getSelectedId(page)).toBe(await getMultiselectDraggableId(page)); +}); + test("open context menu", async ({ page, act }) => { await act(page.keyboard.press(cmdOrCtrl("Enter"))); From c24118084a0b02bcc0a3910e7b582e77452ed80f Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Tue, 15 Sep 2026 13:17:54 -0700 Subject: [PATCH 66/67] Blockly 13 makes comment button object IDs match their DOM IDs https://github.com/RaspberryPiFoundation/blockly/commit/e3b6810dcdb75deb3e76407fa562cdf857459d6d --- test-e2e/test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-e2e/test.ts b/test-e2e/test.ts index 5183bd8..6298dd8 100644 --- a/test-e2e/test.ts +++ b/test-e2e/test.ts @@ -323,7 +323,7 @@ export const getFocusedCommentButton = ( const focusedNode = Blockly.getFocusManager().getFocusedNode(); if (!(focusedNode instanceof Blockly.comments.CommentBarButton)) return null; - const commentId = (focusedNode as unknown as { id: string }).id; + const commentId = focusedNode.getCommentView().commentId; if (focusedNode instanceof Blockly.comments.CollapseCommentBarButton) { return { commentId, type: "collapse" as const }; } From 4ae05c11625970f032eec05923cd05504c1b96f1 Mon Sep 17 00:00:00 2001 From: Michael Gallagher Date: Tue, 15 Sep 2026 13:52:30 -0700 Subject: [PATCH 67/67] Blockly 13 moves dropdowns into the workspace container https://github.com/RaspberryPiFoundation/blockly/commit/09d19d8f7bae9f181852285865a71f9bd7f0f300 https://github.com/RaspberryPiFoundation/blockly/commit/8946e47f04c8b37672609c7ca1aadcc84b960eed --- src/multiselect.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/multiselect.js b/src/multiselect.js index 256a90a..7b508b4 100644 --- a/src/multiselect.js +++ b/src/multiselect.js @@ -78,9 +78,7 @@ export class Multiselect { } } }); - this.dropdownDivObserver_.observe( - document.querySelector('.blocklyDropDownDiv').parentNode, - {childList: true}); + this.dropdownDivObserver_.observe(injectionDiv, {childList: true}); injectionDiv.addEventListener('mouseenter', () => { if (options.workspaceAutoFocus === false || Blockly.getFocusManager().getFocusedTree() === this.workspace_) {