Repository: web-scrobbler/web-scrobbler
Pull request #4421 (2024-01-13 16:32:03)

Add connector for Zing MP3
Connector for https://zingmp3.vn/

Related to #3404


Patch:
diff --git a/.gitignore b/.gitignore
index 96607c71..c716680c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,3 +30,7 @@ coverage/
 
 # Cache
 .eslintcache
+
+# Yarn
+yarn.lock
+
diff --git a/.tx/config b/.tx/config
index d197a136..69187370 100644
--- a/.tx/config
+++ b/.tx/config
@@ -1,5 +1,5 @@
 [main]
-host = https://www.transifex.com
+host = https://app.transifex.com
 
 [o:web-scrobbler:p:web-scrobbler:r:messages-json--master]
 file_filter   = src/_locales/<lang>/messages.json
diff --git a/package-lock.json b/package-lock.json
index b60077bc..fb249ecd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,19 +1,18 @@
 {
   "name": "web-scrobbler",
-  "version": "3.4.0",
+  "version": "3.5.0",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "web-scrobbler",
-      "version": "3.4.0",
+      "version": "3.5.0",
       "license": "MIT",
       "dependencies": {
-        "@suid/icons-material": "0.6.11",
         "@web-scrobbler/metadata-filter": "3.1.0",
         "blueimp-md5": "2.19.0",
         "stylelint": "15.11.0",
-        "vite": "4.5.1",
+        "vite": "5.0.12",
         "webextension-polyfill": "0.10.0"
       },
       "devDependencies": {
@@ -22,10 +21,10 @@
         "@types/fs-extra": "11.0.3",
         "@types/imagemin": "8.0.5",
         "@types/imagemin-jpegtran": "5.0.4",
-        "@types/node": "20.10.6",
-        "@types/react": "18.2.47",
+        "@types/node": "20.11.5",
+        "@types/react": "18.2.48",
         "@types/webextension-polyfill": "0.10.7",
-        "@typescript-eslint/eslint-plugin": "6.18.0",
+        "@typescript-eslint/eslint-plugin": "6.19.0",
         "@web-scrobbler/eslint-config": "3.0.0",
         "@web-scrobbler/stylelint-config": "1.0.1",
         "archiver": "6.0.1",
@@ -46,14 +45,14 @@
         "remark": "15.0.1",
         "remark-cli": "12.0.0",
         "remark-preset-lint-recommended": "6.1.3",
-        "sass": "1.69.7",
-        "solid-js": "1.8.8",
+        "sass": "1.70.0",
+        "solid-js": "1.8.11",
         "tsx": "4.7.0",
         "typescript": "5.3.3",
         "vite-plugin-conditional-compiler": "0.2.1",
-        "vite-plugin-solid": "2.8.0",
-        "vite-plugin-static-copy": "0.17.1",
-        "vitest": "0.34.6"
+        "vite-plugin-solid": "2.8.2",
+        "vite-plugin-static-copy": "1.0.0",
+        "vitest": "1.2.1"
       },
       "engines": {
         "node": ">=18.0.0"
@@ -206,12 +205,12 @@
       }
     },
     "node_modules/@babel/generator": {
-      "version": "7.23.5",
-      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.5.tgz",
-      "integrity": "sha512-BPssCHrBD+0YrxviOa3QzpqwhNIXKEtOa2jQrm4FlmkC2apYgRnQcmPWiGZDlGxiNtltnUFolMe8497Esry+jA==",
+      "version": "7.23.6",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz",
+      "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==",
       "dev": true,
       "dependencies": {
-        "@babel/types": "^7.23.5",
+        "@babel/types": "^7.23.6",
         "@jridgewell/gen-mapping": "^0.3.2",
         "@jridgewell/trace-mapping": "^0.3.17",
         "jsesc": "^2.5.1"
@@ -726,9 +725,9 @@
       }
     },
     "node_modules/@babel/types": {
-      "version": "7.23.5",
-      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.5.tgz",
-      "integrity": "sha512-ON5kSOJwVO6xXVRTvOI0eOnWe7VdUcIpsovGo9U/Br4Ie4UVFQTboO2cYnDhAGU6Fp+UxSiT+pMft0SMHfuq6w==",
+      "version": "7.23.6",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz",
+      "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==",
       "dev": true,
       "dependencies": {
         "@babel/helper-string-parser": "^7.23.4",
@@ -843,7 +842,6 @@
       "cpu": [
         "ppc64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "aix"
@@ -859,7 +857,6 @@
       "cpu": [
         "arm"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "android"
@@ -875,7 +872,6 @@
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "android"
@@ -891,7 +887,6 @@
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "android"
@@ -907,7 +902,6 @@
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "darwin"
@@ -923,7 +917,6 @@
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "darwin"
@@ -939,7 +932,6 @@
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "freebsd"
@@ -955,7 +947,6 @@
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "freebsd"
@@ -971,7 +962,6 @@
       "cpu": [
         "arm"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "linux"
@@ -987,7 +977,6 @@
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "linux"
@@ -1003,7 +992,6 @@
       "cpu": [
         "ia32"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "linux"
@@ -1019,7 +1007,6 @@
       "cpu": [
         "loong64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "linux"
@@ -1035,7 +1022,6 @@
       "cpu": [
         "mips64el"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "linux"
@@ -1051,7 +1037,6 @@
       "cpu": [
         "ppc64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "linux"
@@ -1067,7 +1052,6 @@
       "cpu": [
         "riscv64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "linux"
@@ -1083,7 +1067,6 @@
       "cpu": [
         "s390x"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "linux"
@@ -1099,7 +1082,6 @@
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "linux"
@@ -1115,7 +1097,6 @@
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "netbsd"
@@ -1131,7 +1112,6 @@
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "openbsd"
@@ -1147,7 +1127,6 @@
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "sunos"
@@ -1163,7 +1142,6 @@
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "win32"
@@ -1179,7 +1157,6 @@
       "cpu": [
         "ia32"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "win32"
@@ -1195,7 +1172,6 @@
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "win32"
@@ -1590,14 +1566,161 @@
         "node": ">=14"
       }
     },
-    "node_modules/@popperjs/core": {
-      "version": "2.11.8",
-      "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
-      "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/popperjs"
-      }
+    "node_modules/@rollup/rollup-android-arm-eabi": {
+      "version": "4.9.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.4.tgz",
+      "integrity": "sha512-ub/SN3yWqIv5CWiAZPHVS1DloyZsJbtXmX4HxUTIpS0BHm9pW5iYBo2mIZi+hE3AeiTzHz33blwSnhdUo+9NpA==",
+      "cpu": [
+        "arm"
+      ],
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-android-arm64": {
+      "version": "4.9.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.4.tgz",
+      "integrity": "sha512-ehcBrOR5XTl0W0t2WxfTyHCR/3Cq2jfb+I4W+Ch8Y9b5G+vbAecVv0Fx/J1QKktOrgUYsIKxWAKgIpvw56IFNA==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-arm64": {
+      "version": "4.9.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.4.tgz",
+      "integrity": "sha512-1fzh1lWExwSTWy8vJPnNbNM02WZDS8AW3McEOb7wW+nPChLKf3WG2aG7fhaUmfX5FKw9zhsF5+MBwArGyNM7NA==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-x64": {
+      "version": "4.9.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.4.tgz",
+      "integrity": "sha512-Gc6cukkF38RcYQ6uPdiXi70JB0f29CwcQ7+r4QpfNpQFVHXRd0DfWFidoGxjSx1DwOETM97JPz1RXL5ISSB0pA==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+      "version": "4.9.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.4.tgz",
+      "integrity": "sha512-g21RTeFzoTl8GxosHbnQZ0/JkuFIB13C3T7Y0HtKzOXmoHhewLbVTFBQZu+z5m9STH6FZ7L/oPgU4Nm5ErN2fw==",
+      "cpu": [
+        "arm"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-gnu": {
+      "version": "4.9.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.4.tgz",
+      "integrity": "sha512-TVYVWD/SYwWzGGnbfTkrNpdE4HON46orgMNHCivlXmlsSGQOx/OHHYiQcMIOx38/GWgwr/po2LBn7wypkWw/Mg==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-musl": {
+      "version": "4.9.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.4.tgz",
+      "integrity": "sha512-XcKvuendwizYYhFxpvQ3xVpzje2HHImzg33wL9zvxtj77HvPStbSGI9czrdbfrf8DGMcNNReH9pVZv8qejAQ5A==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+      "version": "4.9.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.4.tgz",
+      "integrity": "sha512-LFHS/8Q+I9YA0yVETyjonMJ3UA+DczeBd/MqNEzsGSTdNvSJa1OJZcSH8GiXLvcizgp9AlHs2walqRcqzjOi3A==",
+      "cpu": [
+        "riscv64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-gnu": {
+      "version": "4.9.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.4.tgz",
+      "integrity": "sha512-dIYgo+j1+yfy81i0YVU5KnQrIJZE8ERomx17ReU4GREjGtDW4X+nvkBak2xAUpyqLs4eleDSj3RrV72fQos7zw==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-musl": {
+      "version": "4.9.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.4.tgz",
+      "integrity": "sha512-RoaYxjdHQ5TPjaPrLsfKqR3pakMr3JGqZ+jZM0zP2IkDtsGa4CqYaWSfQmZVgFUCgLrTnzX+cnHS3nfl+kB6ZQ==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-arm64-msvc": {
+      "version": "4.9.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.4.tgz",
+      "integrity": "sha512-T8Q3XHV+Jjf5e49B4EAaLKV74BbX7/qYBRQ8Wop/+TyyU0k+vSjiLVSHNWdVd1goMjZcbhDmYZUYW5RFqkBNHQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-ia32-msvc": {
+      "version": "4.9.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.4.tgz",
+      "integrity": "sha512-z+JQ7JirDUHAsMecVydnBPWLwJjbppU+7LZjffGf+Jvrxq+dVjIE7By163Sc9DKc3ADSU50qPVw0KonBS+a+HQ==",
+      "cpu": [
+        "ia32"
+      ],
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-x64-msvc": {
+      "version": "4.9.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.4.tgz",
+      "integrity": "sha512-LfdGXCV9rdEify1oxlN9eamvDSjv9md9ZVMAbNHA87xqIfFCxImxan9qZ8+Un54iK2nnqPlbnSi4R54ONtbWBw==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "win32"
+      ]
     },
     "node_modules/@sinclair/typebox": {
       "version": "0.27.8",
@@ -1614,101 +1737,6 @@
         "node": ">=4"
       }
     },
-    "node_modules/@suid/base": {
-      "version": "0.8.5",
-      "resolved": "https://registry.npmjs.org/@suid/base/-/base-0.8.5.tgz",
-      "integrity": "sha512-34Nu44f71ALOb2GEaiyhzAO6cA2EdjsySLBGaI9cV4vuHZpCXDrYVH0DjqNGD+ryEzURUf/tGu+zSREjJS1KiA==",
-      "dependencies": {
-        "@popperjs/core": "^2.11.8",
-        "@suid/css": "0.4.0",
-        "@suid/system": "0.10.5",
-        "@suid/types": "0.5.3",
-        "@suid/utils": "0.8.0",
-        "clsx": "^2.0.0"
-      },
-      "peerDependencies": {
-        "solid-js": "^1.7.12"
-      }
-    },
-    "node_modules/@suid/css": {
-      "version": "0.4.0",
-      "resolved": "https://registry.npmjs.org/@suid/css/-/css-0.4.0.tgz",
-      "integrity": "sha512-yzHAlf1CVi7n0SvUrMgs8Z49UiS9669+td1w1frekhRQuRbkXhHoyJkvovaDVJlWRmCPA8Q0f1OTr0uDCUg9mQ=="
-    },
-    "node_modules/@suid/icons-material": {
-      "version": "0.6.11",
-      "resolved": "https://registry.npmjs.org/@suid/icons-material/-/icons-material-0.6.11.tgz",
-      "integrity": "sha512-IAhhT5GlLvLISNcROM/pQ2sxV3RvvvA7tzGwC3KwqyItYRuAaY78uNAeT07XQDXOF/jibekhk3+UD4CJcY7tYQ==",
-      "dependencies": {
-        "@suid/material": "0.15.1"
-      },
-      "peerDependencies": {
-        "solid-js": "^1.7.12"
-      }
-    },
-    "node_modules/@suid/material": {
-      "version": "0.15.1",
-      "resolved": "https://registry.npmjs.org/@suid/material/-/material-0.15.1.tgz",
-      "integrity": "sha512-pHX4qmqqsaIcPmTEdJVurqwwbxoiRXpOzXdE+Q6PZ63xqugBn6iGXFvw5AgNTOw9CMCRhfzVwhn5V2sqASLu+Q==",
-      "dependencies": {
-        "@suid/base": "0.8.5",
-        "@suid/css": "0.4.0",
-        "@suid/system": "0.10.5",
-        "@suid/types": "0.5.3",
-        "@suid/utils": "0.8.0",
-        "clsx": "^2.0.0"
-      },
-      "peerDependencies": {
-        "solid-js": "^1.7.12"
-      }
-    },
-    "node_modules/@suid/styled-engine": {
-      "version": "0.6.1",
-      "resolved": "https://registry.npmjs.org/@suid/styled-engine/-/styled-engine-0.6.1.tgz",
-      "integrity": "sha512-2pcqqz3ZS1mPkjaSiQqLgZs6SWBnyH/np8xZHWpdvNwavkZtsiDM9tuFnfi10JtSSAUvbnfyA0QUWRRrw56XjA==",
-      "dependencies": {
-        "@suid/css": "0.4.0",
-        "@suid/utils": "0.8.0"
-      },
-      "peerDependencies": {
-        "solid-js": "^1.7.12"
-      }
-    },
-    "node_modules/@suid/system": {
-      "version": "0.10.5",
-      "resolved": "https://registry.npmjs.org/@suid/system/-/system-0.10.5.tgz",
-      "integrity": "sha512-56Q2MpIrOUoDn+OeN/QuO4Y+u7fhJ9IPJFpXXCG36n7KAzspQnFxTUZM+SdxPdSYuVwjeKfwMrXqhUkvTdrOzw==",
-      "dependencies": {
-        "@suid/css": "0.4.0",
-        "@suid/styled-engine": "0.6.1",
-        "@suid/types": "0.5.3",
-        "@suid/utils": "0.8.0",
-        "clsx": "^2.0.0",
-        "csstype": "^3.1.2"
-      },
-      "peerDependencies": {
-        "solid-js": "^1.7.12"
-      }
-    },
-    "node_modules/@suid/types": {
-      "version": "0.5.3",
-      "resolved": "https://registry.npmjs.org/@suid/types/-/types-0.5.3.tgz",
-      "integrity": "sha512-lawR7jUYuvCjkqNeF+ptCAWCRukh3ocUBz3v+zdP/EzS6OHCmJiAzoYlMUgrqxkEcI3JfSpd6u3atZ8/pkDdpQ==",
-      "peerDependencies": {
-        "solid-js": "^1.7.12"
-      }
-    },
-    "node_modules/@suid/utils": {
-      "version": "0.8.0",
-      "resolved": "https://registry.npmjs.org/@suid/utils/-/utils-0.8.0.tgz",
-      "integrity": "sha512-4HT5Ou6EWZviXQEm3DoX2BJZ3vASEmfKikXMWwSA5TaqKhU1dk7bSVaM0Ol8kch4Eyvl145KYmifKl9PD2cXAg==",
-      "dependencies": {
-        "@suid/types": "0.5.3"
-      },
-      "peerDependencies": {
-        "solid-js": "^1.7.12"
-      }
-    },
     "node_modules/@tokenizer/token": {
       "version": "0.3.0",
       "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz",
@@ -1771,21 +1799,6 @@
       "integrity": "sha512-dJ9yRry9Olt5GAWlgCtE5dK9d/Dfhn/V7hna86eEO2Pn76+E8Y0S0n61iEUEGhWXXgtKtHxtZLVNwL8X+vLHzg==",
       "dev": true
     },
-    "node_modules/@types/chai": {
-      "version": "4.3.6",
-      "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.6.tgz",
-      "integrity": "sha512-VOVRLM1mBxIRxydiViqPcKn6MIxZytrbMpd6RJLIWKxUNr3zux8no0Oc7kJx0WAPIitgZ0gkrDS+btlqQpubpw==",
-      "dev": true
-    },
-    "node_modules/@types/chai-subset": {
-      "version": "1.3.3",
-      "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.3.tgz",
-      "integrity": "sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==",
-      "dev": true,
-      "dependencies": {
-        "@types/chai": "*"
-      }
-    },
     "node_modules/@types/concat-stream": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/@types/concat-stream/-/concat-stream-2.0.0.tgz",
@@ -1805,10 +1818,9 @@
       }
     },
     "node_modules/@types/estree": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.2.tgz",
-      "integrity": "sha512-VeiPZ9MMwXjO32/Xu7+OwflfmeoRwkE/qzndw42gGtgJwZopBnzy2gD//NN1+go1mADzkDcqf/KnFRSjTJ8xJA==",
-      "dev": true
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
+      "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw=="
     },
     "node_modules/@types/estree-jsx": {
       "version": "1.0.1",
@@ -1898,9 +1910,9 @@
       "dev": true
     },
     "node_modules/@types/node": {
-      "version": "20.10.6",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.6.tgz",
-      "integrity": "sha512-Vac8H+NlRNNlAmDfGUP7b5h/KA+AtWIzuXy0E6OyP8f1tCLYAtPvKRRDJjAPqhpCb0t6U2j7/xqAuLEebW2kiw==",
+      "version": "20.11.5",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.5.tgz",
+      "integrity": "sha512-g557vgQjUUfN76MZAN/dt1z3dzcUsimuysco0KeluHgrPdJXkP/XdAURgyO2W9fZWHRtRBiVKzKn8vyOAwlG+w==",
       "devOptional": true,
       "dependencies": {
         "undici-types": "~5.26.4"
@@ -1918,9 +1930,9 @@
       "dev": true
     },
     "node_modules/@types/react": {
-      "version": "18.2.47",
-      "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.47.tgz",
-      "integrity": "sha512-xquNkkOirwyCgoClNk85BjP+aqnIS+ckAJ8i37gAbDs14jfW/J23f2GItAf33oiUPQnqNMALiFeoM9Y5mbjpVQ==",
+      "version": "18.2.48",
+      "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.48.tgz",
+      "integrity": "sha512-qboRCl6Ie70DQQG9hhNREz81jqC1cs9EVNcjQ1AU+jH6NFfSAhVVbrrY/+nSF+Bsk4AOwm9Qa61InvMCyV+H3w==",
       "dev": true,
       "dependencies": {
         "@types/prop-types": "*",
@@ -1974,16 +1986,16 @@
       "dev": true
     },
     "node_modules/@typescript-eslint/eslint-plugin": {
-      "version": "6.18.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.18.0.tgz",
-      "integrity": "sha512-3lqEvQUdCozi6d1mddWqd+kf8KxmGq2Plzx36BlkjuQe3rSTm/O98cLf0A4uDO+a5N1KD2SeEEl6fW97YHY+6w==",
+      "version": "6.19.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.19.0.tgz",
+      "integrity": "sha512-DUCUkQNklCQYnrBSSikjVChdc84/vMPDQSgJTHBZ64G9bA9w0Crc0rd2diujKbTdp6w2J47qkeHQLoi0rpLCdg==",
       "dev": true,
       "dependencies": {
         "@eslint-community/regexpp": "^4.5.1",
-        "@typescript-eslint/scope-manager": "6.18.0",
-        "@typescript-eslint/type-utils": "6.18.0",
-        "@typescript-eslint/utils": "6.18.0",
-        "@typescript-eslint/visitor-keys": "6.18.0",
+        "@typescript-eslint/scope-manager": "6.19.0",
+        "@typescript-eslint/type-utils": "6.19.0",
+        "@typescript-eslint/utils": "6.19.0",
+        "@typescript-eslint/visitor-keys": "6.19.0",
         "debug": "^4.3.4",
         "graphemer": "^1.4.0",
         "ignore": "^5.2.4",
@@ -2009,13 +2021,13 @@
       }
     },
     "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": {
-      "version": "6.18.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.18.0.tgz",
-      "integrity": "sha512-o/UoDT2NgOJ2VfHpfr+KBY2ErWvCySNUIX/X7O9g8Zzt/tXdpfEU43qbNk8LVuWUT2E0ptzTWXh79i74PP0twA==",
+      "version": "6.19.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.19.0.tgz",
+      "integrity": "sha512-dO1XMhV2ehBI6QN8Ufi7I10wmUovmLU0Oru3n5LVlM2JuzB4M+dVphCPLkVpKvGij2j/pHBWuJ9piuXx+BhzxQ==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/types": "6.18.0",
-        "@typescript-eslint/visitor-keys": "6.18.0"
+        "@typescript-eslint/types": "6.19.0",
+        "@typescript-eslint/visitor-keys": "6.19.0"
       },
       "engines": {
         "node": "^16.0.0 || >=18.0.0"
@@ -2026,9 +2038,9 @@
       }
     },
     "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": {
-      "version": "6.18.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.18.0.tgz",
-      "integrity": "sha512-/RFVIccwkwSdW/1zeMx3hADShWbgBxBnV/qSrex6607isYjj05t36P6LyONgqdUrNLl5TYU8NIKdHUYpFvExkA==",
+      "version": "6.19.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.19.0.tgz",
+      "integrity": "sha512-lFviGV/vYhOy3m8BJ/nAKoAyNhInTdXpftonhWle66XHAtT1ouBlkjL496b5H5hb8dWXHwtypTqgtb/DEa+j5A==",
       "dev": true,
       "engines": {
         "node": "^16.0.0 || >=18.0.0"
@@ -2039,12 +2051,12 @@
       }
     },
     "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": {
-      "version": "6.18.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.18.0.tgz",
-      "integrity": "sha512-1wetAlSZpewRDb2h9p/Q8kRjdGuqdTAQbkJIOUMLug2LBLG+QOjiWoSj6/3B/hA9/tVTFFdtiKvAYoYnSRW/RA==",
+      "version": "6.19.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.19.0.tgz",
+      "integrity": "sha512-hZaUCORLgubBvtGpp1JEFEazcuEdfxta9j4iUwdSAr7mEsYYAp3EAUyCZk3VEEqGj6W+AV4uWyrDGtrlawAsgQ==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/types": "6.18.0",
+        "@typescript-eslint/types": "6.19.0",
         "eslint-visitor-keys": "^3.4.1"
       },
       "engines": {
@@ -2103,13 +2115,13 @@
       }
     },
     "node_modules/@typescript-eslint/type-utils": {
-      "version": "6.18.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.18.0.tgz",
-      "integrity": "sha512-ZeMtrXnGmTcHciJN1+u2CigWEEXgy1ufoxtWcHORt5kGvpjjIlK9MUhzHm4RM8iVy6dqSaZA/6PVkX6+r+ChjQ==",
+      "version": "6.19.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.19.0.tgz",
+      "integrity": "sha512-mcvS6WSWbjiSxKCwBcXtOM5pRkPQ6kcDds/juxcy/727IQr3xMEcwr/YLHW2A2+Fp5ql6khjbKBzOyjuPqGi/w==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/typescript-estree": "6.18.0",
-        "@typescript-eslint/utils": "6.18.0",
+        "@typescript-eslint/typescript-estree": "6.19.0",
+        "@typescript-eslint/utils": "6.19.0",
         "debug": "^4.3.4",
         "ts-api-utils": "^1.0.1"
       },
@@ -2130,9 +2142,9 @@
       }
     },
     "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": {
-      "version": "6.18.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.18.0.tgz",
-      "integrity": "sha512-/RFVIccwkwSdW/1zeMx3hADShWbgBxBnV/qSrex6607isYjj05t36P6LyONgqdUrNLl5TYU8NIKdHUYpFvExkA==",
+      "version": "6.19.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.19.0.tgz",
+      "integrity": "sha512-lFviGV/vYhOy3m8BJ/nAKoAyNhInTdXpftonhWle66XHAtT1ouBlkjL496b5H5hb8dWXHwtypTqgtb/DEa+j5A==",
       "dev": true,
       "engines": {
         "node": "^16.0.0 || >=18.0.0"
@@ -2143,13 +2155,13 @@
       }
     },
     "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": {
-      "version": "6.18.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.18.0.tgz",
-      "integrity": "sha512-klNvl+Ql4NsBNGB4W9TZ2Od03lm7aGvTbs0wYaFYsplVPhr+oeXjlPZCDI4U9jgJIDK38W1FKhacCFzCC+nbIg==",
+      "version": "6.19.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.19.0.tgz",
+      "integrity": "sha512-o/zefXIbbLBZ8YJ51NlkSAt2BamrK6XOmuxSR3hynMIzzyMY33KuJ9vuMdFSXW+H0tVvdF9qBPTHA91HDb4BIQ==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/types": "6.18.0",
-        "@typescript-eslint/visitor-keys": "6.18.0",
+        "@typescript-eslint/types": "6.19.0",
+        "@typescript-eslint/visitor-keys": "6.19.0",
         "debug": "^4.3.4",
         "globby": "^11.1.0",
         "is-glob": "^4.0.3",
@@ -2171,12 +2183,12 @@
       }
     },
     "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": {
-      "version": "6.18.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.18.0.tgz",
-      "integrity": "sha512-1wetAlSZpewRDb2h9p/Q8kRjdGuqdTAQbkJIOUMLug2LBLG+QOjiWoSj6/3B/hA9/tVTFFdtiKvAYoYnSRW/RA==",
+      "version": "6.19.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.19.0.tgz",
+      "integrity": "sha512-hZaUCORLgubBvtGpp1JEFEazcuEdfxta9j4iUwdSAr7mEsYYAp3EAUyCZk3VEEqGj6W+AV4uWyrDGtrlawAsgQ==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/types": "6.18.0",
+        "@typescript-eslint/types": "6.19.0",
         "eslint-visitor-keys": "^3.4.1"
       },
       "engines": {
@@ -2254,17 +2266,17 @@
       }
     },
     "node_modules/@typescript-eslint/utils": {
-      "version": "6.18.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.18.0.tgz",
-      "integrity": "sha512-wiKKCbUeDPGaYEYQh1S580dGxJ/V9HI7K5sbGAVklyf+o5g3O+adnS4UNJajplF4e7z2q0uVBaTdT/yLb4XAVA==",
+      "version": "6.19.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.19.0.tgz",
+      "integrity": "sha512-QR41YXySiuN++/dC9UArYOg4X86OAYP83OWTewpVx5ct1IZhjjgTLocj7QNxGhWoTqknsgpl7L+hGygCO+sdYw==",
       "dev": true,
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.4.0",
         "@types/json-schema": "^7.0.12",
         "@types/semver": "^7.5.0",
-        "@typescript-eslint/scope-manager": "6.18.0",
-        "@typescript-eslint/types": "6.18.0",
-        "@typescript-eslint/typescript-estree": "6.18.0",
+        "@typescript-eslint/scope-manager": "6.19.0",
+        "@typescript-eslint/types": "6.19.0",
+        "@typescript-eslint/typescript-estree": "6.19.0",
         "semver": "^7.5.4"
       },
       "engines": {
@@ -2279,13 +2291,13 @@
       }
     },
     "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/scope-manager": {
-      "version": "6.18.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.18.0.tgz",
-      "integrity": "sha512-o/UoDT2NgOJ2VfHpfr+KBY2ErWvCySNUIX/X7O9g8Zzt/tXdpfEU43qbNk8LVuWUT2E0ptzTWXh79i74PP0twA==",
+      "version": "6.19.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.19.0.tgz",
+      "integrity": "sha512-dO1XMhV2ehBI6QN8Ufi7I10wmUovmLU0Oru3n5LVlM2JuzB4M+dVphCPLkVpKvGij2j/pHBWuJ9piuXx+BhzxQ==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/types": "6.18.0",
-        "@typescript-eslint/visitor-keys": "6.18.0"
+        "@typescript-eslint/types": "6.19.0",
+        "@typescript-eslint/visitor-keys": "6.19.0"
       },
       "engines": {
         "node": "^16.0.0 || >=18.0.0"
@@ -2296,9 +2308,9 @@
       }
     },
     "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": {
-      "version": "6.18.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.18.0.tgz",
-      "integrity": "sha512-/RFVIccwkwSdW/1zeMx3hADShWbgBxBnV/qSrex6607isYjj05t36P6LyONgqdUrNLl5TYU8NIKdHUYpFvExkA==",
+      "version": "6.19.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.19.0.tgz",
+      "integrity": "sha512-lFviGV/vYhOy3m8BJ/nAKoAyNhInTdXpftonhWle66XHAtT1ouBlkjL496b5H5hb8dWXHwtypTqgtb/DEa+j5A==",
       "dev": true,
       "engines": {
         "node": "^16.0.0 || >=18.0.0"
@@ -2309,13 +2321,13 @@
       }
     },
     "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": {
-      "version": "6.18.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.18.0.tgz",
-      "integrity": "sha512-klNvl+Ql4NsBNGB4W9TZ2Od03lm7aGvTbs0wYaFYsplVPhr+oeXjlPZCDI4U9jgJIDK38W1FKhacCFzCC+nbIg==",
+      "version": "6.19.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.19.0.tgz",
+      "integrity": "sha512-o/zefXIbbLBZ8YJ51NlkSAt2BamrK6XOmuxSR3hynMIzzyMY33KuJ9vuMdFSXW+H0tVvdF9qBPTHA91HDb4BIQ==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/types": "6.18.0",
-        "@typescript-eslint/visitor-keys": "6.18.0",
+        "@typescript-eslint/types": "6.19.0",
+        "@typescript-eslint/visitor-keys": "6.19.0",
         "debug": "^4.3.4",
         "globby": "^11.1.0",
         "is-glob": "^4.0.3",
@@ -2337,12 +2349,12 @@
       }
     },
     "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/visitor-keys": {
-      "version": "6.18.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.18.0.tgz",
-      "integrity": "sha512-1wetAlSZpewRDb2h9p/Q8kRjdGuqdTAQbkJIOUMLug2LBLG+QOjiWoSj6/3B/hA9/tVTFFdtiKvAYoYnSRW/RA==",
+      "version": "6.19.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.19.0.tgz",
+      "integrity": "sha512-hZaUCORLgubBvtGpp1JEFEazcuEdfxta9j4iUwdSAr7mEsYYAp3EAUyCZk3VEEqGj6W+AV4uWyrDGtrlawAsgQ==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/types": "6.18.0",
+        "@typescript-eslint/types": "6.19.0",
         "eslint-visitor-keys": "^3.4.1"
       },
       "engines": {
@@ -2402,13 +2414,13 @@
       "dev": true
     },
     "node_modules/@vitest/expect": {
-      "version": "0.34.6",
-      "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.34.6.tgz",
-      "integrity": "sha512-QUzKpUQRc1qC7qdGo7rMK3AkETI7w18gTCUrsNnyjjJKYiuUB9+TQK3QnR1unhCnWRC0AbKv2omLGQDF/mIjOw==",
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.2.1.tgz",
+      "integrity": "sha512-/bqGXcHfyKgFWYwIgFr1QYDaR9e64pRKxgBNWNXPefPFRhgm+K3+a/dS0cUGEreWngets3dlr8w8SBRw2fCfFQ==",
       "dev": true,
       "dependencies": {
-        "@vitest/spy": "0.34.6",
-        "@vitest/utils": "0.34.6",
+        "@vitest/spy": "1.2.1",
+        "@vitest/utils": "1.2.1",
         "chai": "^4.3.10"
       },
       "funding": {
@@ -2416,13 +2428,13 @@
       }
     },
     "node_modules/@vitest/runner": {
-      "version": "0.34.6",
-      "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.34.6.tgz",
-      "integrity": "sha512-1CUQgtJSLF47NnhN+F9X2ycxUP0kLHQ/JWvNHbeBfwW8CzEGgeskzNnHDyv1ieKTltuR6sdIHV+nmR6kPxQqzQ==",
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.2.1.tgz",
+      "integrity": "sha512-zc2dP5LQpzNzbpaBt7OeYAvmIsRS1KpZQw4G3WM/yqSV1cQKNKwLGmnm79GyZZjMhQGlRcSFMImLjZaUQvNVZQ==",
       "dev": true,
       "dependencies": {
-        "@vitest/utils": "0.34.6",
-        "p-limit": "^4.0.0",
+        "@vitest/utils": "1.2.1",
+        "p-limit": "^5.0.0",
         "pathe": "^1.1.1"
       },
       "funding": {
@@ -2430,15 +2442,15 @@
       }
     },
     "node_modules/@vitest/runner/node_modules/p-limit": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz",
-      "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==",
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz",
+      "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==",
       "dev": true,
       "dependencies": {
         "yocto-queue": "^1.0.0"
       },
       "engines": {
-        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+        "node": ">=18"
       },
       "funding": {
         "url": "https://github.com/sponsors/sindresorhus"
@@ -2457,40 +2469,41 @@
       }
     },
     "node_modules/@vitest/snapshot": {
-      "version": "0.34.6",
-      "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.34.6.tgz",
-      "integrity": "sha512-B3OZqYn6k4VaN011D+ve+AA4whM4QkcwcrwaKwAbyyvS/NB1hCWjFIBQxAQQSQir9/RtyAAGuq+4RJmbn2dH4w==",
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.2.1.tgz",
+      "integrity": "sha512-Tmp/IcYEemKaqAYCS08sh0vORLJkMr0NRV76Gl8sHGxXT5151cITJCET20063wk0Yr/1koQ6dnmP6eEqezmd/Q==",
       "dev": true,
       "dependencies": {
-        "magic-string": "^0.30.1",
+        "magic-string": "^0.30.5",
         "pathe": "^1.1.1",
-        "pretty-format": "^29.5.0"
+        "pretty-format": "^29.7.0"
       },
       "funding": {
         "url": "https://opencollective.com/vitest"
       }
     },
     "node_modules/@vitest/spy": {
-      "version": "0.34.6",
-      "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.34.6.tgz",
-      "integrity": "sha512-xaCvneSaeBw/cz8ySmF7ZwGvL0lBjfvqc1LpQ/vcdHEvpLn3Ff1vAvjw+CoGn0802l++5L/pxb7whwcWAw+DUQ==",
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.2.1.tgz",
+      "integrity": "sha512-vG3a/b7INKH7L49Lbp0IWrG6sw9j4waWAucwnksPB1r1FTJgV7nkBByd9ufzu6VWya/QTvQW4V9FShZbZIB2UQ==",
       "dev": true,
       "dependencies": {
-        "tinyspy": "^2.1.1"
+        "tinyspy": "^2.2.0"
       },
       "funding": {
         "url": "https://opencollective.com/vitest"
       }
     },
     "node_modules/@vitest/utils": {
-      "version": "0.34.6",
-      "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.34.6.tgz",
-      "integrity": "sha512-IG5aDD8S6zlvloDsnzHw0Ut5xczlF+kv2BOTo+iXfPr54Yhi5qbVOgGB1hZaVq4iJ4C/MZ2J0y15IlsV/ZcI0A==",
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.2.1.tgz",
+      "integrity": "sha512-bsH6WVZYe/J2v3+81M5LDU8kW76xWObKIURpPrOXm2pjBniBu2MERI/XP60GpS4PHU3jyK50LUutOwrx4CyHUg==",
       "dev": true,
       "dependencies": {
-        "diff-sequences": "^29.4.3",
-        "loupe": "^2.3.6",
-        "pretty-format": "^29.5.0"
+        "diff-sequences": "^29.6.3",
+        "estree-walker": "^3.0.3",
+        "loupe": "^2.3.7",
+        "pretty-format": "^29.7.0"
       },
       "funding": {
         "url": "https://opencollective.com/vitest"
@@ -2566,9 +2579,9 @@
       }
     },
     "node_modules/acorn-walk": {
-      "version": "8.2.0",
-      "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
-      "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==",
+      "version": "8.3.2",
+      "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz",
+      "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==",
       "dev": true,
       "engines": {
         "node": ">=0.4.0"
@@ -3714,9 +3727,9 @@
       }
     },
     "node_modules/chai": {
-      "version": "4.3.10",
-      "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz",
-      "integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==",
+      "version": "4.4.1",
+      "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz",
+      "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==",
       "dev": true,
       "dependencies": {
         "assertion-error": "^1.1.0",
@@ -3829,14 +3842,6 @@
         "mimic-response": "^1.0.0"
       }
     },
-    "node_modules/clsx": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz",
-      "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==",
-      "engines": {
-        "node": ">=6"
-      }
-    },
     "node_modules/color-convert": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -4105,7 +4110,8 @@
     "node_modules/csstype": {
       "version": "3.1.2",
       "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
-      "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ=="
+      "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==",
+      "dev": true
     },
     "node_modules/currently-unhandled": {
       "version": "0.4.1",
@@ -4742,7 +4748,6 @@
       "version": "0.19.11",
       "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.11.tgz",
       "integrity": "sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA==",
-      "dev": true,
       "hasInstallScript": true,
       "bin": {
         "esbuild": "bin/esbuild"
@@ -5046,6 +5051,15 @@
         "node": ">=4.0"
       }
     },
+    "node_modules/estree-walker": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+      "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+      "dev": true,
+      "dependencies": {
+        "@types/estree": "^1.0.0"
+      }
+    },
     "node_modules/esutils": {
       "version": "2.0.3",
       "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
@@ -7254,10 +7268,14 @@
       }
     },
     "node_modules/local-pkg": {
-      "version": "0.4.3",
-      "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz",
-      "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==",
+      "version": "0.5.0",
+      "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz",
+      "integrity": "sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==",
       "dev": true,
+      "dependencies": {
+        "mlly": "^1.4.2",
+        "pkg-types": "^1.0.3"
+      },
       "engines": {
         "node": ">=14"
       },
@@ -7360,12 +7378,12 @@
       "dev": true
     },
     "node_modules/loupe": {
-      "version": "2.3.6",
-      "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz",
-      "integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==",
+      "version": "2.3.7",
+      "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz",
+      "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==",
       "dev": true,
       "dependencies": {
-        "get-func-name": "^2.0.0"
+        "get-func-name": "^2.0.1"
       }
     },
     "node_modules/lowercase-keys": {
@@ -7405,9 +7423,9 @@
       }
     },
     "node_modules/magic-string": {
-      "version": "0.30.3",
-      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.3.tgz",
-      "integrity": "sha512-B7xGbll2fG/VjP+SWg4sX3JynwIU0mjoTc6MPpKNuIvftk6u6vqhDnk1R80b8C2GBR6ywqy+1DcKBrevBg+bmw==",
+      "version": "0.30.5",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz",
+      "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==",
       "dev": true,
       "dependencies": {
         "@jridgewell/sourcemap-codec": "^1.4.15"
@@ -8240,9 +8258,9 @@
       "dev": true
     },
     "node_modules/nanoid": {
-      "version": "3.3.6",
-      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
-      "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
+      "version": "3.3.7",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
+      "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
       "funding": [
         {
           "type": "github",
@@ -9007,9 +9025,9 @@
       "dev": true
     },
     "node_modules/postcss": {
-      "version": "8.4.31",
-      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
-      "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
+      "version": "8.4.33",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz",
+      "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==",
       "funding": [
         {
           "type": "opencollective",
@@ -9025,7 +9043,7 @@
         }
       ],
       "dependencies": {
-        "nanoid": "^3.3.6",
+        "nanoid": "^3.3.7",
         "picocolors": "^1.0.0",
         "source-map-js": "^1.0.2"
       },
@@ -10941,6 +10959,9 @@
       "version": "3.29.3",
       "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.3.tgz",
       "integrity": "sha512-T7du6Hum8jOkSWetjRgbwpM6Sy0nECYrYRSmZjayFcOddtKJWU4d17AC3HNUk7HRuqy4p+G7aEZclSHytqUmEg==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
       "bin": {
         "rollup": "dist/bin/rollup"
       },
@@ -11039,9 +11060,9 @@
       }
     },
     "node_modules/sass": {
-      "version": "1.69.7",
-      "resolved": "https://registry.npmjs.org/sass/-/sass-1.69.7.tgz",
-      "integrity": "sha512-rzj2soDeZ8wtE2egyLXgOOHQvaC2iosZrkF6v3EUG+tBwEvhqUCzm0VP3k9gHF9LXbSrRhT5SksoI56Iw8NPnQ==",
+      "version": "1.70.0",
+      "resolved": "https://registry.npmjs.org/sass/-/sass-1.70.0.tgz",
+      "integrity": "sha512-uUxNQ3zAHeAx5nRFskBnrWzDUJrrvpCPD5FNAoRvTi0WwremlheES3tg+56PaVtCs5QDRX5CBLxxKMDJMEa1WQ==",
       "devOptional": true,
       "dependencies": {
         "chokidar": ">=3.0.0 <4.0.0",
@@ -11127,8 +11148,21 @@
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.0.3.tgz",
       "integrity": "sha512-v5Psun3O2OdehTS/BpTC9EezHxyQRM4lqtvOdbngEq8qOZUECqG5XaoFNZgBQrJKXZFgcyZtMH6Dv7G96pKSSQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/seroval-plugins": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.0.4.tgz",
+      "integrity": "sha512-DQ2IK6oQVvy8k+c2V5x5YCtUa/GGGsUwUBNN9UqohrZ0rWdUapBFpNMYP1bCyRHoxOJjdKGl+dieacFIpU/i1A==",
+      "dev": true,
       "engines": {
         "node": ">=10"
+      },
+      "peerDependencies": {
+        "seroval": "^1.0"
       }
     },
     "node_modules/set-blocking": {
@@ -11283,23 +11317,25 @@
       }
     },
     "node_modules/solid-js": {
-      "version": "1.8.8",
-      "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.8.8.tgz",
-      "integrity": "sha512-9CtL5xWTYX1WWjQKqht3Tl0AJzgz4YWVQk8hoscO9TzRCgzlpAauEOexXa6bPG30W+fWLnFVE7XUiAzQFNeUKw==",
+      "version": "1.8.11",
+      "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.8.11.tgz",
+      "integrity": "sha512-WdwmER+TwBJiN4rVQTVBxocg+9pKlOs41KzPYntrC86xO5sek8TzBYozPEZPL1IRWDouf2lMrvSbIs3CanlPvQ==",
+      "dev": true,
       "dependencies": {
         "csstype": "^3.1.0",
-        "seroval": "^1.0.2"
+        "seroval": "^1.0.3",
+        "seroval-plugins": "^1.0.3"
       }
     },
     "node_modules/solid-refresh": {
-      "version": "0.5.3",
-      "resolved": "https://registry.npmjs.org/solid-refresh/-/solid-refresh-0.5.3.tgz",
-      "integrity": "sha512-Otg5it5sjOdZbQZJnvo99TEBAr6J7PQ5AubZLNU6szZzg3RQQ5MX04oteBIIGDs0y2Qv8aXKm9e44V8z+UnFdw==",
+      "version": "0.6.3",
+      "resolved": "https://registry.npmjs.org/solid-refresh/-/solid-refresh-0.6.3.tgz",
+      "integrity": "sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA==",
       "dev": true,
       "dependencies": {
-        "@babel/generator": "^7.21.1",
-        "@babel/helper-module-imports": "^7.18.6",
-        "@babel/types": "^7.21.2"
+        "@babel/generator": "^7.23.6",
+        "@babel/helper-module-imports": "^7.22.15",
+        "@babel/types": "^7.23.6"
       },
       "peerDependencies": {
         "solid-js": "^1.3"
@@ -11450,9 +11486,9 @@
       "dev": true
     },
     "node_modules/std-env": {
-      "version": "3.4.3",
-      "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.4.3.tgz",
-      "integrity": "sha512-f9aPhy8fYBuMN+sNfakZV18U39PbalgjXG3lLB9WkaYTxijru61wb57V9wxxNthXM5Sd88ETBWi29qLAsHO52Q==",
+      "version": "3.7.0",
+      "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz",
+      "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==",
       "dev": true
     },
     "node_modules/streamx": {
@@ -12394,18 +12430,18 @@
       "dev": true
     },
     "node_modules/tinypool": {
-      "version": "0.7.0",
-      "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.7.0.tgz",
-      "integrity": "sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww==",
+      "version": "0.8.1",
+      "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.1.tgz",
+      "integrity": "sha512-zBTCK0cCgRROxvs9c0CGK838sPkeokNGdQVUUwHAbynHFlmyJYj825f/oRs528HaIJ97lo0pLIlDUzwN+IorWg==",
       "dev": true,
       "engines": {
         "node": ">=14.0.0"
       }
     },
     "node_modules/tinyspy": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.1.1.tgz",
-      "integrity": "sha512-XPJL2uSzcOyBMky6OFrusqWlzfFrXtE0hPuMgW8A2HmaqrPo4ZQHRN/V0QXN3FSjKxpsbRrFc5LI7KOwBsT1/w==",
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.0.tgz",
+      "integrity": "sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==",
       "dev": true,
       "engines": {
         "node": ">=14.0.0"
@@ -12661,9 +12697,9 @@
       }
     },
     "node_modules/ufo": {
-      "version": "1.3.1",
-      "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.3.1.tgz",
-      "integrity": "sha512-uY/99gMLIOlJPwATcMVYfqDSxUR9//AUcgZMzwfSTJPDKzA1S8mX4VLqa+fiAtveraQUBCz4FFcwVZBGbwBXIw==",
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.3.2.tgz",
+      "integrity": "sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA==",
       "dev": true
     },
     "node_modules/unbox-primitive": {
@@ -13551,28 +13587,28 @@
       }
     },
     "node_modules/vite": {
-      "version": "4.5.1",
-      "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.1.tgz",
-      "integrity": "sha512-AXXFaAJ8yebyqzoNB9fu2pHoo/nWX+xZlaRwoeYUxEqBO+Zj4msE5G+BhGBll9lYEKv9Hfks52PAF2X7qDYXQA==",
+      "version": "5.0.12",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.12.tgz",
+      "integrity": "sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==",
       "dependencies": {
-        "esbuild": "^0.18.10",
-        "postcss": "^8.4.27",
-        "rollup": "^3.27.1"
+        "esbuild": "^0.19.3",
+        "postcss": "^8.4.32",
+        "rollup": "^4.2.0"
       },
       "bin": {
         "vite": "bin/vite.js"
       },
       "engines": {
-        "node": "^14.18.0 || >=16.0.0"
+        "node": "^18.0.0 || >=20.0.0"
       },
       "funding": {
         "url": "https://github.com/vitejs/vite?sponsor=1"
       },
       "optionalDependencies": {
-        "fsevents": "~2.3.2"
+        "fsevents": "~2.3.3"
       },
       "peerDependencies": {
-        "@types/node": ">= 14",
+        "@types/node": "^18.0.0 || >=20.0.0",
         "less": "*",
         "lightningcss": "^1.21.0",
         "sass": "*",
@@ -13605,23 +13641,22 @@
       }
     },
     "node_modules/vite-node": {
-      "version": "0.34.6",
-      "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.34.6.tgz",
-      "integrity": "sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA==",
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.2.1.tgz",
+      "integrity": "sha512-fNzHmQUSOY+y30naohBvSW7pPn/xn3Ib/uqm+5wAJQJiqQsU0NBR78XdRJb04l4bOFKjpTWld0XAfkKlrDbySg==",
       "dev": true,
       "dependencies": {
         "cac": "^6.7.14",
         "debug": "^4.3.4",
-        "mlly": "^1.4.0",
         "pathe": "^1.1.1",
         "picocolors": "^1.0.0",
-        "vite": "^3.0.0 || ^4.0.0 || ^5.0.0-0"
+        "vite": "^5.0.0"
       },
       "bin": {
         "vite-node": "vite-node.mjs"
       },
       "engines": {
-        "node": ">=v14.18.0"
+        "node": "^18.0.0 || >=20.0.0"
       },
       "funding": {
         "url": "https://opencollective.com/vitest"
@@ -13637,9 +13672,9 @@
       }
     },
     "node_modules/vite-plugin-solid": {
-      "version": "2.8.0",
-      "resolved": "https://registry.npmjs.org/vite-plugin-solid/-/vite-plugin-solid-2.8.0.tgz",
-      "integrity": "sha512-n5FAm7ZmTl94VWUoiJCgG7bouF2NlC9CA1wY/qbVnkFbYDWk++bFWyNoU48aLJ+lMtzNeYzJypJXOHzFKxL9xA==",
+      "version": "2.8.2",
+      "resolved": "https://registry.npmjs.org/vite-plugin-solid/-/vite-plugin-solid-2.8.2.tgz",
+      "integrity": "sha512-HcvMs6DTxBaO4kE3psnirPQBCUUdYeQkCNKuB2TpEkJsxb6BGP6/7qkbbCSMxn25PyNdjvzVi1WXi0ou8KPgHw==",
       "dev": true,
       "dependencies": {
         "@babel/core": "^7.23.3",
@@ -13647,7 +13682,7 @@
         "@types/babel__core": "^7.20.4",
         "babel-preset-solid": "^1.8.4",
         "merge-anything": "^5.1.7",
-        "solid-refresh": "^0.5.3",
+        "solid-refresh": "^0.6.3",
         "vitefu": "^0.2.5"
       },
       "peerDependencies": {
@@ -13656,9 +13691,9 @@
       }
     },
     "node_modules/vite-plugin-static-copy": {
-      "version": "0.17.1",
-      "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-0.17.1.tgz",
-      "integrity": "sha512-9h3iaVs0bqnqZOM5YHJXGHqdC5VAVlTZ2ARYsuNpzhEJUHmFqXY7dAK4ZFpjEQ4WLFKcaN8yWbczr81n01U4sQ==",
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-1.0.0.tgz",
+      "integrity": "sha512-kMlrB3WDtC5GzFedNIPkpjnOAr8M11PfWOiUaONrUZ3AqogTsOmIhTt6w7Fh311wl8pN81ld7sfuOEogFJ9N8A==",
       "dev": true,
       "dependencies": {
         "chokidar": "^3.5.3",
@@ -13667,376 +13702,41 @@
         "picocolors": "^1.0.0"
       },
       "engines": {
-        "node": "^14.18.0 || >=16.0.0"
+        "node": "^18.0.0 || >=20.0.0"
       },
       "peerDependencies": {
-        "vite": "^3.0.0 || ^4.0.0 || ^5.0.0"
+        "vite": "^5.0.0"
       }
     },
-    "node_modules/vite/node_modules/@esbuild/android-arm": {
-      "version": "0.18.20",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
-      "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==",
-      "cpu": [
-        "arm"
-      ],
-      "optional": true,
-      "os": [
-        "android"
-      ],
+    "node_modules/vite/node_modules/rollup": {
+      "version": "4.9.4",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.9.4.tgz",
+      "integrity": "sha512-2ztU7pY/lrQyXSCnnoU4ICjT/tCG9cdH3/G25ERqE3Lst6vl2BCM5hL2Nw+sslAvAf+ccKsAq1SkKQALyqhR7g==",
+      "dependencies": {
+        "@types/estree": "1.0.5"
+      },
+      "bin": {
+        "rollup": "dist/bin/rollup"
+      },
       "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/android-arm64": {
-      "version": "0.18.20",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz",
-      "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==",
-      "cpu": [
-        "arm64"
-      ],
-      "optional": true,
-      "os": [
-        "android"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/android-x64": {
-      "version": "0.18.20",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz",
-      "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==",
-      "cpu": [
-        "x64"
-      ],
-      "optional": true,
-      "os": [
-        "android"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/darwin-arm64": {
-      "version": "0.18.20",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz",
-      "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==",
-      "cpu": [
-        "arm64"
-      ],
-      "optional": true,
-      "os": [
-        "darwin"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/darwin-x64": {
-      "version": "0.18.20",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz",
-      "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==",
-      "cpu": [
-        "x64"
-      ],
-      "optional": true,
-      "os": [
-        "darwin"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/freebsd-arm64": {
-      "version": "0.18.20",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz",
-      "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==",
-      "cpu": [
-        "arm64"
-      ],
-      "optional": true,
-      "os": [
-        "freebsd"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/freebsd-x64": {
-      "version": "0.18.20",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz",
-      "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==",
-      "cpu": [
-        "x64"
-      ],
-      "optional": true,
-      "os": [
-        "freebsd"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/linux-arm": {
-      "version": "0.18.20",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz",
-      "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==",
-      "cpu": [
-        "arm"
-      ],
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/linux-arm64": {
-      "version": "0.18.20",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz",
-      "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==",
-      "cpu": [
-        "arm64"
-      ],
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/linux-ia32": {
-      "version": "0.18.20",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz",
-      "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==",
-      "cpu": [
-        "ia32"
-      ],
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/linux-loong64": {
-      "version": "0.18.20",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz",
-      "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==",
-      "cpu": [
-        "loong64"
-      ],
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/linux-mips64el": {
-      "version": "0.18.20",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz",
-      "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==",
-      "cpu": [
-        "mips64el"
-      ],
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/linux-ppc64": {
-      "version": "0.18.20",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz",
-      "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==",
-      "cpu": [
-        "ppc64"
-      ],
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/linux-riscv64": {
-      "version": "0.18.20",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz",
-      "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==",
-      "cpu": [
-        "riscv64"
-      ],
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/linux-s390x": {
-      "version": "0.18.20",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz",
-      "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==",
-      "cpu": [
-        "s390x"
-      ],
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/linux-x64": {
-      "version": "0.18.20",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
-      "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==",
-      "cpu": [
-        "x64"
-      ],
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/netbsd-x64": {
-      "version": "0.18.20",
-      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz",
-      "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==",
-      "cpu": [
-        "x64"
-      ],
-      "optional": true,
-      "os": [
-        "netbsd"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/openbsd-x64": {
-      "version": "0.18.20",
-      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
-      "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==",
-      "cpu": [
-        "x64"
-      ],
-      "optional": true,
-      "os": [
-        "openbsd"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/sunos-x64": {
-      "version": "0.18.20",
-      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz",
-      "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==",
-      "cpu": [
-        "x64"
-      ],
-      "optional": true,
-      "os": [
-        "sunos"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/win32-arm64": {
-      "version": "0.18.20",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz",
-      "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==",
-      "cpu": [
-        "arm64"
-      ],
-      "optional": true,
-      "os": [
-        "win32"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/win32-ia32": {
-      "version": "0.18.20",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz",
-      "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==",
-      "cpu": [
-        "ia32"
-      ],
-      "optional": true,
-      "os": [
-        "win32"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/@esbuild/win32-x64": {
-      "version": "0.18.20",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
-      "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==",
-      "cpu": [
-        "x64"
-      ],
-      "optional": true,
-      "os": [
-        "win32"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/vite/node_modules/esbuild": {
-      "version": "0.18.20",
-      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
-      "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
-      "hasInstallScript": true,
-      "bin": {
-        "esbuild": "bin/esbuild"
-      },
-      "engines": {
-        "node": ">=12"
-      },
-      "optionalDependencies": {
-        "@esbuild/android-arm": "0.18.20",
-        "@esbuild/android-arm64": "0.18.20",
-        "@esbuild/android-x64": "0.18.20",
-        "@esbuild/darwin-arm64": "0.18.20",
-        "@esbuild/darwin-x64": "0.18.20",
-        "@esbuild/freebsd-arm64": "0.18.20",
-        "@esbuild/freebsd-x64": "0.18.20",
-        "@esbuild/linux-arm": "0.18.20",
-        "@esbuild/linux-arm64": "0.18.20",
-        "@esbuild/linux-ia32": "0.18.20",
-        "@esbuild/linux-loong64": "0.18.20",
-        "@esbuild/linux-mips64el": "0.18.20",
-        "@esbuild/linux-ppc64": "0.18.20",
-        "@esbuild/linux-riscv64": "0.18.20",
-        "@esbuild/linux-s390x": "0.18.20",
-        "@esbuild/linux-x64": "0.18.20",
-        "@esbuild/netbsd-x64": "0.18.20",
-        "@esbuild/openbsd-x64": "0.18.20",
-        "@esbuild/sunos-x64": "0.18.20",
-        "@esbuild/win32-arm64": "0.18.20",
-        "@esbuild/win32-ia32": "0.18.20",
-        "@esbuild/win32-x64": "0.18.20"
+        "node": ">=18.0.0",
+        "npm": ">=8.0.0"
+      },
+      "optionalDependencies": {
+        "@rollup/rollup-android-arm-eabi": "4.9.4",
+        "@rollup/rollup-android-arm64": "4.9.4",
+        "@rollup/rollup-darwin-arm64": "4.9.4",
+        "@rollup/rollup-darwin-x64": "4.9.4",
+        "@rollup/rollup-linux-arm-gnueabihf": "4.9.4",
+        "@rollup/rollup-linux-arm64-gnu": "4.9.4",
+        "@rollup/rollup-linux-arm64-musl": "4.9.4",
+        "@rollup/rollup-linux-riscv64-gnu": "4.9.4",
+        "@rollup/rollup-linux-x64-gnu": "4.9.4",
+        "@rollup/rollup-linux-x64-musl": "4.9.4",
+        "@rollup/rollup-win32-arm64-msvc": "4.9.4",
+        "@rollup/rollup-win32-ia32-msvc": "4.9.4",
+        "@rollup/rollup-win32-x64-msvc": "4.9.4",
+        "fsevents": "~2.3.2"
       }
     },
     "node_modules/vitefu": {
@@ -14054,59 +13754,57 @@
       }
     },
     "node_modules/vitest": {
-      "version": "0.34.6",
-      "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.34.6.tgz",
-      "integrity": "sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q==",
-      "dev": true,
-      "dependencies": {
-        "@types/chai": "^4.3.5",
-        "@types/chai-subset": "^1.3.3",
-        "@types/node": "*",
-        "@vitest/expect": "0.34.6",
-        "@vitest/runner": "0.34.6",
-        "@vitest/snapshot": "0.34.6",
-        "@vitest/spy": "0.34.6",
-        "@vitest/utils": "0.34.6",
-        "acorn": "^8.9.0",
-        "acorn-walk": "^8.2.0",
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.2.1.tgz",
+      "integrity": "sha512-TRph8N8rnSDa5M2wKWJCMnztCZS9cDcgVTQ6tsTFTG/odHJ4l5yNVqvbeDJYJRZ6is3uxaEpFs8LL6QM+YFSdA==",
+      "dev": true,
+      "dependencies": {
+        "@vitest/expect": "1.2.1",
+        "@vitest/runner": "1.2.1",
+        "@vitest/snapshot": "1.2.1",
+        "@vitest/spy": "1.2.1",
+        "@vitest/utils": "1.2.1",
+        "acorn-walk": "^8.3.2",
         "cac": "^6.7.14",
         "chai": "^4.3.10",
         "debug": "^4.3.4",
-        "local-pkg": "^0.4.3",
-        "magic-string": "^0.30.1",
+        "execa": "^8.0.1",
+        "local-pkg": "^0.5.0",
+        "magic-string": "^0.30.5",
         "pathe": "^1.1.1",
         "picocolors": "^1.0.0",
-        "std-env": "^3.3.3",
-        "strip-literal": "^1.0.1",
-        "tinybench": "^2.5.0",
-        "tinypool": "^0.7.0",
-        "vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0",
-        "vite-node": "0.34.6",
+        "std-env": "^3.5.0",
+        "strip-literal": "^1.3.0",
+        "tinybench": "^2.5.1",
+        "tinypool": "^0.8.1",
+        "vite": "^5.0.0",
+        "vite-node": "1.2.1",
         "why-is-node-running": "^2.2.2"
       },
       "bin": {
         "vitest": "vitest.mjs"
       },
       "engines": {
-        "node": ">=v14.18.0"
+        "node": "^18.0.0 || >=20.0.0"
       },
       "funding": {
         "url": "https://opencollective.com/vitest"
       },
       "peerDependencies": {
         "@edge-runtime/vm": "*",
-        "@vitest/browser": "*",
-        "@vitest/ui": "*",
+        "@types/node": "^18.0.0 || >=20.0.0",
+        "@vitest/browser": "^1.0.0",
+        "@vitest/ui": "^1.0.0",
         "happy-dom": "*",
-        "jsdom": "*",
-        "playwright": "*",
-        "safaridriver": "*",
-        "webdriverio": "*"
+        "jsdom": "*"
       },
       "peerDependenciesMeta": {
         "@edge-runtime/vm": {
           "optional": true
         },
+        "@types/node": {
+          "optional": true
+        },
         "@vitest/browser": {
           "optional": true
         },
@@ -14118,18 +13816,131 @@
         },
         "jsdom": {
           "optional": true
-        },
-        "playwright": {
-          "optional": true
-        },
-        "safaridriver": {
-          "optional": true
-        },
-        "webdriverio": {
-          "optional": true
         }
       }
     },
+    "node_modules/vitest/node_modules/execa": {
+      "version": "8.0.1",
+      "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
+      "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==",
+      "dev": true,
+      "dependencies": {
+        "cross-spawn": "^7.0.3",
+        "get-stream": "^8.0.1",
+        "human-signals": "^5.0.0",
+        "is-stream": "^3.0.0",
+        "merge-stream": "^2.0.0",
+        "npm-run-path": "^5.1.0",
+        "onetime": "^6.0.0",
+        "signal-exit": "^4.1.0",
+        "strip-final-newline": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=16.17"
+      },
+      "funding": {
+        "url": "https://github.com/sindresorhus/execa?sponsor=1"
+      }
+    },
+    "node_modules/vitest/node_modules/get-stream": {
+      "version": "8.0.1",
+      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz",
+      "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==",
+      "dev": true,
+      "engines": {
+        "node": ">=16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/vitest/node_modules/human-signals": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
+      "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=16.17.0"
+      }
+    },
+    "node_modules/vitest/node_modules/is-stream": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
+      "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
+      "dev": true,
+      "engines": {
+        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/vitest/node_modules/mimic-fn": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
+      "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
+      "dev": true,
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/vitest/node_modules/npm-run-path": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.2.0.tgz",
+      "integrity": "sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg==",
+      "dev": true,
+      "dependencies": {
+        "path-key": "^4.0.0"
+      },
+      "engines": {
+        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/vitest/node_modules/onetime": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
+      "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
+      "dev": true,
+      "dependencies": {
+        "mimic-fn": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/vitest/node_modules/path-key": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
+      "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/vitest/node_modules/strip-final-newline": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
+      "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
+      "dev": true,
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/walk-up-path": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-3.0.1.tgz",
diff --git a/package.json b/package.json
index 8bf4782e..576307d7 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "web-scrobbler",
-  "version": "3.4.0",
+  "version": "3.5.0",
   "author": "David Šabata <posta@davidsabata.cz>",
   "contributors": [
     "@mountainash (https://mountainash.id.au/)"
@@ -28,11 +28,10 @@
   },
   "homepage": "https://web-scrobbler.com/",
   "dependencies": {
-    "@suid/icons-material": "0.6.11",
     "@web-scrobbler/metadata-filter": "3.1.0",
     "blueimp-md5": "2.19.0",
     "stylelint": "15.11.0",
-    "vite": "4.5.1",
+    "vite": "5.0.12",
     "webextension-polyfill": "0.10.0"
   },
   "devDependencies": {
@@ -41,10 +40,10 @@
     "@types/fs-extra": "11.0.3",
     "@types/imagemin": "8.0.5",
     "@types/imagemin-jpegtran": "5.0.4",
-    "@types/node": "20.10.6",
-    "@types/react": "18.2.47",
+    "@types/node": "20.11.5",
+    "@types/react": "18.2.48",
     "@types/webextension-polyfill": "0.10.7",
-    "@typescript-eslint/eslint-plugin": "6.18.0",
+    "@typescript-eslint/eslint-plugin": "6.19.0",
     "@web-scrobbler/eslint-config": "3.0.0",
     "@web-scrobbler/stylelint-config": "1.0.1",
     "archiver": "6.0.1",
@@ -65,14 +64,14 @@
     "remark": "15.0.1",
     "remark-cli": "12.0.0",
     "remark-preset-lint-recommended": "6.1.3",
-    "sass": "1.69.7",
-    "solid-js": "1.8.8",
+    "sass": "1.70.0",
+    "solid-js": "1.8.11",
     "tsx": "4.7.0",
     "typescript": "5.3.3",
     "vite-plugin-conditional-compiler": "0.2.1",
-    "vite-plugin-solid": "2.8.0",
-    "vite-plugin-static-copy": "0.17.1",
-    "vitest": "0.34.6"
+    "vite-plugin-solid": "2.8.2",
+    "vite-plugin-static-copy": "1.0.0",
+    "vitest": "1.2.1"
   },
   "engines": {
     "node": ">=18.0.0"
diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json
index 7ef6af04..fac5dd29 100644
--- a/src/_locales/en/messages.json
+++ b/src/_locales/en/messages.json
@@ -155,6 +155,14 @@
         "message": "Dark",
         "description": "Option label"
     },
+    "optionThemeHighContrastDark": {
+        "message": "High Contrast Dark",
+        "description": "Option label"
+    },
+    "optionThemeHighContrastLight": {
+        "message": "High Contrast Light",
+        "description": "Option label"
+    },
     "optionThemeSystem": {
         "message": "System",
         "description": "Option label"
@@ -287,6 +295,22 @@
         "message": "Import",
         "description": "Button to import edited tracks"
     },
+    "optionsDelete": {
+        "message": "Delete $1",
+        "desciption": "Option label"
+    },
+    "optionsEnabled": {
+        "message": "Enabled",
+        "description": "Option label"
+    },
+    "optionsDisabled": {
+        "message": "Disabled",
+        "description": "Option label"
+    },
+    "optionsIndeterminate": {
+        "message": "Same as global option",
+        "description": "Option label"
+    },
     "optionsConnectors": {
         "message": "Connectors",
         "description": "'Connectors' section"
@@ -867,6 +891,14 @@
         "message": "Click the notification to connect your accounts.",
         "description": "Notification text"
     },
+    "notificationNativeScrobbler": {
+      "message": "$1 has a built-in scrobbler!",
+      "description": "Notification title, $1 denotes the name of the website."
+    },
+    "notificationNativeScrobblerText": {
+        "message": "Make sure to turn off either Web Scrobbler or the built-in scrobbler!",
+        "description": "Notification text"
+    },
     "notificationAuthError": {
         "message": "Authentication error",
         "description": "Notification title"
diff --git a/src/connectors/bilibili.ts b/src/connectors/bilibili.ts
index c750de3e..c6528875 100644
--- a/src/connectors/bilibili.ts
+++ b/src/connectors/bilibili.ts
@@ -1,8 +1,8 @@
 export {};
 
 /*
-	Since BiliBili is a video site, accurately scrobbling song information from the title can be challenging. 
-	However, people often include the artist's name and track title in the video tags. 
+	Since BiliBili is a video site, accurately scrobbling song information from the title can be challenging.
+	However, people often include the artist's name and track title in the video tags.
 
 	Therefore, this connector uses tags to identify artist and track information and determines whether we should scrobble this video.
 	The video title is then used to determine which tag is most likely to contain the artist or track name.
@@ -195,6 +195,8 @@ Connector.getChannelId = () =>
 		.split('/')
 		.pop();
 
+Connector.channelLabelSelector = '.up-name';
+
 Connector.isScrobblingAllowed = () => {
 	const tags = videoInfo.tags;
 
@@ -216,7 +218,7 @@ Connector.getArtistTrack = () => {
 };
 
 /*
-	helper functions	
+	helper functions
 */
 
 /**
@@ -349,7 +351,7 @@ function grepSongInfo() {
 
 	// select
 	Util.debugLog(
-		`PossibleTrack: ${possibleTrack.toString()} 
+		`PossibleTrack: ${possibleTrack.toString()}
 PossibleArtist: ${possibleArtist.toString()}
 FilteredTags: ${filteredTags.toString()}`,
 		'log',
diff --git a/src/connectors/planetradio.ts b/src/connectors/planetradio.ts
index 4af6b6d7..2aed7d56 100644
--- a/src/connectors/planetradio.ts
+++ b/src/connectors/planetradio.ts
@@ -1,26 +1,11 @@
 export {};
 
-Connector.playerSelector = [
-	'#__next > div > div > div:first-child',
-	'.now-playing-wrapper',
-];
+Connector.playerSelector = '#__next > div > div > div:first-child';
 
-Connector.artistSelector = [
-	'#__next > div > div > div:first-child > div:nth-child(5) > div > div:nth-child(2) > p:nth-child(2)',
-	'.text-right .title',
-];
+Connector.artistSelector = '#playbar-play-button + div p';
 
-Connector.trackSelector = [
-	'#__next > div > div > div:first-child > div:nth-child(5) > div > div:nth-child(2) > p:nth-child(3)',
-	'.text-right .track',
-];
+Connector.trackSelector = '#playbar-play-button + div p:last-child';
 
-Connector.trackArtSelector = [
-	'#__next > div > div > div:first-child > div:nth-child(5) img',
-	'.now-playing-block.right .image',
-];
+Connector.trackArtSelector = '#playbar-play-button + div picture img';
 
-Connector.playButtonSelector = [
-	'#__next > div > div > div:first-child [data-icon="play"]',
-	'.play',
-];
+Connector.playButtonSelector = '#playbar-play-button svg[data-icon="play"]';
diff --git a/src/connectors/youtube.ts b/src/connectors/youtube.ts
index f96d815b..6d0731c4 100644
--- a/src/connectors/youtube.ts
+++ b/src/connectors/youtube.ts
@@ -96,11 +96,17 @@ Connector.scrobbleInfoStyle = {
 Connector.getChannelId = () =>
 	new URL(
 		(
-			document.querySelector(
+			Util.queryElements([
 				'#upload-info .ytd-channel-name .yt-simple-endpoint',
-			) as HTMLAnchorElement
-		)?.href ?? 'https://youtube.com/',
-	).pathname.slice(2);
+				'.slim-owner-icon-and-title',
+			]) as NodeListOf<HTMLAnchorElement>
+		)?.[0]?.href ?? 'https://youtube.com/',
+	).pathname.slice(1);
+
+Connector.channelLabelSelector = [
+	'#primary #title+#top-row ytd-channel-name .yt-formatted-string',
+	'.slim-owner-icon-and-title .yt-core-attributed-string',
+];
 
 Connector.getTrackInfo = () => {
 	const trackInfo: TrackInfoWithAlbum = {};
diff --git a/src/connectors/zingmp3.ts b/src/connectors/zingmp3.ts
new file mode 100644
index 00000000..c87b4294
--- /dev/null
+++ b/src/connectors/zingmp3.ts
@@ -0,0 +1,19 @@
+export {};
+
+const playerBar = '.player-controls__container';
+
+Connector.useMediaSessionApi();
+
+Connector.playerSelector = playerBar;
+
+Connector.artistSelector = `${playerBar} .is-one-line.is-truncate.subtitle`;
+
+Connector.trackSelector = `${playerBar} .song-title-item`;
+
+Connector.trackArtSelector = '.thumbnail-wrapper .thumbnail .image';
+
+Connector.currentTimeSelector = `${playerBar} .time.left`;
+
+Connector.durationSelector = `${playerBar} .time.right`;
+
+Connector.pauseButtonSelector = `${playerBar} .ic-pause-circle-outline`;
diff --git a/src/core/background/action.ts b/src/core/background/action.ts
index 632327e1..c0b4d4cc 100644
--- a/src/core/background/action.ts
+++ b/src/core/background/action.ts
@@ -3,7 +3,7 @@ import {
 	contextMenus,
 	getBrowserPreferredTheme,
 	getChannelDetails,
-	isChannelBlocklisted,
+	getChannelBlocklistLabel,
 } from './util';
 import { sendPopupMessage } from '@/util/communication';
 import { ManagerTab } from '@/core/storage/wrapper';
@@ -91,7 +91,7 @@ async function updateMenus(tab: ManagerTab): Promise<void> {
 	const channelDetails = await getChannelDetails(tab.tabId);
 	if (
 		!channelDetails ||
-		!channelDetails.channelId ||
+		!channelDetails.channelInfo?.id ||
 		!channelDetails.connector
 	) {
 		browser.contextMenus?.update(contextMenus.DISABLE_CHANNEL, {
@@ -101,8 +101,8 @@ async function updateMenus(tab: ManagerTab): Promise<void> {
 			visible: false,
 		});
 	} else if (
-		await isChannelBlocklisted(
-			channelDetails.channelId,
+		await getChannelBlocklistLabel(
+			channelDetails.channelInfo.id,
 			channelDetails.connector,
 		)
 	) {
@@ -111,12 +111,12 @@ async function updateMenus(tab: ManagerTab): Promise<void> {
 		});
 		browser.contextMenus?.update(contextMenus.ENABLE_CHANNEL, {
 			visible: true,
-			title: t('menuEnableChannel', channelDetails.channelId),
+			title: t('menuEnableChannel', channelDetails.channelInfo.label),
 		});
 	} else {
 		browser.contextMenus?.update(contextMenus.DISABLE_CHANNEL, {
 			visible: true,
-			title: t('menuDisableChannel', channelDetails.channelId),
+			title: t('menuDisableChannel', channelDetails.channelInfo.label),
 		});
 		browser.contextMenus?.update(contextMenus.ENABLE_CHANNEL, {
 			visible: false,
diff --git a/src/core/background/scrobble.ts b/src/core/background/scrobble.ts
index 33dea67d..244efc21 100644
--- a/src/core/background/scrobble.ts
+++ b/src/core/background/scrobble.ts
@@ -1,3 +1,4 @@
+import { showNativeScrobblerWarning } from '@/util/notifications';
 import ClonedSong from '../object/cloned-song';
 import scrobbleService from '../object/scrobble-service';
 import { ServiceCallResult } from '../object/service-call-result';
@@ -8,6 +9,7 @@ export async function sendNowPlaying(
 	song: BaseSong,
 ): Promise<ServiceCallResult[]> {
 	await scrobbleService.bindAllScrobblers();
+	void showNativeScrobblerWarning(song.connector);
 	return scrobbleService.sendNowPlaying(song);
 }
 
diff --git a/src/core/background/util.ts b/src/core/background/util.ts
index 5de15fe5..dc5d6b29 100644
--- a/src/core/background/util.ts
+++ b/src/core/background/util.ts
@@ -77,18 +77,21 @@ export async function getChannelDetails(tabId: number) {
 }
 
 /**
- * Is current channel blocklisted
+ * Checks if current channel is blocklisted and returns its label if so
  *
  * @param channelId - ID of the channel to check
  * @param connector - Details about the connector to check
- * @returns true if current channel is blocklisted; false otherwise
+ * @returns string label of channel if current channel is blocklisted; null otherwise
  */
-export async function isChannelBlocklisted(
+export async function getChannelBlocklistLabel(
 	channelId: string,
 	connector: ConnectorMeta,
-): Promise<boolean> {
-	const blocklist = await blocklistStorage.get();
-	return blocklist?.[connector.id]?.[channelId] ?? false;
+): Promise<string | null> {
+	const blocklist = (await blocklistStorage.get())?.[connector.id];
+	if (!blocklist || !blocklist[channelId]) {
+		return null;
+	}
+	return blocklist[channelId];
 }
 
 /**
@@ -281,7 +284,7 @@ export function addToBlocklist(tabId: number) {
 /**
  * Enables scrobbling current channel for a tab
  *
- * @param tabId - tab id of tab to disable scrobbling channel for
+ * @param tabId - tab id of tab to enable scrobbling channel for
  */
 export function removeFromBlocklist(tabId: number) {
 	sendBackgroundMessage(tabId, {
diff --git a/src/core/connectors.ts b/src/core/connectors.ts
index e29247fd..411d11bc 100644
--- a/src/core/connectors.ts
+++ b/src/core/connectors.ts
@@ -9,6 +9,11 @@ export interface ConnectorMeta {
 	 * true if connector uses blocklist. Connector must implement {@link Connector.getChannelId}
 	 */
 	usesBlocklist?: true;
+
+	/**
+	 * true if website has its own scrobbling system the user needs to be aware of.
+	 */
+	hasNativeScrobbler?: true;
 }
 
 export default <ConnectorMeta[]>[
@@ -125,6 +130,7 @@ export default <ConnectorMeta[]>[
 		matches: ['*://open.spotify.com/*'],
 		js: 'spotify.js',
 		id: 'spotify',
+		hasNativeScrobbler: true,
 	},
 	{
 		label: 'plug.dj',
@@ -2342,4 +2348,10 @@ export default <ConnectorMeta[]>[
 		js: 'jam.coop.js',
 		id: 'jam.coop',
 	},
+	{
+		label: 'Zing MP3',
+		matches: ['*://zingmp3.vn/*'],
+		js: 'zingmp3.js',
+		id: 'zingmp3',
+	},
 ];
diff --git a/src/core/content/connector.ts b/src/core/content/connector.ts
index a70d7f8c..84b74b34 100644
--- a/src/core/content/connector.ts
+++ b/src/core/content/connector.ts
@@ -152,6 +152,44 @@ export default class BaseConnector {
 	 */
 	public getChannelId: (() => string | null | undefined) | null = null;
 
+	/**
+	 * Selector of an element containing channel label.
+	 *
+	 * Only applies when default implementation of
+	 * {@link getChannelLabel} is used.
+	 */
+	public channelLabelSelector: string | string[] | null = null;
+
+	/**
+	 * Function that gets a label for the channel of ID fetched by {@link getChannelId}.
+	 *
+	 * This is the name that will be displayed to the user, and has no bearing on internal logic.
+	 * If not specified, {@link getChannelId} will be used.
+	 */
+	public getChannelLabel: (() => string | null | undefined) | null = () =>
+		Util.getTextFromSelectors(this.channelLabelSelector);
+
+	/**
+	 * Function that gets ID and label for a channel.
+	 *
+	 * A connector can specify this in lieu of {@link getChannelId}, as this is basically combo of
+	 * both {@link getChannelId} and {@link getChannelLabel}
+	 *
+	 * You may return null in this function, but if you return a result for one property,
+	 * you must return a result for both properties, even if the label is just
+	 * a duplicate of the id.
+	 */
+	public getChannelInfo: () => Util.ChannelInfo | null | undefined = () => {
+		const id = this.getChannelId?.();
+		if (!id) {
+			return null;
+		}
+		return {
+			id,
+			label: this.getChannelLabel?.() || id,
+		};
+	};
+
 	/**
 	 * Selector of element contains a track art of now playing song.
 	 * Default implementation looks for track art URL in `src` attribute or
diff --git a/src/core/content/util.ts b/src/core/content/util.ts
index d103e873..27560766 100644
--- a/src/core/content/util.ts
+++ b/src/core/content/util.ts
@@ -1067,3 +1067,8 @@ export function getInfoBoxText(
 			return t(`pageAction${mode}`, trackInfo);
 	}
 }
+
+export interface ChannelInfo {
+	id: string;
+	label: string;
+}
diff --git a/src/core/object/cloned-song.ts b/src/core/object/cloned-song.ts
index 70e7581a..858e38c1 100644
--- a/src/core/object/cloned-song.ts
+++ b/src/core/object/cloned-song.ts
@@ -7,6 +7,7 @@ import {
 	ParsedSongData,
 	ProcessedSongData,
 } from './song';
+import { ConnectorMeta } from '../connectors';
 
 /**
  * This class is used to create a song object from a cloned song.
@@ -17,8 +18,7 @@ export default class ClonedSong extends BaseSong {
 	public noRegex: ProcessedSongData;
 	public flags: Flags;
 	public metadata: Metadata;
-	public connectorLabel: string;
-	public connectorId: string;
+	public connector: ConnectorMeta;
 	public controllerTabId: number;
 
 	constructor(song: CloneableSong, controllerTabId: number) {
@@ -28,8 +28,7 @@ export default class ClonedSong extends BaseSong {
 		this.noRegex = song.noRegex;
 		this.flags = song.flags;
 		this.metadata = song.metadata;
-		this.connectorLabel = song.connectorLabel;
-		this.connectorId = song.connectorId;
+		this.connector = song.connector;
 		this.controllerTabId = controllerTabId;
 	}
 
diff --git a/src/core/object/controller/controller.ts b/src/core/object/controller/controller.ts
index e67380b4..256efdf3 100644
--- a/src/core/object/controller/controller.ts
+++ b/src/core/object/controller/controller.ts
@@ -98,7 +98,7 @@ export default class Controller {
 		return (
 			this.currentSong?.parsed.isScrobblingAllowed &&
 			(await this.blocklist.shouldScrobbleChannel(
-				this.connector.getChannelId?.(),
+				this.connector.getChannelInfo?.()?.id,
 			))
 		);
 	}
@@ -301,14 +301,14 @@ export default class Controller {
 				type: 'getChannelDetails',
 				fn: () => ({
 					connector: this.connector.meta,
-					channelId: this.connector.getChannelId?.(),
+					channelInfo: this.connector.getChannelInfo?.(),
 				}),
 			}),
 			contentListener({
 				type: 'addToBlocklist',
 				fn: async () => {
 					await this.blocklist.addToBlocklist(
-						this.connector.getChannelId?.() ?? '',
+						this.connector.getChannelInfo?.() ?? null,
 					);
 					this.setMode(ControllerMode.Disallowed);
 				},
@@ -317,7 +317,7 @@ export default class Controller {
 				type: 'removeFromBlocklist',
 				fn: async () => {
 					await this.blocklist.removeFromBlocklist(
-						this.connector.getChannelId?.() ?? '',
+						this.connector.getChannelInfo?.()?.id ?? null,
 					);
 					if (this.shouldHaveScrobbled) {
 						void this.scrobbleSong();
diff --git a/src/core/object/pipeline/coverartarchive/coverartarchive.ts b/src/core/object/pipeline/coverartarchive/coverartarchive.ts
index 85b9be0a..19c14d47 100644
--- a/src/core/object/pipeline/coverartarchive/coverartarchive.ts
+++ b/src/core/object/pipeline/coverartarchive/coverartarchive.ts
@@ -1,6 +1,12 @@
 import Song from '@/core/object/song';
 import { MusicBrainzSearch } from './coverartarchive.types';
 import { debugLog } from '@/core/content/util';
+import { timeoutPromise } from '@/util/util';
+
+/**
+ * How long to wait before giving up on musicbrainz request.
+ */
+const REQUEST_TIMEOUT = 5000;
 
 /**
  * Fetch coverart from MusicBrainz archive.
@@ -65,7 +71,7 @@ async function getMusicBrainzId(endpoint: string, song: Song) {
 		`title:+"${track ?? ''}"^3 ${track ?? ''} artistname:+"${
 			artist ?? ''
 		}"^4${artist ?? ''}`;
-	const response = await fetch(url);
+	const response = await timeoutPromise(REQUEST_TIMEOUT, fetch(url));
 	if (!response.ok) {
 		throw new Error('Unable to fetch MusicBrainz ID');
 	}
@@ -91,7 +97,10 @@ async function getMusicBrainzId(endpoint: string, song: Song) {
  */
 async function checkCoverArt(mbid: string) {
 	const coverArtUrl = `https://coverartarchive.org/release/${mbid}/front-500`;
-	const response = await fetch(coverArtUrl, { method: 'HEAD' });
+	const response = await timeoutPromise(
+		REQUEST_TIMEOUT,
+		fetch(coverArtUrl, { method: 'HEAD' }),
+	);
 	if (response.ok) {
 		return coverArtUrl;
 	}
diff --git a/src/core/object/song.ts b/src/core/object/song.ts
index b5d36351..8a36cb81 100644
--- a/src/core/object/song.ts
+++ b/src/core/object/song.ts
@@ -59,8 +59,7 @@ export interface CloneableSong {
 	noRegex: ProcessedSongData;
 	flags: Flags;
 	metadata: Metadata;
-	connectorLabel: string;
-	connectorId: string;
+	connector: ConnectorMeta;
 }
 
 export abstract class BaseSong {
@@ -69,8 +68,7 @@ export abstract class BaseSong {
 	public abstract noRegex: ProcessedSongData;
 	public abstract flags: Flags;
 	public abstract metadata: Metadata;
-	public abstract connectorLabel: string;
-	public abstract connectorId: string;
+	public abstract connector: ConnectorMeta;
 
 	/**
 	 * Get song artist.
@@ -227,8 +225,7 @@ export abstract class BaseSong {
 			processed: this.processed,
 			metadata: this.metadata,
 			flags: this.flags,
-			connectorLabel: this.connectorLabel,
-			connectorId: this.connectorId,
+			connector: this.connector,
 		};
 	}
 
@@ -304,8 +301,7 @@ export default class Song extends BaseSong {
 	public noRegex: ProcessedSongData;
 	public flags: Flags;
 	public metadata: Metadata;
-	public connectorLabel: string;
-	public connectorId: string;
+	public connector: ConnectorMeta;
 	/**
 	 * @param parsedData - Current state received from connector
 	 * @param connector - Connector match object
@@ -368,8 +364,7 @@ export default class Song extends BaseSong {
 			/* Filled in `initMetadata` method */
 		};
 
-		this.connectorLabel = connector.label;
-		this.connectorId = connector.id;
+		this.connector = connector;
 
 		this.initSongData();
 	}
@@ -452,7 +447,7 @@ export default class Song extends BaseSong {
 			 */
 			startTimestamp: Math.floor(Date.now() / 1000),
 
-			label: this.connectorLabel,
+			label: this.connector.label,
 		};
 	}
 
diff --git a/src/core/storage/blocklist.ts b/src/core/storage/blocklist.ts
index e7b1cb6d..1bbc1a12 100644
--- a/src/core/storage/blocklist.ts
+++ b/src/core/storage/blocklist.ts
@@ -1,4 +1,5 @@
 import * as BrowserStorage from '@/core/storage/browser-storage';
+import type { ChannelInfo } from '../content/util';
 
 export default class Blocklist {
 	private storage = BrowserStorage.getStorage(BrowserStorage.BLOCKLISTS);
@@ -37,8 +38,10 @@ export default class Blocklist {
 	 *
 	 * @param id - ID of channel to add
 	 */
-	public async addToBlocklist(id: string): Promise<void> {
-		if (!id) {
+	public async addToBlocklist(
+		channelInfo: ChannelInfo | null,
+	): Promise<void> {
+		if (!channelInfo?.id) {
 			return;
 		}
 		await this.isReady;
@@ -49,7 +52,7 @@ export default class Blocklist {
 		}
 		data[this.connectorId] = {
 			...data[this.connectorId],
-			[id]: true,
+			[channelInfo.id]: channelInfo.label || channelInfo.id,
 		};
 		await this.storage.setLocking(data);
 	}
@@ -59,7 +62,7 @@ export default class Blocklist {
 	 *
 	 * @param id - ID of channel to remove
 	 */
-	public async removeFromBlocklist(id: string) {
+	public async removeFromBlocklist(id: string | null) {
 		if (!id) {
 			return;
 		}
@@ -76,20 +79,31 @@ export default class Blocklist {
 	/**
 	 * @param id - ID of channel to check
 	 *
-	 * @returns true if channel isn't blocklisted; false if it is
+	 * @returns label if channel is blocklisted; null if it isn't
 	 */
-	public async shouldScrobbleChannel(
+	public async getChannelLabel(
 		id: string | undefined | null,
-	): Promise<boolean> {
+	): Promise<string | null> {
 		if (!id) {
-			return true;
+			return null;
 		}
 		await this.isReady;
 		const data = await this.storage.get();
 		if (!data || !(this.connectorId in data) || !data[this.connectorId]) {
-			return true;
+			return null;
 		}
 
-		return data[this.connectorId][id] !== true;
+		return data[this.connectorId][id];
+	}
+
+	/**
+	 * @param id - ID of channel to check
+	 *
+	 * @returns true if channel isn't blocklisted; false if it is.
+	 */
+	public async shouldScrobbleChannel(
+		id: string | undefined | null,
+	): Promise<boolean> {
+		return !(await this.getChannelLabel(id));
 	}
 }
diff --git a/src/core/storage/browser-storage.ts b/src/core/storage/browser-storage.ts
index 2572dc23..e879064b 100644
--- a/src/core/storage/browser-storage.ts
+++ b/src/core/storage/browser-storage.ts
@@ -62,6 +62,12 @@ export const NOTIFICATIONS = 'Notifications';
  */
 export const LOCAL_CACHE = 'LocalCache';
 
+/**
+ * This storage contains the list of websites with native scrobbling
+ * for which a notification has already been sent to the user before.
+ */
+export const NATIVE_SCROBBLER_NOTIFICATION = 'NativeScrobblerNotification';
+
 /**
  * This storage contains the blocklist of each connector.
  * Each blocklist is an object with channel IDs as keys.
@@ -135,6 +141,7 @@ const storageTypeMap = {
 	Webhook: LOCAL,
 	Pleroma: LOCAL,
 
+	[NATIVE_SCROBBLER_NOTIFICATION]: LOCAL,
 	[BLOCKLISTS]: LOCAL,
 	[LOCAL_CACHE]: LOCAL,
 	[REGEX_EDITS]: LOCAL,
diff --git a/src/core/storage/native-scrobbler-notification.ts b/src/core/storage/native-scrobbler-notification.ts
new file mode 100644
index 00000000..cd4706ff
--- /dev/null
+++ b/src/core/storage/native-scrobbler-notification.ts
@@ -0,0 +1,39 @@
+import * as BrowserStorage from '@/core/storage/browser-storage';
+
+export default class NativeScrobblerNotification {
+	private storage = BrowserStorage.getStorage(
+		BrowserStorage.NATIVE_SCROBBLER_NOTIFICATION,
+	);
+
+	/**
+	 * Add connector ID to list of connectors that has been notified about.
+	 *
+	 * @param id - ID of connector to add
+	 */
+	public async saveHasNotified(id: string): Promise<void> {
+		if (!id) {
+			return;
+		}
+		let data = await this.storage.getLocking();
+		if (!data) {
+			data = {};
+		}
+		data[id] = true;
+		await this.storage.setLocking(data);
+	}
+
+	/**
+	 * @param id - ID of connector to check
+	 *
+	 * @returns true if notification has not been given for connector; false otherwise
+	 */
+	public async shouldNotifyAboutNativeScrobbler(
+		id: string,
+	): Promise<boolean> {
+		const data = await this.storage.get();
+		if (!data) {
+			return true;
+		}
+		return !data[id];
+	}
+}
diff --git a/src/core/storage/wrapper.ts b/src/core/storage/wrapper.ts
index 8eb526ed..fccd33a8 100644
--- a/src/core/storage/wrapper.ts
+++ b/src/core/storage/wrapper.ts
@@ -13,6 +13,7 @@ import {
 	STATE_MANAGEMENT,
 	StorageNamespace,
 	BLOCKLISTS,
+	NATIVE_SCROBBLER_NOTIFICATION,
 } from '@/core/storage/browser-storage';
 import {
 	ConnectorOptions,
@@ -87,7 +88,7 @@ export interface StateManagement {
 
 export type Blocklists = Record<string, Blocklist>;
 
-export type Blocklist = Record<string, true>;
+export type Blocklist = Record<string, string>;
 
 export interface DataModels extends ScrobblerModels {
 	/* sync options */
@@ -110,6 +111,9 @@ export interface DataModels extends ScrobblerModels {
 			[key in (typeof connectors)[number]['id']]: true;
 		};
 	};
+	[NATIVE_SCROBBLER_NOTIFICATION]: {
+		[key in (typeof connectors)[number]['id']]: true;
+	};
 }
 
 type StorageEvents = {
diff --git a/src/theme/themes.scss b/src/theme/themes.scss
index 600a6f54..0e2b1bbf 100644
--- a/src/theme/themes.scss
+++ b/src/theme/themes.scss
@@ -5,16 +5,21 @@ $themes: (
 		always-last-fm-hover: #fc3434,
 		background: #fff,
 		checkbox-inactive: #8e8e8e,
+		checkbox-active: #d82323,
 		sidebar-bg: rgb(250 235 235 / 70%),
 		context-bg: rgb(250 235 235 / 95%),
 		modal-bg: rgb(255 255 255 / 95%),
 		last-fm-bg: #faebeb,
 		muted-text: #555,
+		active-text: #fff,
 		text: #222,
 		last-fm: #d82323,
 		confirm: #00b300,
+		reject: #d82323,
+		indeterminate: #8e8e8e,
 		site-image: url('../img/main/site-light.jpg'),
 		invert-filter: invert(0%),
+		interactable-border: none,
 	),
 	dark: (
 		always-light: #fff,
@@ -22,16 +27,65 @@ $themes: (
 		always-last-fm-hover: #fc3434,
 		background: #1e1e1e,
 		checkbox-inactive: #8e8e8e,
+		checkbox-active: #d82323,
 		sidebar-bg: rgb(194 57 55 / 70%),
 		context-bg: rgb(194 57 55 / 95%),
 		modal-bg: rgb(40 40 40 / 95%),
 		last-fm-bg: #c23937,
 		muted-text: #bbb,
+		active-text: #fff,
 		text: #eee,
 		last-fm: #fc3434,
 		confirm: #00b300,
+		reject: #fc3434,
+		indeterminate: #8e8e8e,
 		site-image: url('../img/main/site-dark.jpg'),
 		invert-filter: invert(100%),
+		interactable-border: none,
+	),
+	high-contrast-dark: (
+		always-light: yellow,
+		always-last-fm: black,
+		always-last-fm-hover: lavender,
+		background: black,
+		checkbox-inactive: black,
+		checkbox-active: #8e8e8e,
+		sidebar-bg: black,
+		context-bg: black,
+		modal-bg: black,
+		last-fm-bg: black,
+		muted-text: yellow,
+		text: yellow,
+		active-text: black,
+		last-fm: lavender,
+		confirm: lime,
+		reject: red,
+		indeterminate: #8e8e8e,
+		site-image: black,
+		invert-filter: invert(100%),
+		interactable-border: 1px solid yellow,
+	),
+	high-contrast-light: (
+		always-light: black,
+		always-last-fm: seashell,
+		always-last-fm-hover: saddlebrown,
+		background: seashell,
+		checkbox-inactive: seashell,
+		checkbox-active: #8e8e8e,
+		sidebar-bg: seashell,
+		context-bg: seashell,
+		modal-bg: seashell,
+		last-fm-bg: seashell,
+		muted-text: black,
+		text: black,
+		active-text: seashell,
+		last-fm: saddlebrown,
+		confirm: lime,
+		reject: red,
+		indeterminate: #8e8e8e,
+		site-image: seashell,
+		invert-filter: invert(0%),
+		interactable-border: 1px solid black,
 	),
 );
 
@@ -43,7 +97,21 @@ $themes: (
 	}
 }
 
+// stylelint-disable-next-line
+.theme-high-contrast-dark a {
+	border: var(--interactable-border);
+}
+
+// stylelint-disable-next-line
+.theme-high-contrast-light a {
+	border: var(--interactable-border);
+}
+
 body {
 	// Avoid animation flickering in iOS
 	-webkit-transform: translate3d(0, 0, 0);
 }
+
+* {
+	-ms-high-contrast-adjust: none;
+}
diff --git a/src/theme/themes.ts b/src/theme/themes.ts
index aa889d65..1ba297ee 100644
--- a/src/theme/themes.ts
+++ b/src/theme/themes.ts
@@ -35,6 +35,20 @@ export async function updateTheme(theme: ModifiedTheme) {
 	});
 }
 
+/**
+ * @returns true if user has dark theme enabled on system/browser level; false otherwise
+ */
+const prefersDarkTheme = () =>
+	window.matchMedia('(prefers-color-scheme: dark)').matches;
+
+/**
+ * @returns true if user has high contrast mode enabled on system/browser level; false otherwise
+ */
+const usesHighContrast = () =>
+	window.matchMedia(
+		'(forced-colors: active), (prefers-contrast: more), (-ms-high-contrast: active)',
+	).matches;
+
 /**
  * Process the current theme, to get the correct display theme for themes that have multiple possibilities
  *
@@ -43,18 +57,28 @@ export async function updateTheme(theme: ModifiedTheme) {
  */
 function processTheme(theme: string) {
 	if (theme === 'theme-system') {
-		if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
-			return 'theme-dark';
-		}
-		return 'theme-light';
+		const themeColor = prefersDarkTheme() ? 'dark' : 'light';
+		const highContrastString = usesHighContrast() ? 'high-contrast-' : '';
+		return `theme-${highContrastString}${themeColor}`;
 	}
 	return theme;
 }
 
-type Theme = 'system' | 'dark' | 'light';
+type Theme =
+	| 'system'
+	| 'dark'
+	| 'light'
+	| 'high-contrast-dark'
+	| 'high-contrast-light';
 export type ModifiedTheme = `theme-${Theme}`;
 
-export const themeList: Theme[] = ['system', 'dark', 'light'];
+export const themeList: Theme[] = [
+	'system',
+	'dark',
+	'light',
+	'high-contrast-dark',
+	'high-contrast-light',
+];
 export const modifiedThemeList = themeList.map(
 	(theme) => `theme-${theme}`,
 ) as ModifiedTheme[];
diff --git a/src/ui/components/context-menu/context-menu.module.scss b/src/ui/components/context-menu/context-menu.module.scss
index ff635817..2aa9e1fe 100644
--- a/src/ui/components/context-menu/context-menu.module.scss
+++ b/src/ui/components/context-menu/context-menu.module.scss
@@ -4,6 +4,7 @@
 	display: flex;
 
 	.contextMenuItem {
+		border: var(--interactable-border);
 		flex-grow: 1;
 		padding: 0.5em 0;
 		position: relative;
@@ -11,7 +12,7 @@
 		@supports (-webkit-touch-callout: none) {
 			&.activeDialogButton {
 				background-color: var(--always-last-fm-hover);
-				color: var(--always-light);
+				color: var(--active-text);
 			}
 		}
 
@@ -34,6 +35,7 @@
 
 .contextMenu,
 .contextMenuDialog {
+	border: var(--interactable-border);
 	z-index: 1000000;
 
 	// stylelint-disable-next-line no-duplicate-selectors
@@ -45,7 +47,6 @@
 
 	.contextMenuItem {
 		background-color: transparent;
-		border: none;
 		color: var(--text);
 		cursor: pointer;
 		display: flex;
@@ -62,7 +63,7 @@
 		@supports not (-webkit-touch-callout: none) {
 			&:hover {
 				background-color: var(--always-last-fm-hover);
-				color: var(--always-light);
+				color: var(--active-text);
 			}
 		}
 	}
@@ -71,7 +72,6 @@
 .contextMenuDialog {
 	animation: 0s ease-in-out forwards hide;
 	background-color: var(--context-bg);
-	border: none;
 	border-radius: 0.5em;
 	color: var(--text);
 	display: flex;
@@ -111,6 +111,7 @@
 	}
 
 	.contextMenuItem {
+		border: none;
 		padding: 1em;
 
 		&:not(:last-child) {
diff --git a/src/ui/components/context-menu/context-menu.tsx b/src/ui/components/context-menu/context-menu.tsx
index 63daa89c..908d47aa 100644
--- a/src/ui/components/context-menu/context-menu.tsx
+++ b/src/ui/components/context-menu/context-menu.tsx
@@ -130,7 +130,9 @@ function ContextMenuItem(props: {
 				}}
 			>
 				<props.item.icon />
-				<label>{t(props.item.namei18n)}</label>
+				<label>
+					{t(props.item.namei18n, props.item.i18nSubstitution)}
+				</label>
 			</button>
 			<Show when={!itemIsSingular(props.item)}>
 				<dialog
diff --git a/src/ui/components/icons.module.scss b/src/ui/components/icons.module.scss
new file mode 100644
index 00000000..d3b24b9c
--- /dev/null
+++ b/src/ui/components/icons.module.scss
@@ -0,0 +1,10 @@
+.materialIcon {
+	display: inline-block;
+	fill: currentcolor;
+	flex-shrink: 0;
+	font-size: 1.5rem;
+	height: 1em;
+	transition: fill 200ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
+	user-select: none;
+	width: 1em;
+}
diff --git a/src/ui/components/icons.tsx b/src/ui/components/icons.tsx
new file mode 100644
index 00000000..ba295331
--- /dev/null
+++ b/src/ui/components/icons.tsx
@@ -0,0 +1,249 @@
+import { JSXElement, createMemo } from 'solid-js';
+import styles from './icons.module.scss';
+
+function createMaterialIcon(Path: () => JSXElement) {
+	const Component = (props: { class?: string }) => {
+		const className = createMemo(() =>
+			props.class
+				? `${styles.materialIcon} ${props.class}`
+				: styles.materialIcon,
+		);
+
+		return (
+			<svg
+				aria-hidden="true"
+				{...{
+					['focusable']: 'false',
+				}}
+				viewBox="0 0 24 24"
+				{...props}
+				class={className()}
+			>
+				<Path />
+			</svg>
+		);
+	};
+	return Component;
+}
+
+export const AddOutlined = createMaterialIcon(() => (
+	<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
+));
+
+export const AutorenewOutlined = createMaterialIcon(() => (
+	<path d="M12 6v3l4-4-4-4v3c-4.42 0-8 3.58-8 8 0 1.57.46 3.03 1.24 4.26L6.7 14.8c-.45-.83-.7-1.79-.7-2.8 0-3.31 2.69-6 6-6zm6.76 1.74L17.3 9.2c.44.84.7 1.79.7 2.8 0 3.31-2.69 6-6 6v-3l-4 4 4 4v-3c4.42 0 8-3.58 8-8 0-1.57-.46-3.03-1.24-4.26z" />
+));
+
+export const BlockOutlined = createMaterialIcon(() => (
+	<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zM4 12c0-4.42 3.58-8 8-8 1.85 0 3.55.63 4.9 1.69L5.69 16.9C4.63 15.55 4 13.85 4 12zm8 8c-1.85 0-3.55-.63-4.9-1.69L18.31 7.1C19.37 8.45 20 10.15 20 12c0 4.42-3.58 8-8 8z" />
+));
+
+export const CancelScheduleSendOutlined = createMaterialIcon(() => (
+	<g>
+		<g>
+			<path d="M16.5,9c-0.42,0-0.83,0.04-1.24,0.11L1.01,3L1,10l10.06,1.34c-0.42,0.44-0.78,0.93-1.09,1.46L1,14l0.01,7l8.07-3.46 C9.59,21.19,12.71,24,16.5,24c4.14,0,7.5-3.36,7.5-7.5S20.64,9,16.5,9z M3,8.25l0.01-2.22l7.51,3.22L3,8.25z M9.1,15.36L3,17.97 v-2.22l6.17-0.82C9.14,15.07,9.12,15.21,9.1,15.36z M16.5,22c-3.03,0-5.5-2.47-5.5-5.5s2.47-5.5,5.5-5.5s5.5,2.47,5.5,5.5 S19.53,22,16.5,22z" />
+			<polygon points="18.27,14.03 16.5,15.79 14.73,14.03 14.03,14.73 15.79,16.5 14.03,18.27 14.73,18.97 16.5,17.21 18.27,18.97 18.97,18.27 17.21,16.5 18.97,14.73" />
+		</g>
+	</g>
+));
+
+export const CheckOutlined = createMaterialIcon(() => (
+	<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z" />
+));
+
+export const CloseOutlined = createMaterialIcon(() => (
+	<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
+));
+
+export const CodeOutlined = createMaterialIcon(() => (
+	<path d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z" />
+));
+
+export const ContactsOutlined = createMaterialIcon(() => (
+	<path d="M20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 14H4V6h16v12zM4 0h16v2H4zm0 22h16v2H4zm8-10c1.38 0 2.5-1.12 2.5-2.5S13.38 7 12 7 9.5 8.12 9.5 9.5 10.62 12 12 12zm0-3.5c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm5 7.49C17 13.9 13.69 13 12 13s-5 .9-5 2.99V17h10v-1.01zm-8.19-.49c.61-.52 2.03-1 3.19-1 1.17 0 2.59.48 3.2 1H8.81z" />
+));
+
+export const DeleteOutlined = createMaterialIcon(() => (
+	<path d="M16 9v10H8V9h8m-1.5-6h-5l-1 1H5v2h14V4h-3.5l-1-1zM18 7H6v12c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7z" />
+));
+
+export const DownloadOutlined = createMaterialIcon(() => (
+	<path d="M19 9h-4V3H9v6H5l7 7 7-7zm-8 2V5h2v6h1.17L12 13.17 9.83 11H11zm-6 7h14v2H5z" />
+));
+
+export const EditOutlined = createMaterialIcon(() => (
+	<path d="M14.06 9.02l.92.92L5.92 19H5v-.92l9.06-9.06M17.66 3c-.25 0-.51.1-.7.29l-1.83 1.83 3.75 3.75 1.83-1.83c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.2-.2-.45-.29-.71-.29zm-3.6 3.19L3 17.25V21h3.75L17.81 9.94l-3.75-3.75z" />
+));
+
+export const ErrorOutlined = createMaterialIcon(() => (
+	<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z" />
+));
+
+export const ExpandMoreOutlined = createMaterialIcon(() => (
+	<>
+		<path d="M24 24H0V0h24v24z" fill="none" opacity=".87" />
+		<path d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6-1.41-1.41z" />
+	</>
+));
+
+export const ExtensionOutlined = createMaterialIcon(() => (
+	<path d="M10.5 4.5c.28 0 .5.22.5.5v2h6v6h2c.28 0 .5.22.5.5s-.22.5-.5.5h-2v6h-2.12c-.68-1.75-2.39-3-4.38-3s-3.7 1.25-4.38 3H4v-2.12c1.75-.68 3-2.39 3-4.38 0-1.99-1.24-3.7-2.99-4.38L4 7h6V5c0-.28.22-.5.5-.5m0-2C9.12 2.5 8 3.62 8 5H4c-1.1 0-1.99.9-1.99 2v3.8h.29c1.49 0 2.7 1.21 2.7 2.7s-1.21 2.7-2.7 2.7H2V20c0 1.1.9 2 2 2h3.8v-.3c0-1.49 1.21-2.7 2.7-2.7s2.7 1.21 2.7 2.7v.3H17c1.1 0 2-.9 2-2v-4c1.38 0 2.5-1.12 2.5-2.5S20.38 11 19 11V7c0-1.1-.9-2-2-2h-4c0-1.38-1.12-2.5-2.5-2.5z" />
+));
+
+export const FavoriteOutlined = createMaterialIcon(() => (
+	<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" />
+));
+
+export const HeartBrokenOutlined = createMaterialIcon(() => (
+	<g>
+		<path d="M16.5,3c-0.96,0-1.9,0.25-2.73,0.69L12,9h3l-3,10l1-9h-3l1.54-5.39C10.47,3.61,9.01,3,7.5,3C4.42,3,2,5.42,2,8.5 c0,4.13,4.16,7.18,10,12.5c5.47-4.94,10-8.26,10-12.5C22,5.42,19.58,3,16.5,3z M10.24,16.73C6.45,13.34,4,11,4,8.5 C4,6.54,5.54,5,7.5,5c0.59,0,1.19,0.15,1.73,0.42L7.35,12h3.42L10.24,16.73z M15.13,15.53L17.69,7h-2.91l0.61-1.82 C15.75,5.06,16.13,5,16.5,5C18.46,5,20,6.54,20,8.5C20,10.71,17.98,12.93,15.13,15.53z" />
+	</g>
+));
+
+export const HelpOutlined = createMaterialIcon(() => (
+	<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 17h-2v-2h2v2zm2.07-7.75l-.9.92C13.45 12.9 13 13.5 13 15h-2v-.5c0-1.1.45-2.1 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41 0-1.1-.9-2-2-2s-2 .9-2 2H8c0-2.21 1.79-4 4-4s4 1.79 4 4c0 .88-.36 1.68-.93 2.25z" />
+));
+
+export const IndeterminateCheckBoxOutlined = createMaterialIcon(() => (
+	<>
+		<path d="M0 0h24v24H0z" fill="none" />
+		<path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V5h14v14zM7 11h10v2H7z" />
+	</>
+));
+
+export const InfoOutlined = createMaterialIcon(() => (
+	<path d="M11 7h2v2h-2zm0 4h2v6h-2zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z" />
+));
+
+export const LockOutlined = createMaterialIcon(() => (
+	<>
+		<g fill="none">
+			<path d="M0 0h24v24H0V0z" />
+			<path d="M0 0h24v24H0V0z" opacity=".87" />
+		</g>
+		<path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zM9 6c0-1.66 1.34-3 3-3s3 1.34 3 3v2H9V6zm9 14H6V10h12v10zm-6-3c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z" />
+	</>
+));
+
+export const ManageAccountsOutlined = createMaterialIcon(() => (
+	<>
+		<g>
+			<path d="M0,0h24v24H0V0z" fill="none" />
+		</g>
+		<g>
+			<g>
+				<path d="M4,18v-0.65c0-0.34,0.16-0.66,0.41-0.81C6.1,15.53,8.03,15,10,15c0.03,0,0.05,0,0.08,0.01c0.1-0.7,0.3-1.37,0.59-1.98 C10.45,13.01,10.23,13,10,13c-2.42,0-4.68,0.67-6.61,1.82C2.51,15.34,2,16.32,2,17.35V20h9.26c-0.42-0.6-0.75-1.28-0.97-2H4z" />
+				<path d="M10,12c2.21,0,4-1.79,4-4s-1.79-4-4-4C7.79,4,6,5.79,6,8S7.79,12,10,12z M10,6c1.1,0,2,0.9,2,2s-0.9,2-2,2 c-1.1,0-2-0.9-2-2S8.9,6,10,6z" />
+				<path d="M20.75,16c0-0.22-0.03-0.42-0.06-0.63l1.14-1.01l-1-1.73l-1.45,0.49c-0.32-0.27-0.68-0.48-1.08-0.63L18,11h-2l-0.3,1.49 c-0.4,0.15-0.76,0.36-1.08,0.63l-1.45-0.49l-1,1.73l1.14,1.01c-0.03,0.21-0.06,0.41-0.06,0.63s0.03,0.42,0.06,0.63l-1.14,1.01 l1,1.73l1.45-0.49c0.32,0.27,0.68,0.48,1.08,0.63L16,21h2l0.3-1.49c0.4-0.15,0.76-0.36,1.08-0.63l1.45,0.49l1-1.73l-1.14-1.01 C20.72,16.42,20.75,16.22,20.75,16z M17,18c-1.1,0-2-0.9-2-2s0.9-2,2-2s2,0.9,2,2S18.1,18,17,18z" />
+			</g>
+		</g>
+	</>
+));
+
+export const MusicNoteOutlined = createMaterialIcon(() => (
+	<path d="M12 3l.01 10.55c-.59-.34-1.27-.55-2-.55C7.79 13 6 14.79 6 17s1.79 4 4.01 4S14 19.21 14 17V7h4V3h-6zm-1.99 16c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z" />
+));
+
+export const MusicOffOutlined = createMaterialIcon(() => (
+	<path d="M14 7h4V3h-6v4.61l2 2zm-2 3.44L4.41 2.86 3 4.27l9 9v.28c-.94-.54-2.1-.75-3.33-.32-1.34.48-2.37 1.67-2.61 3.07-.46 2.74 1.86 5.08 4.59 4.65 1.96-.31 3.35-2.11 3.35-4.1v-1.58L19.73 21l1.41-1.41L12 10.44zM10 19c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z" />
+));
+
+export const PersonOffOutlined = createMaterialIcon(() => (
+	<>
+		<rect fill="none" height="24" width="24" />
+		<path d="M20,17.17l-3.37-3.38c0.64,0.22,1.23,0.48,1.77,0.76C19.37,15.06,19.98,16.07,20,17.17z M21.19,21.19l-1.41,1.41L17.17,20H4 v-2.78c0-1.12,0.61-2.15,1.61-2.66c1.29-0.66,2.87-1.22,4.67-1.45L1.39,4.22l1.41-1.41L21.19,21.19z M15.17,18l-3-3 c-0.06,0-0.11,0-0.17,0c-2.37,0-4.29,0.73-5.48,1.34C6.2,16.5,6,16.84,6,17.22V18H15.17z M12,6c1.1,0,2,0.9,2,2 c0,0.86-0.54,1.59-1.3,1.87l1.48,1.48C15.28,10.64,16,9.4,16,8c0-2.21-1.79-4-4-4c-1.4,0-2.64,0.72-3.35,1.82l1.48,1.48 C10.41,6.54,11.14,6,12,6z" />
+	</>
+));
+
+export const PersonOutlined = createMaterialIcon(() => (
+	<path d="M12 6c1.1 0 2 .9 2 2s-.9 2-2 2-2-.9-2-2 .9-2 2-2m0 10c2.7 0 5.8 1.29 6 2H6c.23-.72 3.31-2 6-2m0-12C9.79 4 8 5.79 8 8s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm0 10c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" />
+));
+
+export const PublishedWithChangesOutlined = createMaterialIcon(() => (
+	<>
+		<rect fill="none" height="24" width="24" />
+		<path d="M18.6,19.5H21v2h-6v-6h2v2.73c1.83-1.47,3-3.71,3-6.23c0-4.07-3.06-7.44-7-7.93V2.05c5.05,0.5,9,4.76,9,9.95 C22,14.99,20.68,17.67,18.6,19.5z M4,12c0-2.52,1.17-4.77,3-6.23V8.5h2v-6H3v2h2.4C3.32,6.33,2,9.01,2,12c0,5.19,3.95,9.45,9,9.95 v-2.02C7.06,19.44,4,16.07,4,12z M16.24,8.11l-5.66,5.66l-2.83-2.83l-1.41,1.41l4.24,4.24l7.07-7.07L16.24,8.11z" />
+	</>
+));
+
+export const RestartAltOutlined = createMaterialIcon(() => (
+	<>
+		<g>
+			<path d="M0,0h24v24H0V0z" fill="none" />
+		</g>
+		<g>
+			<g>
+				<path d="M6,13c0-1.65,0.67-3.15,1.76-4.24L6.34,7.34C4.9,8.79,4,10.79,4,13c0,4.08,3.05,7.44,7,7.93v-2.02 C8.17,18.43,6,15.97,6,13z M20,13c0-4.42-3.58-8-8-8c-0.06,0-0.12,0.01-0.18,0.01l1.09-1.09L11.5,2.5L8,6l3.5,3.5l1.41-1.41 l-1.08-1.08C11.89,7.01,11.95,7,12,7c3.31,0,6,2.69,6,6c0,2.97-2.17,5.43-5,5.91v2.02C16.95,20.44,20,17.08,20,13z" />
+			</g>
+		</g>
+	</>
+));
+
+export const SaveOutlined = createMaterialIcon(() => (
+	<path d="M17 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V7l-4-4zm2 16H5V5h11.17L19 7.83V19zm-7-7c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3zM6 6h9v4H6z" />
+));
+
+export const Send = createMaterialIcon(() => (
+	<>
+		<path d="M0 0h24v24H0z" fill="none" />
+		<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z" />
+	</>
+));
+
+export const SentimentDissatisfiedOutlined = createMaterialIcon(() => (
+	<>
+		<circle cx="15.5" cy="9.5" r="1.5" />
+		<circle cx="8.5" cy="9.5" r="1.5" />
+		<path d="M12 14c-2.33 0-4.32 1.45-5.12 3.5h1.67c.69-1.19 1.97-2 3.45-2s2.75.81 3.45 2h1.67c-.8-2.05-2.79-3.5-5.12-3.5zm-.01-12C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z" />
+	</>
+));
+
+export const SettingsOutlined = createMaterialIcon(() => (
+	<path d="M19.43 12.98c.04-.32.07-.64.07-.98 0-.34-.03-.66-.07-.98l2.11-1.65c.19-.15.24-.42.12-.64l-2-3.46c-.09-.16-.26-.25-.44-.25-.06 0-.12.01-.17.03l-2.49 1c-.52-.4-1.08-.73-1.69-.98l-.38-2.65C14.46 2.18 14.25 2 14 2h-4c-.25 0-.46.18-.49.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1c-.06-.02-.12-.03-.18-.03-.17 0-.34.09-.43.25l-2 3.46c-.13.22-.07.49.12.64l2.11 1.65c-.04.32-.07.65-.07.98 0 .33.03.66.07.98l-2.11 1.65c-.19.15-.24.42-.12.64l2 3.46c.09.16.26.25.44.25.06 0 .12-.01.17-.03l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65c.03.24.24.42.49.42h4c.25 0 .46-.18.49-.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1c.06.02.12.03.18.03.17 0 .34-.09.43-.25l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.65zm-1.98-1.71c.04.31.05.52.05.73 0 .21-.02.43-.05.73l-.14 1.13.89.7 1.08.84-.7 1.21-1.27-.51-1.04-.42-.9.68c-.43.32-.84.56-1.25.73l-1.06.43-.16 1.13-.2 1.35h-1.4l-.19-1.35-.16-1.13-1.06-.43c-.43-.18-.83-.41-1.23-.71l-.91-.7-1.06.43-1.27.51-.7-1.21 1.08-.84.89-.7-.14-1.13c-.03-.31-.05-.54-.05-.74s.02-.43.05-.73l.14-1.13-.89-.7-1.08-.84.7-1.21 1.27.51 1.04.42.9-.68c.43-.32.84-.56 1.25-.73l1.06-.43.16-1.13.2-1.35h1.39l.19 1.35.16 1.13 1.06.43c.43.18.83.41 1.23.71l.91.7 1.06-.43 1.27-.51.7 1.21-1.07.85-.89.7.14 1.13zM12 8c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm0 6c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z" />
+));
+
+export const TimerOutlined = createMaterialIcon(() => (
+	<g>
+		<g>
+			<g>
+				<path d="M15,1H9v2h6V1z M11,14h2V8h-2V14z M19.03,7.39l1.42-1.42c-0.43-0.51-0.9-0.99-1.41-1.41l-1.42,1.42 C16.07,4.74,14.12,4,12,4c-4.97,0-9,4.03-9,9s4.02,9,9,9s9-4.03,9-9C21,10.88,20.26,8.93,19.03,7.39z M12,20c-3.87,0-7-3.13-7-7 s3.13-7,7-7s7,3.13,7,7S15.87,20,12,20z" />
+			</g>
+		</g>
+	</g>
+));
+
+export const ToggleOffOutlined = createMaterialIcon(() => (
+	<path d="M17 6H7c-3.31 0-6 2.69-6 6s2.69 6 6 6h10c3.31 0 6-2.69 6-6s-2.69-6-6-6zm0 10H7c-2.21 0-4-1.79-4-4s1.79-4 4-4h10c2.21 0 4 1.79 4 4s-1.79 4-4 4zM7 9c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z" />
+));
+
+export const ToggleOnOutlined = createMaterialIcon(() => (
+	<>
+		<path d="M0 0h24v24H0z" fill="none" />
+		<path d="M17 6H7c-3.31 0-6 2.69-6 6s2.69 6 6 6h10c3.31 0 6-2.69 6-6s-2.69-6-6-6zm0 10H7c-2.21 0-4-1.79-4-4s1.79-4 4-4h10c2.21 0 4 1.79 4 4s-1.79 4-4 4zm0-7c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z" />
+	</>
+));
+
+export const TuneOutlined = createMaterialIcon(() => (
+	<path d="M3 17v2h6v-2H3zM3 5v2h10V5H3zm10 16v-2h8v-2h-8v-2h-2v6h2zM7 9v2H3v2h4v2h2V9H7zm14 4v-2H11v2h10zm-6-4h2V7h4V5h-4V3h-2v6z" />
+));
+
+export const UploadOutlined = createMaterialIcon(() => (
+	<path d="M9 16h6v-6h4l-7-7-7 7h4v6zm3-10.17L14.17 8H13v6h-2V8H9.83L12 5.83zM5 18h14v2H5z" />
+));
+
+export const VisibilityOutlined = createMaterialIcon(() => (
+	<path d="M12 6c3.79 0 7.17 2.13 8.82 5.5C19.17 14.87 15.79 17 12 17s-7.17-2.13-8.82-5.5C4.83 8.13 8.21 6 12 6m0-2C7 4 2.73 7.11 1 11.5 2.73 15.89 7 19 12 19s9.27-3.11 11-7.5C21.27 7.11 17 4 12 4zm0 5c1.38 0 2.5 1.12 2.5 2.5S13.38 14 12 14s-2.5-1.12-2.5-2.5S10.62 9 12 9m0-2c-2.48 0-4.5 2.02-4.5 4.5S9.52 16 12 16s4.5-2.02 4.5-4.5S14.48 7 12 7z" />
+));
+
+export const VolunteerActivismOutlined = createMaterialIcon(() => (
+	<g>
+		<g>
+			<g>
+				<g>
+					<path d="M16,13c3.09-2.81,6-5.44,6-7.7C22,3.45,20.55,2,18.7,2c-1.04,0-2.05,0.49-2.7,1.25C15.34,2.49,14.34,2,13.3,2 C11.45,2,10,3.45,10,5.3C10,7.56,12.91,10.19,16,13z M13.3,4c0.44,0,0.89,0.21,1.18,0.55L16,6.34l1.52-1.79 C17.81,4.21,18.26,4,18.7,4C19.44,4,20,4.56,20,5.3c0,1.12-2.04,3.17-4,4.99c-1.96-1.82-4-3.88-4-4.99C12,4.56,12.56,4,13.3,4z" />
+					<path d="M19,16h-2c0-1.2-0.75-2.28-1.87-2.7L8.97,11H1v11h6v-1.44l7,1.94l8-2.5v-1C22,17.34,20.66,16,19,16z M3,20v-7h2v7H3z M13.97,20.41L7,18.48V13h1.61l5.82,2.17C14.77,15.3,15,15.63,15,16c0,0-1.99-0.05-2.3-0.15l-2.38-0.79l-0.63,1.9l2.38,0.79 c0.51,0.17,1.04,0.26,1.58,0.26H19c0.39,0,0.74,0.23,0.9,0.56L13.97,20.41z" />
+				</g>
+			</g>
+		</g>
+	</g>
+));
diff --git a/src/ui/components/util.tsx b/src/ui/components/util.tsx
index 33bf86e9..9919c6d0 100644
--- a/src/ui/components/util.tsx
+++ b/src/ui/components/util.tsx
@@ -28,8 +28,13 @@ export function isIos() {
  *
  * @param url - The url to open in a new tab.
  */
-function openInNewTabFromPopup(url: string) {
-	browser.tabs.create({ url });
+async function openInNewTabFromPopup(url: string) {
+	try {
+		const tab = await browser.tabs.query({ active: true });
+		browser.tabs.create({ url, index: tab[0].index + 1 });
+	} catch {
+		browser.tabs.create({ url });
+	}
 }
 
 /**
@@ -46,10 +51,11 @@ export function PopupAnchor(props: {
 		<a
 			href={props.href}
 			title={props.title}
+			aria-label={props.title}
 			class={props.class}
 			onClick={(e) => {
 				e.preventDefault();
-				openInNewTabFromPopup(props.href);
+				void openInNewTabFromPopup(props.href);
 			}}
 		>
 			{props.children}
diff --git a/src/ui/options/components/accounts.tsx b/src/ui/options/components/accounts.tsx
index 97b7616d..920a8931 100644
--- a/src/ui/options/components/accounts.tsx
+++ b/src/ui/options/components/accounts.tsx
@@ -15,10 +15,12 @@ import {
 } from 'solid-js';
 import styles from './components.module.scss';
 import browser from 'webextension-polyfill';
-import Delete from '@suid/icons-material/DeleteOutlined';
-import Save from '@suid/icons-material/SaveOutlined';
-import AutoRenew from '@suid/icons-material/AutorenewOutlined';
-import Check from '@suid/icons-material/CheckOutlined';
+import {
+	DeleteOutlined,
+	SaveOutlined,
+	AutorenewOutlined,
+	CheckOutlined,
+} from '@/ui/components/icons';
 import { debugLog } from '@/util/util';
 import { sendContentMessage } from '@/util/communication';
 import { Dynamic } from 'solid-js/web';
@@ -141,10 +143,11 @@ function ScrobblerDisplay(props: { label: ScrobblerLabel }) {
 	};
 	window.addEventListener('focus', onFocusWrapper);
 	onCleanup(() => window.removeEventListener('focus', onFocusWrapper));
+	const scrobblerLabel = createMemo(() => rawScrobbler()?.getLabel());
 
 	return (
-		<>
-			<h2>{rawScrobbler()?.getLabel()}</h2>
+		<div role="group" aria-label={scrobblerLabel()}>
+			<h2>{scrobblerLabel()}</h2>
 			<Switch
 				fallback={
 					<Show when={!showLocalProps()}>
@@ -214,7 +217,7 @@ function ScrobblerDisplay(props: { label: ScrobblerLabel }) {
 					<ArrayProperties scrobbler={rawScrobbler()} />
 				</Match>
 			</Switch>
-		</>
+		</div>
 	);
 }
 
@@ -257,9 +260,9 @@ function SaveButton() {
 	const [state, setState] = createSignal(SaveState.BASE);
 
 	const icons = {
-		[SaveState.BASE]: Save,
-		[SaveState.SAVING]: AutoRenew,
-		[SaveState.SAVED]: Check,
+		[SaveState.BASE]: SaveOutlined,
+		[SaveState.SAVING]: AutorenewOutlined,
+		[SaveState.SAVED]: CheckOutlined,
 	};
 
 	return (
@@ -404,7 +407,7 @@ function ArrayProperties(props: { scrobbler: Scrobbler | null }) {
 											});
 										}}
 									>
-										<Delete />
+										<DeleteOutlined />
 									</button>
 									<For each={Object.values(item)}>
 										{(val) => (
diff --git a/src/ui/options/components/components.module.scss b/src/ui/options/components/components.module.scss
index b8058d3b..08b000ac 100644
--- a/src/ui/options/components/components.module.scss
+++ b/src/ui/options/components/components.module.scss
@@ -11,13 +11,14 @@
 	}
 
 	.tripleCheckboxWrapper {
+		border: var(--interactable-border);
 		border-radius: $size;
 		display: flex;
 		overflow: hidden;
 
 		.tripleCheckboxLabel {
 			--btn-color: var(--last-fm);
-			--btn-active-color: var(--always-light);
+			--btn-active-color: var(--active-text);
 			--btn-active-background-color: var(--last-fm);
 			align-items: center;
 			background-color: var(--background);
@@ -35,6 +36,11 @@
 				color: var(--btn-active-color);
 			}
 
+			&.unchecked {
+				--btn-color: var(--reject);
+				--btn-active-background-color: var(--reject);
+			}
+
 			&.checked {
 				--btn-color: var(--confirm);
 				--btn-active-background-color: var(--confirm);
@@ -42,10 +48,19 @@
 
 			&.indeterminate {
 				--btn-color: var(--text);
-				--btn-active-background-color: var(--checkbox-inactive);
+				--btn-active-background-color: var(--indeterminate);
+			}
+
+			&:nth-child(2) {
+				border-left: var(--interactable-border);
+				border-right: var(--interactable-border);
 			}
 		}
 
+		&:has(input:focus-visible) {
+			outline: 2px solid var(--text) !important;
+		}
+
 		.tripleCheckbox {
 			-webkit-appearance: none;
 			background-color: #fff;
@@ -77,7 +92,7 @@
 .button {
 	align-items: center;
 	background-color: var(--always-last-fm);
-	border: none;
+	border: var(--interactable-border);
 	border-radius: 3px;
 	color: var(--always-light);
 	cursor: pointer;
@@ -93,6 +108,7 @@
 
 	&:hover {
 		background-color: var(--always-last-fm-hover);
+		color: var(--active-text);
 	}
 
 	span {
@@ -139,6 +155,7 @@
 		--entry-color: var(--text);
 		--entry-margin-top: 5px;
 		align-items: center;
+		border: var(--interactable-border);
 		color: var(--entry-color);
 		display: flex;
 		padding: 0.5em 0;
@@ -201,7 +218,7 @@
 
 	select {
 		background-color: var(--background);
-		border: none;
+		border: var(--interactable-border);
 		border-radius: $size;
 		color: var(--text);
 		display: inline-block;
@@ -228,6 +245,7 @@
 
 	.checkboxWrapper {
 		background-color: var(--checkbox-background);
+		border: var(--interactable-border);
 		border-radius: $checkbox-size;
 		display: inline-block;
 		height: $checkbox-size;
@@ -250,7 +268,7 @@
 	}
 
 	input:checked ~ .checkboxWrapper {
-		--checkbox-background: var(--always-last-fm);
+		--checkbox-background: var(--checkbox-active);
 		--checkbox-offset: calc(
 			#{$tick-width} - #{$tick-offset} - #{$tick-size}
 		);
@@ -275,9 +293,16 @@
 	}
 
 	.bigLabel:hover,
-	&.summaryCheckbox .checkboxWrapper:hover {
+	&.summaryCheckbox .checkboxWrapper:hover,
+	.bigLabel input:focus-visible + .checkboxWrapper,
+	.summarySpan input:focus-visible + .checkboxWrapper {
 		filter: brightness(1.1);
 	}
+
+	.bigLabel input:focus-visible + .checkboxWrapper,
+	.summarySpan input:focus-visible + .checkboxWrapper {
+		outline: 2px solid var(--text);
+	}
 }
 
 .muted {
@@ -286,6 +311,7 @@
 }
 
 summary {
+	border: var(--interactable-border);
 	list-style: none;
 
 	&::-webkit-details-marker {
@@ -294,8 +320,8 @@ summary {
 }
 
 .radioButtons {
-	li {
-		margin-bottom: 1em;
+	.radioButton {
+		margin-bottom: 0.75em;
 	}
 
 	input {
@@ -306,6 +332,7 @@ summary {
 
 	.radioLabel {
 		background-color: var(--background);
+		border: var(--interactable-border);
 		border-radius: 0.5em;
 		color: var(--text);
 		cursor: pointer;
@@ -317,7 +344,11 @@ summary {
 	.radioLabel:hover,
 	input:checked + .radioLabel {
 		background-color: var(--last-fm);
-		color: var(--always-light);
+		color: var(--active-text);
+	}
+
+	input:focus-visible + .radioLabel {
+		outline: 2px solid var(--text);
 	}
 }
 
@@ -353,6 +384,7 @@ summary {
 
 .patternWrapper {
 	$size: 0.5em;
+	border: var(--interactable-border);
 	border-radius: $size;
 	display: flex;
 	margin-bottom: $size;
@@ -365,6 +397,11 @@ summary {
 		color: var(--text);
 		font-size: 1.2em;
 	}
+
+	.button {
+		border: none;
+		border-left: var(--interactable-border);
+	}
 }
 
 .propLabel {
@@ -377,7 +414,7 @@ summary {
 .propInput {
 	$size: 0.5em;
 	background-color: var(--background);
-	border: none;
+	border: var(--interactable-border);
 	border-radius: $size;
 	color: var(--text);
 	font-size: 1em;
@@ -387,13 +424,14 @@ summary {
 
 .permissionsPopup {
 	background-color: var(--sidebar-bg);
-	color: var(--text);
+	border-bottom: var(--interactable-border);
 	margin: 0;
 	text-align: center;
 
 	button {
 		background: none;
 		border: none;
+		color: var(--text);
 		cursor: pointer;
 		font-size: 1.25em;
 		margin: 0;
@@ -403,6 +441,7 @@ summary {
 
 .attentionWindow {
 	background-color: var(--sidebar-bg);
+	border: var(--interactable-border);
 	color: var(--text);
 	font-size: 1.2em;
 	line-height: 1.25;
@@ -532,19 +571,21 @@ summary {
 
 		&::-webkit-slider-runnable-track {
 			background: var(--checkbox-inactive);
+			border: var(--interactable-border);
 			border-radius: 0.5rem;
 			height: 0.5rem;
 		}
 
 		&::-moz-range-track {
 			background: var(--checkbox-inactive);
+			border: var(--interactable-border);
 			border-radius: 0.5rem;
 			height: 0.5rem;
 		}
 
 		&::-moz-range-thumb {
 			background-color: var(--always-last-fm);
-			border: none;
+			border: var(--interactable-border);
 			border-radius: 0.25rem;
 			height: 1.5rem;
 			width: 1rem;
@@ -554,6 +595,7 @@ summary {
 			-webkit-appearance: none;
 			appearance: none;
 			background-color: var(--always-last-fm);
+			border: var(--interactable-border);
 			border-radius: 0.25rem;
 			height: 1.5rem;
 			margin-top: -0.5rem;
@@ -604,7 +646,7 @@ summary {
 			-moz-appearance: textfield;
 			appearance: textfield;
 			background-color: var(--background);
-			border: none;
+			border: var(--interactable-border);
 			border-radius: 0.5em;
 			color: var(--text);
 			font-size: 1.2em;
diff --git a/src/ui/options/components/connector-override.tsx b/src/ui/options/components/connector-override.tsx
index 1c3392d9..11cb04f9 100644
--- a/src/ui/options/components/connector-override.tsx
+++ b/src/ui/options/components/connector-override.tsx
@@ -11,11 +11,13 @@ import {
 	createSignal,
 } from 'solid-js';
 import styles from './components.module.scss';
-import Settings from '@suid/icons-material/SettingsOutlined';
-import ExpandMore from '@suid/icons-material/ExpandMoreOutlined';
-import Delete from '@suid/icons-material/DeleteOutlined';
+import {
+	SettingsOutlined,
+	ExpandMoreOutlined,
+	DeleteOutlined,
+	AddOutlined,
+} from '@/ui/components/icons';
 import connectors, { ConnectorMeta } from '@/core/connectors';
-import Add from '@suid/icons-material/AddOutlined';
 import {
 	Checkbox,
 	ConnectorTripleCheckbox,
@@ -60,7 +62,7 @@ export default function ConnectorOverrideOptions(props: {
 			<p innerHTML={t('optionsCustomPatternsHint')} />
 			<ul class={`${styles.connectorOptionsList} ${styles.optionList}`}>
 				<li>
-					<Settings />
+					<SettingsOutlined />
 					<Checkbox
 						title={t('optionsToggle')}
 						label={t('optionsToggle')}
@@ -151,7 +153,7 @@ function ConnectorOption(props: {
 		<li>
 			<details ref={setRef}>
 				<summary>
-					<ExpandMore class={styles.expandVector} />
+					<ExpandMoreOutlined class={styles.expandVector} />
 					<SummaryCheckbox
 						title={props.connector.label}
 						label={props.connector.label}
@@ -210,142 +212,149 @@ function ConnectorOverrideOptionDetails(props: {
 }) {
 	return (
 		<Show when={props.active()}>
-			<h3>{t('optionsGeneral')}</h3>
-			<Show when={browser.notifications}>
+			<div role="group" aria-label={props.connector.label}>
+				<h3>{t('optionsGeneral')}</h3>
+				<Show when={browser.notifications}>
+					<ConnectorTripleCheckbox
+						title={t('optionUseNotificationsTitle')}
+						label={t('optionUseNotifications')}
+						connector={props.connector}
+						option={Options.USE_NOTIFICATIONS}
+						overrideOptions={overrideOptions}
+						setOverrideOptions={setOverrideOptions}
+						connectorOverrideOptions={connectorOverrideOptions}
+					/>
+					<ConnectorTripleCheckbox
+						title={t('optionUnrecognizedNotificationsTitle')}
+						label={t('optionUnrecognizedNotifications')}
+						connector={props.connector}
+						option={Options.USE_UNRECOGNIZED_SONG_NOTIFICATIONS}
+						overrideOptions={overrideOptions}
+						setOverrideOptions={setOverrideOptions}
+						connectorOverrideOptions={connectorOverrideOptions}
+					/>
+				</Show>
 				<ConnectorTripleCheckbox
-					title={t('optionUseNotificationsTitle')}
-					label={t('optionUseNotifications')}
+					title={t('optionUseInfoboxTitle')}
+					label={t('optionUseInfobox')}
 					connector={props.connector}
-					option={Options.USE_NOTIFICATIONS}
+					option={Options.USE_INFOBOX}
 					overrideOptions={overrideOptions}
 					setOverrideOptions={setOverrideOptions}
 					connectorOverrideOptions={connectorOverrideOptions}
 				/>
 				<ConnectorTripleCheckbox
-					title={t('optionUnrecognizedNotificationsTitle')}
-					label={t('optionUnrecognizedNotifications')}
+					title={t('optionScrobblePodcastsTitle')}
+					label={t('optionScrobblePodcasts')}
 					connector={props.connector}
-					option={Options.USE_UNRECOGNIZED_SONG_NOTIFICATIONS}
+					option={Options.SCROBBLE_PODCASTS}
 					overrideOptions={overrideOptions}
 					setOverrideOptions={setOverrideOptions}
 					connectorOverrideOptions={connectorOverrideOptions}
 				/>
-			</Show>
-			<ConnectorTripleCheckbox
-				title={t('optionUseInfoboxTitle')}
-				label={t('optionUseInfobox')}
-				connector={props.connector}
-				option={Options.USE_INFOBOX}
-				overrideOptions={overrideOptions}
-				setOverrideOptions={setOverrideOptions}
-				connectorOverrideOptions={connectorOverrideOptions}
-			/>
-			<ConnectorTripleCheckbox
-				title={t('optionScrobblePodcastsTitle')}
-				label={t('optionScrobblePodcasts')}
-				connector={props.connector}
-				option={Options.SCROBBLE_PODCASTS}
-				overrideOptions={overrideOptions}
-				setOverrideOptions={setOverrideOptions}
-				connectorOverrideOptions={connectorOverrideOptions}
-			/>
-			<h3>{t('optionsScrobbleBehavior')}</h3>
-			<RadioButtons
-				buttons={[
-					{
-						label: t('optionForceRecognize'),
-						title: t('optionForceRecognizeTitle'),
-						value: Options.FORCE_RECOGNIZE,
-					},
-					{
-						label: t('optionScrobbleRecognizedTracks'),
-						title: t('optionScrobbleRecognizedTracksTitle'),
-						value: Options.SCROBBLE_RECOGNIZED_TRACKS,
-					},
-					{
-						label: t('optionScrobbleEditedTracksOnly'),
-						title: t('optionScrobbleEditedTracksOnlyTitle'),
-						value: Options.SCROBBLE_EDITED_TRACKS_ONLY,
-					},
-				]}
-				name={`${props.connector.id}-scrobbleBehavior`}
-				value={() => {
-					if (
-						overrideOptions()?.[props.connector.id]?.[
-							Options.FORCE_RECOGNIZE
-						]
-					) {
-						return Options.FORCE_RECOGNIZE;
-					}
-					if (
-						overrideOptions()?.[props.connector.id]?.[
-							Options.SCROBBLE_EDITED_TRACKS_ONLY
-						]
-					) {
-						return Options.SCROBBLE_EDITED_TRACKS_ONLY;
-					}
-					if (
-						overrideOptions()?.[props.connector.id]?.[
-							Options.SCROBBLE_RECOGNIZED_TRACKS
-						]
-					) {
-						return Options.SCROBBLE_RECOGNIZED_TRACKS;
-					}
+				<h3 id={`${props.connector.id}-scrobble-behavior`}>
+					{t('optionsScrobbleBehavior')}
+				</h3>
+				<RadioButtons
+					labelledby={`${props.connector.id}-scrobble-behavior`}
+					buttons={[
+						{
+							label: t('optionForceRecognize'),
+							title: t('optionForceRecognizeTitle'),
+							value: Options.FORCE_RECOGNIZE,
+						},
+						{
+							label: t('optionScrobbleRecognizedTracks'),
+							title: t('optionScrobbleRecognizedTracksTitle'),
+							value: Options.SCROBBLE_RECOGNIZED_TRACKS,
+						},
+						{
+							label: t('optionScrobbleEditedTracksOnly'),
+							title: t('optionScrobbleEditedTracksOnlyTitle'),
+							value: Options.SCROBBLE_EDITED_TRACKS_ONLY,
+						},
+					]}
+					name={`${props.connector.id}-scrobbleBehavior`}
+					value={() => {
+						if (
+							overrideOptions()?.[props.connector.id]?.[
+								Options.FORCE_RECOGNIZE
+							]
+						) {
+							return Options.FORCE_RECOGNIZE;
+						}
+						if (
+							overrideOptions()?.[props.connector.id]?.[
+								Options.SCROBBLE_EDITED_TRACKS_ONLY
+							]
+						) {
+							return Options.SCROBBLE_EDITED_TRACKS_ONLY;
+						}
+						if (
+							overrideOptions()?.[props.connector.id]?.[
+								Options.SCROBBLE_RECOGNIZED_TRACKS
+							]
+						) {
+							return Options.SCROBBLE_RECOGNIZED_TRACKS;
+						}
 
-					return '';
-				}}
-				onChange={(e) => {
-					const value = e.currentTarget.value;
-					const connector = props.connector;
-					setOverrideOptions.mutate((o) => {
-						const newOptions = {
-							...(o ?? {}),
-						};
-						newOptions[connector.id] = {
-							...(newOptions[connector.id] ?? {}),
-							[Options.FORCE_RECOGNIZE]:
-								value === Options.FORCE_RECOGNIZE,
-							[Options.SCROBBLE_EDITED_TRACKS_ONLY]:
-								value === Options.SCROBBLE_EDITED_TRACKS_ONLY,
-							[Options.SCROBBLE_RECOGNIZED_TRACKS]:
-								value === Options.SCROBBLE_RECOGNIZED_TRACKS,
-						};
+						return '';
+					}}
+					onChange={(e) => {
+						const value = e.currentTarget.value;
+						const connector = props.connector;
+						setOverrideOptions.mutate((o) => {
+							const newOptions = {
+								...(o ?? {}),
+							};
+							newOptions[connector.id] = {
+								...(newOptions[connector.id] ?? {}),
+								[Options.FORCE_RECOGNIZE]:
+									value === Options.FORCE_RECOGNIZE,
+								[Options.SCROBBLE_EDITED_TRACKS_ONLY]:
+									value ===
+									Options.SCROBBLE_EDITED_TRACKS_ONLY,
+								[Options.SCROBBLE_RECOGNIZED_TRACKS]:
+									value ===
+									Options.SCROBBLE_RECOGNIZED_TRACKS,
+							};
 
-						connectorOverrideOptions.set(newOptions);
-						return newOptions;
-					});
-				}}
-				reset={() => {
-					const connector = props.connector;
-					setOverrideOptions.mutate((o) => {
-						const newOptions = {
-							...(o ?? {}),
-						};
-						delete newOptions[connector.id][
-							Options.FORCE_RECOGNIZE
-						];
-						delete newOptions[connector.id][
-							Options.SCROBBLE_EDITED_TRACKS_ONLY
-						];
-						delete newOptions[connector.id][
-							Options.SCROBBLE_RECOGNIZED_TRACKS
-						];
+							connectorOverrideOptions.set(newOptions);
+							return newOptions;
+						});
+					}}
+					reset={() => {
+						const connector = props.connector;
+						setOverrideOptions.mutate((o) => {
+							const newOptions = {
+								...(o ?? {}),
+							};
+							delete newOptions[connector.id][
+								Options.FORCE_RECOGNIZE
+							];
+							delete newOptions[connector.id][
+								Options.SCROBBLE_EDITED_TRACKS_ONLY
+							];
+							delete newOptions[connector.id][
+								Options.SCROBBLE_RECOGNIZED_TRACKS
+							];
 
-						connectorOverrideOptions.set(newOptions);
-						return newOptions;
-					});
-				}}
-			/>
-			<h3>{t('customPatterns')}</h3>
-			<p>{t('customPatternsHint')}</p>
-			<EditCustomPatterns connector={props.connector} />
-			<Show when={props.connector.usesBlocklist}>
-				<BlockedChannels
-					setActiveModal={props.setActiveModal}
-					modal={props.modal}
-					connector={props.connector}
+							connectorOverrideOptions.set(newOptions);
+							return newOptions;
+						});
+					}}
 				/>
-			</Show>
+				<h3>{t('customPatterns')}</h3>
+				<p>{t('customPatternsHint')}</p>
+				<EditCustomPatterns connector={props.connector} />
+				<Show when={props.connector.usesBlocklist}>
+					<BlockedChannels
+						setActiveModal={props.setActiveModal}
+						modal={props.modal}
+						connector={props.connector}
+					/>
+				</Show>
+			</div>
 		</Show>
 	);
 }
@@ -400,7 +409,7 @@ function EditCustomPatterns(props: { connector: ConnectorMeta }) {
 								});
 							}}
 						>
-							<Delete />
+							<DeleteOutlined />
 						</button>
 					</div>
 				)}
@@ -423,7 +432,7 @@ function EditCustomPatterns(props: { connector: ConnectorMeta }) {
 					});
 				}}
 			>
-				<Add />
+				<AddOutlined />
 				{t('customPatternsAdd')}
 			</button>
 		</>
diff --git a/src/ui/options/components/contact.tsx b/src/ui/options/components/contact.tsx
index 4f60e75d..e52ab07f 100644
--- a/src/ui/options/components/contact.tsx
+++ b/src/ui/options/components/contact.tsx
@@ -1,7 +1,7 @@
 import browser from 'webextension-polyfill';
 import githubIcon from './icons/github-mark.svg';
 import twitterBird from './icons/twitter-logo.svg';
-import Lock from '@suid/icons-material/LockOutlined';
+import { LockOutlined } from '@/ui/components/icons';
 import { t } from '@/util/i18n';
 import styles from './components.module.scss';
 
@@ -68,7 +68,7 @@ export default function ContactComponent() {
 					<li
 						class={`${styles.contactEntry} ${styles.contactPrivacy}`}
 					>
-						<Lock />
+						<LockOutlined />
 						<span class={styles.brandText}>Privacy</span>
 						<span class={styles.desc}>
 							{t('contactPrivacyPolicyDesc')}
diff --git a/src/ui/options/components/edit-options/blocked-channels.tsx b/src/ui/options/components/edit-options/blocked-channels.tsx
index 4b08b3ca..eb6dc03e 100644
--- a/src/ui/options/components/edit-options/blocked-channels.tsx
+++ b/src/ui/options/components/edit-options/blocked-channels.tsx
@@ -8,7 +8,7 @@ import {
 } from 'solid-js';
 import * as BrowserStorage from '@/core/storage/browser-storage';
 import styles from '../components.module.scss';
-import Delete from '@suid/icons-material/DeleteOutlined';
+import { DeleteOutlined } from '@/ui/components/icons';
 import { ExportBlocklist, ImportBlocklist, ViewBlocklist } from './util';
 import { ModalType } from '../navigator';
 import { ConnectorMeta } from '@/core/connectors';
@@ -36,7 +36,11 @@ export default function BlockedChannels(props: {
 		<>
 			<h3>{t('optionsBlockedSources')}</h3>
 			<p>{t('optionsBlockedSourcesDesc')}</p>
-			<div class={styles.buttonContainer}>
+			<div
+				class={styles.buttonContainer}
+				role="group"
+				aria-label={t('optionsBlockedSources')}
+			>
 				<ViewBlocklist
 					setActiveModal={props.setActiveModal}
 					modal={props.modal}
@@ -78,9 +82,10 @@ export function BlocklistModal() {
 						blocklists()?.[connector()?.id ?? ''] ?? {},
 					)}
 				>
-					{([channel]) => (
+					{([channelId, channelLabel]) => (
 						<ChannelInfo
-							channel={channel}
+							channelId={channelId}
+							channelLabel={channelLabel}
 							connector={connector()}
 							mutate={mutate}
 						/>
@@ -95,7 +100,8 @@ export function BlocklistModal() {
  * Component that shows a blocked channel and allows the user to delete it.
  */
 function ChannelInfo(props: {
-	channel: string;
+	channelId: string;
+	channelLabel: string;
 	connector: ConnectorMeta | undefined;
 	mutate: Setter<Blocklists | null | undefined>;
 }) {
@@ -105,23 +111,24 @@ function ChannelInfo(props: {
 				class={`${styles.button} ${styles.small} ${styles.marginRight}`}
 				onClick={(event) => {
 					event.stopPropagation();
-					const channel = props.channel;
+					const channelId = props.channelId;
 					const connector = props.connector;
 					props.mutate((e) => {
 						if (!e || !connector || !e[connector.id]) {
 							return e;
 						}
-						delete e[connector.id][channel];
+						delete e[connector.id][channelId];
 						blocklistStorage.set(e);
 						return {
 							...e,
 						};
 					});
 				}}
+				title={t('optionsDelete', props.channelLabel)}
 			>
-				<Delete />
+				<DeleteOutlined />
 			</button>
-			<span>{props.channel}</span>
+			<span>{props.channelLabel}</span>
 		</li>
 	);
 }
diff --git a/src/ui/options/components/edit-options/edited-tracks.tsx b/src/ui/options/components/edit-options/edited-tracks.tsx
index ae645994..43e18b77 100644
--- a/src/ui/options/components/edit-options/edited-tracks.tsx
+++ b/src/ui/options/components/edit-options/edited-tracks.tsx
@@ -1,9 +1,9 @@
 import { t } from '@/util/i18n';
-import { For, Setter, createResource } from 'solid-js';
+import { For, Setter, createMemo, createResource } from 'solid-js';
 import * as Options from '@/core/storage/options';
 import * as BrowserStorage from '@/core/storage/browser-storage';
 import styles from '../components.module.scss';
-import Delete from '@suid/icons-material/DeleteOutlined';
+import { DeleteOutlined } from '@/ui/components/icons';
 import { ExportEdits, ImportEdits, ViewEdits } from './util';
 import { ModalType } from '../navigator';
 
@@ -21,7 +21,11 @@ export default function EditedTracks(props: {
 		<>
 			<h2>{t('optionsEditedTracks')}</h2>
 			<p>{t('optionsEditedTracksDesc')}</p>
-			<div class={styles.buttonContainer}>
+			<div
+				class={styles.buttonContainer}
+				role="group"
+				aria-label={t('optionsEditedTracks')}
+			>
 				<ViewEdits
 					setActiveModal={props.setActiveModal}
 					modal={props.modal}
@@ -75,6 +79,9 @@ function TrackInfo(props: {
 		| undefined
 	>;
 }) {
+	const label = createMemo(
+		() => `${props.track.artist} - ${props.track.track}`,
+	);
 	return (
 		<li class={styles.deleteListing}>
 			<button
@@ -93,12 +100,11 @@ function TrackInfo(props: {
 						};
 					});
 				}}
+				title={t('optionsDelete', label())}
 			>
-				<Delete />
+				<DeleteOutlined />
 			</button>
-			<span>
-				{props.track.artist} - {props.track.track}
-			</span>
+			<span>{label()}</span>
 		</li>
 	);
 }
diff --git a/src/ui/options/components/edit-options/regex-edits.tsx b/src/ui/options/components/edit-options/regex-edits.tsx
index f5c6827b..8e700b97 100644
--- a/src/ui/options/components/edit-options/regex-edits.tsx
+++ b/src/ui/options/components/edit-options/regex-edits.tsx
@@ -1,8 +1,8 @@
 import { t } from '@/util/i18n';
-import { For, Setter, createResource } from 'solid-js';
+import { For, Setter, createMemo, createResource } from 'solid-js';
 import * as BrowserStorage from '@/core/storage/browser-storage';
 import styles from '../components.module.scss';
-import Delete from '@suid/icons-material/DeleteOutlined';
+import { DeleteOutlined } from '@/ui/components/icons';
 import { FieldType, RegexEdit, pascalCaseField } from '@/util/regex';
 import { ExportEdits, ImportEdits, ViewEdits } from './util';
 import { ModalType } from '../navigator';
@@ -21,7 +21,11 @@ export default function RegexEdits(props: {
 		<>
 			<h2>{t('optionsRegexEdits')}</h2>
 			<p>{t('optionsRegexEditsDesc')}</p>
-			<div class={styles.buttonContainer}>
+			<div
+				class={styles.buttonContainer}
+				role="group"
+				aria-label={t('optionsRegexEdits')}
+			>
 				<ViewEdits
 					setActiveModal={props.setActiveModal}
 					modal={props.modal}
@@ -69,6 +73,10 @@ function EditInfo(props: {
 	edit: RegexEdit;
 	mutate: Setter<RegexEdit[] | null | undefined>;
 }) {
+	const label = createMemo(
+		() =>
+			`${props.edit.search.track} → ${props.edit.replace.track};${props.edit.search.artist} → ${props.edit.replace.artist};${props.edit.search.album} → ${props.edit.replace.album};${props.edit.search.albumArtist} → ${props.edit.replace.albumArtist}`,
+	);
 	return (
 		<li class={styles.deleteListing}>
 			<button
@@ -85,8 +93,9 @@ function EditInfo(props: {
 						return o;
 					});
 				}}
+				title={label()}
 			>
-				<Delete />
+				<DeleteOutlined />
 			</button>
 			<div class={styles.regexDeleteContent}>
 				<span class={styles.regexDeleteSearchLabel}>
diff --git a/src/ui/options/components/edit-options/util.tsx b/src/ui/options/components/edit-options/util.tsx
index 39f230a4..4d55525e 100644
--- a/src/ui/options/components/edit-options/util.tsx
+++ b/src/ui/options/components/edit-options/util.tsx
@@ -1,9 +1,11 @@
 import StorageWrapper, { Blocklists } from '@/core/storage/wrapper';
 import * as Options from '@/core/storage/options';
 import * as BrowserStorage from '@/core/storage/browser-storage';
-import Visibility from '@suid/icons-material/VisibilityOutlined';
-import Upload from '@suid/icons-material/UploadOutlined';
-import Download from '@suid/icons-material/DownloadOutlined';
+import {
+	VisibilityOutlined,
+	UploadOutlined,
+	DownloadOutlined,
+} from '@/ui/components/icons';
 import styles from '../components.module.scss';
 import { t } from '@/util/i18n';
 import { Setter, createSignal } from 'solid-js';
@@ -40,7 +42,7 @@ export function ViewEdits(props: {
 				props.modal?.showModal();
 			}}
 		>
-			<Visibility />
+			<VisibilityOutlined />
 			{t('optionsViewEdited')}
 		</button>
 	);
@@ -63,7 +65,7 @@ export function ViewBlocklist(props: {
 				props.modal?.showModal();
 			}}
 		>
-			<Visibility />
+			<VisibilityOutlined />
 			{t('optionsViewEdited')}
 		</button>
 	);
@@ -83,7 +85,7 @@ export function ExportEdits(props: {
 				void downloadEdits(props.editWrapper, props.filename)
 			}
 		>
-			<Upload />
+			<UploadOutlined />
 			{t('optionsExportEdited')}
 		</button>
 	);
@@ -108,7 +110,7 @@ export function ExportBlocklist(props: {
 				)
 			}
 		>
-			<Upload />
+			<UploadOutlined />
 			{t('optionsExportEdited')}
 		</button>
 	);
@@ -166,7 +168,7 @@ export function ImportBlocklist(props: {
 			class={`${styles.button} ${styles.shiftLeft}`}
 			onClick={() => ref()?.click()}
 		>
-			<Download />
+			<DownloadOutlined />
 			{t('optionsImportEdited')}
 			<input
 				hidden={true}
@@ -199,7 +201,7 @@ export function ImportEdits(props: {
 			class={`${styles.button} ${styles.shiftLeft}`}
 			onClick={() => ref()?.click()}
 		>
-			<Download />
+			<DownloadOutlined />
 			{t('optionsImportEdited')}
 			<input
 				hidden={true}
@@ -273,7 +275,7 @@ function pushBlocklist(
 			void (async (e) => {
 				const newBlocklist = JSON.parse(
 					e.target?.result as string,
-				) as Record<string, true>;
+				) as Record<string, string>;
 				let blocklists = await blocklistWrapper.get();
 				if (!blocklists) {
 					blocklists = {};
diff --git a/src/ui/options/components/inputs.tsx b/src/ui/options/components/inputs.tsx
index 957108e5..363ff6ce 100644
--- a/src/ui/options/components/inputs.tsx
+++ b/src/ui/options/components/inputs.tsx
@@ -4,10 +4,12 @@ import * as Options from '@/core/storage/options';
 import { t } from '@/util/i18n';
 import StorageWrapper from '@/core/storage/wrapper';
 import * as BrowserStorage from '@/core/storage/browser-storage';
-import Check from '@suid/icons-material/CheckOutlined';
-import Close from '@suid/icons-material/CloseOutlined';
-import RestartAlt from '@suid/icons-material/RestartAltOutlined';
-import IndeterminateCheckBox from '@suid/icons-material/IndeterminateCheckBoxOutlined';
+import {
+	CheckOutlined,
+	CloseOutlined,
+	RestartAltOutlined,
+	IndeterminateCheckBoxOutlined,
+} from '@/ui/components/icons';
 import { ConnectorMeta } from '@/core/connectors';
 import { clamp } from '@/util/util';
 
@@ -44,6 +46,35 @@ export function Checkbox(props: {
 	);
 }
 
+/**
+ * Handles clicks and keyboard inputs for {@link SummaryCheckbox}.
+ *
+ * @param e - input event that triggered checkbox change.
+ * @param id - id of checkbox element.
+ * @param onInput - input event to run for checkbox.
+ */
+function handleSummaryCheckboxInput(
+	event: Event & {
+		currentTarget: HTMLLabelElement;
+		target: Element;
+	},
+	id: string,
+	onInput: (
+		e: Event & {
+			currentTarget: HTMLInputElement;
+			target: Element;
+		},
+	) => void,
+) {
+	event.preventDefault();
+	const checkbox = document.getElementById(id) as HTMLInputElement;
+	checkbox.checked = !checkbox.checked;
+	onInput({
+		...event,
+		currentTarget: checkbox,
+	});
+}
+
 /**
  * Checkbox option component made for being inside detail summary element.
  * Safari does not behave well with just a typical old checkbox, so we have to do some working around that.
@@ -68,16 +99,17 @@ export function SummaryCheckbox(props: {
 					onClick={(e) => {
 						// Safari doesn't like labeled checkboxes in detail summaries
 						// hacky but it works, hopefully it doesnt stop working
-						e.preventDefault();
-						const checkbox = document.getElementById(
-							props.id,
-						) as HTMLInputElement;
-						checkbox.checked = !checkbox.checked;
-						props.onInput({
-							...e,
-							currentTarget: checkbox,
-						});
+						handleSummaryCheckboxInput(e, props.id, props.onInput);
+					}}
+					onKeyUp={(e) => {
+						// keyboard navigation does not like checkbox being inside summary.
+						// handle this behavior ourselves.
+						if (e.key !== ' ') {
+							return;
+						}
+						handleSummaryCheckboxInput(e, props.id, props.onInput);
 					}}
+					title={t('menuEnableConnector', props.label)}
 				>
 					<input
 						id={props.id}
@@ -119,12 +151,17 @@ export function RadioButtons(props: {
 			target: Element;
 		},
 	) => void;
+	labelledby: string;
 }) {
 	return (
-		<ul class={`${styles.radioButtons} ${styles.optionList}`}>
+		<div
+			class={`${styles.radioButtons} ${styles.optionList}`}
+			role="radiogroup"
+			aria-labelledby={props.labelledby}
+		>
 			<For each={props.buttons}>
 				{(button) => (
-					<li>
+					<div class={styles.radioButton}>
 						<input
 							type="radio"
 							id={`${props.name}-${button.value}`}
@@ -141,23 +178,23 @@ export function RadioButtons(props: {
 						>
 							{button.label}
 						</label>
-					</li>
+					</div>
 				)}
 			</For>
 			<Show when={props.reset}>
-				<li>
+				<div class={styles.radioButton}>
 					<button
 						class={`${styles.button} ${styles.shiftLeft}`}
 						onClick={(e) => {
 							props.reset?.(e);
 						}}
 					>
-						<RestartAlt />
+						<RestartAltOutlined />
 						{t('buttonReset')}
 					</button>
-				</li>
+				</div>
 			</Show>
-		</ul>
+		</div>
 	);
 }
 
@@ -268,7 +305,13 @@ export function RangeOptionEntry(props: {
 	key: keyof Options.GlobalOptions;
 }) {
 	return (
-		<li class={styles.rangeInput}>
+		<li
+			class={styles.rangeInput}
+			role="group"
+			aria-label={`${t(props.prefixi18n)} ${
+				props.options()?.[props.key] as number
+			}% ${t(props.suffixi18n)}`}
+		>
 			<div>
 				<span class={styles.rangeInputLabel}>
 					{t(props.prefixi18n)}
@@ -303,6 +346,9 @@ export function RangeOptionEntry(props: {
 								return newOptions;
 							});
 						}}
+						title={`${t(props.prefixi18n)} ${
+							props.options()?.[props.key] as number
+						}% ${t(props.suffixi18n)}`}
 					/>
 				</div>
 				<span class={styles.rangeInputLabel}>
@@ -344,6 +390,9 @@ export function RangeOptionEntry(props: {
 						return newOptions;
 					});
 				}}
+				title={`${t(props.prefixi18n)} ${
+					props.options()?.[props.key] as number
+				}% ${t(props.suffixi18n)}`}
 			/>
 		</li>
 	);
@@ -374,7 +423,11 @@ export function TripleCheckbox(props: {
 		<div class={styles.tripleCheckboxOption}>
 			<span title={props.title}>
 				{props.label}
-				<div class={styles.tripleCheckboxWrapper}>
+				<div
+					class={styles.tripleCheckboxWrapper}
+					role="radiogroup"
+					aria-label={props.label}
+				>
 					<label
 						class={`${styles.tripleCheckboxLabel} ${
 							styles.unchecked
@@ -383,6 +436,7 @@ export function TripleCheckbox(props: {
 								? ` ${styles.activeBox}`
 								: ''
 						}`}
+						title={t('optionsDisabled')}
 					>
 						<input
 							class={styles.tripleCheckbox}
@@ -396,7 +450,7 @@ export function TripleCheckbox(props: {
 								props.onInput(TripleCheckboxState.Unchecked)
 							}
 						/>
-						<Close />
+						<CloseOutlined />
 					</label>
 					<label
 						class={`${styles.tripleCheckboxLabel} ${
@@ -406,6 +460,7 @@ export function TripleCheckbox(props: {
 								? ` ${styles.activeBox}`
 								: ''
 						}`}
+						title={t('optionsIndeterminate')}
 					>
 						<input
 							class={styles.tripleCheckbox}
@@ -420,7 +475,7 @@ export function TripleCheckbox(props: {
 								props.onInput(TripleCheckboxState.Indeterminate)
 							}
 						/>
-						<IndeterminateCheckBox />
+						<IndeterminateCheckBoxOutlined />
 					</label>
 					<label
 						class={`${styles.tripleCheckboxLabel} ${
@@ -430,6 +485,7 @@ export function TripleCheckbox(props: {
 								? ` ${styles.activeBox}`
 								: ''
 						}`}
+						title={t('optionsEnabled')}
 					>
 						<input
 							class={styles.tripleCheckbox}
@@ -443,7 +499,7 @@ export function TripleCheckbox(props: {
 								props.onInput(TripleCheckboxState.Checked)
 							}
 						/>
-						<Check />
+						<CheckOutlined />
 					</label>
 				</div>
 			</span>
diff --git a/src/ui/options/components/navigator.ts b/src/ui/options/components/navigator.ts
index 64b3af26..4be79002 100644
--- a/src/ui/options/components/navigator.ts
+++ b/src/ui/options/components/navigator.ts
@@ -1,30 +1,39 @@
 import { Component, Setter } from 'solid-js';
-import Info from '@suid/icons-material/InfoOutlined';
-import Help from '@suid/icons-material/HelpOutlined';
-import Contacts from '@suid/icons-material/ContactsOutlined';
-import Settings from '@suid/icons-material/SettingsOutlined';
-import Edit from '@suid/icons-material/EditOutlined';
-import Extension from '@suid/icons-material/ExtensionOutlined';
-import ManageAccounts from '@suid/icons-material/ManageAccountsOutlined';
+import {
+	InfoOutlined,
+	HelpOutlined,
+	ContactsOutlined,
+	SettingsOutlined,
+	EditOutlined,
+	ExtensionOutlined,
+	ManageAccountsOutlined,
+	TuneOutlined,
+	ToggleOnOutlined,
+	ToggleOffOutlined,
+	TimerOutlined,
+	FavoriteOutlined,
+	PersonOutlined,
+	PersonOffOutlined,
+} from '@/ui/components/icons';
+
 import InfoComponent from '@/ui/options/components/info';
 import ShowSomeLove from '@/ui/options/components/showSomeLove';
 import FAQ from '@/ui/options/components/faq';
 import ContactComponent from '@/ui/options/components/contact';
 import OptionsComponent from '@/ui/options/components/options/options';
 import Accounts from '@/ui/options/components/accounts';
-import Tune from '@suid/icons-material/TuneOutlined';
-import ToggleOn from '@suid/icons-material/ToggleOnOutlined';
-import ToggleOff from '@suid/icons-material/ToggleOffOutlined';
-import Timer from '@suid/icons-material/TimerOutlined';
 import ConnectorOverrideOptions from '@/ui/options/components/connector-override';
 import EditOptions from '@/ui/options/components/edit-options/edit-options';
-import Favorite from '@suid/icons-material/FavoriteOutlined';
 import browser from 'webextension-polyfill';
 import {
+	addToBlocklist,
 	disableConnector,
 	disableUntilClosed,
 	enableConnector,
+	getChannelBlocklistLabel,
+	getChannelDetails,
 	getCurrentTab,
+	removeFromBlocklist,
 } from '@/core/background/util';
 import * as ControllerMode from '@/core/object/controller/controller-mode';
 import AdvancedOptionsComponent from './advanced-settings';
@@ -35,36 +44,39 @@ import AdvancedOptionsComponent from './advanced-settings';
 export type ModalType = 'savedEdits' | 'regexEdits' | 'blocklist' | '';
 
 /**
- * Singular navigator button
+ * Mutual base button
  */
-export type NavigatorNavigationButton = {
+interface NavigatorButtonBase {
 	namei18n: string;
-	icon: typeof ManageAccounts;
+	i18nSubstitution?: string | string[];
+	icon: typeof ManageAccountsOutlined;
+}
+
+/**
+ * Singular navigator button
+ */
+export interface NavigatorNavigationButton extends NavigatorButtonBase {
 	element: Component<{
 		setActiveModal: Setter<ModalType>;
 		modal: HTMLDialogElement | undefined;
 	}>;
-};
+}
 
 /**
  * Singular navigator button that performs an action rather than opening a page
  */
-export type NavigatorActionButton = {
-	namei18n: string;
-	icon: typeof ManageAccounts;
+export interface NavigatorActionButton extends NavigatorButtonBase {
 	action: () => void;
-};
+}
 
 export type NavigatorButton = NavigatorNavigationButton | NavigatorActionButton;
 
 /**
  * Group of navigator buttons
  */
-export type NavigatorButtonGroup = {
-	namei18n: string;
-	icon: typeof ManageAccounts;
+export interface NavigatorButtonGroup extends NavigatorButtonBase {
 	group: NavigatorButton[];
-};
+}
 
 /**
  * A navigator consisting of a list of buttons and button groups
@@ -73,37 +85,37 @@ export type Navigator = (NavigatorButton | NavigatorButtonGroup)[];
 
 export const accountItem: NavigatorNavigationButton = {
 	namei18n: 'optionsAccounts',
-	icon: ManageAccounts,
+	icon: ManageAccountsOutlined,
 	element: Accounts,
 };
 
 export const optionsItem: NavigatorNavigationButton = {
 	namei18n: 'optionsOptions',
-	icon: Settings,
+	icon: SettingsOutlined,
 	element: OptionsComponent,
 };
 
 export const editOptionsItem: NavigatorNavigationButton = {
 	namei18n: 'optionsEdits',
-	icon: Edit,
+	icon: EditOutlined,
 	element: EditOptions,
 };
 
 export const connectorOverrideOptionsItem: NavigatorNavigationButton = {
 	namei18n: 'optionsConnectors',
-	icon: Extension,
+	icon: ExtensionOutlined,
 	element: ConnectorOverrideOptions,
 };
 
 export const advancedOptionsItem: NavigatorNavigationButton = {
 	namei18n: 'optionsAdvanced',
-	icon: Tune,
+	icon: TuneOutlined,
 	element: AdvancedOptionsComponent,
 };
 
 export const optionsGroup: NavigatorButtonGroup = {
 	namei18n: 'optionsOptions',
-	icon: Settings,
+	icon: SettingsOutlined,
 	group: [
 		optionsItem,
 		editOptionsItem,
@@ -114,31 +126,31 @@ export const optionsGroup: NavigatorButtonGroup = {
 
 export const contactItem: NavigatorNavigationButton = {
 	namei18n: 'contactTitle',
-	icon: Contacts,
+	icon: ContactsOutlined,
 	element: ContactComponent,
 };
 
 export const faqItem: NavigatorNavigationButton = {
 	namei18n: 'faqTitle',
-	icon: Help,
+	icon: HelpOutlined,
 	element: FAQ,
 };
 
 export const aboutItem: NavigatorNavigationButton = {
 	namei18n: 'optionsAbout',
-	icon: Info,
+	icon: InfoOutlined,
 	element: InfoComponent,
 };
 
 export const aboutGroup: NavigatorButtonGroup = {
 	namei18n: 'optionsAbout',
-	icon: Info,
+	icon: InfoOutlined,
 	group: [contactItem, faqItem, aboutItem],
 };
 
 export const showSomeLoveItem: NavigatorNavigationButton = {
 	namei18n: 'showSomeLoveTitle',
-	icon: Favorite,
+	icon: FavoriteOutlined,
 	element: ShowSomeLove,
 };
 
@@ -160,45 +172,77 @@ async function getToggleNavigators(): Promise<NavigatorActionButton[]> {
 	if (tab.mode === ControllerMode.Unsupported) {
 		return [];
 	}
+	const navigators: NavigatorActionButton[] = [];
 
 	if (tab.mode === ControllerMode.Disabled) {
-		return [
+		navigators.push({
+			namei18n: 'menuEnableConnector',
+			icon: ToggleOnOutlined,
+			action: () => {
+				enableConnector(tab.tabId);
+			},
+		});
+	} else {
+		navigators.push(
 			{
-				namei18n: 'menuEnableConnector',
-				icon: ToggleOn,
+				namei18n: 'menuDisableConnector',
+				icon: ToggleOffOutlined,
 				action: () => {
-					enableConnector(tab.tabId);
+					disableConnector(tab.tabId);
 				},
 			},
-		];
+			{
+				namei18n: 'menuDisableUntilTabClosed',
+				icon: TimerOutlined,
+				action: () => {
+					disableUntilClosed(tab.tabId);
+				},
+			},
+		);
 	}
 
-	return [
-		{
-			namei18n: 'menuDisableConnector',
-			icon: ToggleOff,
+	const channelDetails = await getChannelDetails(tab.tabId);
+	if (
+		!channelDetails ||
+		!channelDetails.channelInfo?.id ||
+		!channelDetails.connector
+	) {
+		return navigators;
+	} else if (
+		await getChannelBlocklistLabel(
+			channelDetails.channelInfo.id,
+			channelDetails.connector,
+		)
+	) {
+		navigators.push({
+			namei18n: 'menuEnableChannel',
+			i18nSubstitution: channelDetails.channelInfo.label,
+			icon: PersonOutlined,
 			action: () => {
-				disableConnector(tab.tabId);
+				removeFromBlocklist(tab.tabId);
 			},
-		},
-		{
-			namei18n: 'menuDisableUntilTabClosed',
-			icon: Timer,
+		});
+	} else {
+		navigators.push({
+			namei18n: 'menuDisableChannel',
+			i18nSubstitution: channelDetails.channelInfo.label,
+			icon: PersonOffOutlined,
 			action: () => {
-				disableUntilClosed(tab.tabId);
+				addToBlocklist(tab.tabId);
 			},
-		},
-	];
+		});
+	}
+	return navigators;
 }
 
 export async function getMobileNavigatorGroup(): Promise<NavigatorButtonGroup> {
 	const group: NavigatorButtonGroup = {
 		namei18n: 'optionsOptions',
-		icon: Settings,
+		icon: SettingsOutlined,
 		group: [
 			{
 				namei18n: 'optionsOptions',
-				icon: Settings,
+				icon: SettingsOutlined,
 				action: () => {
 					browser.tabs.create({
 						url: browser.runtime.getURL(
diff --git a/src/ui/options/components/options/global-options.tsx b/src/ui/options/components/options/global-options.tsx
index 915b8255..c7ac84b4 100644
--- a/src/ui/options/components/options/global-options.tsx
+++ b/src/ui/options/components/options/global-options.tsx
@@ -5,7 +5,7 @@ import browser from 'webextension-polyfill';
 import { GlobalOptionEntry } from '../inputs';
 import styles from '../components.module.scss';
 import { t } from '@/util/i18n';
-import { capitalizeFirstLetter, debugLog } from '@/util/util';
+import { debugLog, kebabCaseToPascalCase } from '@/util/util';
 import {
 	ModifiedTheme,
 	getTheme,
@@ -105,7 +105,7 @@ function ThemeSelector() {
 						{(themeName) => (
 							<option value={`theme-${themeName}`}>
 								{t(
-									`optionTheme${capitalizeFirstLetter(
+									`optionTheme${kebabCaseToPascalCase(
 										themeName,
 									)}`,
 								)}
diff --git a/src/ui/options/components/options/scrobble-behavior.tsx b/src/ui/options/components/options/scrobble-behavior.tsx
index 877c13a9..f129b9f5 100644
--- a/src/ui/options/components/options/scrobble-behavior.tsx
+++ b/src/ui/options/components/options/scrobble-behavior.tsx
@@ -18,7 +18,9 @@ export default function ScrobbleBehavior(props: {
 }) {
 	return (
 		<>
-			<h2>{t('optionsScrobbleBehavior')}</h2>
+			<h2 id="header-scrobble-behavior">
+				{t('optionsScrobbleBehavior')}
+			</h2>
 			<RadioButtons
 				buttons={[
 					{
@@ -68,6 +70,7 @@ export default function ScrobbleBehavior(props: {
 						return newOptions;
 					});
 				}}
+				labelledby="header-scrobble-behavior"
 			/>
 		</>
 	);
diff --git a/src/ui/options/components/showSomeLove.tsx b/src/ui/options/components/showSomeLove.tsx
index 45da4079..13e50dd9 100644
--- a/src/ui/options/components/showSomeLove.tsx
+++ b/src/ui/options/components/showSomeLove.tsx
@@ -1,5 +1,5 @@
 import { t } from '@/util/i18n';
-import VolunteerActivism from '@suid/icons-material/VolunteerActivismOutlined';
+import { VolunteerActivismOutlined } from '@/ui/components/icons';
 import styles from './components.module.scss';
 
 /**
@@ -20,7 +20,7 @@ export default function ShowSomeLove() {
 				target="_blank"
 				rel="noopener noreferrer"
 			>
-				<VolunteerActivism />
+				<VolunteerActivismOutlined />
 				<span>{t('donationButtonLabel')}</span>
 			</a>
 		</>
diff --git a/src/ui/options/main.tsx b/src/ui/options/main.tsx
index e457e70a..819af045 100644
--- a/src/ui/options/main.tsx
+++ b/src/ui/options/main.tsx
@@ -3,7 +3,7 @@ import styles from './settings.module.scss';
 import { initializeThemes } from '@/theme/themes';
 import '@/theme/themes.scss';
 import { Show, createSignal, onCleanup } from 'solid-js';
-import Close from '@suid/icons-material/CloseOutlined';
+import { CloseOutlined } from '@/ui/components/icons';
 import Sidebar from './sidebar/sidebar';
 import { EditsModal } from './components/edit-options/edited-tracks';
 import Permissions from './components/permissions';
@@ -125,7 +125,7 @@ function Options() {
 					class={styles.modalClose}
 					onClick={() => modal?.close()}
 				>
-					<Close />
+					<CloseOutlined />
 				</button>
 			</dialog>
 		</>
diff --git a/src/ui/options/settings.module.scss b/src/ui/options/settings.module.scss
index 9b11bd8c..bc4cbeb2 100644
--- a/src/ui/options/settings.module.scss
+++ b/src/ui/options/settings.module.scss
@@ -86,7 +86,7 @@ a {
 
 .modal {
 	background-color: var(--modal-bg);
-	border: none;
+	border: var(--interactable-border);
 	border-radius: 3em;
 	color: var(--text);
 	height: 80vh;
@@ -116,7 +116,10 @@ a {
 
 	.modalClose {
 		background-color: var(--always-last-fm);
-		border: none;
+		border-bottom: var(--interactable-border);
+		border-left: var(--interactable-border);
+		border-right: none;
+		border-top: none;
 		color: var(--always-light);
 		cursor: pointer;
 		padding: 0.75em 1.75em 0.5em 1em;
diff --git a/src/ui/options/sidebar/sidebar.module.scss b/src/ui/options/sidebar/sidebar.module.scss
index ace5b544..0a893625 100644
--- a/src/ui/options/sidebar/sidebar.module.scss
+++ b/src/ui/options/sidebar/sidebar.module.scss
@@ -8,7 +8,7 @@
 		.sidebarButton {
 			align-items: center;
 			background-color: transparent;
-			border: none;
+			border: var(--interactable-border);
 			color: var(--text);
 			cursor: pointer;
 			display: flex;
@@ -24,7 +24,7 @@
 			&.active,
 			&:hover {
 				background-color: var(--last-fm);
-				color: var(--always-light);
+				color: var(--active-text);
 			}
 		}
 	}
diff --git a/src/ui/popup/base.tsx b/src/ui/popup/base.tsx
index c1ed0ed4..530d764c 100644
--- a/src/ui/popup/base.tsx
+++ b/src/ui/popup/base.tsx
@@ -1,7 +1,6 @@
 import { t } from '@/util/i18n';
-import MusicNote from '@suid/icons-material/MusicNoteOutlined';
+import { ExpandMoreOutlined, MusicNoteOutlined } from '@/ui/components/icons';
 import styles from './popup.module.scss';
-import ExpandMore from '@suid/icons-material/ExpandMoreOutlined';
 import { TPopupAnchor } from '../components/util';
 
 /**
@@ -10,11 +9,11 @@ import { TPopupAnchor } from '../components/util';
 export default function Base() {
 	return (
 		<div class={styles.alertPopup}>
-			<MusicNote class={styles.bigIcon} />
+			<MusicNoteOutlined class={styles.bigIcon} />
 			<h1>{t('getStartedHeader')}</h1>
 			<details class={styles.alreadyPlaying}>
 				<summary>
-					<ExpandMore class={styles.expandVector} />
+					<ExpandMoreOutlined class={styles.expandVector} />
 					{t('getStartedSubheader')}
 				</summary>
 				<p>{t('getStartedSiteChanged')}</p>
diff --git a/src/ui/popup/disabled.tsx b/src/ui/popup/disabled.tsx
index fc9611d4..783c6255 100644
--- a/src/ui/popup/disabled.tsx
+++ b/src/ui/popup/disabled.tsx
@@ -1,8 +1,7 @@
 import { t } from '@/util/i18n';
-import MusicOff from '@suid/icons-material/MusicOffOutlined';
+import { MusicOffOutlined, SettingsOutlined } from '@/ui/components/icons';
 import styles from './popup.module.scss';
 import browser from 'webextension-polyfill';
-import Settings from '@suid/icons-material/SettingsOutlined';
 import optionComponentStyles from '../options/components/components.module.scss';
 import { PopupAnchor } from '../components/util';
 
@@ -12,7 +11,7 @@ import { PopupAnchor } from '../components/util';
 export default function Disabled() {
 	return (
 		<div class={styles.alertPopup}>
-			<MusicOff class={styles.bigIcon} />
+			<MusicOffOutlined class={styles.bigIcon} />
 			<h1>{t('disabledSiteHeader')}</h1>
 			<p>{t('disabledSiteDesc')}</p>
 			<PopupAnchor
@@ -21,7 +20,7 @@ export default function Disabled() {
 				)}
 				class={`${optionComponentStyles.button} ${optionComponentStyles.centered}`}
 			>
-				<Settings />
+				<SettingsOutlined />
 				{t('disabledSiteButton')}
 			</PopupAnchor>
 		</div>
diff --git a/src/ui/popup/disallowed.tsx b/src/ui/popup/disallowed.tsx
index b3e80aa0..8d5d672d 100644
--- a/src/ui/popup/disallowed.tsx
+++ b/src/ui/popup/disallowed.tsx
@@ -4,8 +4,7 @@ import { Resource, Show, createMemo, createSignal } from 'solid-js';
 import styles from './popup.module.scss';
 import optionComponentStyles from '../options/components/components.module.scss';
 import { t } from '@/util/i18n';
-import MusicOff from '@suid/icons-material/MusicOffOutlined';
-import Send from '@suid/icons-material/Send';
+import { MusicOffOutlined, Send } from '@/ui/components/icons';
 import { sendBackgroundMessage } from '@/util/communication';
 
 export default function Disallowed(props: { tab: Resource<ManagerTab> }) {
@@ -25,7 +24,7 @@ export default function Disallowed(props: { tab: Resource<ManagerTab> }) {
 	return (
 		<Show when={!isLoading()}>
 			<div class={styles.widePopup}>
-				<MusicOff class={styles.bigIcon} />
+				<MusicOffOutlined class={styles.bigIcon} />
 				<h1>{t('disallowedHeader')}</h1>
 				<p>
 					{t('disallowedDesc1', [
diff --git a/src/ui/popup/edit.tsx b/src/ui/popup/edit.tsx
index f303b08e..b2259282 100644
--- a/src/ui/popup/edit.tsx
+++ b/src/ui/popup/edit.tsx
@@ -16,9 +16,11 @@ import {
 import { ManagerTab } from '@/core/storage/wrapper';
 import browser from 'webextension-polyfill';
 import ClonedSong from '@/core/object/cloned-song';
-import Check from '@suid/icons-material/CheckOutlined';
-import Code from '@suid/icons-material/CodeOutlined';
-import PublishedWithChanges from '@suid/icons-material/PublishedWithChangesOutlined';
+import {
+	CheckOutlined,
+	CodeOutlined,
+	PublishedWithChangesOutlined,
+} from '@/ui/components/icons';
 import { sendBackgroundMessage } from '@/util/communication';
 import savedEdits from '@/core/storage/saved-edits';
 import Regex, { RegexEditContextMenu } from './regex';
@@ -237,7 +239,7 @@ export default function Edit(props: { tab: Resource<ManagerTab> }) {
 											});
 										}}
 									>
-										<Check />
+										<CheckOutlined />
 									</button>
 									<button
 										class={styles.controlButton}
@@ -256,15 +258,35 @@ export default function Edit(props: { tab: Resource<ManagerTab> }) {
 													albumArtist() || null,
 											});
 										}}
+										onKeyDown={(event) => {
+											if (event.key !== 'Enter') {
+												return;
+											}
+											event.stopImmediatePropagation();
+											saveEdit(props.tab, clonedSong(), {
+												artist: track(),
+												track: artist(),
+												album: album() || null,
+												albumArtist:
+													albumArtist() || null,
+											});
+										}}
 									>
-										<PublishedWithChanges />
+										<PublishedWithChangesOutlined />
 									</button>
 									<button
 										class={styles.controlButton}
 										title={t('infoRegexTitle')}
 										onClick={() => setIsRegex(true)}
+										onKeyDown={(event) => {
+											if (event.key !== 'Enter') {
+												return;
+											}
+											event.stopImmediatePropagation();
+											setIsRegex(true);
+										}}
 									>
-										<Code />
+										<CodeOutlined />
 									</button>
 								</div>
 							</Show>
@@ -293,7 +315,7 @@ function EditContextMenu(props: {
 					!props.track() || !props.artist()
 						? 'infoSubmitUnableTitleShort'
 						: 'infoSubmitTitleShort',
-				icon: Check,
+				icon: CheckOutlined,
 				action: () =>
 					void saveEdit(props.tab, props.clonedSong, {
 						artist: props.artist(),
@@ -307,7 +329,7 @@ function EditContextMenu(props: {
 					!props.track() || !props.artist()
 						? 'infoSwapUnableTitleShort'
 						: 'infoSwapTitleShort',
-				icon: PublishedWithChanges,
+				icon: PublishedWithChangesOutlined,
 				action: () =>
 					void saveEdit(props.tab, props.clonedSong, {
 						artist: props.track(),
@@ -318,7 +340,7 @@ function EditContextMenu(props: {
 			},
 			{
 				namei18n: 'infoRegexTitleShort',
-				icon: Code,
+				icon: CodeOutlined,
 				action: () => props.setIsRegex(true),
 			},
 		];
diff --git a/src/ui/popup/err.tsx b/src/ui/popup/err.tsx
index 3936c514..60c10ff2 100644
--- a/src/ui/popup/err.tsx
+++ b/src/ui/popup/err.tsx
@@ -1,8 +1,7 @@
 import { t } from '@/util/i18n';
-import Error from '@suid/icons-material/ErrorOutlined';
+import { ErrorOutlined, SettingsOutlined } from '@/ui/components/icons';
 import styles from './popup.module.scss';
 import browser from 'webextension-polyfill';
-import Settings from '@suid/icons-material/SettingsOutlined';
 import optionComponentStyles from '../options/components/components.module.scss';
 import { PopupAnchor, TPopupAnchor } from '../components/util';
 
@@ -12,7 +11,7 @@ import { PopupAnchor, TPopupAnchor } from '../components/util';
 export default function Err() {
 	return (
 		<div class={styles.alertPopup}>
-			<Error class={styles.bigIcon} />
+			<ErrorOutlined class={styles.bigIcon} />
 			<h1>{t('serviceErrorHeader')}</h1>
 			<p>
 				<TPopupAnchor messageName="serviceErrorDesc" />
@@ -23,7 +22,7 @@ export default function Err() {
 				)}
 				class={`${optionComponentStyles.button} ${optionComponentStyles.centered}`}
 			>
-				<Settings />
+				<SettingsOutlined />
 				{t('disabledSiteButton')}
 			</PopupAnchor>
 		</div>
diff --git a/src/ui/popup/ignored.tsx b/src/ui/popup/ignored.tsx
index b7d51668..e6ba7ee9 100644
--- a/src/ui/popup/ignored.tsx
+++ b/src/ui/popup/ignored.tsx
@@ -3,7 +3,7 @@ import { Resource, createMemo } from 'solid-js';
 import styles from './popup.module.scss';
 import { t } from '@/util/i18n';
 import { PopupAnchor } from '../components/util';
-import CancelScheduleSend from '@suid/icons-material/CancelScheduleSendOutlined';
+import { CancelScheduleSendOutlined } from '@/ui/components/icons';
 import ClonedSong from '@/core/object/cloned-song';
 
 export default function Ignored(props: { tab: Resource<ManagerTab> }) {
@@ -15,7 +15,7 @@ export default function Ignored(props: { tab: Resource<ManagerTab> }) {
 	});
 	return (
 		<div class={styles.alertPopup}>
-			<CancelScheduleSend class={styles.bigIcon} />
+			<CancelScheduleSendOutlined class={styles.bigIcon} />
 			<h1>{t('ignoredHeader')}</h1>
 			<p>
 				{t('ignoredDesc', [
diff --git a/src/ui/popup/main.tsx b/src/ui/popup/main.tsx
index ab20b864..904c21c5 100644
--- a/src/ui/popup/main.tsx
+++ b/src/ui/popup/main.tsx
@@ -18,7 +18,7 @@ import Disabled from './disabled';
 import Err from './err';
 import NowPlaying from './nowplaying';
 import Edit from './edit';
-import Settings from '@suid/icons-material/SettingsOutlined';
+import { SettingsOutlined } from '@/ui/components/icons';
 import browser from 'webextension-polyfill';
 import styles from './popup.module.scss';
 import { t } from '@/util/i18n';
@@ -131,7 +131,7 @@ function Popup() {
 					class={styles.settingsIcon}
 					title={t('disabledSiteButton')}
 				>
-					<Settings />
+					<SettingsOutlined />
 				</PopupAnchor>
 			</Show>
 		</>
diff --git a/src/ui/popup/nowplaying.tsx b/src/ui/popup/nowplaying.tsx
index a2d785a7..403bd8d9 100644
--- a/src/ui/popup/nowplaying.tsx
+++ b/src/ui/popup/nowplaying.tsx
@@ -20,11 +20,13 @@ import browser from 'webextension-polyfill';
 import ClonedSong from '@/core/object/cloned-song';
 import Base from './base';
 import { LastFMIcon } from '@/util/icons';
-import Edit from '@suid/icons-material/EditOutlined';
-import Block from '@suid/icons-material/BlockOutlined';
-import Favorite from '@suid/icons-material/FavoriteOutlined';
-import HeartBroken from '@suid/icons-material/HeartBrokenOutlined';
-import RestartAlt from '@suid/icons-material/RestartAltOutlined';
+import {
+	EditOutlined,
+	BlockOutlined,
+	FavoriteOutlined,
+	HeartBrokenOutlined,
+	RestartAltOutlined,
+} from '@/ui/components/icons';
 import { sendBackgroundMessage } from '@/util/communication';
 import * as ControllerMode from '@/core/object/controller/controller-mode';
 import EditComponent from './edit';
@@ -168,7 +170,7 @@ function NowPlayingContextMenu(props: {
 					props.tab()?.mode === ControllerMode.Playing
 						? 'infoEditTitleShort'
 						: 'infoEditUnableTitleShort',
-				icon: Edit,
+				icon: EditOutlined,
 				action: () => props.setIsEditing(true),
 			},
 		];
@@ -178,13 +180,13 @@ function NowPlayingContextMenu(props: {
 					props.tab()?.mode === ControllerMode.Playing
 						? 'infoRevertTitleShort'
 						: 'infoRevertUnableTitleShort',
-				icon: RestartAlt,
+				icon: RestartAltOutlined,
 				action: () => actionResetSongData(props.tab),
 			});
 		}
 		items.push({
 			namei18n: getSkipLabel(props.tab, true),
-			icon: Block,
+			icon: BlockOutlined,
 			action: () => actionSkipCurrentSong(props.tab),
 		});
 		if (!navigatorResource.loading) {
@@ -244,7 +246,7 @@ function IOSLoveTrack(props: {
 					: t('infoLove')
 			}
 		>
-			<Favorite />
+			<FavoriteOutlined />
 		</button>
 	);
 }
@@ -320,7 +322,7 @@ function TrackMetadata(props: { song: Accessor<ClonedSong | null> }) {
 				<LastFMIcon />
 				{props.song()?.metadata.userPlayCount || 0}
 			</PopupLink>
-			<span class={styles.label}>{props.song()?.connectorLabel}</span>
+			<span class={styles.label}>{props.song()?.connector.label}</span>
 		</div>
 	);
 }
@@ -345,7 +347,7 @@ function TrackControls(props: {
 				}
 				onClick={() => props.setIsEditing(true)}
 			>
-				<Edit />
+				<EditOutlined />
 			</button>
 			<Show when={props.song()?.flags.isCorrectedByUser}>
 				<button
@@ -358,7 +360,7 @@ function TrackControls(props: {
 					}
 					onClick={() => actionResetSongData(props.tab)}
 				>
-					<RestartAlt />
+					<RestartAltOutlined />
 				</button>
 			</Show>
 			<button
@@ -375,7 +377,7 @@ function TrackControls(props: {
 				onClick={() => actionSkipCurrentSong(props.tab)}
 				title={t(getSkipLabel(props.tab, false))}
 			>
-				<Block />
+				<BlockOutlined />
 			</button>
 			<button
 				class={`${styles.controlButton}${
@@ -389,14 +391,14 @@ function TrackControls(props: {
 				}
 			>
 				<span class={styles.nonHover}>
-					<Favorite />
+					<FavoriteOutlined />
 				</span>
 				<span class={styles.hover}>
 					<Show
 						when={props.song()?.metadata.userloved}
-						fallback={<Favorite />}
+						fallback={<FavoriteOutlined />}
 					>
-						<HeartBroken />
+						<HeartBrokenOutlined />
 					</Show>
 				</span>
 			</button>
diff --git a/src/ui/popup/popup.module.scss b/src/ui/popup/popup.module.scss
index 328f8c41..7cb9218a 100644
--- a/src/ui/popup/popup.module.scss
+++ b/src/ui/popup/popup.module.scss
@@ -1,10 +1,11 @@
 @mixin ios-text-input {
 	appearance: none;
 	background-color: var(--last-fm-bg);
-	border: none;
+	border-radius: 0.25em;
 	box-sizing: border-box;
 	color: var(--text);
 	font-size: 1.2em;
+	padding: 0.25em 0.5em;
 }
 
 .alertPopup,
@@ -107,10 +108,10 @@ a {
 			width: fit-content;
 		}
 
-		@supports (-webkit-touch-callout: none) {
-			.editField {
-				@include ios-text-input;
-			}
+		.editField {
+			border: 1px solid var(--text);
+			border-radius: 3px;
+			padding: 0.15em 0.25em;
 		}
 	}
 
@@ -125,7 +126,7 @@ a {
 		gap: 0.25em;
 
 		svg {
-			fill: var(--always-light);
+			fill: var(--active-text);
 			height: 1.2em;
 			width: 1.2em;
 		}
@@ -134,7 +135,7 @@ a {
 	.label {
 		background-color: var(--last-fm);
 		border-radius: 0.25em;
-		color: var(--always-light);
+		color: var(--active-text);
 		padding: 0.25em 0.5em;
 	}
 
@@ -144,7 +145,7 @@ a {
 
 		.controlButton {
 			background-color: transparent;
-			border: none;
+			border: var(--interactable-border);
 			color: var(--text);
 			cursor: pointer;
 			padding: 0.25em 0.5em;
@@ -192,20 +193,25 @@ a {
 .regexContainer {
 	align-items: center;
 	box-sizing: border-box;
-	display: grid;
-	gap: 0.5em 1em;
-	grid-template:
-		'. search replace preview' 2em
-		'track tracksearch trackreplace trackpreview' 2em
-		'artist artistsearch artistreplace artistpreview' 2em
-		'album albumsearch albumreplace albumpreview' 2em
-		'albumartist albumartistsearch albumartistreplace albumartistpreview' 2em
-		'footer footer footer footer' auto / auto 1fr 1fr 1fr;
+	display: flex;
+	flex-direction: column;
+	gap: 0.5em;
 	padding: 1em;
 	width: 800px;
 
+	--max-cell-width: 1fr;
+
+	.entryWrapper {
+		align-items: center;
+		display: grid;
+		gap: 1em;
+		grid-template: 2em / var(--max-cell-width) 1fr 1fr 1fr;
+		width: 100%;
+	}
+
 	.entryLabel {
 		justify-self: right;
+		white-space: nowrap;
 	}
 
 	.searchWrapper {
@@ -238,93 +244,14 @@ a {
 	}
 
 	input {
+		border: 1px solid var(--text);
+		border-radius: 3px;
 		padding: 0.25em 0.5em;
 	}
 
 	.regexFooter {
 		display: flex;
-		flex-direction: column;
-		grid-area: footer;
-	}
-
-	/**
-	 * assign grid areas
-	 */
-
-	.searchLabel {
-		grid-area: search;
-	}
-
-	.replaceLabel {
-		grid-area: replace;
-	}
-
-	.previewLabel {
-		grid-area: preview;
-	}
-
-	.trackLabel {
-		grid-area: track;
-	}
-
-	.artistLabel {
-		grid-area: artist;
-	}
-
-	.albumLabel {
-		grid-area: album;
-	}
-
-	.albumArtistLabel {
-		grid-area: albumartist;
-	}
-
-	.trackSearch {
-		grid-area: tracksearch;
-	}
-
-	.artistSearch {
-		grid-area: artistsearch;
-	}
-
-	.albumSearch {
-		grid-area: albumsearch;
-	}
-
-	.albumArtistSearch {
-		grid-area: albumartistsearch;
-	}
-
-	.trackReplace {
-		grid-area: trackreplace;
-	}
-
-	.artistReplace {
-		grid-area: artistreplace;
-	}
-
-	.albumReplace {
-		grid-area: albumreplace;
-	}
-
-	.albumArtistReplace {
-		grid-area: albumartistreplace;
-	}
-
-	.trackPreview {
-		grid-area: trackpreview;
-	}
-
-	.artistPreview {
-		grid-area: artistpreview;
-	}
-
-	.albumPreview {
-		grid-area: albumpreview;
-	}
-
-	.albumArtistPreview {
-		grid-area: albumartistpreview;
+		width: 100%;
 	}
 }
 
@@ -385,11 +312,17 @@ a {
 
 		.songDetails {
 			align-items: center;
+
+			.editField {
+				border: var(--interactable-border);
+
+				@include ios-text-input;
+			}
 		}
 
 		.iosLoveButton {
 			background-color: transparent;
-			border: none;
+			border: var(--interactable-border);
 			color: var(--text);
 			cursor: pointer;
 			padding: 0.25em 0.5em;
@@ -409,7 +342,7 @@ a {
 		align-items: center;
 		display: flex;
 		flex-direction: column;
-		width: 100%;
+		width: 70%;
 
 		.iosSearchLabel,
 		.iosReplaceLabel,
@@ -418,8 +351,10 @@ a {
 		}
 
 		input {
-			@include ios-text-input;
+			border: var(--interactable-border);
 			width: 100%;
+
+			@include ios-text-input;
 		}
 
 		.entryWrapper {
@@ -428,6 +363,7 @@ a {
 			border-radius: 1em;
 			display: flex;
 			flex-direction: column;
+			gap: 0;
 
 			.entryWrapperLegend {
 				padding: 0 1em;
diff --git a/src/ui/popup/regex.tsx b/src/ui/popup/regex.tsx
index 09c90c7e..a0759dc0 100644
--- a/src/ui/popup/regex.tsx
+++ b/src/ui/popup/regex.tsx
@@ -1,13 +1,13 @@
 import ClonedSong from '@/core/object/cloned-song';
 import styles from './popup.module.scss';
-import Check from '@suid/icons-material/CheckOutlined';
-import Close from '@suid/icons-material/CloseOutlined';
+import { CheckOutlined, CloseOutlined } from '@/ui/components/icons';
 
 import { t } from '@/util/i18n';
 import {
 	JSXElement,
 	Match,
 	Resource,
+	Setter,
 	Show,
 	Switch,
 	createEffect,
@@ -62,6 +62,15 @@ export default function Regex(props: {
 	clonedSong: ClonedSong | undefined;
 	tab: Resource<ManagerTab>;
 }) {
+	const [maxCellWidth, setMaxCellWidth] = createSignal(0);
+	const [regexContainer, setRegexContainer] = createSignal<HTMLDivElement>();
+	createEffect(() => {
+		regexContainer()?.style.setProperty(
+			'--max-cell-width',
+			`${maxCellWidth()}px`,
+		);
+	});
+
 	return (
 		<div
 			class={styles.regexContainer}
@@ -70,16 +79,42 @@ export default function Regex(props: {
 					saveEdit(props.tab);
 				}
 			}}
+			ref={setRegexContainer}
 		>
 			<Show when={!isIos()}>
-				<span class={styles.searchLabel}>{t('infoSearchLabel')}</span>
-				<span class={styles.replaceLabel}>{t('infoReplaceLabel')}</span>
-				<span class={styles.previewLabel}>{t('infoPreviewLabel')}</span>
+				<div class={styles.entryWrapper}>
+					<span />
+					<span class={styles.searchLabel}>
+						{t('infoSearchLabel')}
+					</span>
+					<span class={styles.replaceLabel}>
+						{t('infoReplaceLabel')}
+					</span>
+					<span class={styles.previewLabel}>
+						{t('infoPreviewLabel')}
+					</span>
+				</div>
 			</Show>
-			<Entry clonedSong={props.clonedSong} type="track" />
-			<Entry clonedSong={props.clonedSong} type="artist" />
-			<Entry clonedSong={props.clonedSong} type="album" />
-			<Entry clonedSong={props.clonedSong} type="albumArtist" />
+			<Entry
+				clonedSong={props.clonedSong}
+				setMaxCellWidth={setMaxCellWidth}
+				type="track"
+			/>
+			<Entry
+				clonedSong={props.clonedSong}
+				setMaxCellWidth={setMaxCellWidth}
+				type="artist"
+			/>
+			<Entry
+				clonedSong={props.clonedSong}
+				setMaxCellWidth={setMaxCellWidth}
+				type="album"
+			/>
+			<Entry
+				clonedSong={props.clonedSong}
+				setMaxCellWidth={setMaxCellWidth}
+				type="albumArtist"
+			/>
 			<Footer tab={props.tab} clonedSong={props.clonedSong} />
 		</div>
 	);
@@ -88,7 +123,11 @@ export default function Regex(props: {
 /**
  * Label, inputs, and preview for a single song field.
  */
-function Entry(props: { clonedSong: ClonedSong | undefined; type: FieldType }) {
+function Entry(props: {
+	clonedSong: ClonedSong | undefined;
+	type: FieldType;
+	setMaxCellWidth: Setter<number>;
+}) {
 	onMount(() => {
 		const type = props.type;
 		const clonedSong = props.clonedSong;
@@ -108,14 +147,16 @@ function Entry(props: { clonedSong: ClonedSong | undefined; type: FieldType }) {
 		);
 	});
 
+	const [pcLabel, setPcLabel] = createSignal<HTMLSpanElement>();
+	createEffect(() => {
+		const curWidth = pcLabel()?.clientWidth ?? 0;
+		props.setMaxCellWidth((prevMax) => Math.max(prevMax, curWidth));
+	});
+
 	return (
 		<EntryWrapper type={props.type}>
 			<Show when={!isIos()}>
-				<span
-					class={`${styles[`${props.type}Label`]} ${
-						styles.entryLabel
-					}`}
-				>
+				<span class={styles.entryLabel} ref={setPcLabel}>
 					{t(`info${pascalCaseField(props.type)}Label`)}
 				</span>
 			</Show>
@@ -131,7 +172,18 @@ function Entry(props: { clonedSong: ClonedSong | undefined; type: FieldType }) {
  */
 function EntryWrapper(props: { type: FieldType; children: JSXElement }) {
 	return (
-		<Show when={isIos()} fallback={props.children}>
+		<Show
+			when={isIos()}
+			fallback={
+				<div
+					class={styles.entryWrapper}
+					role="group"
+					aria-label={t(`info${pascalCaseField(props.type)}Label`)}
+				>
+					{props.children}
+				</div>
+			}
+		>
 			<fieldset class={styles.entryWrapper}>
 				<legend class={styles.entryWrapperLegend}>
 					{t(`info${pascalCaseField(props.type)}Label`)}
@@ -150,7 +202,7 @@ function SearchField(props: {
 	clonedSong: ClonedSong | undefined;
 }) {
 	return (
-		<div class={`${styles[`${props.type}Search`]} ${styles.searchWrapper}`}>
+		<div class={styles.searchWrapper}>
 			<Show when={isIos()}>
 				<label
 					for={`${props.type}SearchInput`}
@@ -169,10 +221,11 @@ function SearchField(props: {
 						[props.type]: e.currentTarget.value || null,
 					}))
 				}
+				title={t('infoSearchLabel')}
 			/>
 			<Switch
 				fallback={
-					<Close
+					<CloseOutlined
 						class={`${styles.regexTest} ${styles.regexFailure}`}
 					/>
 				}
@@ -186,7 +239,7 @@ function SearchField(props: {
 						getSongFieldNoRegex(props.clonedSong, props.type),
 					)}
 				>
-					<Check
+					<CheckOutlined
 						class={`${styles.regexTest} ${styles.regexSuccess}`}
 					/>
 				</Match>
@@ -212,13 +265,13 @@ function ReplaceField(props: { type: FieldType }) {
 			<input
 				id={`${props.type}ReplaceInput`}
 				type="text"
-				class={styles[`${props.type}Replace`]}
 				onInput={(e) =>
 					setReplaces((prev) => ({
 						...prev,
 						[props.type]: e.currentTarget.value || null,
 					}))
 				}
+				title={t('infoReplaceLabel')}
 			/>
 		</>
 	);
@@ -240,7 +293,7 @@ function PreviewOutput(props: { type: FieldType }) {
 			</Show>
 			<output
 				id={`${props.type}PreviewOutput`}
-				class={styles[`${props.type}Preview`]}
+				title={t('infoPreviewLabel')}
 			>
 				{previews()[props.type]}
 			</output>
@@ -269,7 +322,7 @@ function Footer(props: {
 							saveEdit(props.tab);
 						}}
 					>
-						<Check />
+						<CheckOutlined />
 					</button>
 				</div>
 			</Show>
@@ -286,7 +339,7 @@ export function RegexEditContextMenu(props: { tab: Resource<ManagerTab> }) {
 		const items: Navigator = [
 			{
 				namei18n: 'infoSubmitTitleShort',
-				icon: Check,
+				icon: CheckOutlined,
 				action: () => void saveEdit(props.tab),
 			},
 		];
diff --git a/src/ui/popup/unsupported.tsx b/src/ui/popup/unsupported.tsx
index 824710b9..89c54c53 100644
--- a/src/ui/popup/unsupported.tsx
+++ b/src/ui/popup/unsupported.tsx
@@ -1,5 +1,5 @@
 import { t } from '@/util/i18n';
-import SentimentDissatisfied from '@suid/icons-material/SentimentDissatisfiedOutlined';
+import { SentimentDissatisfiedOutlined } from '@/ui/components/icons';
 import styles from './popup.module.scss';
 import { PopupAnchor } from '../components/util';
 
@@ -9,7 +9,7 @@ import { PopupAnchor } from '../components/util';
 export default function Unsupported() {
 	return (
 		<div class={styles.alertPopup}>
-			<SentimentDissatisfied class={styles.bigIcon} />
+			<SentimentDissatisfiedOutlined class={styles.bigIcon} />
 			<h1>{t('unsupportedWebsiteHeader')}</h1>
 			<p>{t('unsupportedWebsiteDesc')}</p>
 			<p>{t('unsupportedWebsiteUpdateNote')}</p>
diff --git a/src/util/communication.ts b/src/util/communication.ts
index b83b661a..9cd8e05f 100644
--- a/src/util/communication.ts
+++ b/src/util/communication.ts
@@ -1,4 +1,5 @@
 import { ConnectorMeta } from '@/core/connectors';
+import type { ChannelInfo } from '@/core/content/util';
 import { ControllerModeStr } from '@/core/object/controller/controller';
 import { ServiceCallResult } from '@/core/object/service-call-result';
 import { CloneableSong } from '@/core/object/song';
@@ -168,7 +169,7 @@ interface BackgroundCommunications {
 		payload: undefined;
 		response: {
 			connector: ConnectorMeta;
-			channelId: string | null | undefined;
+			channelInfo: ChannelInfo | null | undefined;
 		};
 	};
 }
diff --git a/src/util/notifications.ts b/src/util/notifications.ts
index 1e521b78..4d1753ba 100644
--- a/src/util/notifications.ts
+++ b/src/util/notifications.ts
@@ -7,6 +7,7 @@ import { ConnectorMeta } from '@/core/connectors';
 import { Scrobbler } from '@/core/object/scrobble-service';
 import { debugLog } from '@/core/content/util';
 import * as BrowserStorage from '@/core/storage/browser-storage';
+import NativeScrobblerNotification from '@/core/storage/native-scrobbler-notification';
 
 /**
  * Notification service.
@@ -157,6 +158,46 @@ async function showNotification(
 	return notificationId;
 }
 
+/**
+ * Show notification warning about native scrobbler
+ * @param connector - Connector for which to show it
+ */
+export async function showNativeScrobblerWarning(
+	connector: ConnectorMeta,
+): Promise<void> {
+	const nativeScrobblerNotification = new NativeScrobblerNotification();
+	if (
+		!connector.hasNativeScrobbler ||
+		!(await nativeScrobblerNotification.shouldNotifyAboutNativeScrobbler(
+			connector.id,
+		))
+	) {
+		return;
+	}
+
+	const options = {
+		title: browser.i18n.getMessage(
+			'notificationNativeScrobbler',
+			connector.label,
+		),
+		message: browser.i18n.getMessage('notificationNativeScrobblerText'),
+	};
+
+	try {
+		await showNotification(options, () => {
+			browser.tabs.create({
+				url: browser.runtime.getURL(
+					'src/ui/options/index.html?p=connectors',
+				),
+			});
+		});
+		await nativeScrobblerNotification.saveHasNotified(connector.id);
+	} catch (err) {
+		debugLog('Unable to show native scrobbler notification: ', 'warn');
+		debugLog(err, 'warn');
+	}
+}
+
 /**
  * Show 'Now playing' notification.
  * @param song - Copy of song instance
diff --git a/src/util/util.ts b/src/util/util.ts
index ad590122..278a8766 100644
--- a/src/util/util.ts
+++ b/src/util/util.ts
@@ -230,8 +230,11 @@ export function areAllResults<T>(results: T[], result: T): boolean {
  * @param text - The string to capitalize the first letter of
  * @returns The string with the first letter capitalized
  */
-export function capitalizeFirstLetter(text: string): string {
-	return text[0].toUpperCase() + text.slice(1);
+export function kebabCaseToPascalCase(text: string): string {
+	return text
+		.split('-')
+		.map((s) => s[0].toUpperCase() + s.slice(1))
+		.join('');
 }
 
 /**
diff --git a/tsconfig.json b/tsconfig.json
index da971ddd..ac01de54 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -3,7 +3,7 @@
     "allowSyntheticDefaultImports": true,
     "baseUrl": ".",
     "module": "ES2020",
-    "moduleResolution": "Node",
+    "moduleResolution": "Bundler",
     "isolatedModules": true,
     "outDir": "build",
     "jsx": "preserve",


Test patch:
diff --git a/tests/background/connectors.test.ts b/tests/background/connectors.test.ts
index e9374038..51a53d26 100644
--- a/tests/background/connectors.test.ts
+++ b/tests/background/connectors.test.ts
@@ -8,6 +8,7 @@ import * as UrlMatch from '@/util/url-match';
 
 const PROP_TYPES: {
 	usesBlocklist: 'boolean';
+	hasNativeScrobbler: 'boolean';
 	allFrames: 'boolean';
 	matches: 'array';
 	label: 'string';
@@ -15,6 +16,7 @@ const PROP_TYPES: {
 	id: 'string';
 } = {
 	usesBlocklist: 'boolean',
+	hasNativeScrobbler: 'boolean',
 	allFrames: 'boolean',
 	matches: 'array',
 	label: 'string',