From 551e422e800233e7ae84caec479a7157d7060b5a Mon Sep 17 00:00:00 2001 From: Marian Rudzynski Date: Wed, 3 Dec 2025 23:44:16 +0000 Subject: [PATCH 1/2] feat(deps): upgrade ESLint and react-hooks with code fixes --- app/utils/responsive.ts | 3 - eslint.config.js | 7 + package-lock.json | 335 ++++++++++--------- package.json | 10 +- src/components/BinarySensorCard.tsx | 30 +- src/components/CameraCard.tsx | 10 +- src/components/Dashboard.tsx | 3 +- src/components/InputTextCard.tsx | 42 +-- src/components/KeepAlive.tsx | 34 +- src/components/LightCard.tsx | 23 +- src/components/ScreenConfigDialog.tsx | 20 +- src/components/__tests__/CardConfig.test.tsx | 2 +- src/components/widgets/WeatherWidget.tsx | 14 +- src/hooks/useWebRTC.ts | 9 +- src/panel.ts | 2 +- 15 files changed, 277 insertions(+), 267 deletions(-) diff --git a/app/utils/responsive.ts b/app/utils/responsive.ts index 423b409..864d924 100644 --- a/app/utils/responsive.ts +++ b/app/utils/responsive.ts @@ -73,9 +73,6 @@ export function useMediaQuery(query: string): boolean { setMatches(e.matches) } - // Set initial value - setMatches(mediaQuery.matches) - // Add listener mediaQuery.addEventListener('change', handleChange) diff --git a/eslint.config.js b/eslint.config.js index 8aa5087..338693f 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -33,6 +33,13 @@ export default [ 'react/prop-types': 'off', '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], 'no-undef': 'off', + // Disable new experimental React Compiler rules from react-hooks v7 + // These can be enabled gradually as the codebase is refactored + 'react-hooks/refs': 'off', + 'react-hooks/static-components': 'off', + 'react-hooks/set-state-in-effect': 'off', + 'react-hooks/incompatible-library': 'off', + 'react-hooks/preserve-manual-memoization': 'off', }, settings: { react: { diff --git a/package-lock.json b/package-lock.json index 9acd687..1c7650b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,13 +44,13 @@ "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "@types/react-grid-layout": "^1.3.5", - "@typescript-eslint/eslint-plugin": "^8.35.0", - "@typescript-eslint/parser": "^8.35.0", + "@typescript-eslint/eslint-plugin": "^8.48.1", + "@typescript-eslint/parser": "^8.48.1", "@vitejs/plugin-react": "^4.6.0", - "eslint": "^9.30.0", - "eslint-config-prettier": "^10.1.5", + "eslint": "^9.39.1", + "eslint-config-prettier": "^10.1.8", "eslint-plugin-react": "^7.37.5", - "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-hooks": "^7.0.1", "husky": "^9.1.7", "jsdom": "^26.1.0", "lint-staged": "^16.1.2", @@ -130,7 +130,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.7.tgz", "integrity": "sha512-BU2f9tlKQ5CAthiMIgpzAh4eDTLWo1mqi9jqE2OxMG0E/OM199VJt2q8BztTxpnSW0i1ymdwLXRJnYzvDM5r2w==", "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -702,7 +701,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -726,7 +724,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -1166,9 +1163,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", "dev": true, "license": "MIT", "dependencies": { @@ -1208,13 +1205,13 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", - "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.6", + "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -1247,19 +1244,22 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", - "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", - "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1341,9 +1341,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.30.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.30.0.tgz", - "integrity": "sha512-Wzw3wQwPvc9sHM+NjakWTcPx11mbZyiYHuwWa/QfZ7cIRX7WK54PSk7bdyXDaoaopUcMatv1zaQvOAAO8hCdww==", + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", + "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", "dev": true, "license": "MIT", "engines": { @@ -1354,9 +1354,9 @@ } }, "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1364,32 +1364,19 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.3.tgz", - "integrity": "sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.15.1", + "@eslint/core": "^0.17.0", "levn": "^0.4.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", - "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, "node_modules/@fastify/busboy": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.1.1.tgz", @@ -4770,7 +4757,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -5314,7 +5300,6 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.122.0.tgz", "integrity": "sha512-vppDHkAxpy0SrpPFDiov70TCyzzKyC5OjADBTnWUq/3bEZFUD5jFGqQn2PwBHZup6dCjFUq95oWkHiv50Xgl2g==", "license": "MIT", - "peer": true, "dependencies": { "@tanstack/history": "1.121.34", "@tanstack/react-store": "^0.7.0", @@ -5496,7 +5481,6 @@ "resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.122.0.tgz", "integrity": "sha512-4b5ID+OKxBfoXr/k3y/GTDJeFDrbNnNRw+pUnEDGVHPfN+L5ocbZXSUl6adSIMP9W3W3ydTyhPw9ge9TH5vVGw==", "license": "MIT", - "peer": true, "dependencies": { "@tanstack/history": "1.121.34", "@tanstack/store": "^0.7.0", @@ -5994,7 +5978,8 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/babel__code-frame": { "version": "7.0.6", @@ -6142,7 +6127,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -6153,7 +6137,6 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -6197,17 +6180,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.35.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.0.tgz", - "integrity": "sha512-ijItUYaiWuce0N1SoSMrEd0b6b6lYkYt99pqCPfybd+HKVXtEvYhICfLdwp42MhiI5mp0oq7PKEL+g1cNiz/Eg==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.1.tgz", + "integrity": "sha512-X63hI1bxl5ohelzr0LY5coufyl0LJNthld+abwxpCoo6Gq+hSqhKwci7MUWkXo67mzgUK6YFByhmaHmUcuBJmA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.35.0", - "@typescript-eslint/type-utils": "8.35.0", - "@typescript-eslint/utils": "8.35.0", - "@typescript-eslint/visitor-keys": "8.35.0", + "@typescript-eslint/scope-manager": "8.48.1", + "@typescript-eslint/type-utils": "8.48.1", + "@typescript-eslint/utils": "8.48.1", + "@typescript-eslint/visitor-keys": "8.48.1", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -6221,23 +6204,22 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.35.0", + "@typescript-eslint/parser": "^8.48.1", "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.35.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.35.0.tgz", - "integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.48.1.tgz", + "integrity": "sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.35.0", - "@typescript-eslint/types": "8.35.0", - "@typescript-eslint/typescript-estree": "8.35.0", - "@typescript-eslint/visitor-keys": "8.35.0", + "@typescript-eslint/scope-manager": "8.48.1", + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/typescript-estree": "8.48.1", + "@typescript-eslint/visitor-keys": "8.48.1", "debug": "^4.3.4" }, "engines": { @@ -6249,17 +6231,17 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.35.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.35.0.tgz", - "integrity": "sha512-41xatqRwWZuhUMF/aZm2fcUsOFKNcG28xqRSS6ZVr9BVJtGExosLAm5A1OxTjRMagx8nJqva+P5zNIGt8RIgbQ==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.48.1.tgz", + "integrity": "sha512-HQWSicah4s9z2/HifRPQ6b6R7G+SBx64JlFQpgSSHWPKdvCZX57XCbszg/bapbRsOEv42q5tayTYcEFpACcX1w==", "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.35.0", - "@typescript-eslint/types": "^8.35.0", + "@typescript-eslint/tsconfig-utils": "^8.48.1", + "@typescript-eslint/types": "^8.48.1", "debug": "^4.3.4" }, "engines": { @@ -6270,18 +6252,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.35.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.35.0.tgz", - "integrity": "sha512-+AgL5+mcoLxl1vGjwNfiWq5fLDZM1TmTPYs2UkyHfFhgERxBbqHlNjRzhThJqz+ktBqTChRYY6zwbMwy0591AA==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.48.1.tgz", + "integrity": "sha512-rj4vWQsytQbLxC5Bf4XwZ0/CKd362DkWMUkviT7DCS057SK64D5lH74sSGzhI6PDD2HCEq02xAP9cX68dYyg1w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.35.0", - "@typescript-eslint/visitor-keys": "8.35.0" + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/visitor-keys": "8.48.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6292,9 +6274,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.35.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.0.tgz", - "integrity": "sha512-04k/7247kZzFraweuEirmvUj+W3bJLI9fX6fbo1Qm2YykuBvEhRTPl8tcxlYO8kZZW+HIXfkZNoasVb8EV4jpA==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.1.tgz", + "integrity": "sha512-k0Jhs4CpEffIBm6wPaCXBAD7jxBtrHjrSgtfCjUvPp9AZ78lXKdTR8fxyZO5y4vWNlOvYXRtngSZNSn+H53Jkw==", "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6304,18 +6286,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.35.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.35.0.tgz", - "integrity": "sha512-ceNNttjfmSEoM9PW87bWLDEIaLAyR+E6BoYJQ5PfaDau37UGca9Nyq3lBk8Bw2ad0AKvYabz6wxc7DMTO2jnNA==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.48.1.tgz", + "integrity": "sha512-1jEop81a3LrJQLTf/1VfPQdhIY4PlGDBc/i67EVWObrtvcziysbLN3oReexHOM6N3jyXgCrkBsZpqwH0hiDOQg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.35.0", - "@typescript-eslint/utils": "8.35.0", + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/typescript-estree": "8.48.1", + "@typescript-eslint/utils": "8.48.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -6328,13 +6311,13 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.35.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.0.tgz", - "integrity": "sha512-0mYH3emanku0vHw2aRLNGqe7EXh9WHEhi7kZzscrMDf6IIRUQ5Jk4wp1QrledE/36KtdZrVfKnE32eZCf/vaVQ==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.1.tgz", + "integrity": "sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q==", "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6345,20 +6328,19 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.35.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.0.tgz", - "integrity": "sha512-F+BhnaBemgu1Qf8oHrxyw14wq6vbL8xwWKKMwTMwYIRmFFY/1n/9T/jpbobZL8vp7QyEUcC6xGrnAO4ua8Kp7w==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.48.1.tgz", + "integrity": "sha512-/9wQ4PqaefTK6POVTjJaYS0bynCgzh6ClJHGSBj06XEHjkfylzB+A3qvyaXnErEZSaxhIo4YdyBgq6j4RysxDg==", "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.35.0", - "@typescript-eslint/tsconfig-utils": "8.35.0", - "@typescript-eslint/types": "8.35.0", - "@typescript-eslint/visitor-keys": "8.35.0", + "@typescript-eslint/project-service": "8.48.1", + "@typescript-eslint/tsconfig-utils": "8.48.1", + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/visitor-keys": "8.48.1", "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", + "tinyglobby": "^0.2.15", "ts-api-utils": "^2.1.0" }, "engines": { @@ -6369,7 +6351,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { @@ -6385,16 +6367,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.35.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.35.0.tgz", - "integrity": "sha512-nqoMu7WWM7ki5tPgLVsmPM8CkqtoPUG6xXGeefM5t4x3XumOEKMoUZPdi+7F+/EotukN4R9OWdmDxN80fqoZeg==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.48.1.tgz", + "integrity": "sha512-fAnhLrDjiVfey5wwFRwrweyRlCmdz5ZxXz2G/4cLn0YDLjTapmN4gcCsTBR1N2rWnZSDeWpYtgLDsJt+FpmcwA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.35.0", - "@typescript-eslint/types": "8.35.0", - "@typescript-eslint/typescript-estree": "8.35.0" + "@typescript-eslint/scope-manager": "8.48.1", + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/typescript-estree": "8.48.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6405,16 +6387,16 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.35.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.0.tgz", - "integrity": "sha512-zTh2+1Y8ZpmeQaQVIc/ZZxsx8UzgKJyNg1PTvjzC7WMhPSVS8bfDX34k1SrwOf016qd5RU3az2UxUNue3IfQ5g==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.48.1.tgz", + "integrity": "sha512-BmxxndzEWhE4TIEEMBs8lP3MBWN3jFPs/p6gPm/wkv02o41hI6cq9AuSmGAaTTHPtA1FTi2jBre4A9rm5ZmX+Q==", "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.35.0", + "@typescript-eslint/types": "8.48.1", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -6474,7 +6456,6 @@ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.6.0.tgz", "integrity": "sha512-5Kgff+m8e2PB+9j51eGHEpn5kUzRKH2Ry0qGoe8ItJg7pqnkPrYPkDQZGgGmTa0EGarHrkjLvOdU3b1fzI8otQ==", "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "^7.27.4", "@babel/plugin-transform-react-jsx-self": "^7.27.1", @@ -6771,7 +6752,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -7297,7 +7277,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", @@ -8248,8 +8227,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", @@ -8370,7 +8348,6 @@ "resolved": "https://registry.npmjs.org/db0/-/db0-0.3.2.tgz", "integrity": "sha512-xzWNQ6jk/+NtdfLyXEipbX55dmDSeteLFt/ayF+wZUU5bzKgmrDOxmInUTbyVRp46YwnJdkDA1KhB7WIXFofJw==", "license": "MIT", - "peer": true, "peerDependencies": { "@electric-sql/pglite": "*", "@libsql/client": "*", @@ -8743,7 +8720,8 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/dom-serializer": { "version": "2.0.0", @@ -9233,26 +9211,24 @@ } }, "node_modules/eslint": { - "version": "9.30.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.30.0.tgz", - "integrity": "sha512-iN/SiPxmQu6EVkf+m1qpBxzUhE12YqFLOSySuOyVLJLEF9nzTf+h/1AJYc1JWzCnktggeNrjvQGLngDzXirU6g==", + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", + "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.0", - "@eslint/core": "^0.14.0", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.30.0", - "@eslint/plugin-kit": "^0.3.1", + "@eslint/js": "9.39.1", + "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", @@ -9295,9 +9271,9 @@ } }, "node_modules/eslint-config-prettier": { - "version": "10.1.5", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz", - "integrity": "sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==", + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", "bin": { @@ -9344,13 +9320,20 @@ } }, "node_modules/eslint-plugin-react-hooks": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", - "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", "dev": true, "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, "engines": { - "node": ">=10" + "node": ">=18" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" @@ -10542,6 +10525,23 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, "node_modules/home-assistant-js-websocket": { "version": "9.5.0", "resolved": "https://registry.npmjs.org/home-assistant-js-websocket/-/home-assistant-js-websocket-9.5.0.tgz", @@ -10850,7 +10850,6 @@ "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.6.1.tgz", "integrity": "sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA==", "license": "MIT", - "peer": true, "dependencies": { "@ioredis/commands": "^1.1.1", "cluster-key-slot": "^1.1.0", @@ -12246,6 +12245,7 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -12264,7 +12264,6 @@ "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", "license": "MIT", - "peer": true, "dependencies": { "@babel/parser": "^7.25.4", "@babel/types": "^7.25.4", @@ -14233,7 +14232,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -14341,6 +14339,7 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -14356,6 +14355,7 @@ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -14366,6 +14366,7 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -14378,7 +14379,8 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/process": { "version": "0.11.10", @@ -14800,7 +14802,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz", "integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -14810,7 +14811,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz", "integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -15313,7 +15313,6 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.44.1.tgz", "integrity": "sha512-x8H8aPvD+xbl0Do8oez5f5o8eMS3trfCghc4HhLAnCkj7Vl0d1JWGs0UF/D886zLW2rOj2QymV/JcSSsw+XDNg==", "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -15594,7 +15593,6 @@ "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.3.2.tgz", "integrity": "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=10" } @@ -16522,8 +16520,7 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/tiny-warning": { "version": "1.0.3", @@ -16545,13 +16542,13 @@ "license": "MIT" }, "node_modules/tinyglobby": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", - "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "license": "MIT", "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" + "fdir": "^6.5.0", + "picomatch": "^4.0.3" }, "engines": { "node": ">=12.0.0" @@ -16561,10 +16558,13 @@ } }, "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", - "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, "peerDependencies": { "picomatch": "^3 || ^4" }, @@ -16575,11 +16575,10 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -16896,7 +16895,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -17646,7 +17644,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -17778,7 +17775,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -18528,6 +18524,19 @@ "url": "https://github.com/sponsors/colinhacks" } }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/package.json b/package.json index 860fbf9..7f5db03 100644 --- a/package.json +++ b/package.json @@ -63,13 +63,13 @@ "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "@types/react-grid-layout": "^1.3.5", - "@typescript-eslint/eslint-plugin": "^8.35.0", - "@typescript-eslint/parser": "^8.35.0", + "@typescript-eslint/eslint-plugin": "^8.48.1", + "@typescript-eslint/parser": "^8.48.1", "@vitejs/plugin-react": "^4.6.0", - "eslint": "^9.30.0", - "eslint-config-prettier": "^10.1.5", + "eslint": "^9.39.1", + "eslint-config-prettier": "^10.1.8", "eslint-plugin-react": "^7.37.5", - "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-hooks": "^7.0.1", "husky": "^9.1.7", "jsdom": "^26.1.0", "lint-staged": "^16.1.2", diff --git a/src/components/BinarySensorCard.tsx b/src/components/BinarySensorCard.tsx index b492885..6c6bbb6 100644 --- a/src/components/BinarySensorCard.tsx +++ b/src/components/BinarySensorCard.tsx @@ -1,6 +1,6 @@ import { Flex, Text } from '@radix-ui/themes' import { useEntity } from '~/hooks' -import { memo, useState } from 'react' +import { memo, useState, useMemo } from 'react' import { SkeletonCard, ErrorDisplay } from './ui' import { GridCardWithComponents as GridCard } from './GridCard' import { useDashboardStore, dashboardStore, dashboardActions } from '~/store' @@ -8,6 +8,7 @@ import { CardConfig } from './CardConfig' import type { GridItem } from '~/store/types' import { getTablerIcon } from '~/utils/icons' import { getIcon } from '~/utils/iconList' +import { IconCircle, IconCircleCheck } from '@tabler/icons-react' interface BinarySensorCardProps { entityId: string @@ -58,6 +59,19 @@ function BinarySensorCardComponent({ // Get config from item const config = (item?.config as { onIcon?: string; offIcon?: string }) || {} + // Compute icon values - must be before early returns to follow rules of hooks + const isOn = entity?.state === 'on' + const deviceClass = entity?.attributes?.device_class as string | undefined + const defaults = getDefaultIcons(deviceClass) + const onIconName = config.onIcon || defaults.onIcon + const offIconName = config.offIcon || defaults.offIcon + const iconName = isOn ? onIconName : offIconName + + // Get the icon component - memoized to avoid recreating during render + const IconComponent = useMemo(() => { + return getTablerIcon(iconName) || getIcon(iconName) || (isOn ? IconCircleCheck : IconCircle) + }, [iconName, isOn]) + // Show skeleton while loading initial data if (isEntityLoading || (!entity && isConnected)) { return @@ -76,22 +90,8 @@ function BinarySensorCardComponent({ } const friendlyName = entity.attributes.friendly_name || entity.entity_id - const isOn = entity.state === 'on' const isUnavailable = entity.state === 'unavailable' - // Get device class and default icons - const deviceClass = entity.attributes.device_class as string | undefined - const defaults = getDefaultIcons(deviceClass) - - // Get configured icons or use defaults - const onIconName = config.onIcon || defaults.onIcon - const offIconName = config.offIcon || defaults.offIcon - - // Get the icon component - const iconName = isOn ? onIconName : offIconName - const IconComponent = - getTablerIcon(iconName) || (isOn ? getIcon('CircleCheck') : getIcon('Circle')) || (() => null) - // Get icon size based on card size const iconSize = size === 'large' ? 24 : size === 'medium' ? 20 : 16 diff --git a/src/components/CameraCard.tsx b/src/components/CameraCard.tsx index 9d3b780..f06a037 100644 --- a/src/components/CameraCard.tsx +++ b/src/components/CameraCard.tsx @@ -1,4 +1,4 @@ -import { Flex, Text, Button, Spinner, Card, Badge, Separator, Grid } from '@radix-ui/themes' +import { Flex, Text, Button, Spinner, Card } from '@radix-ui/themes' import { VideoIcon, ReloadIcon, @@ -40,13 +40,13 @@ const SUPPORT_STREAM = 2 // Stats display component function CameraStats({ size, - hasFrameWarning, + _hasFrameWarning, isStreaming, videoElement, peerConnection, }: { size: 'small' | 'medium' | 'large' - hasFrameWarning: boolean + _hasFrameWarning: boolean isStreaming: boolean videoElement: HTMLVideoElement | null peerConnection: RTCPeerConnection | null @@ -691,7 +691,7 @@ function CameraCardComponent({ {showStats && supportsStream && !streamError && ( - {/* Screen Config Dialog */} + {/* Screen Config Dialog - key forces remount when screen changes to reset form state */} { setAddViewOpen(open) diff --git a/src/components/InputTextCard.tsx b/src/components/InputTextCard.tsx index cefb28e..0f782cd 100644 --- a/src/components/InputTextCard.tsx +++ b/src/components/InputTextCard.tsx @@ -1,4 +1,4 @@ -import React, { memo, useCallback, useState, useEffect } from 'react' +import React, { memo, useCallback, useState } from 'react' import { Box, Flex, IconButton, Text, TextField } from '@radix-ui/themes' import { Archive, Check, Edit2, Type, X } from 'lucide-react' import { useEntity } from '../hooks/useEntity' @@ -33,21 +33,20 @@ export const InputTextCard = memo(function InputTextCard({ const { entity, isConnected, isLoading: isEntityLoading } = useEntity(entityId) const { setValue, loading, error } = useServiceCall() - const [localValue, setLocalValue] = useState('') const [isEditing, setIsEditing] = useState(false) + // Local value for editing - initialized when entering edit mode + const [localValue, setLocalValue] = useState('') - // Update local value when entity changes - useEffect(() => { - if (entity && !isEditing) { - setLocalValue(entity.state) - } - }, [entity, isEditing]) + // Computed display value - entity state when not editing, local value when editing + const displayValue = isEditing ? localValue : (entity?.state ?? '') const handleClick = useCallback(() => { - if (!isEditing) { + if (!isEditing && entity) { + // Initialize local value with entity state when entering edit mode + setLocalValue(entity.state) setIsEditing(true) } - }, [isEditing]) + }, [isEditing, entity]) const handleSubmit = useCallback( (e: React.FormEvent) => { @@ -58,13 +57,15 @@ export const InputTextCard = memo(function InputTextCard({ // Validate length constraints if (attributes.min && localValue.length < attributes.min) { - setLocalValue(entity.state) + // Invalid - exit edit mode, displayValue reverts to entity.state setIsEditing(false) return } if (attributes.max && localValue.length > attributes.max) { - setLocalValue(localValue.substring(0, attributes.max)) + // Truncate value + const truncated = localValue.substring(0, attributes.max) + setLocalValue(truncated) return } @@ -72,7 +73,7 @@ export const InputTextCard = memo(function InputTextCard({ if (attributes.pattern) { const regex = new RegExp(attributes.pattern) if (!regex.test(localValue)) { - setLocalValue(entity.state) + // Invalid - exit edit mode, displayValue reverts to entity.state setIsEditing(false) return } @@ -85,11 +86,9 @@ export const InputTextCard = memo(function InputTextCard({ ) const handleCancel = useCallback(() => { - if (entity) { - setLocalValue(entity.state) - setIsEditing(false) - } - }, [entity]) + // Just exit editing mode - displayValue will show entity.state again + setIsEditing(false) + }, []) const handleFieldClick = useCallback((e: React.MouseEvent) => { e.stopPropagation() @@ -145,8 +144,8 @@ export const InputTextCard = memo(function InputTextCard({ const isStale = attributes._stale === true const isPassword = attributes.mode === 'password' - // Display value (mask if password and not editing) - const displayValue = isPassword && !isEditing ? '••••••••' : entity.state + // For display: mask if password and not editing, otherwise show displayValue (computed at top) + const shownValue = isPassword && !isEditing ? '••••••••' : displayValue return ( - {displayValue || '(empty)'} + {shownValue || '(empty)'} { e.stopPropagation() + setLocalValue(entity.state) setIsEditing(true) }} > diff --git a/src/components/KeepAlive.tsx b/src/components/KeepAlive.tsx index fd949bb..0450ad7 100644 --- a/src/components/KeepAlive.tsx +++ b/src/components/KeepAlive.tsx @@ -1,4 +1,4 @@ -import { useRef, useEffect, ReactNode } from 'react' +import { useState, useEffect, ReactNode } from 'react' import { createPortal } from 'react-dom' interface KeepAliveProps { @@ -11,36 +11,40 @@ interface KeepAliveProps { const portalCache = new Map() export function KeepAlive({ children, cacheKey, containerRef }: KeepAliveProps) { - const portalElementRef = useRef(null) + // Use state instead of ref to track portal element - this triggers re-render when set + const [portalElement, setPortalElement] = useState(() => { + // Initialize from cache synchronously during initial render + return portalCache.get(cacheKey) ?? null + }) useEffect(() => { // Get or create portal element for this cache key - let portalElement = portalCache.get(cacheKey) + let element = portalCache.get(cacheKey) - if (!portalElement) { - portalElement = document.createElement('div') - portalElement.style.width = '100%' - portalElement.style.height = '100%' - portalCache.set(cacheKey, portalElement) + if (!element) { + element = document.createElement('div') + element.style.width = '100%' + element.style.height = '100%' + portalCache.set(cacheKey, element) } - portalElementRef.current = portalElement + setPortalElement(element) // Append portal element to container - if (containerRef.current && portalElement) { - containerRef.current.appendChild(portalElement) + if (containerRef.current && element) { + containerRef.current.appendChild(element) } return () => { // Remove portal element from current container but don't destroy it - if (portalElement && portalElement.parentNode) { - portalElement.parentNode.removeChild(portalElement) + if (element && element.parentNode) { + element.parentNode.removeChild(element) } } }, [cacheKey, containerRef]) // Render children into the cached portal element - if (!portalElementRef.current) return null + if (!portalElement) return null - return createPortal(children, portalElementRef.current) + return createPortal(children, portalElement) } diff --git a/src/components/LightCard.tsx b/src/components/LightCard.tsx index 3c74aad..77e8a5e 100644 --- a/src/components/LightCard.tsx +++ b/src/components/LightCard.tsx @@ -81,22 +81,25 @@ function LightCardComponent({ ) const lightAttributes = entity?.attributes as LightAttributes | undefined + // Check if light supports brightness control + const supportedColorModes = lightAttributes?.supported_color_modes + const supportedFeatures = lightAttributes?.supported_features ?? 0 const supportsBrightness = useMemo(() => { // Modern Home Assistant uses supported_color_modes - if (lightAttributes?.supported_color_modes) { + if (supportedColorModes) { return ( - lightAttributes.supported_color_modes.includes('brightness') || - lightAttributes.supported_color_modes.includes('color_temp') || - lightAttributes.supported_color_modes.includes('hs') || - lightAttributes.supported_color_modes.includes('xy') || - lightAttributes.supported_color_modes.includes('rgb') || - lightAttributes.supported_color_modes.includes('rgbw') || - lightAttributes.supported_color_modes.includes('rgbww') + supportedColorModes.includes('brightness') || + supportedColorModes.includes('color_temp') || + supportedColorModes.includes('hs') || + supportedColorModes.includes('xy') || + supportedColorModes.includes('rgb') || + supportedColorModes.includes('rgbw') || + supportedColorModes.includes('rgbww') ) } // Fallback to old supported_features check - return (lightAttributes?.supported_features ?? 0) & SUPPORT_BRIGHTNESS - }, [lightAttributes?.supported_features, lightAttributes?.supported_color_modes]) + return supportedFeatures & SUPPORT_BRIGHTNESS + }, [supportedColorModes, supportedFeatures]) // These will be used for color picker implementation // const supportsColor = useMemo(() => { diff --git a/src/components/ScreenConfigDialog.tsx b/src/components/ScreenConfigDialog.tsx index 46dba88..8600a16 100644 --- a/src/components/ScreenConfigDialog.tsx +++ b/src/components/ScreenConfigDialog.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react' +import { useState } from 'react' import { TextField, Flex, Text, Select, Modal, Button } from '~/components/ui' import { dashboardActions, useDashboardStore } from '../store' import type { ScreenConfig } from '../store/types' @@ -13,26 +13,14 @@ interface ScreenConfigDialogProps { export function ScreenConfigDialog({ open, onOpenChange, screen }: ScreenConfigDialogProps) { const screens = useDashboardStore((state) => state.screens) - const [viewName, setViewName] = useState('') - const [viewSlug, setViewSlug] = useState('') + // Initialize form values from screen prop - component uses key prop for remounting + const [viewName, setViewName] = useState(screen?.name ?? '') + const [viewSlug, setViewSlug] = useState(screen?.slug ?? '') const [parentId, setParentId] = useState('') const navigate = useNavigate() const isEditMode = !!screen - // Initialize form values when editing - useEffect(() => { - if (screen) { - setViewName(screen.name) - setViewSlug(screen.slug) - // TODO: Find parent ID if screen is nested - } else { - setViewName('') - setViewSlug('') - setParentId('') - } - }, [screen]) - const handleSubmit = (e: React.FormEvent) => { e.preventDefault() diff --git a/src/components/__tests__/CardConfig.test.tsx b/src/components/__tests__/CardConfig.test.tsx index d25ecd1..30cdf03 100644 --- a/src/components/__tests__/CardConfig.test.tsx +++ b/src/components/__tests__/CardConfig.test.tsx @@ -1,5 +1,5 @@ import { describe, it, expect, vi, beforeEach } from 'vitest' -import { render, screen, fireEvent, waitFor, within } from '@testing-library/react' +import { render, screen, waitFor, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { CardConfig } from '../CardConfig' import { Theme } from '@radix-ui/themes' diff --git a/src/components/widgets/WeatherWidget.tsx b/src/components/widgets/WeatherWidget.tsx index 1aec011..3143191 100644 --- a/src/components/widgets/WeatherWidget.tsx +++ b/src/components/widgets/WeatherWidget.tsx @@ -1,5 +1,6 @@ import { Card, Flex, Text, Heading, Grid, Separator, ScrollArea } from '@radix-ui/themes' import { useStore } from '@tanstack/react-store' +import { useMemo } from 'react' import { entityStore } from '../../store/entityStore' import { Cloud, @@ -13,6 +14,7 @@ import { Eye, Gauge, Navigation, + type LucideIcon, } from 'lucide-react' import type { WidgetConfig } from '../../store/types' @@ -49,7 +51,7 @@ interface ForecastDay { wind_bearing?: number } -function getWeatherIcon(condition: string) { +function getWeatherIcon(condition: string): LucideIcon { const lowerCondition = condition.toLowerCase() if (lowerCondition.includes('clear') || lowerCondition.includes('sunny')) return Sun if (lowerCondition.includes('rain')) return CloudRain @@ -97,6 +99,12 @@ export function WeatherWidget({ widget }: WeatherWidgetProps) { Object.keys(entities).find((id) => id.startsWith('weather.')) const weatherEntity = entityId ? entities[entityId] : undefined + // Get the weather icon - must be before any early returns to follow rules of hooks + const CurrentWeatherIcon = useMemo( + () => (weatherEntity ? getWeatherIcon(weatherEntity.state) : Cloud), + [weatherEntity] + ) + if (!weatherEntity) { return ( @@ -121,8 +129,6 @@ export function WeatherWidget({ widget }: WeatherWidgetProps) { // In the future, we can enhance this to use the get_forecasts service const forecast = attributes?.forecast?.slice(0, 8) || [] - const WeatherIcon = getWeatherIcon(weatherEntity.state) - return ( @@ -130,7 +136,7 @@ export function WeatherWidget({ widget }: WeatherWidgetProps) { Weather - + {/* Current conditions */} diff --git a/src/hooks/useWebRTC.ts b/src/hooks/useWebRTC.ts index 2112ab1..2fc0bf9 100644 --- a/src/hooks/useWebRTC.ts +++ b/src/hooks/useWebRTC.ts @@ -124,15 +124,12 @@ export function useWebRTC({ entityId, enabled = true }: UseWebRTCOptions): UseWe let lastDecodedFrames = 0 let hasReceivedFirstFrame = false let debugLogTimer = Date.now() - let frameCount = 0 - let lastDebugFrameCount = 0 let videoFrameCallbackId: number | null = null // Use requestVideoFrameCallback if available for accurate frame detection if ('requestVideoFrameCallback' in video) { const onVideoFrame = (_now: number) => { frameMonitorRef.current.lastFrameTime = Date.now() - frameCount++ if (!hasReceivedFirstFrame) { hasReceivedFirstFrame = true @@ -194,7 +191,6 @@ export function useWebRTC({ entityId, enabled = true }: UseWebRTCOptions): UseWe frameMonitorRef.current.lastFrameTime = now lastTime = currentTime lastDecodedFrames = videoFrames - frameCount++ // We're receiving frames - set streaming to true if (!hasReceivedFirstFrame) { @@ -217,10 +213,9 @@ export function useWebRTC({ entityId, enabled = true }: UseWebRTCOptions): UseWe } } - // Update debug timer without logging + // Update debug timer (for potential future debugging) if (now - debugLogTimer >= 5000) { debugLogTimer = now - lastDebugFrameCount = frameCount } // Only check for stale frames if we've received at least one frame @@ -271,7 +266,7 @@ export function useWebRTC({ entityId, enabled = true }: UseWebRTCOptions): UseWe ).cancelVideoFrameCallback(videoFrameCallbackId) } } - }, [cleanup, entityId, hasFrameWarning]) + }, [cleanup, hasFrameWarning]) const initializeWebRTC = useCallback(async () => { if (!hass || !enabled || !videoElementRef.current) return diff --git a/src/panel.ts b/src/panel.ts index 94cd86f..036c2d9 100644 --- a/src/panel.ts +++ b/src/panel.ts @@ -437,7 +437,7 @@ const startGlobalPanelGuardian = () => { container.appendChild(panel) console.log('[Global Panel Guardian] Successfully restored panel to container') break - } catch (error) { + } catch { // Continue trying other containers } } From 2095081cd9c76ce79311ef2b96ced2aad17c8a93 Mon Sep 17 00:00:00 2001 From: Marian Rudzynski Date: Wed, 3 Dec 2025 23:52:26 +0000 Subject: [PATCH 2/2] refactor: address Copilot PR review feedback --- app/utils/responsive.ts | 3 +++ src/components/BinarySensorCard.tsx | 19 ++++++++++--------- src/components/InputTextCard.tsx | 16 ++++++++++------ src/components/widgets/WeatherWidget.tsx | 5 +++-- 4 files changed, 26 insertions(+), 17 deletions(-) diff --git a/app/utils/responsive.ts b/app/utils/responsive.ts index 864d924..297f297 100644 --- a/app/utils/responsive.ts +++ b/app/utils/responsive.ts @@ -69,6 +69,9 @@ export function useMediaQuery(query: string): boolean { useEffect(() => { const mediaQuery = window.matchMedia(query) + // Sync state when query changes (useState initializer only runs on mount) + setMatches(mediaQuery.matches) + const handleChange = (e: MediaQueryListEvent) => { setMatches(e.matches) } diff --git a/src/components/BinarySensorCard.tsx b/src/components/BinarySensorCard.tsx index 6c6bbb6..f4548c9 100644 --- a/src/components/BinarySensorCard.tsx +++ b/src/components/BinarySensorCard.tsx @@ -58,19 +58,20 @@ function BinarySensorCardComponent({ // Get config from item const config = (item?.config as { onIcon?: string; offIcon?: string }) || {} - - // Compute icon values - must be before early returns to follow rules of hooks - const isOn = entity?.state === 'on' const deviceClass = entity?.attributes?.device_class as string | undefined - const defaults = getDefaultIcons(deviceClass) - const onIconName = config.onIcon || defaults.onIcon - const offIconName = config.offIcon || defaults.offIcon - const iconName = isOn ? onIconName : offIconName - // Get the icon component - memoized to avoid recreating during render + // Memoize icon computation based on primitive values - must be before early returns const IconComponent = useMemo(() => { + const isOn = entity?.state === 'on' + const defaults = getDefaultIcons(deviceClass) + const onIconName = config.onIcon || defaults.onIcon + const offIconName = config.offIcon || defaults.offIcon + const iconName = isOn ? onIconName : offIconName return getTablerIcon(iconName) || getIcon(iconName) || (isOn ? IconCircleCheck : IconCircle) - }, [iconName, isOn]) + }, [entity?.state, config.onIcon, config.offIcon, deviceClass]) + + // Compute isOn for use in rendering (after useMemo to follow rules of hooks) + const isOn = entity?.state === 'on' // Show skeleton while loading initial data if (isEntityLoading || (!entity && isConnected)) { diff --git a/src/components/InputTextCard.tsx b/src/components/InputTextCard.tsx index 0f782cd..881633c 100644 --- a/src/components/InputTextCard.tsx +++ b/src/components/InputTextCard.tsx @@ -40,13 +40,18 @@ export const InputTextCard = memo(function InputTextCard({ // Computed display value - entity state when not editing, local value when editing const displayValue = isEditing ? localValue : (entity?.state ?? '') - const handleClick = useCallback(() => { - if (!isEditing && entity) { - // Initialize local value with entity state when entering edit mode + const enterEditMode = useCallback(() => { + if (entity) { setLocalValue(entity.state) setIsEditing(true) } - }, [isEditing, entity]) + }, [entity]) + + const handleClick = useCallback(() => { + if (!isEditing) { + enterEditMode() + } + }, [isEditing, enterEditMode]) const handleSubmit = useCallback( (e: React.FormEvent) => { @@ -222,8 +227,7 @@ export const InputTextCard = memo(function InputTextCard({ variant="ghost" onClick={(e) => { e.stopPropagation() - setLocalValue(entity.state) - setIsEditing(true) + enterEditMode() }} > diff --git a/src/components/widgets/WeatherWidget.tsx b/src/components/widgets/WeatherWidget.tsx index 3143191..57d2d2d 100644 --- a/src/components/widgets/WeatherWidget.tsx +++ b/src/components/widgets/WeatherWidget.tsx @@ -100,9 +100,10 @@ export function WeatherWidget({ widget }: WeatherWidgetProps) { const weatherEntity = entityId ? entities[entityId] : undefined // Get the weather icon - must be before any early returns to follow rules of hooks + const weatherState = weatherEntity?.state const CurrentWeatherIcon = useMemo( - () => (weatherEntity ? getWeatherIcon(weatherEntity.state) : Cloud), - [weatherEntity] + () => (weatherState ? getWeatherIcon(weatherState) : Cloud), + [weatherState] ) if (!weatherEntity) {