From b9eda09da98e539bc34be731972a9b68fbc75ce0 Mon Sep 17 00:00:00 2001 From: Sanaei Date: Wed, 19 Aug 2026 17:48:28 +0200 Subject: [PATCH] chore(frontend): update dependencies and adapt to oxlint 1.79 npm install was failing with ERESOLVE: the lockfile pinned storybook 10.5.7 and vitest 4.1.10 as peers while package.json asked for ^10.5.9 and ^4.1.11, and npm would not move either. Neither npm update, a targeted install, nor --package-lock-only broke the cycle, so node_modules and package-lock.json were regenerated from scratch (601 packages, 0 vulnerabilities). oxlint 1.79.0 then promoted five React Compiler rules into the correctness category, flagging 101 pre-existing sites. 1.78.0 exits 0 on the same tree, so nothing in our code changed - the rule set grew. They are fixed rather than suppressed: - refs (31): latest-value ref writes moved out of render into an effect. onlineClientsRef turned out to be write-only and is gone; expireDiffRef and trafficDiffRef were replaced by reading the values directly. - set-state-in-effect (55): reset-on-open modals now adjust state during render; where an effect mixed a synchronous reset with an async fetch, the reset moved to render and the effect kept only the request. useMediaQuery became useSyncExternalStore. - preserve-manual-memoization (11): optional-chained deps the compiler cannot match, hoisted to locals or dropped where the memo wrapped a string concat. - purity (3): Date.now() in render replaced by a state-backed clock, which also refreshes the expiry tag every 60s instead of freezing it until the next unrelated re-render. - immutability (1): applyClientStatsEvent merged websocket traffic into DBInbound rows in place; it now rebuilds only the rows it touches. Two things fell out of that. clientCount is derived with useMemo instead of an imperative rebuildClientCount() called from five sites, which also fixes a staleness bug where changing the expiry or traffic threshold left the counts alone until some later rebuild. statsVersion existed only to force a re-render after an in-place mutation, is meaningless now that rows are replaced, and nothing read it, so it is removed. Also adds a lint:fix script - oxlint --fix was previously only reachable through the lint-staged hook. --- frontend/package-lock.json | 1138 ++++++++--------- frontend/package.json | 29 +- .../src/components/feedback/PromptModal.tsx | 22 +- .../src/components/feedback/TextModal.tsx | 15 +- .../geodata/GeoBrowserModal.stories.tsx | 8 +- .../components/geodata/GeoBrowserModal.tsx | 56 +- .../geodata/GeoTokenInput.stories.tsx | 6 +- .../src/components/geodata/GeoTokenInput.tsx | 16 +- frontend/src/components/utility/LazyMount.tsx | 6 +- frontend/src/components/viz/Sparkline.tsx | 6 +- frontend/src/hooks/useClients.ts | 4 +- frontend/src/hooks/useMediaQuery.ts | 24 +- frontend/src/hooks/useServerDraft.ts | 41 +- frontend/src/hooks/useXraySetting.ts | 62 +- frontend/src/layouts/AppSidebar.tsx | 8 +- .../lib/xray/forms/fields/FinalMaskField.tsx | 4 +- .../lib/xray/forms/fields/SniffingField.tsx | 4 +- .../xray/forms/fields/SockoptCustomField.tsx | 4 +- .../pages/clients/BulkAttachInboundsModal.tsx | 10 +- .../pages/clients/BulkDetachInboundsModal.tsx | 10 +- .../src/pages/clients/ClientBulkAddModal.tsx | 14 +- .../src/pages/clients/ClientInfoModal.tsx | 40 +- frontend/src/pages/clients/ClientQrModal.tsx | 46 +- .../src/pages/groups/GroupAddClientsModal.tsx | 17 +- .../pages/groups/GroupRemoveClientsModal.tsx | 17 +- frontend/src/pages/hosts/HostFormModal.tsx | 11 +- .../hosts/json-forms/HostFinalMaskForm.tsx | 4 +- .../inbounds/clients/AttachClientsModal.tsx | 24 +- .../clients/AttachExistingClientsModal.tsx | 15 +- .../inbounds/clients/DetachClientsModal.tsx | 20 +- .../pages/inbounds/form/protocols/vless.tsx | 8 +- .../security/RealityTargetScannerModal.tsx | 33 +- .../pages/inbounds/info/InboundInfoModal.tsx | 51 +- .../pages/inbounds/list/InboundStatsModal.tsx | 12 +- .../src/pages/inbounds/qr/QrCodeModal.tsx | 37 +- frontend/src/pages/inbounds/qr/QrPanel.tsx | 2 +- frontend/src/pages/inbounds/useInbounds.ts | 443 +++---- frontend/src/pages/index/GeodataSection.tsx | 22 +- frontend/src/pages/index/LogModal.tsx | 29 +- .../src/pages/index/SystemHistoryModal.tsx | 143 ++- frontend/src/pages/index/VersionModal.tsx | 9 +- frontend/src/pages/index/XrayLogModal.tsx | 31 +- frontend/src/pages/index/XrayMetricsModal.tsx | 250 ++-- .../src/pages/index/useOverviewHistory.ts | 9 +- frontend/src/pages/nodes/NodeFormModal.tsx | 12 +- frontend/src/pages/settings/SecurityTab.tsx | 14 +- frontend/src/pages/settings/SettingsPage.tsx | 13 +- .../pages/settings/SubJsonFinalMaskForm.tsx | 4 +- .../src/pages/settings/TwoFactorModal.tsx | 43 +- .../xray/balancers/BalancerFormModal.tsx | 9 +- .../src/pages/xray/balancers/BalancersTab.tsx | 9 +- .../src/pages/xray/overrides/NordModal.tsx | 10 +- .../src/pages/xray/overrides/WarpModal.tsx | 22 +- .../src/pages/xray/routing/RoutingTab.tsx | 9 +- 54 files changed, 1497 insertions(+), 1408 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 10406c77c..07814ea45 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,13 +11,13 @@ "@ant-design/icons": "^6.3.2", "@codemirror/lang-json": "^6.0.2", "@codemirror/theme-one-dark": "^6.1.3", - "@hookform/resolvers": "^5.7.1", + "@hookform/resolvers": "^5.9.1", "@noble/hashes": "^2.3.0", "@tanstack/react-query": "^5.101.4", "@tanstack/react-query-devtools": "^5.101.4", - "antd": "^6.6.0", + "antd": "^6.6.1", "codemirror": "^6.0.2", - "dayjs": "^1.11.21", + "dayjs": "^1.11.23", "i18next": "^26.3.6", "otpauth": "^9.5.1", "persian-calendar-suite": "^1.5.6", @@ -26,35 +26,35 @@ "react-hook-form": "^7.85.0", "react-i18next": "^17.0.11", "react-router": "^8.3.0", - "swagger-ui-react": "^5.32.13", + "swagger-ui-react": "^5.32.14", "uplot": "^1.6.32", "zod": "^4.4.3" }, "devDependencies": { - "@storybook/addon-a11y": "^10.5.7", - "@storybook/addon-docs": "^10.5.7", - "@storybook/addon-vitest": "^10.5.7", - "@storybook/react-vite": "^10.5.7", + "@storybook/addon-a11y": "^10.5.9", + "@storybook/addon-docs": "^10.5.9", + "@storybook/addon-vitest": "^10.5.9", + "@storybook/react-vite": "^10.5.9", "@testing-library/dom": "^10.4.1", "@testing-library/react": "^16.3.2", "@types/react": "^19.2.18", "@types/react-dom": "^19.2.4", "@types/swagger-ui-react": "^5.18.0", "@vitejs/plugin-react": "^6.0.5", - "@vitest/browser-playwright": "4.1.10", - "@vitest/coverage-v8": "^4.1.10", + "@vitest/browser-playwright": "4.1.11", + "@vitest/coverage-v8": "^4.1.11", "husky": "^9.1.7", "jsdom": "^30.0.1", "lint-staged": "^17.3.0", "msw": "^2.15.0", - "oxfmt": "0.63.0", - "oxlint": "1.78.0", + "oxfmt": "0.64.0", + "oxlint": "1.79.0", "oxlint-tsgolint": "^7.0.2001", "playwright": "^1.62.1", - "storybook": "^10.5.7", + "storybook": "^10.5.9", "typescript": "7.0.2", "vite": "8.2.1", - "vitest": "^4.1.10" + "vitest": "^4.1.11" }, "engines": { "node": ">=24.0.0", @@ -178,16 +178,6 @@ "node": "^22.13.0 || >=24.0.0" } }, - "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { - "version": "11.5.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", - "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, "node_modules/@asamuzakjp/dom-selector": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-8.3.2.tgz", @@ -204,16 +194,6 @@ "node": "^22.13.0 || >=24.0.0" } }, - "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { - "version": "11.5.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", - "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -270,6 +250,16 @@ "url": "https://opencollective.com/babel" } }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/generator": { "version": "7.29.8", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", @@ -304,6 +294,26 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/helper-globals": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", @@ -518,9 +528,9 @@ } }, "node_modules/@codemirror/commands": { - "version": "6.10.4", - "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.4.tgz", - "integrity": "sha512-Ryk9y9T0FFVF0cUGhAknveAyUOl/A1qReTFi+qPKtOh2Z9F4AUBz3XOrYD4ZEgZirdugVzHvd/2/Wcwy5OliTg==", + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.11.0.tgz", + "integrity": "sha512-/K4Rl5BN0OtTiPWmJCdqODu38XnDMsDxKY5rgrPnCkutPTJf2wVbkoixLfealF5Kwse/s8P8M5jAiURiwSwnFA==", "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", @@ -597,9 +607,9 @@ } }, "node_modules/@codemirror/view": { - "version": "6.43.8", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.8.tgz", - "integrity": "sha512-qtItTDssZ/5GFfi94hrILu9j/VUeFPDPkhovEfmWFj2ipTxnzPB8DdHgfbb8HYTzLTYhrndKmyQxXUz/PDLenw==", + "version": "6.43.9", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.9.tgz", + "integrity": "sha512-sTuUzTpPMFebRhg6dawChoKKgndIwfjmJgKVxBefPElcU2NwQ6AFroupk0SFqEerQyZOGRfDNnSN8Dw/lMAsXw==", "license": "MIT", "dependencies": { "@codemirror/state": "^6.7.0", @@ -609,9 +619,9 @@ } }, "node_modules/@csstools/color-helpers": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.0.tgz", - "integrity": "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.1.tgz", + "integrity": "sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w==", "dev": true, "funding": [ { @@ -653,9 +663,9 @@ } }, "node_modules/@csstools/css-color-parser": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.10.tgz", - "integrity": "sha512-UZhQLIUyJaaMepqehrCODwCg2KW25vFvLWBmqYFaPclYvvxzj/sG8LBOhBFCp11i9uE7t1EyS+RAoV9tztPFyw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.0.tgz", + "integrity": "sha512-5+5LEmFuY1AjXdYhmgjTJogtQnP1evJ1zrBZGUNZ0thkpwnnmKxcHdAMn/OtFjAb25zA+jKDVYVRl+5G7rjv1A==", "dev": true, "funding": [ { @@ -669,7 +679,7 @@ ], "license": "MIT", "dependencies": { - "@csstools/color-helpers": "^6.1.0", + "@csstools/color-helpers": "^6.1.1", "@csstools/css-calc": "^3.3.0" }, "engines": { @@ -704,9 +714,9 @@ } }, "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.7.tgz", - "integrity": "sha512-fQ+05118eQS1cofO3aJpB5efgpBZMvIzwr/sbC8kDLVA5XLG8q1kJV5yzrUAI1f7lvhPnm8fgIjzFB8/O/5Dig==", + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.8.tgz", + "integrity": "sha512-CpMLjAvwQg3BL5S0IeqsZNMH7EQrEWi0kLKOC13ZBF0ZwERiLWlibNPJr8G1kdU3Ms/r2KiNrF81pUh2HwAHdg==", "dev": true, "funding": [ { @@ -1255,9 +1265,9 @@ } }, "node_modules/@hookform/resolvers": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.7.1.tgz", - "integrity": "sha512-8wS/P4UDr5sQDe4nFaV51TVyfDPrWgNIXweqG0Bs9Z5LSuzKLb+RQNPvkN2oHM5SRrJyWrVH/F+LOUcFjUyvwQ==", + "version": "5.9.1", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.9.1.tgz", + "integrity": "sha512-7b7vsbraJxKgjVSA1Nur9tLwj539WGJUBLA7QNvXnFoT2pM5Z7G+6rlukk4B2/QrTZy6huRtH6wKeESPKuIr6w==", "license": "MIT", "dependencies": { "@standard-schema/utils": "^0.3.0" @@ -1279,13 +1289,13 @@ "fluentvalidation-ts": "^3.0.0", "fp-ts": "^2.7.0", "io-ts": "^2.0.0", - "joi": "^17.0.0", + "joi": "^17.0.0 || ^18.0.0", "nope-validator": ">=0.12.0", "react-hook-form": "^7.55.0", "superstruct": ">=0.12.0", "typanion": "^3.3.2", "valibot": ">=0.31.0 || ^1.0.0-beta.4 || ^1.0.0-rc", - "vest": ">=3.0.0", + "vest": ">=6.0.0", "yup": "^1.0.0", "zod": "^3.25.0 || ^4.0.0" }, @@ -1451,6 +1461,26 @@ } } }, + "node_modules/@joshwooding/vite-plugin-react-docgen-typescript": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@joshwooding/vite-plugin-react-docgen-typescript/-/vite-plugin-react-docgen-typescript-0.7.0.tgz", + "integrity": "sha512-qvsTEwEFefhdirGOPnu9Wp6ChfIwy2dBCRuETU3uE+4cC+PFoxMSiiEhxk4lOluA34eARHA0OxqsEUYDqRMgeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^13.0.1", + "react-docgen-typescript": "^2.2.2" + }, + "peerDependencies": { + "typescript": ">= 4.3.x", + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -2350,9 +2380,9 @@ ] }, "node_modules/@oxfmt/binding-android-arm-eabi": { - "version": "0.63.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.63.0.tgz", - "integrity": "sha512-YmRth4ZPGgEXcgmkhvANbC9uD67dxmSobW7DQuyt5tOBOKvPnIpk5SVHBj88E+7wMNRI2FhqaDbOhQFBix+b8A==", + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.64.0.tgz", + "integrity": "sha512-o6uzh/jTOQeAY5TdkAeXdqv7MBRcPxiRA08zrcBtkKj5cSu/FMu0Hl7Q6Fi1KCKyCWZ6lJVjBzdsJvsKltUsGQ==", "cpu": [ "arm" ], @@ -2367,9 +2397,9 @@ } }, "node_modules/@oxfmt/binding-android-arm64": { - "version": "0.63.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.63.0.tgz", - "integrity": "sha512-icbahX8X2X3sRamOMecvdYeZXWjPDazRDIfvWfy7Ca1nc/ZDT2Y9k5Nt7s46EqFd7NQPdgk+CM3/SgIT5LPCaQ==", + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.64.0.tgz", + "integrity": "sha512-jRGSUeeP7p3Gynw2YaCVtjBIA6ZxY6bEB/ES5i54OhqmRTyuVg7ZgstEtzgq6GOAJd+2QZ5pvf+bFfmW5Mp9cw==", "cpu": [ "arm64" ], @@ -2384,9 +2414,9 @@ } }, "node_modules/@oxfmt/binding-darwin-arm64": { - "version": "0.63.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.63.0.tgz", - "integrity": "sha512-WV+Ze5v5gI2qoj8jpAovt8KBTW8pjEz/AiMXXjeTQS+Bmf/MmZXTS40S8xNPDszX+W8WDv2Bbk6qKrMTtUGu1A==", + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.64.0.tgz", + "integrity": "sha512-JINwtU2lW7nOFSqi+H2qplipNUqah9Gc1jgGmB82kTD4UnZrZIVxCJ9qEmFiKfjNq27gYLFhrUb0to86aCwMjw==", "cpu": [ "arm64" ], @@ -2401,9 +2431,9 @@ } }, "node_modules/@oxfmt/binding-darwin-x64": { - "version": "0.63.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.63.0.tgz", - "integrity": "sha512-CJGSBdDxXOWIpoFXHpverimCvz084KA7L483rqJ44c3jDtzv6d4qOSoR/V9ywSHfV+Ks1lwIj2P49BFhunLNAA==", + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.64.0.tgz", + "integrity": "sha512-gCmuswrgrOSajV4HCRFkVCGIruPq8bjYuPYgSE2WQB3mD6XrdyZ3JMSRZCkQ8zCxOyGWriBo6QoZ5nmMHQ1BfA==", "cpu": [ "x64" ], @@ -2418,9 +2448,9 @@ } }, "node_modules/@oxfmt/binding-freebsd-x64": { - "version": "0.63.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.63.0.tgz", - "integrity": "sha512-BDfKY+KhL2078cgswBBFQPAYuxCy93bS/iC5frdSeSbTLcGrR6VC2hsuPTanoJmg84+wSyWl0wWC1eR+uTnkRg==", + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.64.0.tgz", + "integrity": "sha512-Ab8g7a38pT0MMImjh7anRSTve6buWBIlcXIFBYa5xl4s6UxEgKSc2xOOhbGtLwvXnEi2PsEDGoJh3oUU7xkehQ==", "cpu": [ "x64" ], @@ -2435,9 +2465,9 @@ } }, "node_modules/@oxfmt/binding-linux-arm-gnueabihf": { - "version": "0.63.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.63.0.tgz", - "integrity": "sha512-Ov1cQEXT4mj7cojAokWSS1eoxkoyvbDfAbxNsGIKY2o36kvdAaFzPxRN6NxFRk9fD72B8oCoTTX/NuYTUWlpsg==", + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.64.0.tgz", + "integrity": "sha512-BgvS3CoQ+Xy2deoZqEN8JVKabcCZi2RxA3yant8G9OAv9KuPJ9TCjHkqigzdHUVwErZxEP5d2bzLIEyKYyBDLg==", "cpu": [ "arm" ], @@ -2452,9 +2482,9 @@ } }, "node_modules/@oxfmt/binding-linux-arm-musleabihf": { - "version": "0.63.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.63.0.tgz", - "integrity": "sha512-0LE7ro3+6L79jcMANycAZfRaC7zxr9YZ2+vEL5uMD9QlEep+rS/r1kSJsnuLl991NXJZD60euh0PC1GHrR20vw==", + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.64.0.tgz", + "integrity": "sha512-QXpNxwoMj0YvnceCNZadNSden3bIcnvjn/sDp/rwZhRoZoZYGpHvtPyhGsdJz9uvT9GkaMW7SsLddurU56dt8w==", "cpu": [ "arm" ], @@ -2469,9 +2499,9 @@ } }, "node_modules/@oxfmt/binding-linux-arm64-gnu": { - "version": "0.63.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.63.0.tgz", - "integrity": "sha512-izPk+2Z4gjuZK32Fqh5qXoMpT/2NXzLh++ob57HiEiVSQZ1iYXu8EKMzb+K5AvWyIEXhdDIt7ADjGGtFhkT9Bw==", + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.64.0.tgz", + "integrity": "sha512-BBgH3I1ppDsI5pZ4Pdhw0ceYxwVCfbU/bZEBCeZ6caRS9x0ZabErxubP7riGUn11PXZBhe8DYdjkDKP1FlVQ5w==", "cpu": [ "arm64" ], @@ -2489,9 +2519,9 @@ } }, "node_modules/@oxfmt/binding-linux-arm64-musl": { - "version": "0.63.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.63.0.tgz", - "integrity": "sha512-alPmbOuWXFXiSo+lOtv6X71C7SYMEDW2WVvywOvf9BwKgEhSNGhMTLeFVSjKUMCamcjbbgVdsWF8GN1uy8xshg==", + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.64.0.tgz", + "integrity": "sha512-v19HSjC/BGXdt26qEvKZtwAHgGmQ2Agcap2kQP+KIqoRZqivVzYth3ui2dJA1i+6/fjpjga85lIOaJJjQ/bOOw==", "cpu": [ "arm64" ], @@ -2509,9 +2539,9 @@ } }, "node_modules/@oxfmt/binding-linux-ppc64-gnu": { - "version": "0.63.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.63.0.tgz", - "integrity": "sha512-BdzCPvolJc4AWZ+YMzgUDJcDzbQWrFjYuqBHoNHNqP1aCaluQRJNs4k3vNU5IG7vTpjf9zeD73D7MFM1TecZpg==", + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.64.0.tgz", + "integrity": "sha512-PElLnOo4xFTBZrxPhgTIj0eHqZXwEBQoNWtb7facUV170T0B0FRET0iNbb3LUeLWTybkUW+vsdyv4ihOdyXGyw==", "cpu": [ "ppc64" ], @@ -2529,9 +2559,9 @@ } }, "node_modules/@oxfmt/binding-linux-riscv64-gnu": { - "version": "0.63.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.63.0.tgz", - "integrity": "sha512-7sIgfLzqtNKSkMGsGVyRpHwpjNezRg2XONvUOheFZs95TSZpM0JAuPpA8KrQFsWc4wPU95roX2O69JgH8igOgw==", + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.64.0.tgz", + "integrity": "sha512-Qzsg15n4F5CH+MorcRW4MkAEMiLzXmeG+DiDSbP/bBTqCmWOH3K9DHryNrve+JHlV0txS+B6Z9P5Xz+cmWeL+g==", "cpu": [ "riscv64" ], @@ -2549,9 +2579,9 @@ } }, "node_modules/@oxfmt/binding-linux-riscv64-musl": { - "version": "0.63.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.63.0.tgz", - "integrity": "sha512-9Tcg0y0WcVa6Mm9AgcgFMseDS+VkFJZpKZ8We9SpDY4gg5jewSwln+0sO04QLcTS1BtfDl9MwR+NfID8L7PUTg==", + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.64.0.tgz", + "integrity": "sha512-/GZ358wnQ/Ez4UVnCcZIi56JkY0sOdZ+B108pqXKqZz3jLS59F4KEAB1Qv3fRlObrFEk+3L2vUQ/xoPx+3vjXw==", "cpu": [ "riscv64" ], @@ -2569,9 +2599,9 @@ } }, "node_modules/@oxfmt/binding-linux-s390x-gnu": { - "version": "0.63.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.63.0.tgz", - "integrity": "sha512-qWKC1pEOpx1qYhXaugPhHUeXwSfqEOk2wJH2LqVXGPV5iQYfdAZdt+d2XDiX4DTSWA2QDMUcFB+wEORh3Xn/sA==", + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.64.0.tgz", + "integrity": "sha512-/C9We3DXegowfLXtVCYHeNiU9azwCDr5cQkEtCVlc74vyn+lLQSPApJ1CZmxAduqeq/Oi3gQ+IVptyhCaTMtkQ==", "cpu": [ "s390x" ], @@ -2589,9 +2619,9 @@ } }, "node_modules/@oxfmt/binding-linux-x64-gnu": { - "version": "0.63.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.63.0.tgz", - "integrity": "sha512-S9wXYOiGSqYGS4Fx/TFsY+xDd/7dE5s+rUgbA4TsHiVF9e8J3ZcKmP7dsP/7iqLI9Wz7Ic7TzEr3mdthRCTdrA==", + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.64.0.tgz", + "integrity": "sha512-91KM2CeRWscIEHlj1NsW2WSnzGeq1Ehq+39bfDowTdkn+fcvK/x4Y1RcyqT7glyBjZio0ldkeCG6Usj3v7ASog==", "cpu": [ "x64" ], @@ -2609,9 +2639,9 @@ } }, "node_modules/@oxfmt/binding-linux-x64-musl": { - "version": "0.63.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.63.0.tgz", - "integrity": "sha512-5eGyTJuMZNwBSHCivXt8Yuta6GeTYksOPXRk2MIhajiyFGQx7bjaHIwY+ZusAoFHhT157A9x6sktLjYo9D5oMQ==", + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.64.0.tgz", + "integrity": "sha512-gw7uEk9I+7zoT1EYLra1eWArIzNcz8e3jkv+Noo2+o2T7wPvsNSQbfoa4DSfZlvn1i6mJ05RiZ4/omaXPDNhQg==", "cpu": [ "x64" ], @@ -2629,9 +2659,9 @@ } }, "node_modules/@oxfmt/binding-openharmony-arm64": { - "version": "0.63.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.63.0.tgz", - "integrity": "sha512-Rz7hx+Dv3DoW/S6pwVAyjfFXp7/trdQ1zg+vNmsdsdDNlUccugp4XNqambSuEAeP0DaG9k72AtNyfDXCEg0AGw==", + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.64.0.tgz", + "integrity": "sha512-HYHFf616FHSPSO07c09mjmXBfQ73wIVM3m0txOiooa5XZkGoxFd6B14PVj0LB0DXIqJ6wAO/dDR/NX/5UUaqnw==", "cpu": [ "arm64" ], @@ -2646,9 +2676,9 @@ } }, "node_modules/@oxfmt/binding-win32-arm64-msvc": { - "version": "0.63.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.63.0.tgz", - "integrity": "sha512-T/IuizKN9mr4Xw6YYnptkXRNdLkyIlUZ7c8zfTOBpoytZyJ1BAsMUvsMDEx0X4YvSMpaivm+DR8112rQfzC25g==", + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.64.0.tgz", + "integrity": "sha512-uQjFp081IZSWD6VAofX2iO2z01awAdHmfC+NrieWIPKrT2hZKQDyq/U18M7ifC0sm0Wz8aHY/p6+FDYIzs/CrQ==", "cpu": [ "arm64" ], @@ -2663,9 +2693,9 @@ } }, "node_modules/@oxfmt/binding-win32-ia32-msvc": { - "version": "0.63.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.63.0.tgz", - "integrity": "sha512-XjrO5FJ5Wl9vsAxtCP1G/eaeT6y1K2s9CICUHGE42cEjou32/J6S+B1KnrOAboj6E7uhJnwPbRSvznWcxNdA0g==", + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.64.0.tgz", + "integrity": "sha512-lNM6byTAQ881jugzFu8juJTbNRgsUTlswMA6pJmwi1XDvmIqnnb49lcUAs5gz94fCJLrVN+/X3s3jOKqx23WIQ==", "cpu": [ "ia32" ], @@ -2680,9 +2710,9 @@ } }, "node_modules/@oxfmt/binding-win32-x64-msvc": { - "version": "0.63.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.63.0.tgz", - "integrity": "sha512-sgsHCQy432OTQH4Ikk3tZptp3GqwnhwUDuY0loBH41zyHWfMZY9v8Dy78wsnSofHejvFozZGgJgBB1A0LQRwMQ==", + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.64.0.tgz", + "integrity": "sha512-BtmbtL/QjMtF1a6C3CqoDluH2IfB6fJt62E+B9RFfUPtFk4Iz9PFS6+y/SzzOvSxc7aUk2Kphwg7Dh8lMbwu6g==", "cpu": [ "x64" ], @@ -2781,9 +2811,9 @@ ] }, "node_modules/@oxlint/binding-android-arm-eabi": { - "version": "1.78.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.78.0.tgz", - "integrity": "sha512-Bu819lmAfZMUHErrpe0cEWj3iaefuUODHSU8+UbXy67V/r7/7f4K3FL0NmbD85E+wiFLDYuhP8Zlv0XnVeXshw==", + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.79.0.tgz", + "integrity": "sha512-TebFaaMklO/RXzTv7PucaCq9l3X6D1gA+C8H6K4njtjFOV+zWE9MKLpulcJZN9bzytbUbQIY0mZuz12nQ5Kv4Q==", "cpu": [ "arm" ], @@ -2798,9 +2828,9 @@ } }, "node_modules/@oxlint/binding-android-arm64": { - "version": "1.78.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.78.0.tgz", - "integrity": "sha512-CDfxZgB61B7buRdY2FJoAYYPPXCZ1EoC1LKscnC5dg3kjobdxiconvAvvN1BmHyW4PyFT3jRLDag/BY/roSNBQ==", + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.79.0.tgz", + "integrity": "sha512-KqqnOtAVgNsPPF0YSodkFZA1O80jcKoCZCTu3bgsszxA+MrMP9TLzfXitKjEj1FmrPprKDMdRDMmY3weESO9sg==", "cpu": [ "arm64" ], @@ -2815,9 +2845,9 @@ } }, "node_modules/@oxlint/binding-darwin-arm64": { - "version": "1.78.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.78.0.tgz", - "integrity": "sha512-2Y2U9Ahrz+OO0Ej88f9SJYq51/jUBp1Mc7iZu0ukrbeeZ3gpRGfzIFnoqfHDY96xr0GEfNrPUBFEy0nN5aD7HA==", + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.79.0.tgz", + "integrity": "sha512-BVC2nsMzqQzRDPc5RhixkZ+m1p7iH4bxRRvqkbwDXX0PlQKm1BPy8J8cRjnAFafOq2QzI+BfO3vE8w2GZ3CBag==", "cpu": [ "arm64" ], @@ -2832,9 +2862,9 @@ } }, "node_modules/@oxlint/binding-darwin-x64": { - "version": "1.78.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.78.0.tgz", - "integrity": "sha512-rpych6eJq6m9jDRypTEaPD1xysaEW5h9+xuxhGK/QhOg+/xaqPZrCrTNoIl/f3nEjuJeCEmstNDlrE9rJi/3/g==", + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.79.0.tgz", + "integrity": "sha512-p6Lm+snmhGuLKL1+CpCV8L6ijkE/qJzK2H2jG9+eKJT0n31RbY4FLsdhexekgP3bLpw4Kgde+9DZuDZQ4yIInA==", "cpu": [ "x64" ], @@ -2849,9 +2879,9 @@ } }, "node_modules/@oxlint/binding-freebsd-x64": { - "version": "1.78.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.78.0.tgz", - "integrity": "sha512-IcMGrQT3QizkOESUJd5et+rOhVqSkNDfNik1cvrKDqIbzqx9KMtRswpFgkCuNTSwylCFLKhGUu8KmqY1ZnC0Dg==", + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.79.0.tgz", + "integrity": "sha512-qDMm0dXZnoHyRqSL4N4xUq82T4sqK5cbKSjvd/dF/YbMUXc2R1wEPf+vmA5S0qUmi0nwXfNbjXBtZaIqzQLIMg==", "cpu": [ "x64" ], @@ -2866,9 +2896,9 @@ } }, "node_modules/@oxlint/binding-linux-arm-gnueabihf": { - "version": "1.78.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.78.0.tgz", - "integrity": "sha512-/uLdoJ0IXE6vo/0f0LKjinQAp+re+VMaCWaNT8ENIv2EOCkSsc8SGaflXAuW0Jua2dq5+GLVWm1NQK7P3UFSNQ==", + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.79.0.tgz", + "integrity": "sha512-2od7s0nuKPzqyUZAWk9KkCyGg7eI9dwFPZg+20lB15fKFkVZ0c9ZFxqPfiBAyDTlTkh9stPI0t+JlPCqMbItVA==", "cpu": [ "arm" ], @@ -2883,9 +2913,9 @@ } }, "node_modules/@oxlint/binding-linux-arm-musleabihf": { - "version": "1.78.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.78.0.tgz", - "integrity": "sha512-7xi4Wb/O8NRJhLoUXmDJMUVpNYvB5kefdhFU1Jb8rtae4QoXlTiLwI14X4YvAXVZLNZChP8m5qO9SQAlWQTbkQ==", + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.79.0.tgz", + "integrity": "sha512-ZOQUjkzDnvlhSE3+tWC3YXx94MMl+sYMlwH+u1+YGApGHOJP/YAc8ZBRFOXZ6eOBmxtXAWuS/fBcdZr8qqNO1A==", "cpu": [ "arm" ], @@ -2900,9 +2930,9 @@ } }, "node_modules/@oxlint/binding-linux-arm64-gnu": { - "version": "1.78.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.78.0.tgz", - "integrity": "sha512-4hFW0+fVXa3OIh1Y4A5SPkmvI4wuuBSrCVKzOyE7PTjhc7yEqZ1pmvEEeS5Lj/MaqvegFxXyF33N+6jkehxdyg==", + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.79.0.tgz", + "integrity": "sha512-lu158FR4nGqGeRS3BQvtG85wRgU/Fy4MD5Cxp1hzJXizGiLo6u2742wJSCDKh8cFcZntvX7fcxlq4mMmfryH1g==", "cpu": [ "arm64" ], @@ -2920,9 +2950,9 @@ } }, "node_modules/@oxlint/binding-linux-arm64-musl": { - "version": "1.78.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.78.0.tgz", - "integrity": "sha512-oC0mvsgBJjlMijSDEhx9KuvR9zYeHXceA9MjbuXB1F8NSR78Yj2unOBrstEvTVaq+pko+kuue6DajC00eqvTdg==", + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.79.0.tgz", + "integrity": "sha512-mbpKQeE2aflTjddaHK7MP8KP/OFbUM++lt5M635ENM8IyIdK0jm2t9pb+2v9mVVIvhF6TqA4l7F79Pll1mi+uw==", "cpu": [ "arm64" ], @@ -2940,9 +2970,9 @@ } }, "node_modules/@oxlint/binding-linux-ppc64-gnu": { - "version": "1.78.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.78.0.tgz", - "integrity": "sha512-XAllT5SUZS+ohjuZ3/5S0cwe0r7eboiuigeStCZ5DXRYx/2KVM2UvQXvAfyzXEimtQjAB7cDQ2YxDe2Zl2WNQQ==", + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.79.0.tgz", + "integrity": "sha512-WpGNua7gaxaHnpSDeog2ji8IDHn/QLPl9LPzwkR/FvVv58vT5BcXjRXnU+wbu3N75cpeha8CdC7ho/U2OIsB4g==", "cpu": [ "ppc64" ], @@ -2960,9 +2990,9 @@ } }, "node_modules/@oxlint/binding-linux-riscv64-gnu": { - "version": "1.78.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.78.0.tgz", - "integrity": "sha512-trucMER/0QtecoXvc1y/UVqE3kwJipDwrx4oHfj+nNm3dq2zjP44WT0CfHNDPM3G1DXIkx/gY6lAD21NSCZVhA==", + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.79.0.tgz", + "integrity": "sha512-tK1E93A5LVzISg4ngpKJnfTs7EqtIUceGI7MQ4GyDjJiLi8wPCkEyKlj2xkyKWZ1yzkDJyLHTBJ5/iFWRdnJvg==", "cpu": [ "riscv64" ], @@ -2980,9 +3010,9 @@ } }, "node_modules/@oxlint/binding-linux-riscv64-musl": { - "version": "1.78.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.78.0.tgz", - "integrity": "sha512-cm3O4F/HQbdzOUX5mKHqG5KDL6E5w0pnlZ+fbBy2rmLryPOowkuLagFHTopQsEIpjcaZoPOrL+BmmAytAG9HFg==", + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.79.0.tgz", + "integrity": "sha512-qhQvUIrngXivA2A9pQ+xPCychztn/5qUv7yS3gDwXv3w7Rag+eTeeXWmRyx+t7XsW5x6LuY/8AsTq36UgFIblg==", "cpu": [ "riscv64" ], @@ -3000,9 +3030,9 @@ } }, "node_modules/@oxlint/binding-linux-s390x-gnu": { - "version": "1.78.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.78.0.tgz", - "integrity": "sha512-33wRf6HqGNsybJ3qX4cGaQN2ODPxNmc1rMa0mrTmx3eFq1VzOnvQooi9bIGVYakW8a/wmqVx1mgsUm8R2xfTiw==", + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.79.0.tgz", + "integrity": "sha512-sv6AaVgU/eE6u+6WFiQVDcPPwTxP6IJMSB9k701W2r/r6Tx465e8vPvVyRxquNH4Vy6KwRNu90mVbxXJN8+5gg==", "cpu": [ "s390x" ], @@ -3020,9 +3050,9 @@ } }, "node_modules/@oxlint/binding-linux-x64-gnu": { - "version": "1.78.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.78.0.tgz", - "integrity": "sha512-rRdISSYegj6VganMZ9tjRjijowfHJ09IZU01i0toBAqr6n5LEtwHq2IeS4FjW2RoskOHlb6efB26H5izYb3GEQ==", + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.79.0.tgz", + "integrity": "sha512-iFZL02deziHslb3jEX9KdqlAkYoo4fGyotchKDzdfK1f5mxlIBeiQeHhvK3iFpuEJSB4ma/qeFn9oxPiwnhUPQ==", "cpu": [ "x64" ], @@ -3040,9 +3070,9 @@ } }, "node_modules/@oxlint/binding-linux-x64-musl": { - "version": "1.78.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.78.0.tgz", - "integrity": "sha512-GmsP4rW0xTL6u5CVdcDsaN5Fbc7hBc382Wmar1kttbnwSEviM+rSINKOMQ+UQ6iH+AGwC+8gaAiwu134Tgh6Lg==", + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.79.0.tgz", + "integrity": "sha512-3DtZR2raqObnh7wXZoFYFd0Fw7skBvcb3f7A+/lkEiDuh8hrE6vv9b/62Qxao1a9/OeHLw/FcXlXzgsW9wTRFg==", "cpu": [ "x64" ], @@ -3060,9 +3090,9 @@ } }, "node_modules/@oxlint/binding-openharmony-arm64": { - "version": "1.78.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.78.0.tgz", - "integrity": "sha512-sy9yeYuADc8a+n4TLBayzMCZiHPW78DcIFVpOXTmdKHWQeM9xe5uzkqIIZmi326D5hY9XVwacipEB1p7tQjPAg==", + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.79.0.tgz", + "integrity": "sha512-Oatt4GuA1WJkqzk2ozx4HrWROOi7opV3AKDw/U8qDIqeTqzsjn5K2x3REJMNjU3/KU/Bkq96Zi3CknaiDTaC/Q==", "cpu": [ "arm64" ], @@ -3077,9 +3107,9 @@ } }, "node_modules/@oxlint/binding-win32-arm64-msvc": { - "version": "1.78.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.78.0.tgz", - "integrity": "sha512-rjc2hF1KfMi8fZj1X/m3AmnHbdsF3rL0v6KQg0Uc880Yb2khjz+3U14sfdZ7jWTpRnN1m1NQa/TT7uU9lJWPrA==", + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.79.0.tgz", + "integrity": "sha512-NAgZr9Qp8nIA9rpo0JEvwiabTF/2UVqBNnupBG9X4kxXcQoScJUTi+qHhvabb9s/thgj5wQ4XcIaJvb+ZMgoKw==", "cpu": [ "arm64" ], @@ -3094,9 +3124,9 @@ } }, "node_modules/@oxlint/binding-win32-ia32-msvc": { - "version": "1.78.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.78.0.tgz", - "integrity": "sha512-zcuXFVrEFHIafRfkCQT8w/Xe41o07ozl/vwHq7p94vB29xVzsB0sZGYORU1jhcYKv3Lr0J3HbJ2T4fHH5rWmvA==", + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.79.0.tgz", + "integrity": "sha512-+KyXjIvcpaXmWW/j9NNY5yWjrIVxaX18VyIheQy3jwc2GSYgpCr7MGI/HxIGQ/shAL5IWEKbhsqoMpAO5Stiog==", "cpu": [ "ia32" ], @@ -3111,9 +3141,9 @@ } }, "node_modules/@oxlint/binding-win32-x64-msvc": { - "version": "1.78.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.78.0.tgz", - "integrity": "sha512-Sb5ocmLSuYeOuXd+CFOToGKp/gjXUEWDnvIGwhnh8aq8wY4TMmEnKnvbogSW7RdMZv77JSARduS7/gv+khYEjA==", + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.79.0.tgz", + "integrity": "sha512-mEelcCMMBS57sIXh2veGMNy+pQwuGtcMxHxGIZWQ5Ba9pJ5jCCUFOZB9E2JhBaxGsURe+WGe0zJp4RVre52gpQ==", "cpu": [ "x64" ], @@ -3855,10 +3885,27 @@ "integrity": "sha512-sL6cvO2IfkSu/iU+zs2S/w01B7A8V7suXSIKEN4hPFFdZoiPGxrj5pAG0lCaqLWiEIrjKzdznIWuaLcxPR53qw==", "license": "MIT" }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.5.tgz", + "integrity": "sha512-DLe/i+l8ynIBY7XEQ191TeZvCoowIGa18R+dIV30GW7DiOtp74i/xX8hs8GUjW5ARV7VZuie3d6AumSmCwbeRA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.4.tgz", - "integrity": "sha512-jHC2cnyKz5xU2fhECtFl8OZ83cYNt13GZQD+0uMJ/X3o+ijmd56okHhTUwxVSHPx1IRVIJEZ1/1pPzeLCU6XKA==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.5.tgz", + "integrity": "sha512-zXcwKlQApYAOELHd8PwKDFkagYF9Wy4e0RJ+0qnzl9Pjnpj75TEG8ufv40p2J7kCEfwZAsNiuzRIyNNMWT38ig==", "cpu": [ "arm64" ], @@ -3873,9 +3920,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-Dc5mPD8F5F/FS8i01syd7FTF6yB2fVthH/TRkjwJkzUK6EpoxHtqvZQP5Zwq80/5z19TWYHIg1KOHboCgVx/aQ==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.5.tgz", + "integrity": "sha512-dK4QakI42nzWgJT5sm4y4y/O//D4OxM75/cH28RLV+nzIN9AY+YsbuUVrUTjlLjXR6vpyxFbSsbmNuJ6BP9sww==", "cpu": [ "arm64" ], @@ -3889,27 +3936,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.4.tgz", - "integrity": "sha512-fpDm4oBo6SqLvWUYCmFhdde3U9KH2fRNNMeAnAPAIwxRL345xutL0EtEUcuoxsoazdJGv/MuDBQHlCDrtbvqOg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.4.tgz", - "integrity": "sha512-rSJoreDE/HoIzoaib6MTp5jQtCTdMHKIvItAKT/ImS6Y6Ww76oUaeMyp4Vc/fAgd/ehji068IxetHXAnqUwN9A==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.5.tgz", + "integrity": "sha512-/vCnNxlkxs9tKxNDcyWUePpJ/PgTzxIaVhoM5SmG8UV+GR/IcPam4VYxi7GIMo7PSDuNqlJqvprqii9NqqVCMw==", "cpu": [ "x64" ], @@ -3924,9 +3954,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.4.tgz", - "integrity": "sha512-/jm8OGHgn7oGaJu3i/qZI9spUGcJ+y/lk43ttQ/iO1tOd9NissG6o97bighBCiL+BKRngmcDuR6ikfwYdJmVuQ==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.5.tgz", + "integrity": "sha512-abk0NLA519LxRCszmbE0jYKuQ9YPocOXTiOXOo6Yr+YAT95VH+PtqYAjOJvGKt3viEd/x4qzabAlwd5bHOOARg==", "cpu": [ "arm" ], @@ -3940,30 +3970,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.4.tgz", - "integrity": "sha512-tIP06BeD9EqvECBrPZ+sqdPlYrT+aYaAiu1wYziVx5elRK/ftm33JxVDy2bXGbr6J0CrtirCkR87/X5a2euEng==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.4.tgz", - "integrity": "sha512-Ql1Q0EQqVThvn9VAVlwNzsUvbSFtCMGjLpRRi4pk5i7NZZ4n5ISiLMjHYtus4VQ2PvkSw24zyaCVsiS+sXPj1w==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.5.tgz", + "integrity": "sha512-xMvZgnbZg4YVnR/AX2b3oOPDTFYJvUVaJg5FedA/LuvexAtXibZQej4cnTkw3rjsJ/ggUROB64TdtETiim+FYA==", "cpu": [ "arm64" ], @@ -3981,9 +3991,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.4.tgz", - "integrity": "sha512-GjbjXD4XXfN19D0LZNbmiCBUoDiRACsYHr0yaIbbn8aFsXjHZifcYqu/W5Er5X2X990WjHXFrxarn5chzItorQ==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.5.tgz", + "integrity": "sha512-GRjeqTUDHTo5GwntsLaAMcBahG3nlpjftXWZLN73HiYQlhwEowvarFgQnRnQZtIp4keXX7quXFbG38uPZBa2EA==", "cpu": [ "ppc64" ], @@ -4001,9 +4011,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.4.tgz", - "integrity": "sha512-p5WR0NOwaRmJ/B1b6IjEFLLivwEsf3PrdBIhRbhTCQisbo2SvHHpG4ELB/+FgQNnB88LTOF86upmJmbvZdQ2lw==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.5.tgz", + "integrity": "sha512-vLNTR45F2Uwc8AufkNXPmB4VliaXs+FvcheEogIzOXzO4l+LzieXF5A/TWxLy5HtqpsRCHUfd0lPVrrdgXdLHQ==", "cpu": [ "s390x" ], @@ -4021,9 +4031,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.4.tgz", - "integrity": "sha512-4/GyVjmhR+Tc6HLJvwc1sOhPqAZtySiSMesOZyX6JQ5XBxoTDEMKQzvo07NIK6nTon/SivlZqvhzvuVBNQhObQ==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.5.tgz", + "integrity": "sha512-Mgj59/HTuYeK9Gz2MA+mBWKnHsAgkBSec15ZMb1st3oIfFbX7gCjOae7GydHhzcyQi9Z/7M1QuN9bR3oFqF0jQ==", "cpu": [ "x64" ], @@ -4041,9 +4051,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.4.tgz", - "integrity": "sha512-l9eeLsCNvPpmSXUej0etw/J1eqV0Jj1D5G/xG6YTijmE6dkv6E2QezgWbTfQk63v952DPqrjOCoiqxq7Bw0YUQ==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.5.tgz", + "integrity": "sha512-mY8AP0/ichsbhAxGnLa3d3+MwV0EfgrPND2bplI3Ym8T6R2pJ0N87bvrKVwNXmdy3jnr6eQBecdqx/HMknBmpA==", "cpu": [ "x64" ], @@ -4061,9 +4071,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.4.tgz", - "integrity": "sha512-e0F355MSTMm3+UOqtV3L24gFUp2N5m1f8L/7d56deik6va+AXdrt9F8LbzGpeWGWRbZEDq4m8NVnJDeBtf9DZg==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.5.tgz", + "integrity": "sha512-8SLssA2oweAxyRgDp789ACfRb/3P+zNRJpzZxSizxF9m8NUDQ4+3xjo8ttjhVGGw6Qxb70oZiEtIjaKikCO7Yw==", "cpu": [ "arm64" ], @@ -4078,9 +4088,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.4.tgz", - "integrity": "sha512-AWLi0uBRYh6QlE7OKhiz+phZC0qwtij2QZmhmOdsLdFn64m7oMpooE9ICE3lhm9xMb4SpDo2WbHcxX1iFLFtqw==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.5.tgz", + "integrity": "sha512-vGbruD5zquhoc8D9SViXgN2FBJtNdTyQ4DtG+SWiEGlJiAzoKcZ2xp+xuXCffhubVdt0NJlTZqkeRuERy7g8Cw==", "cpu": [ "arm64" ], @@ -4095,9 +4105,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.4.tgz", - "integrity": "sha512-UwSDJOg3dqCAejWdxclJjCsh3Qq4vLYMDxmyHqo1btz3stK2VqgwNd3mm5tuIwzSlGIQ/1H9Hr+Zn09mrezNqQ==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.5.tgz", + "integrity": "sha512-e/SXpgISz+IoqVcSSI0rx/d/he8zqLex+/rCWpnHpmVfmPIUjag9H6P7zotf0gJHwPUhQxZ/mF8tr6acebT9yw==", "cpu": [ "x64" ], @@ -4162,9 +4172,9 @@ "license": "MIT" }, "node_modules/@storybook/addon-a11y": { - "version": "10.5.7", - "resolved": "https://registry.npmjs.org/@storybook/addon-a11y/-/addon-a11y-10.5.7.tgz", - "integrity": "sha512-I30rsNz6aA3xg3811MEry40uJDHP3l5SOkfqtmNkp7y4NdqTDdKhmbhhDAZQX1WWEk6GeMBGr3AJ3TCz7r7JmQ==", + "version": "10.5.9", + "resolved": "https://registry.npmjs.org/@storybook/addon-a11y/-/addon-a11y-10.5.9.tgz", + "integrity": "sha512-MvXkoJIVRcZrqhWi+wynYAmFkvTWm5aHdJfYohhgPeEJVDRF91zejtOsN7zPRE/lQvBDY+IiomKxeRN3UhWNVw==", "dev": true, "license": "MIT", "dependencies": { @@ -4176,20 +4186,20 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^10.5.7" + "storybook": "^10.5.9" } }, "node_modules/@storybook/addon-docs": { - "version": "10.5.7", - "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.5.7.tgz", - "integrity": "sha512-KNARJfjICaizinsR3INMEiipZm1ObYo+xw+E26gteu50Bcy2dIZUtk5uHY5XdtardU3AXX6yRXoBZ2HCY3lbHA==", + "version": "10.5.9", + "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.5.9.tgz", + "integrity": "sha512-8sFsMkZYrrdqCLdV+hnwTwDF7RaBsBPRwl4wfc8ve9Q/7Yhi5REe/Xjvd8x1yn6fBPPw9tnID9dx6Agdnr81fw==", "dev": true, "license": "MIT", "dependencies": { "@mdx-js/react": "^3.0.0", - "@storybook/csf-plugin": "10.5.7", + "@storybook/csf-plugin": "10.5.9", "@storybook/icons": "^2.0.2", - "@storybook/react-dom-shim": "10.5.7", + "@storybook/react-dom-shim": "10.5.9", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "ts-dedent": "^2.0.0" @@ -4200,7 +4210,7 @@ }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "storybook": "^10.5.7" + "storybook": "^10.5.9" }, "peerDependenciesMeta": { "@types/react": { @@ -4209,9 +4219,9 @@ } }, "node_modules/@storybook/addon-vitest": { - "version": "10.5.7", - "resolved": "https://registry.npmjs.org/@storybook/addon-vitest/-/addon-vitest-10.5.7.tgz", - "integrity": "sha512-7NK7Kzazc2vb2h8nGlH15QlUn5J6/LV0xF70VziAK0bxu3r1JupLCoBvckX39vHzFXiAZljXZtt1Wq/OidBxow==", + "version": "10.5.9", + "resolved": "https://registry.npmjs.org/@storybook/addon-vitest/-/addon-vitest-10.5.9.tgz", + "integrity": "sha512-TgHczbDANprH9bEj+Z/DZ4b2LaCHpNrBdMOWK7gd+ZRGZz0ZZnGm8H5WoIh2uW4AHYibZmYCyWDSf69Plblz2w==", "dev": true, "license": "MIT", "dependencies": { @@ -4226,7 +4236,7 @@ "@vitest/browser": "^3.0.0 || ^4.0.0", "@vitest/browser-playwright": "^4.0.0", "@vitest/runner": "^3.0.0 || ^4.0.0", - "storybook": "^10.5.7", + "storybook": "^10.5.9", "vitest": "^3.0.0 || ^4.0.0" }, "peerDependenciesMeta": { @@ -4245,13 +4255,13 @@ } }, "node_modules/@storybook/builder-vite": { - "version": "10.5.7", - "resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-10.5.7.tgz", - "integrity": "sha512-fShF/aQaITqcJuMCLr42BGNUAbhDi4IboqvlbZqXAwgrrTslnZEUnY8GcEcvpZmjl11VwlmazhMJdH50fIgBPg==", + "version": "10.5.9", + "resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-10.5.9.tgz", + "integrity": "sha512-Zg4JbGQiHFPGlFJ9HM+XPgzKmU/RFPCymhohVRJhBBYfmgaQgz0flWWzscseCDpl638MNd8/r/H+nwuoBgSYDg==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/csf-plugin": "10.5.7", + "@storybook/csf-plugin": "10.5.9", "ts-dedent": "^2.0.0" }, "funding": { @@ -4259,14 +4269,14 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^10.5.7", + "storybook": "^10.5.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "node_modules/@storybook/csf-plugin": { - "version": "10.5.7", - "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.5.7.tgz", - "integrity": "sha512-IaX8FlM0H36HNFhJ2+4L9bCldqfvHGqcLg841SJNyK/DhfMlM7JsvY/GDH2ZFuWrUf8FSOx96GRRnHq6XfRKag==", + "version": "10.5.9", + "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.5.9.tgz", + "integrity": "sha512-4H5QIHQVtQYCuL43GCRLGjNQhZpQg9gL03ja0DV80kO2Dn9LEt6ol87bSnSjn4VDgcAXtgTzXFvRLknfVgAAqg==", "dev": true, "license": "MIT", "dependencies": { @@ -4279,7 +4289,7 @@ "peerDependencies": { "esbuild": "*", "rollup": "*", - "storybook": "^10.5.7", + "storybook": "^10.5.9", "vite": "*", "webpack": "*" }, @@ -4315,105 +4325,15 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/@storybook/react-dom-shim": { - "version": "10.5.7", - "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.5.7.tgz", - "integrity": "sha512-lxOkyh+wu/MiBXvYQHjZfD+DRKOa4bHBzbuGuiHXnHXmdOcTRdcrQTsoeN2FPtfugmmOG66cZUEgDwNX+k5eRA==", - "dev": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "storybook": "^10.5.7" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@storybook/react-vite": { - "version": "10.5.7", - "resolved": "https://registry.npmjs.org/@storybook/react-vite/-/react-vite-10.5.7.tgz", - "integrity": "sha512-eEo3eVa2pvqrzQukKxAzx7YvswDAA1s6k/y+tdMxmRvWyHX6QEOsb9Tda6wcVaa7c8BeJM7Ggq+289cRMTH6Iw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@joshwooding/vite-plugin-react-docgen-typescript": "^0.7.0", - "@rollup/pluginutils": "^5.0.2", - "@storybook/builder-vite": "10.5.7", - "@storybook/react": "10.5.7", - "empathic": "^2.0.0", - "magic-string": "^0.30.0", - "react-docgen": "^8.0.2", - "resolve": "^1.22.8", - "tsconfig-paths": "^4.2.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "storybook": "^10.5.7", - "typescript": ">= 4.9.x", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@storybook/react-vite/node_modules/@joshwooding/vite-plugin-react-docgen-typescript": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@joshwooding/vite-plugin-react-docgen-typescript/-/vite-plugin-react-docgen-typescript-0.7.0.tgz", - "integrity": "sha512-qvsTEwEFefhdirGOPnu9Wp6ChfIwy2dBCRuETU3uE+4cC+PFoxMSiiEhxk4lOluA34eARHA0OxqsEUYDqRMgeQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "glob": "^13.0.1", - "react-docgen-typescript": "^2.2.2" - }, - "peerDependencies": { - "typescript": ">= 4.3.x", - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@storybook/react-vite/node_modules/@joshwooding/vite-plugin-react-docgen-typescript/node_modules/react-docgen-typescript": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/react-docgen-typescript/-/react-docgen-typescript-2.4.0.tgz", - "integrity": "sha512-ZtAp5XTO5HRzQctjPU0ybY0RRCQO19X/8fxn3w7y2VVTUbGHDKULPTL4ky3vB05euSgG5NpALhEhDPvQ56wvXg==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "typescript": ">= 4.3.x" - } - }, - "node_modules/@storybook/react-vite/node_modules/@storybook/react": { - "version": "10.5.7", - "resolved": "https://registry.npmjs.org/@storybook/react/-/react-10.5.7.tgz", - "integrity": "sha512-uFvty2MMdFXzW5PcQe1JqDAZkz6cQq7q/9G/cbGVnBEvP6zsOVeL+bmrQ0/WBlFQN0Ko9+ZoCTvaQ9s65zBa5g==", + "node_modules/@storybook/react": { + "version": "10.5.9", + "resolved": "https://registry.npmjs.org/@storybook/react/-/react-10.5.9.tgz", + "integrity": "sha512-kApGOuNT26NkpioTsr1iT/Q2c44tA7OIsNUSyFqtT7W8k3fRn/jQWfrDegYxty0WG0wxdNMZq2ndRfOOF8HaHw==", "dev": true, "license": "MIT", "dependencies": { "@storybook/global": "^5.0.0", - "@storybook/react-dom-shim": "10.5.7", + "@storybook/react-dom-shim": "10.5.9", "react-docgen": "^8.0.2", "react-docgen-typescript": "^2.2.2" }, @@ -4426,7 +4346,7 @@ "@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "storybook": "^10.5.7", + "storybook": "^10.5.9", "typescript": ">= 4.9.x" }, "peerDependenciesMeta": { @@ -4441,14 +4361,64 @@ } } }, - "node_modules/@storybook/react-vite/node_modules/@storybook/react/node_modules/react-docgen-typescript": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/react-docgen-typescript/-/react-docgen-typescript-2.4.0.tgz", - "integrity": "sha512-ZtAp5XTO5HRzQctjPU0ybY0RRCQO19X/8fxn3w7y2VVTUbGHDKULPTL4ky3vB05euSgG5NpALhEhDPvQ56wvXg==", + "node_modules/@storybook/react-dom-shim": { + "version": "10.5.9", + "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.5.9.tgz", + "integrity": "sha512-7qZD6CSa64p1m/zX9tG4ALcEHlEk0Bx+5++4RL3MFlRCgCFfAomXsCHTzF0RyF8cI4vl+hS7Y0ryjQchVs6UQA==", "dev": true, "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, "peerDependencies": { - "typescript": ">= 4.3.x" + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "storybook": "^10.5.9" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@storybook/react-vite": { + "version": "10.5.9", + "resolved": "https://registry.npmjs.org/@storybook/react-vite/-/react-vite-10.5.9.tgz", + "integrity": "sha512-mDI+UssrOEP8IwgBsefLG+gqW1gZbZEPbpH5PoHK/P1WPi8BfrLEqrFi9H9wmMkal/Vh8SmL1awgOi0ir3XwcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@joshwooding/vite-plugin-react-docgen-typescript": "^0.7.0", + "@rollup/pluginutils": "^5.0.2", + "@storybook/builder-vite": "10.5.9", + "@storybook/react": "10.5.9", + "empathic": "^2.0.0", + "magic-string": "^0.30.0", + "react-docgen": "^8.0.2", + "resolve": "^1.22.8", + "tsconfig-paths": "^4.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "storybook": "^10.5.9", + "typescript": ">= 4.9.x", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/@swagger-api/apidom-ast": { @@ -5289,9 +5259,9 @@ } }, "node_modules/@testing-library/user-event": { - "version": "14.6.4", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.4.tgz", - "integrity": "sha512-QCGwP6QrjypBLwyj5cuyfVamkaIEy/XGY+1VDehbtbQqOggYmTFpFOdWR5mPz14vX8vXLMVjDHlRNBcClyO9ew==", + "version": "14.6.5", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.5.tgz", + "integrity": "sha512-FhqjldLTpteueBaKflhNFlMT3+PM0O5fiBUivht6b9CZ1eesJyy7+g3Jr7XwJzt/Hip3ZG5hWwK1MX1FuDiE4w==", "dev": true, "license": "MIT", "engines": { @@ -5858,15 +5828,15 @@ } }, "node_modules/@vitest/browser": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.1.10.tgz", - "integrity": "sha512-UDwuWGwXj646CBx/bQHOaJSX7np0I8JL/UKQYa1e4QrVHH8VdWtx8eaOuf8sy0ShwDgR6NjJAsp5eF6vjF6qng==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.1.11.tgz", + "integrity": "sha512-bwMovvAeuTFOK5kIFevw4VEf+1gVEICv4SYK4k3knJOxl6b1zEWud8mYKD73e1B0odAn174h1MofURy2TPWf3w==", "dev": true, "license": "MIT", "dependencies": { "@blazediff/core": "1.9.1", - "@vitest/mocker": "4.1.10", - "@vitest/utils": "4.1.10", + "@vitest/mocker": "4.1.11", + "@vitest/utils": "4.1.11", "magic-string": "^0.30.21", "pngjs": "^7.0.0", "sirv": "^3.0.2", @@ -5877,18 +5847,18 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": "4.1.10" + "vitest": "4.1.11" } }, "node_modules/@vitest/browser-playwright": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.1.10.tgz", - "integrity": "sha512-nMoXGEiRpT7m3W7NsbvrM2aKNwiNHZf+zEpUCvMteGjZFvfT96Q9fh7QyB98dvDWXiKvrLxA7bJ1mCOOv+JQPw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.1.11.tgz", + "integrity": "sha512-riLBxPqwnJ0lWs2DN2WeUfYeKLoAjbP2Xx8cLQdSddzMi20sksIa6K2mPz79DyMZKKVKH2ksOC2yJvtNcZg8cg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/browser": "4.1.10", - "@vitest/mocker": "4.1.10", + "@vitest/browser": "4.1.11", + "@vitest/mocker": "4.1.11", "tinyrainbow": "^3.1.0" }, "funding": { @@ -5896,7 +5866,7 @@ }, "peerDependencies": { "playwright": "*", - "vitest": "4.1.10" + "vitest": "4.1.11" }, "peerDependenciesMeta": { "playwright": { @@ -5905,14 +5875,14 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.10.tgz", - "integrity": "sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.11.tgz", + "integrity": "sha512-8MVGEFnJIcdGjcbfKmeq8z0pZHH0JlVtoVZH9Q/qwUp6wyFnEJUBMrw9DCaj+ra3vShGmhavjalMIhPNxZAUcw==", "dev": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^1.0.2", - "@vitest/utils": "4.1.10", + "@vitest/utils": "4.1.11", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", @@ -5926,8 +5896,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "4.1.10", - "vitest": "4.1.10" + "@vitest/browser": "4.1.11", + "vitest": "4.1.11" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -6004,13 +5974,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", - "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz", + "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.10", + "@vitest/spy": "4.1.11", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -6041,9 +6011,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", - "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", + "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==", "dev": true, "license": "MIT", "dependencies": { @@ -6054,13 +6024,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", - "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz", + "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.10", + "@vitest/utils": "4.1.11", "pathe": "^2.0.3" }, "funding": { @@ -6068,14 +6038,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", - "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz", + "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.10", - "@vitest/utils": "4.1.10", + "@vitest/pretty-format": "4.1.11", + "@vitest/utils": "4.1.11", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -6084,9 +6054,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", - "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz", + "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==", "dev": true, "license": "MIT", "funding": { @@ -6094,13 +6064,13 @@ } }, "node_modules/@vitest/utils": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", - "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz", + "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.10", + "@vitest/pretty-format": "4.1.11", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" }, @@ -6164,9 +6134,9 @@ } }, "node_modules/antd": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/antd/-/antd-6.6.0.tgz", - "integrity": "sha512-UDwWIbpmrCHB9ZQ+bPh4vQfB6DTI2ulIyoQ0Tc9xxalFblttiNGHl3ySBD9SyV/8+gUjFzfSx1+iU1Fog2i46w==", + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/antd/-/antd-6.6.1.tgz", + "integrity": "sha512-QHIHYoUk9N9nJy1T9fyxWKjY0qApdTEDd/6lzqYng8Uryv9FejNmbhKvYF7obGqB+TuLXQsPVF7fOVgyzM1KrQ==", "license": "MIT", "dependencies": { "@ant-design/colors": "^8.0.1", @@ -6200,16 +6170,16 @@ "@rc-component/rate": "~1.0.1", "@rc-component/resize-observer": "^1.1.2", "@rc-component/segmented": "~1.3.0", - "@rc-component/select": "~1.10.0", + "@rc-component/select": "~1.10.1", "@rc-component/slider": "~1.1.1", "@rc-component/steps": "~1.2.2", "@rc-component/switch": "~1.0.3", - "@rc-component/table": "~1.11.0", + "@rc-component/table": "~1.11.1", "@rc-component/tabs": "~1.12.0", "@rc-component/tooltip": "~1.5.0", "@rc-component/tour": "~2.4.0", "@rc-component/tree": "~1.4.0", - "@rc-component/tree-select": "~1.16.0", + "@rc-component/tree-select": "~1.16.1", "@rc-component/trigger": "^3.10.1", "@rc-component/upload": "~1.1.1", "@rc-component/util": "^1.12.0", @@ -6383,9 +6353,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.11.13", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.13.tgz", - "integrity": "sha512-k9HNuUVMlqVjQ9UHzfPjIqiDbWw7WqT1AoT7GL8VwvF3r0ZfArtgiSPAlmupyNquNgOJHTuH4CKYf8ttMTWBTQ==", + "version": "2.11.15", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.15.tgz", + "integrity": "sha512-FwMjJJ7HnyZpWe+oWxegG0fezZyBZUagI5LZEoO3GCbtbKNwRfMH9Ue5d5v01PNePBy1QSfPSDTTeVL0Hb9EzA==", "dev": true, "license": "Apache-2.0", "bin": { @@ -6831,9 +6801,9 @@ } }, "node_modules/dayjs": { - "version": "1.11.21", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", - "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", + "version": "1.11.23", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.23.tgz", + "integrity": "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==", "license": "MIT" }, "node_modules/debug": { @@ -6902,9 +6872,9 @@ } }, "node_modules/default-browser": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", - "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.1.tgz", + "integrity": "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==", "dev": true, "license": "MIT", "dependencies": { @@ -7011,9 +6981,9 @@ "license": "MIT" }, "node_modules/dompurify": { - "version": "3.4.13", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.13.tgz", - "integrity": "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==", + "version": "3.4.14", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.14.tgz", + "integrity": "sha512-dVoH9z+MY+C9IilgGCk3YfFqjLi3fChm2OiKJMzh6axrJ5qwxqWaZamgmHrpv22CN/KdbZJuGEGgfQoL00LTdg==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -7043,12 +7013,19 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.405", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.405.tgz", - "integrity": "sha512-bNglH7lPH5l+yHOes7Zr4VqxhOy4BQ9ZBUX4VdoFgxMpzJk7W1ZoO3Vgd9Pxa9PyjQ76sfm2aKH/nzEcCNRlew==", + "version": "1.5.411", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.411.tgz", + "integrity": "sha512-gglkxzokjHfawpGxq75XdBV2/l3BAPzrsMs70qgaZdTW5rpV1tC4MdgJVP9fN126bODA4ZJQkn1wryEzJyQXIg==", "dev": true, "license": "ISC" }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, "node_modules/empathic": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.1.tgz", @@ -7091,9 +7068,9 @@ } }, "node_modules/es-module-lexer": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", - "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", "dev": true, "license": "MIT" }, @@ -8003,16 +7980,6 @@ } } }, - "node_modules/jsdom/node_modules/lru-cache": { - "version": "11.5.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", - "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -8398,13 +8365,13 @@ } }, "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" } }, "node_modules/lz-string": { @@ -8455,19 +8422,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/make-dir/node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -8653,9 +8607,9 @@ } }, "node_modules/neotraverse": { - "version": "0.6.18", - "resolved": "https://registry.npmjs.org/neotraverse/-/neotraverse-0.6.18.tgz", - "integrity": "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/neotraverse/-/neotraverse-1.0.1.tgz", + "integrity": "sha512-WmmLty1YWwJl9yZi77v2dVIV6X2kuYV8YYBI/G3LWGKdGHmHUvL1z7FW0iDvEvGAwNEoc5x1tOOOyDnf5jJw/w==", "license": "MIT", "engines": { "node": ">= 10" @@ -8668,9 +8622,9 @@ "license": "MIT" }, "node_modules/node-addon-api": { - "version": "8.9.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.9.1.tgz", - "integrity": "sha512-4eUQWVPCUUUiBjLnHS3cXWeC6ryoPUc0U3rP7IuzapoGbzMqd/r6KKO0clr0b+snQhsrueFEhCZDdK+LK7hxKg==", + "version": "8.9.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.9.2.tgz", + "integrity": "sha512-VijLXbi3UACN69I0JVXJsX4tjACjNoQDgv2gTF6sx2wWEi8tkSg2eX8p5gSIFi8z2+DL3oHmY6OyKce38SDolg==", "license": "MIT", "optional": true, "engines": { @@ -8866,9 +8820,9 @@ } }, "node_modules/oxfmt": { - "version": "0.63.0", - "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.63.0.tgz", - "integrity": "sha512-kgdDwv35wvVf6554U2Ab8Jnd0zTM+TsEQWwaB70RAjK3gICFAFGO+2Hd3Be27GMoXj3XRL9IKSNRVl7KBQL6iw==", + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.64.0.tgz", + "integrity": "sha512-XZ4GFBN/PLbXKq+0zrgpQfPKYuJlUuj+nzZJY7UpIbFMNyefNLCdN9EwViycNqnYcv0wrn0jXcQLlqJp8RCKBg==", "dev": true, "license": "MIT", "dependencies": { @@ -8884,25 +8838,25 @@ "url": "https://github.com/sponsors/Boshen" }, "optionalDependencies": { - "@oxfmt/binding-android-arm-eabi": "0.63.0", - "@oxfmt/binding-android-arm64": "0.63.0", - "@oxfmt/binding-darwin-arm64": "0.63.0", - "@oxfmt/binding-darwin-x64": "0.63.0", - "@oxfmt/binding-freebsd-x64": "0.63.0", - "@oxfmt/binding-linux-arm-gnueabihf": "0.63.0", - "@oxfmt/binding-linux-arm-musleabihf": "0.63.0", - "@oxfmt/binding-linux-arm64-gnu": "0.63.0", - "@oxfmt/binding-linux-arm64-musl": "0.63.0", - "@oxfmt/binding-linux-ppc64-gnu": "0.63.0", - "@oxfmt/binding-linux-riscv64-gnu": "0.63.0", - "@oxfmt/binding-linux-riscv64-musl": "0.63.0", - "@oxfmt/binding-linux-s390x-gnu": "0.63.0", - "@oxfmt/binding-linux-x64-gnu": "0.63.0", - "@oxfmt/binding-linux-x64-musl": "0.63.0", - "@oxfmt/binding-openharmony-arm64": "0.63.0", - "@oxfmt/binding-win32-arm64-msvc": "0.63.0", - "@oxfmt/binding-win32-ia32-msvc": "0.63.0", - "@oxfmt/binding-win32-x64-msvc": "0.63.0" + "@oxfmt/binding-android-arm-eabi": "0.64.0", + "@oxfmt/binding-android-arm64": "0.64.0", + "@oxfmt/binding-darwin-arm64": "0.64.0", + "@oxfmt/binding-darwin-x64": "0.64.0", + "@oxfmt/binding-freebsd-x64": "0.64.0", + "@oxfmt/binding-linux-arm-gnueabihf": "0.64.0", + "@oxfmt/binding-linux-arm-musleabihf": "0.64.0", + "@oxfmt/binding-linux-arm64-gnu": "0.64.0", + "@oxfmt/binding-linux-arm64-musl": "0.64.0", + "@oxfmt/binding-linux-ppc64-gnu": "0.64.0", + "@oxfmt/binding-linux-riscv64-gnu": "0.64.0", + "@oxfmt/binding-linux-riscv64-musl": "0.64.0", + "@oxfmt/binding-linux-s390x-gnu": "0.64.0", + "@oxfmt/binding-linux-x64-gnu": "0.64.0", + "@oxfmt/binding-linux-x64-musl": "0.64.0", + "@oxfmt/binding-openharmony-arm64": "0.64.0", + "@oxfmt/binding-win32-arm64-msvc": "0.64.0", + "@oxfmt/binding-win32-ia32-msvc": "0.64.0", + "@oxfmt/binding-win32-x64-msvc": "0.64.0" }, "peerDependencies": { "svelte": "^5.0.0", @@ -8918,9 +8872,9 @@ } }, "node_modules/oxlint": { - "version": "1.78.0", - "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.78.0.tgz", - "integrity": "sha512-QgQePuxIqKOzo1KSjG2EnITEeWvWnKAm77eq8nrMtf6AGoA+zyGc4PFYtDNJSD25g/ibOwfQ851hZ4/SPkMVoA==", + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.79.0.tgz", + "integrity": "sha512-hVJ9hq9m2unPS+Of4eJJgCPdIeCC+3DHEUX3tkmrPJr3OK2hz7PhXwgC+ZP71ZcYu8cCDEtQrqLxWNvxBppBVg==", "dev": true, "license": "MIT", "bin": { @@ -8933,25 +8887,25 @@ "url": "https://github.com/sponsors/Boshen" }, "optionalDependencies": { - "@oxlint/binding-android-arm-eabi": "1.78.0", - "@oxlint/binding-android-arm64": "1.78.0", - "@oxlint/binding-darwin-arm64": "1.78.0", - "@oxlint/binding-darwin-x64": "1.78.0", - "@oxlint/binding-freebsd-x64": "1.78.0", - "@oxlint/binding-linux-arm-gnueabihf": "1.78.0", - "@oxlint/binding-linux-arm-musleabihf": "1.78.0", - "@oxlint/binding-linux-arm64-gnu": "1.78.0", - "@oxlint/binding-linux-arm64-musl": "1.78.0", - "@oxlint/binding-linux-ppc64-gnu": "1.78.0", - "@oxlint/binding-linux-riscv64-gnu": "1.78.0", - "@oxlint/binding-linux-riscv64-musl": "1.78.0", - "@oxlint/binding-linux-s390x-gnu": "1.78.0", - "@oxlint/binding-linux-x64-gnu": "1.78.0", - "@oxlint/binding-linux-x64-musl": "1.78.0", - "@oxlint/binding-openharmony-arm64": "1.78.0", - "@oxlint/binding-win32-arm64-msvc": "1.78.0", - "@oxlint/binding-win32-ia32-msvc": "1.78.0", - "@oxlint/binding-win32-x64-msvc": "1.78.0" + "@oxlint/binding-android-arm-eabi": "1.79.0", + "@oxlint/binding-android-arm64": "1.79.0", + "@oxlint/binding-darwin-arm64": "1.79.0", + "@oxlint/binding-darwin-x64": "1.79.0", + "@oxlint/binding-freebsd-x64": "1.79.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.79.0", + "@oxlint/binding-linux-arm-musleabihf": "1.79.0", + "@oxlint/binding-linux-arm64-gnu": "1.79.0", + "@oxlint/binding-linux-arm64-musl": "1.79.0", + "@oxlint/binding-linux-ppc64-gnu": "1.79.0", + "@oxlint/binding-linux-riscv64-gnu": "1.79.0", + "@oxlint/binding-linux-riscv64-musl": "1.79.0", + "@oxlint/binding-linux-s390x-gnu": "1.79.0", + "@oxlint/binding-linux-x64-gnu": "1.79.0", + "@oxlint/binding-linux-x64-musl": "1.79.0", + "@oxlint/binding-openharmony-arm64": "1.79.0", + "@oxlint/binding-win32-arm64-msvc": "1.79.0", + "@oxlint/binding-win32-ia32-msvc": "1.79.0", + "@oxlint/binding-win32-x64-msvc": "1.79.0" }, "peerDependencies": { "oxlint-tsgolint": ">=7.0.2001", @@ -9046,16 +9000,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.5.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", - "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, "node_modules/path-to-regexp": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", @@ -9377,6 +9321,16 @@ "node": "^20.9.0 || >=22" } }, + "node_modules/react-docgen-typescript": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/react-docgen-typescript/-/react-docgen-typescript-2.4.0.tgz", + "integrity": "sha512-ZtAp5XTO5HRzQctjPU0ybY0RRCQO19X/8fxn3w7y2VVTUbGHDKULPTL4ky3vB05euSgG5NpALhEhDPvQ56wvXg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">= 4.3.x" + } + }, "node_modules/react-dom": { "version": "19.2.8", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", @@ -9714,13 +9668,13 @@ "license": "MIT" }, "node_modules/rolldown": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.4.tgz", - "integrity": "sha512-rSr7irW0K7QRWzjdJXqZowkcRdDtjRduh43rBltnVKd0VFq839l1lJoDvGJb6gl7+4rTTCrPWu+YfujUL8Ug7w==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.5.tgz", + "integrity": "sha512-VD2IE5PUG4Oj8zz2VGykiYd5wbnjdIiSsNQb8Qu5B+noEp+A78mu2iVvpp27g8es14Tk9rofNs5Tku9iQCS4fA==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.144.0", + "@oxc-project/types": "=0.146.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -9730,26 +9684,27 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.2.4", - "@rolldown/binding-darwin-arm64": "1.2.4", - "@rolldown/binding-darwin-x64": "1.2.4", - "@rolldown/binding-freebsd-x64": "1.2.4", - "@rolldown/binding-linux-arm-gnueabihf": "1.2.4", - "@rolldown/binding-linux-arm64-gnu": "1.2.4", - "@rolldown/binding-linux-arm64-musl": "1.2.4", - "@rolldown/binding-linux-ppc64-gnu": "1.2.4", - "@rolldown/binding-linux-s390x-gnu": "1.2.4", - "@rolldown/binding-linux-x64-gnu": "1.2.4", - "@rolldown/binding-linux-x64-musl": "1.2.4", - "@rolldown/binding-openharmony-arm64": "1.2.4", - "@rolldown/binding-win32-arm64-msvc": "1.2.4", - "@rolldown/binding-win32-x64-msvc": "1.2.4" + "@rolldown/binding-android-arm-eabi": "1.2.5", + "@rolldown/binding-android-arm64": "1.2.5", + "@rolldown/binding-darwin-arm64": "1.2.5", + "@rolldown/binding-darwin-x64": "1.2.5", + "@rolldown/binding-freebsd-x64": "1.2.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.5", + "@rolldown/binding-linux-arm64-gnu": "1.2.5", + "@rolldown/binding-linux-arm64-musl": "1.2.5", + "@rolldown/binding-linux-ppc64-gnu": "1.2.5", + "@rolldown/binding-linux-s390x-gnu": "1.2.5", + "@rolldown/binding-linux-x64-gnu": "1.2.5", + "@rolldown/binding-linux-x64-musl": "1.2.5", + "@rolldown/binding-openharmony-arm64": "1.2.5", + "@rolldown/binding-win32-arm64-msvc": "1.2.5", + "@rolldown/binding-win32-x64-msvc": "1.2.5" } }, "node_modules/rolldown/node_modules/@oxc-project/types": { - "version": "0.144.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.144.0.tgz", - "integrity": "sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg==", + "version": "0.146.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.146.0.tgz", + "integrity": "sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA==", "dev": true, "license": "MIT", "funding": { @@ -9818,13 +9773,16 @@ } }, "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/serialize-error": { @@ -10004,9 +9962,9 @@ "license": "MIT" }, "node_modules/storybook": { - "version": "10.5.7", - "resolved": "https://registry.npmjs.org/storybook/-/storybook-10.5.7.tgz", - "integrity": "sha512-oiKvWIwIoOhFP1i6dASYyMXwPHKEtVZMshqSB7EvIVYjWRh0l9H7gHEt1z4Gh2rLGFMekWdsm4s94rvwpR7gkg==", + "version": "10.5.9", + "resolved": "https://registry.npmjs.org/storybook/-/storybook-10.5.9.tgz", + "integrity": "sha512-UfdMKSjEhIKr8LbqYyIE5r7vT/drL/PxN75YaouJ+UG0FssEy6cf49OdTF3kstAqVMHskc+zEqyRoiQHZXHwgA==", "dev": true, "license": "MIT", "dependencies": { @@ -10065,19 +10023,6 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/storybook/node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/strict-event-emitter": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", @@ -10116,13 +10061,6 @@ "node": ">=8" } }, - "node_modules/string-width/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -10198,9 +10136,9 @@ } }, "node_modules/swagger-client": { - "version": "3.37.8", - "resolved": "https://registry.npmjs.org/swagger-client/-/swagger-client-3.37.8.tgz", - "integrity": "sha512-uoKwfq+8DvWVDhoALDrEtex9f26Yi2VkvEFjsrMHd8Gl+TcApJkVXtNiE35p5JQjMsvwkvr1eLVlOFNF4GL1bQ==", + "version": "3.38.0", + "resolved": "https://registry.npmjs.org/swagger-client/-/swagger-client-3.38.0.tgz", + "integrity": "sha512-n7aykm1BEdQ3fKePJJx63UGjYe8/5fuxFMi3qZP4OJGZvzljKvmhxNwIF/MB71sF/lop9NeWZReKvPib9CY+2g==", "license": "Apache-2.0", "dependencies": { "@babel/runtime-corejs3": "^7.22.15", @@ -10215,7 +10153,7 @@ "deepmerge": "~4.3.0", "fast-json-patch": "^3.0.0-1", "js-yaml": "^4.2.0", - "neotraverse": "=0.6.18", + "neotraverse": "=1.0.1", "node-abort-controller": "^3.1.1", "openapi-path-templating": "^2.2.1", "openapi-server-url-templating": "^1.3.0", @@ -10250,9 +10188,9 @@ } }, "node_modules/swagger-ui-react": { - "version": "5.32.13", - "resolved": "https://registry.npmjs.org/swagger-ui-react/-/swagger-ui-react-5.32.13.tgz", - "integrity": "sha512-XIDl+Ny6kE1N8wpSPiOFrjPfAevs4GR4XmV6BT6NLMikkMFIbIVocWbA8pnKYyYXQe8Rccfli5o2zDfySw0FnQ==", + "version": "5.32.14", + "resolved": "https://registry.npmjs.org/swagger-ui-react/-/swagger-ui-react-5.32.14.tgz", + "integrity": "sha512-6LAVBeC78DplbJ7kutm/YeBYo22nPzGOca4bIZAvQG4w2eSetnYDdazaUfY0qzQUlg/H90HnYZX3rg67EmENOw==", "license": "Apache-2.0", "dependencies": { "@babel/runtime-corejs3": "^7.27.1", @@ -10276,7 +10214,7 @@ "react-immutable-proptypes": "2.2.0", "react-immutable-pure-component": "^2.2.0", "react-inspector": "^6.0.1", - "react-redux": "^9.2.0", + "react-redux": "^9.3.0", "react-syntax-highlighter": "^16.0.0", "redux": "^5.0.1", "redux-immutable": "^4.0.0", @@ -10284,7 +10222,7 @@ "reselect": "^5.1.1", "serialize-error": "^8.1.0", "sha.js": "^2.4.12", - "swagger-client": "^3.37.8", + "swagger-client": "^3.38.0", "url-parse": "^1.5.10", "xml": "=1.0.1", "xml-but-prettier": "^1.0.1", @@ -10819,19 +10757,19 @@ } }, "node_modules/vitest": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", - "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", + "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.10", - "@vitest/mocker": "4.1.10", - "@vitest/pretty-format": "4.1.10", - "@vitest/runner": "4.1.10", - "@vitest/snapshot": "4.1.10", - "@vitest/spy": "4.1.10", - "@vitest/utils": "4.1.10", + "@vitest/expect": "4.1.11", + "@vitest/mocker": "4.1.11", + "@vitest/pretty-format": "4.1.11", + "@vitest/runner": "4.1.11", + "@vitest/snapshot": "4.1.11", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -10859,12 +10797,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.10", - "@vitest/browser-preview": "4.1.10", - "@vitest/browser-webdriverio": "4.1.10", - "@vitest/coverage-istanbul": "4.1.10", - "@vitest/coverage-v8": "4.1.10", - "@vitest/ui": "4.1.10", + "@vitest/browser-playwright": "4.1.11", + "@vitest/browser-preview": "4.1.11", + "@vitest/browser-webdriverio": "4.1.11", + "@vitest/coverage-istanbul": "4.1.11", + "@vitest/coverage-v8": "4.1.11", + "@vitest/ui": "4.1.11", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" @@ -10909,16 +10847,16 @@ } }, "node_modules/vitest/node_modules/@vitest/expect": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", - "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", + "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.10", - "@vitest/utils": "4.1.10", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" }, diff --git a/frontend/package.json b/frontend/package.json index 1df6fdd9d..f53a23300 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,6 +13,7 @@ "build": "npm run gen:api && vite build", "preview": "vite preview", "lint": "oxlint src tools", + "lint:fix": "oxlint --fix src tools", "lint:deprecated": "oxlint --type-aware -A all -D typescript/no-deprecated src", "format": "oxfmt src tools", "format:check": "oxfmt --check src tools", @@ -36,13 +37,13 @@ "@ant-design/icons": "^6.3.2", "@codemirror/lang-json": "^6.0.2", "@codemirror/theme-one-dark": "^6.1.3", - "@hookform/resolvers": "^5.7.1", + "@hookform/resolvers": "^5.9.1", "@noble/hashes": "^2.3.0", "@tanstack/react-query": "^5.101.4", "@tanstack/react-query-devtools": "^5.101.4", - "antd": "^6.6.0", + "antd": "^6.6.1", "codemirror": "^6.0.2", - "dayjs": "^1.11.21", + "dayjs": "^1.11.23", "i18next": "^26.3.6", "otpauth": "^9.5.1", "persian-calendar-suite": "^1.5.6", @@ -51,35 +52,35 @@ "react-hook-form": "^7.85.0", "react-i18next": "^17.0.11", "react-router": "^8.3.0", - "swagger-ui-react": "^5.32.13", + "swagger-ui-react": "^5.32.14", "uplot": "^1.6.32", "zod": "^4.4.3" }, "devDependencies": { - "@storybook/addon-a11y": "^10.5.7", - "@storybook/addon-docs": "^10.5.7", - "@storybook/addon-vitest": "^10.5.7", - "@storybook/react-vite": "^10.5.7", + "@storybook/addon-a11y": "^10.5.9", + "@storybook/addon-docs": "^10.5.9", + "@storybook/addon-vitest": "^10.5.9", + "@storybook/react-vite": "^10.5.9", "@testing-library/dom": "^10.4.1", "@testing-library/react": "^16.3.2", "@types/react": "^19.2.18", "@types/react-dom": "^19.2.4", "@types/swagger-ui-react": "^5.18.0", "@vitejs/plugin-react": "^6.0.5", - "@vitest/browser-playwright": "4.1.10", - "@vitest/coverage-v8": "^4.1.10", + "@vitest/browser-playwright": "4.1.11", + "@vitest/coverage-v8": "^4.1.11", "husky": "^9.1.7", "jsdom": "^30.0.1", "lint-staged": "^17.3.0", "msw": "^2.15.0", - "oxfmt": "0.63.0", - "oxlint": "1.78.0", + "oxfmt": "0.64.0", + "oxlint": "1.79.0", "oxlint-tsgolint": "^7.0.2001", "playwright": "^1.62.1", - "storybook": "^10.5.7", + "storybook": "^10.5.9", "typescript": "7.0.2", "vite": "8.2.1", - "vitest": "^4.1.10" + "vitest": "^4.1.11" }, "overrides": { "dompurify": "^3.4.11", diff --git a/frontend/src/components/feedback/PromptModal.tsx b/frontend/src/components/feedback/PromptModal.tsx index 79dff6714..e6082faf7 100644 --- a/frontend/src/components/feedback/PromptModal.tsx +++ b/frontend/src/components/feedback/PromptModal.tsx @@ -33,15 +33,21 @@ export default function PromptModal({ const textareaRef = useRef(null); const inputRef = useRef(null); + const [openedWith, setOpenedWith] = useState(null); + const openKey = open ? `${type}\u0000${initialValue}` : null; + if (openKey !== openedWith) { + setOpenedWith(openKey); + if (open) setValue(initialValue); + } + useEffect(() => { - if (open) { - setValue(initialValue); - setTimeout(() => { - if (type === 'textarea') textareaRef.current?.focus(); - else inputRef.current?.focus(); - }, 50); - } - }, [open, initialValue, type]); + if (!open) return; + const id = setTimeout(() => { + if (type === 'textarea') textareaRef.current?.focus(); + else inputRef.current?.focus(); + }, 50); + return () => clearTimeout(id); + }, [open, type]); function onKeydown(e: React.KeyboardEvent) { if (type !== 'textarea' && e.key === 'Enter') { diff --git a/frontend/src/components/feedback/TextModal.tsx b/frontend/src/components/feedback/TextModal.tsx index f58a42d35..887d63b9f 100644 --- a/frontend/src/components/feedback/TextModal.tsx +++ b/frontend/src/components/feedback/TextModal.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useCallback, useState } from 'react'; import { Button, Input, Modal, Tabs, message } from 'antd'; import { CopyOutlined, DownloadOutlined } from '@ant-design/icons'; import { useTranslation } from 'react-i18next'; @@ -35,9 +35,12 @@ export default function TextModal({ const [messageApi, messageContextHolder] = message.useMessage(); const [activeKey, setActiveKey] = useState(''); - useEffect(() => { - if (open && tabs && tabs.length > 0) setActiveKey(tabs[0].key); - }, [open, tabs]); + // Reset on the way out so the next open starts on the first tab; activeTab + // falls back to tabs[0] whenever activeKey no longer matches. + const close = useCallback(() => { + setActiveKey(''); + onClose(); + }, [onClose]); const activeTab = tabs?.find((tab) => tab.key === activeKey) ?? tabs?.[0]; const activeContent = activeTab ? activeTab.content : content; @@ -46,7 +49,7 @@ export default function TextModal({ const ok = await ClipboardManager.copyText(activeContent || ''); if (ok) { messageApi.success(t('copied')); - onClose(); + close(); } } @@ -61,7 +64,7 @@ export default function TextModal({ diff --git a/frontend/src/components/geodata/GeoBrowserModal.stories.tsx b/frontend/src/components/geodata/GeoBrowserModal.stories.tsx index 502cfb2f4..b4f9e56e3 100644 --- a/frontend/src/components/geodata/GeoBrowserModal.stories.tsx +++ b/frontend/src/components/geodata/GeoBrowserModal.stories.tsx @@ -749,8 +749,12 @@ const withDatabases = withFiles([GEOSITE_FILE, GEOIP_FILE]); function BrowserDemo(props: GeoBrowserModalProps) { const [open, setOpen] = useState(props.open); const [value, setValue] = useState(props.value); - useEffect(() => setOpen(props.open), [props.open]); - useEffect(() => setValue(props.value), [props.value]); + const [synced, setSynced] = useState({ open: props.open, value: props.value }); + if (synced.open !== props.open || synced.value !== props.value) { + setSynced({ open: props.open, value: props.value }); + setOpen(props.open); + setValue(props.value); + } return ( diff --git a/frontend/src/components/geodata/GeoBrowserModal.tsx b/frontend/src/components/geodata/GeoBrowserModal.tsx index f08b014c0..31baacb37 100644 --- a/frontend/src/components/geodata/GeoBrowserModal.tsx +++ b/frontend/src/components/geodata/GeoBrowserModal.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Alert, @@ -69,13 +69,16 @@ export default function GeoBrowserModal({ const [entryPage, setEntryPage] = useState(1); const [selected, setSelected] = useState([]); - const knownRef = useRef>(new Set()); - const seededFilesRef = useRef>(new Set()); + const [known, setKnown] = useState>(() => new Set()); + const [seededFiles, setSeededFiles] = useState>(() => new Set()); const filesQuery = useGeodataFiles(open); const files = useMemo(() => databasesFor(filesQuery.data ?? [], kind), [filesQuery.data, kind]); - const activeFile = files.find((candidate) => candidate.name === file); - const fileKind: GeoKind = activeFile?.kind ?? kind; + const activeFile = useMemo( + () => files.find((candidate) => candidate.name === file), + [files, file], + ); + const fileKind: GeoKind = useMemo(() => activeFile?.kind ?? kind, [activeFile, kind]); const categoriesQuery = useGeodataCategories(file, '', open && !!file); // While a newly picked database loads, the query still serves the previous @@ -117,28 +120,27 @@ export default function GeoBrowserModal({ return () => window.clearTimeout(handle); }, [entryQuery, entryFilter]); - useEffect(() => { - if (!open) return; - knownRef.current = new Set(); - seededFilesRef.current = new Set(); - setCategoryQuery(''); - setEntryQuery(''); - setEntryFilter(''); - setActiveCode(undefined); - setEntryPage(1); - setSelected([]); - }, [open]); - - useEffect(() => { - if (!open || file || files.length === 0) return; + // Opening, picking the default database and seeding the selection are all + // render-time adjustments — an effect would paint the previous state first. + const [wasOpen, setWasOpen] = useState(false); + if (open !== wasOpen) { + setWasOpen(open); + if (open) { + setKnown(new Set()); + setSeededFiles(new Set()); + setCategoryQuery(''); + setEntryQuery(''); + setEntryFilter(''); + setActiveCode(undefined); + setEntryPage(1); + setSelected([]); + } + } else if (open && !file && files.length > 0) { setFile(preferredFile(files, kind)); - }, [open, file, files, kind]); - - useEffect(() => { - if (!open || !file || categories.length === 0 || seededFilesRef.current.has(file)) return; + } else if (open && file && categories.length > 0 && !seededFiles.has(file)) { const tokens = categories.map((category) => tokenFor(file, category.code, fileKind)); - for (const token of tokens) knownRef.current.add(token); - seededFilesRef.current.add(file); + setKnown(new Set([...known, ...tokens])); + setSeededFiles(new Set(seededFiles).add(file)); const fromValue = selectionFromValue(value, new Set(tokens)); if (fromValue.length > 0) { setSelected((previous) => [ @@ -146,7 +148,7 @@ export default function GeoBrowserModal({ ...fromValue.filter((token) => !previous.includes(token)), ]); } - }, [open, file, categories, fileKind, value]); + } const visibleCategories = useMemo(() => { const query = categoryQuery.trim().toLowerCase(); @@ -276,7 +278,7 @@ export default function GeoBrowserModal({ title={t('pages.xray.geoBrowser.title')} width={880} onCancel={onClose} - onOk={() => onApply(mergeSelection(value, selected, knownRef.current))} + onOk={() => onApply(mergeSelection(value, selected, known))} okText={t('pages.xray.geoBrowser.apply')} cancelText={t('close')} className="geo-browser-modal" diff --git a/frontend/src/components/geodata/GeoTokenInput.stories.tsx b/frontend/src/components/geodata/GeoTokenInput.stories.tsx index 6da9b7f00..72daf082b 100644 --- a/frontend/src/components/geodata/GeoTokenInput.stories.tsx +++ b/frontend/src/components/geodata/GeoTokenInput.stories.tsx @@ -216,7 +216,11 @@ const withGeodata: Decorator = function GeodataBackend(Story) { function ControlledTokenInput({ value = '', id = 'geo-rule', ...rest }: GeoTokenInputProps) { const [current, setCurrent] = useState(value); - useEffect(() => setCurrent(value), [value]); + const [synced, setSynced] = useState(value); + if (synced !== value) { + setSynced(value); + setCurrent(value); + } return ( diff --git a/frontend/src/components/geodata/GeoTokenInput.tsx b/frontend/src/components/geodata/GeoTokenInput.tsx index a7b0b4b6b..9d41a07bc 100644 --- a/frontend/src/components/geodata/GeoTokenInput.tsx +++ b/frontend/src/components/geodata/GeoTokenInput.tsx @@ -49,13 +49,21 @@ export default function GeoTokenInput({ const validate = useValidateGeoTokens(); const { mutateAsync } = validate; - useEffect(() => { - const tokens = parseTokens(value); - if (tokens.length === 0) { + // An empty field has nothing to validate, so it clears during render rather + // than waiting a commit for the effect to catch up. + const isEmpty = parseTokens(value).length === 0; + const [wasEmpty, setWasEmpty] = useState(isEmpty); + if (isEmpty !== wasEmpty) { + setWasEmpty(isEmpty); + if (isEmpty) { setIssues([]); setCheckFailed(false); - return; } + } + + useEffect(() => { + const tokens = parseTokens(value); + if (tokens.length === 0) return; let cancelled = false; const timer = setTimeout(() => { mutateAsync({ tokens, kind }) diff --git a/frontend/src/components/utility/LazyMount.tsx b/frontend/src/components/utility/LazyMount.tsx index a5af3da6c..ffbb0c8d9 100644 --- a/frontend/src/components/utility/LazyMount.tsx +++ b/frontend/src/components/utility/LazyMount.tsx @@ -1,4 +1,4 @@ -import { Suspense, useEffect, useState, type ReactNode } from 'react'; +import { Suspense, useState, type ReactNode } from 'react'; import { Spin } from 'antd'; interface LazyMountProps { @@ -13,9 +13,7 @@ interface LazyMountProps { // on heavy list pages to keep the initial bundle small. export default function LazyMount({ when, fallback = , children }: LazyMountProps) { const [mounted, setMounted] = useState(when); - useEffect(() => { - if (when && !mounted) setMounted(true); - }, [when, mounted]); + if (when && !mounted) setMounted(true); if (!mounted) return null; return {children}; } diff --git a/frontend/src/components/viz/Sparkline.tsx b/frontend/src/components/viz/Sparkline.tsx index f8a6f8889..5f59414e0 100644 --- a/frontend/src/components/viz/Sparkline.tsx +++ b/frontend/src/components/viz/Sparkline.tsx @@ -268,9 +268,11 @@ export default function Sparkline(props: SparklineProps) { extrema, }; const cfgRef = useRef(cfg); - cfgRef.current = cfg; const viewRef = useRef({ points, yDomain, yTicks, xTickIndexes, extremaPoints }); - viewRef.current = { points, yDomain, yTicks, xTickIndexes, extremaPoints }; + useEffect(() => { + cfgRef.current = cfg; + viewRef.current = { points, yDomain, yTicks, xTickIndexes, extremaPoints }; + }); const containerRef = useRef(null); const plotRef = useRef(null); diff --git a/frontend/src/hooks/useClients.ts b/frontend/src/hooks/useClients.ts index 7c6b8f0d3..3e087f47c 100644 --- a/frontend/src/hooks/useClients.ts +++ b/frontend/src/hooks/useClients.ts @@ -700,7 +700,9 @@ export function useClients(options: UseClientsOptions = {}) { // WS-driven in-place merges. Page wires these via useWebSocket; the bridge // covers coarse 'invalidate' and 'inbounds' events centrally. const queryRef = useRef(query); - queryRef.current = query; + useEffect(() => { + queryRef.current = query; + }); const applyTrafficEvent = useCallback( (payload: unknown) => { diff --git a/frontend/src/hooks/useMediaQuery.ts b/frontend/src/hooks/useMediaQuery.ts index 6c07ad6dc..359429392 100644 --- a/frontend/src/hooks/useMediaQuery.ts +++ b/frontend/src/hooks/useMediaQuery.ts @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useCallback, useSyncExternalStore } from 'react'; export const MOBILE_BREAKPOINT_PX = 768; @@ -11,17 +11,21 @@ export const MOBILE_BREAKPOINT_PX = 768; */ export function useMediaQuery(breakpoint: number = MOBILE_BREAKPOINT_PX) { const query = `(max-width: ${breakpoint}px)`; - const [isMobile, setIsMobile] = useState(() => - typeof window !== 'undefined' ? window.matchMedia(query).matches : false, + + const subscribe = useCallback( + (onStoreChange: () => void) => { + const mql = window.matchMedia(query); + mql.addEventListener('change', onStoreChange); + return () => mql.removeEventListener('change', onStoreChange); + }, + [query], ); - useEffect(() => { - const mql = window.matchMedia(query); - const onChange = (e: MediaQueryListEvent) => setIsMobile(e.matches); - mql.addEventListener('change', onChange); - setIsMobile(mql.matches); - return () => mql.removeEventListener('change', onChange); - }, [query]); + const isMobile = useSyncExternalStore( + subscribe, + () => window.matchMedia(query).matches, + () => false, + ); return { isMobile }; } diff --git a/frontend/src/hooks/useServerDraft.ts b/frontend/src/hooks/useServerDraft.ts index 70f064f68..f3005aa97 100644 --- a/frontend/src/hooks/useServerDraft.ts +++ b/frontend/src/hooks/useServerDraft.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; export function useServerDraft( server: T | undefined, @@ -6,37 +6,30 @@ export function useServerDraft( equals: (left: T, right: T) => boolean, ) { const cloneRef = useRef(clone); - const equalsRef = useRef(equals); - cloneRef.current = clone; - equalsRef.current = equals; + useEffect(() => { + cloneRef.current = clone; + }); const [draft, setDraft] = useState(); const [baseline, setBaseline] = useState(); - const draftRef = useRef(draft); - const baselineRef = useRef(baseline); - draftRef.current = draft; - baselineRef.current = baseline; + const [syncedServer, setSyncedServer] = useState(); - useEffect(() => { - if (server === undefined) return; - const currentDraft = draftRef.current; - const currentBaseline = baselineRef.current; - const isDirty = - currentDraft !== undefined && - (currentBaseline === undefined || !equalsRef.current(currentDraft, currentBaseline)); - setBaseline(server); - if (isDirty && !equalsRef.current(currentDraft, server)) return; - setDraft(cloneRef.current(server)); - }, [server]); + const isDirty = draft !== undefined && (baseline === undefined || !equals(draft, baseline)); + + // Adopting the server value during render (not in an effect) keeps the + // returned draft and isDirty consistent within the very first render. + if (server !== syncedServer) { + setSyncedServer(server); + if (server !== undefined) { + setBaseline(server); + const keepLocalEdits = isDirty && !equals(draft as T, server); + if (!keepLocalEdits) setDraft(clone(server)); + } + } const markSaved = useCallback((value: T) => { setBaseline(cloneRef.current(value)); }, []); - const isDirty = useMemo( - () => draft !== undefined && (baseline === undefined || !equalsRef.current(draft, baseline)), - [baseline, draft], - ); - return { draft, setDraft, isDirty, markSaved }; } diff --git a/frontend/src/hooks/useXraySetting.ts b/frontend/src/hooks/useXraySetting.ts index 4f336d80d..af614d73f 100644 --- a/frontend/src/hooks/useXraySetting.ts +++ b/frontend/src/hooks/useXraySetting.ts @@ -139,10 +139,14 @@ export function useXraySetting(): UseXraySettingResult { const [outboundTestUrl, setOutboundTestUrlState] = useState(DEFAULT_TEST_URL); const [savedXraySetting, setSavedXraySetting] = useState(''); const [savedOutboundTestUrl, setSavedOutboundTestUrl] = useState(DEFAULT_TEST_URL); - const [inboundTags, setInboundTags] = useState([]); - const [clientReverseTags, setClientReverseTags] = useState([]); - const [subscriptionOutbounds, setSubscriptionOutbounds] = useState([]); - const [subscriptionOutboundTags, setSubscriptionOutboundTags] = useState([]); + const config = configQuery.data; + const inboundTags = useMemo(() => config?.inboundTags || [], [config]); + const clientReverseTags = useMemo(() => config?.clientReverseTags || [], [config]); + const subscriptionOutbounds = useMemo( + () => config?.subscriptionOutbounds || [], + [config], + ); + const subscriptionOutboundTags = useMemo(() => config?.subscriptionOutboundTags || [], [config]); const [outboundTestStates, setOutboundTestStates] = useState>( {}, ); @@ -161,34 +165,34 @@ export function useXraySetting(): UseXraySettingResult { const templateSettingsRef = useRef(null); const subscriptionOutboundsRef = useRef([]); - xraySettingRef.current = xraySetting; - outboundTestUrlRef.current = outboundTestUrl; - savedXraySettingRef.current = savedXraySetting; - savedOutboundTestUrlRef.current = savedOutboundTestUrl; - templateSettingsRef.current = templateSettings; - subscriptionOutboundsRef.current = subscriptionOutbounds; + const [syncedConfig, setSyncedConfig] = useState(); useEffect(() => { - if (!configQuery.data) return; - const obj = configQuery.data; - const pretty = JSON.stringify(obj.xraySetting, null, 2); - const nextUrl = normalizeOutboundTestUrl(obj.outboundTestUrl || ''); - setInboundTags(obj.inboundTags || []); - setClientReverseTags(obj.clientReverseTags || []); - setSubscriptionOutbounds(obj.subscriptionOutbounds || []); - setSubscriptionOutboundTags(obj.subscriptionOutboundTags || []); + xraySettingRef.current = xraySetting; + outboundTestUrlRef.current = outboundTestUrl; + savedXraySettingRef.current = savedXraySetting; + savedOutboundTestUrlRef.current = savedOutboundTestUrl; + templateSettingsRef.current = templateSettings; + subscriptionOutboundsRef.current = subscriptionOutbounds; + }); + + // Adopt a fetched config during render, so the editor never paints one frame + // of the previous config after a refetch. Local edits win over the refetch. + if (config && config !== syncedConfig) { + setSyncedConfig(config); const isDirty = - savedXraySettingRef.current !== xraySettingRef.current || - savedOutboundTestUrlRef.current !== normalizeOutboundTestUrl(outboundTestUrlRef.current); - if (isDirty) return; - syncingRef.current = true; - setXraySettingState(pretty); - setTemplateSettingsState(obj.xraySetting); - setSavedXraySetting(pretty); - syncingRef.current = false; - setOutboundTestUrlState(nextUrl); - setSavedOutboundTestUrl(nextUrl); - }, [configQuery.data]); + savedXraySetting !== xraySetting || + savedOutboundTestUrl !== normalizeOutboundTestUrl(outboundTestUrl); + if (!isDirty) { + const pretty = JSON.stringify(config.xraySetting, null, 2); + const nextUrl = normalizeOutboundTestUrl(config.outboundTestUrl || ''); + setXraySettingState(pretty); + setTemplateSettingsState(config.xraySetting); + setSavedXraySetting(pretty); + setOutboundTestUrlState(nextUrl); + setSavedOutboundTestUrl(nextUrl); + } + } const fetched = configQuery.data !== undefined || configQuery.isError; const fetchError = configQuery.error ? (configQuery.error as Error).message : ''; diff --git a/frontend/src/layouts/AppSidebar.tsx b/frontend/src/layouts/AppSidebar.tsx index 57a941612..a294bc37f 100644 --- a/frontend/src/layouts/AppSidebar.tsx +++ b/frontend/src/layouts/AppSidebar.tsx @@ -287,11 +287,9 @@ export default function AppSidebar() { const openSubmenu = settingsActive ? '/settings' : xrayActive ? '/xray' : null; const [openKeys, setOpenKeys] = useState(() => (openSubmenu ? [openSubmenu] : [])); - useEffect(() => { - if (openSubmenu) { - setOpenKeys((keys) => (keys.includes(openSubmenu) ? keys : [...keys, openSubmenu])); - } - }, [openSubmenu]); + if (openSubmenu && !openKeys.includes(openSubmenu)) { + setOpenKeys([...openKeys, openSubmenu]); + } const toMenuItems = useCallback( (items: typeof tabs): MenuProps['items'] => diff --git a/frontend/src/lib/xray/forms/fields/FinalMaskField.tsx b/frontend/src/lib/xray/forms/fields/FinalMaskField.tsx index 1003e9ec9..0ca265f6f 100644 --- a/frontend/src/lib/xray/forms/fields/FinalMaskField.tsx +++ b/frontend/src/lib/xray/forms/fields/FinalMaskField.tsx @@ -24,7 +24,9 @@ export default function FinalMaskField({ const [form] = Form.useForm(); const [initial] = useState(() => value ?? EMPTY); const onChangeRef = useRef(onChange); - onChangeRef.current = onChange; + useEffect(() => { + onChangeRef.current = onChange; + }); const lastEmitted = useRef(JSON.stringify(initial)); const finalmask = Form.useWatch('finalmask', form) as FinalMaskStreamSettings | undefined; diff --git a/frontend/src/lib/xray/forms/fields/SniffingField.tsx b/frontend/src/lib/xray/forms/fields/SniffingField.tsx index 5369f8d99..9db7e88ef 100644 --- a/frontend/src/lib/xray/forms/fields/SniffingField.tsx +++ b/frontend/src/lib/xray/forms/fields/SniffingField.tsx @@ -14,7 +14,9 @@ export default function SniffingField({ value, onChange, enableLabel }: Sniffing const [form] = Form.useForm(); const [initial] = useState(() => value ?? SniffingSchema.parse({})); const onChangeRef = useRef(onChange); - onChangeRef.current = onChange; + useEffect(() => { + onChangeRef.current = onChange; + }); const lastEmitted = useRef(JSON.stringify(initial)); const sniffing = Form.useWatch('sniffing', { form, preserve: true }) as Sniffing | undefined; diff --git a/frontend/src/lib/xray/forms/fields/SockoptCustomField.tsx b/frontend/src/lib/xray/forms/fields/SockoptCustomField.tsx index 02d7d3215..ea0af61db 100644 --- a/frontend/src/lib/xray/forms/fields/SockoptCustomField.tsx +++ b/frontend/src/lib/xray/forms/fields/SockoptCustomField.tsx @@ -13,7 +13,9 @@ export default function SockoptCustomField({ value, onChange }: SockoptCustomFie const [form] = Form.useForm(); const [initial] = useState(() => value ?? []); const onChangeRef = useRef(onChange); - onChangeRef.current = onChange; + useEffect(() => { + onChangeRef.current = onChange; + }); const lastEmitted = useRef(JSON.stringify(initial)); const list = Form.useWatch('customSockopt', form) as CustomSockopt[] | undefined; diff --git a/frontend/src/pages/clients/BulkAttachInboundsModal.tsx b/frontend/src/pages/clients/BulkAttachInboundsModal.tsx index b154d4346..6b071ea32 100644 --- a/frontend/src/pages/clients/BulkAttachInboundsModal.tsx +++ b/frontend/src/pages/clients/BulkAttachInboundsModal.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Alert, Modal, Select, Typography, message } from 'antd'; @@ -37,9 +37,13 @@ export default function BulkAttachInboundsModal({ const [targetIds, setTargetIds] = useState([]); const [submitting, setSubmitting] = useState(false); - useEffect(() => { + // React resets this during render rather than in an effect so the modal's + // first open frame already shows cleared fields. + const [wasOpen, setWasOpen] = useState(false); + if (open !== wasOpen) { + setWasOpen(open); if (open) setTargetIds([]); - }, [open]); + } const targetOptions = useMemo(() => { return (inbounds || []) diff --git a/frontend/src/pages/clients/BulkDetachInboundsModal.tsx b/frontend/src/pages/clients/BulkDetachInboundsModal.tsx index 3be36699b..2bf888203 100644 --- a/frontend/src/pages/clients/BulkDetachInboundsModal.tsx +++ b/frontend/src/pages/clients/BulkDetachInboundsModal.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Alert, Modal, Select, Typography, message } from 'antd'; @@ -37,9 +37,13 @@ export default function BulkDetachInboundsModal({ const [targetIds, setTargetIds] = useState([]); const [submitting, setSubmitting] = useState(false); - useEffect(() => { + // React resets this during render rather than in an effect so the modal's + // first open frame already shows cleared fields. + const [wasOpen, setWasOpen] = useState(false); + if (open !== wasOpen) { + setWasOpen(open); if (open) setTargetIds([]); - }, [open]); + } const targetOptions = useMemo(() => { return (inbounds || []) diff --git a/frontend/src/pages/clients/ClientBulkAddModal.tsx b/frontend/src/pages/clients/ClientBulkAddModal.tsx index 1c4b5e4d0..d0d87c33c 100644 --- a/frontend/src/pages/clients/ClientBulkAddModal.tsx +++ b/frontend/src/pages/clients/ClientBulkAddModal.tsx @@ -95,12 +95,14 @@ export default function ClientBulkAddModal({ const limitIpDisabled = !fail2ban.usable; const limitIpNotice = getLimitIpNotice(fail2ban, t); - useEffect(() => { - if (!open) return; - - methods.reset(EMPTY); - setDelayedStart(false); - }, [open, methods]); + const [wasOpen, setWasOpen] = useState(false); + if (open !== wasOpen) { + setWasOpen(open); + if (open) { + methods.reset(EMPTY); + setDelayedStart(false); + } + } const flowCapableIds = useMemo(() => { const ids = new Set(); diff --git a/frontend/src/pages/clients/ClientInfoModal.tsx b/frontend/src/pages/clients/ClientInfoModal.tsx index 2886c5c73..75c5921df 100644 --- a/frontend/src/pages/clients/ClientInfoModal.tsx +++ b/frontend/src/pages/clients/ClientInfoModal.tsx @@ -109,14 +109,20 @@ export default function ClientInfoModal({ keyof typeof SUBSCRIPTION_DOWNLOAD_NAMES | null >(null); - useEffect(() => { - if (!open) { + // Clearing on close happens during render; the effect owns only the fetch. + const openSubId = open ? (client?.subId ?? '') : null; + const [syncedSubId, setSyncedSubId] = useState(openSubId); + if (openSubId !== syncedSubId) { + setSyncedSubId(openSubId); + if (openSubId === null) { setLinks([]); setClientIps([]); setIpsModalOpen(false); - return; } - if (!client?.subId) return; + } + + useEffect(() => { + if (!open || !client?.subId) return; let cancelled = false; (async () => { const msg = (await HttpUtil.get( @@ -139,22 +145,16 @@ export default function ClientInfoModal({ return r > 0 ? r : 0; }, [totalBytes, used]); - const subLink = useMemo(() => { - if (!client?.subId || !subSettings?.subURI) return ''; - return subSettings.subURI + client.subId; - }, [client?.subId, subSettings?.subURI]); - - const subJsonLink = useMemo(() => { - if (!client?.subId) return ''; - if (!subSettings?.subJsonEnable || !subSettings?.subJsonURI) return ''; - return subSettings.subJsonURI + client.subId; - }, [client?.subId, subSettings?.subJsonEnable, subSettings?.subJsonURI]); - - const subClashLink = useMemo(() => { - if (!client?.subId) return ''; - if (!subSettings?.subClashEnable || !subSettings?.subClashURI) return ''; - return subSettings.subClashURI + client.subId; - }, [client?.subId, subSettings?.subClashEnable, subSettings?.subClashURI]); + const subId = client?.subId; + const subLink = subId && subSettings?.subURI ? subSettings.subURI + subId : ''; + const subJsonLink = + subId && subSettings?.subJsonEnable && subSettings?.subJsonURI + ? subSettings.subJsonURI + subId + : ''; + const subClashLink = + subId && subSettings?.subClashEnable && subSettings?.subClashURI + ? subSettings.subClashURI + subId + : ''; const showSubscription = !!(subSettings?.enable && client?.subId); const wgInbound = useMemo( diff --git a/frontend/src/pages/clients/ClientQrModal.tsx b/frontend/src/pages/clients/ClientQrModal.tsx index d8a06796b..d7c74d456 100644 --- a/frontend/src/pages/clients/ClientQrModal.tsx +++ b/frontend/src/pages/clients/ClientQrModal.tsx @@ -52,16 +52,13 @@ export default function ClientQrModal({ const [links, setLinks] = useState([]); const [loading, setLoading] = useState(false); - const subLink = useMemo(() => { - if (!client?.subId || !subSettings?.enable || !subSettings?.subURI) return ''; - return subSettings.subURI + client.subId; - }, [client?.subId, subSettings?.enable, subSettings?.subURI]); - - const subJsonLink = useMemo(() => { - if (!client?.subId || !subSettings?.enable) return ''; - if (!subSettings?.subJsonEnable || !subSettings?.subJsonURI) return ''; - return subSettings.subJsonURI + client.subId; - }, [client?.subId, subSettings?.enable, subSettings?.subJsonEnable, subSettings?.subJsonURI]); + const subId = client?.subId; + const subEnabled = !!subSettings?.enable; + const subLink = subId && subEnabled && subSettings?.subURI ? subSettings.subURI + subId : ''; + const subJsonLink = + subId && subEnabled && subSettings?.subJsonEnable && subSettings?.subJsonURI + ? subSettings.subJsonURI + subId + : ''; const wgInbound = useMemo( () => findWireguardInbound(client, inboundsById), @@ -79,13 +76,18 @@ export default function ClientQrModal({ const hasAnything = !!subLink || !!subJsonLink || !!wgConfigText || links.length > 0; + // The reset runs during render so the effect only carries the request. + const openSubId = open ? (client?.subId ?? '') : ''; + const [syncedSubId, setSyncedSubId] = useState(openSubId); + if (openSubId !== syncedSubId) { + setSyncedSubId(openSubId); + setLinks([]); + setLoading(!!openSubId); + } + useEffect(() => { - if (!open || !client?.subId) { - setLinks([]); - return; - } + if (!open || !client?.subId) return; let cancelled = false; - setLoading(true); (async () => { try { const msg = (await HttpUtil.get( @@ -166,13 +168,13 @@ export default function ClientQrModal({ return out; }, [subLink, subJsonLink, wgConfigText, links, client?.email, t]); - useEffect(() => { - if (!open) { - setActiveKey([]); - return; - } - setActiveKey(items.length > 0 ? [items[0].key] : []); - }, [open, items]); + // Expanding the first panel is a render-time adjustment, not a side effect. + const firstKey = open && items.length > 0 ? items[0].key : null; + const [syncedFirstKey, setSyncedFirstKey] = useState(null); + if (firstKey !== syncedFirstKey) { + setSyncedFirstKey(firstKey); + setActiveKey(firstKey ? [firstKey] : []); + } return ( { - if (!open) return; - setSelectedEmails([]); - setSearch(''); - }, [open]); + // React resets this during render rather than in an effect so the modal's + // first open frame already shows cleared fields. + const [wasOpen, setWasOpen] = useState(false); + if (open !== wasOpen) { + setWasOpen(open); + if (open) { + setSelectedEmails([]); + setSearch(''); + } + } const filteredRows = useMemo(() => { const q = search.trim().toLowerCase(); diff --git a/frontend/src/pages/groups/GroupRemoveClientsModal.tsx b/frontend/src/pages/groups/GroupRemoveClientsModal.tsx index 0ea75e651..4c63c82eb 100644 --- a/frontend/src/pages/groups/GroupRemoveClientsModal.tsx +++ b/frontend/src/pages/groups/GroupRemoveClientsModal.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Input, Modal, Space, Table, Tag, Typography, message } from 'antd'; import type { ColumnsType } from 'antd/es/table'; @@ -44,11 +44,16 @@ export default function GroupRemoveClientsModal({ [members], ); - useEffect(() => { - if (!open) return; - setSelectedEmails([]); - setSearch(''); - }, [open]); + // React resets this during render rather than in an effect so the modal's + // first open frame already shows cleared fields. + const [wasOpen, setWasOpen] = useState(false); + if (open !== wasOpen) { + setWasOpen(open); + if (open) { + setSelectedEmails([]); + setSearch(''); + } + } const filteredRows = useMemo(() => { const q = search.trim().toLowerCase(); diff --git a/frontend/src/pages/hosts/HostFormModal.tsx b/frontend/src/pages/hosts/HostFormModal.tsx index d215ea98c..427ad8abe 100644 --- a/frontend/src/pages/hosts/HostFormModal.tsx +++ b/frontend/src/pages/hosts/HostFormModal.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Form, Input, InputNumber, Modal, Select, Switch, Tabs, message } from 'antd'; import { @@ -94,12 +94,17 @@ export default function HostFormModal({ const showTls = security === 'tls' || security === 'reality'; const showTlsExtras = security === 'tls'; - useEffect(() => { + // React resets this during render rather than in an effect so the modal's + // first open frame already shows cleared fields. + const openHost = open ? host : null; + const [syncedHost, setSyncedHost] = useState(openHost); + if (openHost !== syncedHost) { + setSyncedHost(openHost); if (open) { methods.reset(defaultsFor(host)); setLoading(false); } - }, [open, host, methods]); + } const { nodes } = useNodesQuery(); diff --git a/frontend/src/pages/hosts/json-forms/HostFinalMaskForm.tsx b/frontend/src/pages/hosts/json-forms/HostFinalMaskForm.tsx index 89a7e94ab..e367f5a79 100644 --- a/frontend/src/pages/hosts/json-forms/HostFinalMaskForm.tsx +++ b/frontend/src/pages/hosts/json-forms/HostFinalMaskForm.tsx @@ -35,7 +35,9 @@ export default function HostFinalMaskForm({ const [form] = Form.useForm(); const [initial] = useState(() => parseFinalMask(value)); const onChangeRef = useRef(onChange); - onChangeRef.current = onChange; + useEffect(() => { + onChangeRef.current = onChange; + }); const finalmask = Form.useWatch('finalmask', form) as FinalMaskStreamSettings | undefined; diff --git a/frontend/src/pages/inbounds/clients/AttachClientsModal.tsx b/frontend/src/pages/inbounds/clients/AttachClientsModal.tsx index 9083f492e..db546a93b 100644 --- a/frontend/src/pages/inbounds/clients/AttachClientsModal.tsx +++ b/frontend/src/pages/inbounds/clients/AttachClientsModal.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Alert, Input, Modal, Select, Space, Table, Tag, Typography, message } from 'antd'; import type { ColumnsType } from 'antd/es/table'; @@ -57,14 +57,20 @@ export default function AttachClientsModal({ const [selectedEmails, setSelectedEmails] = useState([]); const [search, setSearch] = useState(''); - useEffect(() => { - if (!open) return; - const rows = source ? readClientRows(source.settings) : []; - setClientRows(rows); - setSelectedEmails(rows.map((r) => r.email)); - setTargetIds([]); - setSearch(''); - }, [open, source]); + // React resets this during render rather than in an effect so the modal's + // first open frame already shows cleared fields. + const openSource = open ? source : null; + const [syncedSource, setSyncedSource] = useState(openSource); + if (openSource !== syncedSource) { + setSyncedSource(openSource); + if (openSource) { + const rows = readClientRows(openSource.settings); + setClientRows(rows); + setSelectedEmails(rows.map((r) => r.email)); + setTargetIds([]); + setSearch(''); + } + } const targetOptions = useMemo(() => { if (!source) return []; diff --git a/frontend/src/pages/inbounds/clients/AttachExistingClientsModal.tsx b/frontend/src/pages/inbounds/clients/AttachExistingClientsModal.tsx index 124a73335..cda9c9cc9 100644 --- a/frontend/src/pages/inbounds/clients/AttachExistingClientsModal.tsx +++ b/frontend/src/pages/inbounds/clients/AttachExistingClientsModal.tsx @@ -49,12 +49,21 @@ export default function AttachExistingClientsModal({ const [search, setSearch] = useState(''); const [groupFilter, setGroupFilter] = useState(undefined); + // Reset during render, not in an effect, so the first frame is already clean. + const openTarget = open ? target : null; + const [syncedTarget, setSyncedTarget] = useState(openTarget); + if (openTarget !== syncedTarget) { + setSyncedTarget(openTarget); + if (openTarget) { + setLoading(true); + setSearch(''); + setGroupFilter(undefined); + } + } + useEffect(() => { if (!open || !target) return; let cancelled = false; - setLoading(true); - setSearch(''); - setGroupFilter(undefined); HttpUtil.get('/panel/api/clients/list', undefined, { silent: true }) .then((msg) => { if (cancelled) return; diff --git a/frontend/src/pages/inbounds/clients/DetachClientsModal.tsx b/frontend/src/pages/inbounds/clients/DetachClientsModal.tsx index 5efabfde7..7dee9625f 100644 --- a/frontend/src/pages/inbounds/clients/DetachClientsModal.tsx +++ b/frontend/src/pages/inbounds/clients/DetachClientsModal.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Input, Modal, Space, Table, Tag, Typography, message } from 'antd'; import type { ColumnsType } from 'antd/es/table'; @@ -52,13 +52,17 @@ export default function DetachClientsModal({ const [selectedEmails, setSelectedEmails] = useState([]); const [search, setSearch] = useState(''); - useEffect(() => { - if (!open) return; - const rows = source ? readClientRows(source.settings) : []; - setClientRows(rows); - setSelectedEmails([]); - setSearch(''); - }, [open, source]); + // Reset during render, not in an effect, so the first frame is already clean. + const openSource = open ? source : null; + const [syncedSource, setSyncedSource] = useState(openSource); + if (openSource !== syncedSource) { + setSyncedSource(openSource); + if (openSource) { + setClientRows(readClientRows(openSource.settings)); + setSelectedEmails([]); + setSearch(''); + } + } const filteredRows = useMemo(() => { const q = search.trim().toLowerCase(); diff --git a/frontend/src/pages/inbounds/form/protocols/vless.tsx b/frontend/src/pages/inbounds/form/protocols/vless.tsx index 6821f73f9..628e364e6 100644 --- a/frontend/src/pages/inbounds/form/protocols/vless.tsx +++ b/frontend/src/pages/inbounds/form/protocols/vless.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Button, Form, Input, InputNumber, Select, Space, Typography } from 'antd'; import { Controller, useFormContext } from 'react-hook-form'; @@ -29,9 +29,11 @@ export default function VlessFields({ const { control } = useFormContext(); const [authKind, setAuthKind] = useState(vlessAuthKind ?? 'x25519'); - useEffect(() => { + const [syncedAuthKind, setSyncedAuthKind] = useState(vlessAuthKind); + if (vlessAuthKind !== syncedAuthKind) { + setSyncedAuthKind(vlessAuthKind); setAuthKind(vlessAuthKind ?? 'x25519'); - }, [vlessAuthKind]); + } const authOptions = (Object.entries(VLESS_AUTH_LABEL_KEYS) as [VlessAuthKind, string][]).map( ([value, labelKey]) => ({ value, label: t(labelKey) }), diff --git a/frontend/src/pages/inbounds/form/security/RealityTargetScannerModal.tsx b/frontend/src/pages/inbounds/form/security/RealityTargetScannerModal.tsx index ef727091a..b747a999e 100644 --- a/frontend/src/pages/inbounds/form/security/RealityTargetScannerModal.tsx +++ b/frontend/src/pages/inbounds/form/security/RealityTargetScannerModal.tsx @@ -23,10 +23,11 @@ export default function RealityTargetScannerModal({ const [query, setQuery] = useState(''); const [results, setResults] = useState([]); const scanRef = useRef(scanRealityCandidates); - scanRef.current = scanRealityCandidates; + useEffect(() => { + scanRef.current = scanRealityCandidates; + }); - const runScan = useCallback(async (targets?: string) => { - setLoading(true); + const applyScan = useCallback(async (targets?: string) => { try { setResults(await scanRef.current(targets)); } finally { @@ -34,11 +35,29 @@ export default function RealityTargetScannerModal({ } }, []); + const runScan = useCallback( + (targets?: string) => { + setLoading(true); + setResults([]); + void applyScan(targets); + }, + [applyScan], + ); + + // Clearing the previous results is done during render so the auto-scan effect + // carries only the request itself. + const [scannedOpen, setScannedOpen] = useState(false); + if (open !== scannedOpen) { + setScannedOpen(open); + if (open) { + setResults([]); + setLoading(true); + } + } + useEffect(() => { - if (!open) return; - setResults([]); - runScan(); - }, [open, runScan]); + if (open) void applyScan(); + }, [open, applyScan]); const columns: ColumnsType = [ { diff --git a/frontend/src/pages/inbounds/info/InboundInfoModal.tsx b/frontend/src/pages/inbounds/info/InboundInfoModal.tsx index 0e8ecccdb..0e9305042 100644 --- a/frontend/src/pages/inbounds/info/InboundInfoModal.tsx +++ b/frontend/src/pages/inbounds/info/InboundInfoModal.tsx @@ -99,8 +99,26 @@ export default function InboundInfoModal({ } }, [clientStats, t]); - useEffect(() => { - if (!open || !dbInbound) return; + // The panel's contents are a pure function of the props, so they are adopted + // during render; only the IP lookup below stays asynchronous. + const [syncedProps, setSyncedProps] = useState<{ + dbInbound: typeof dbInbound; + clientIndex: typeof clientIndex; + nodeAddress: typeof nodeAddress; + subSettings: typeof subSettings; + ipLimitEnable: typeof ipLimitEnable; + } | null>(null); + if ( + open && + dbInbound && + (syncedProps === null || + syncedProps.dbInbound !== dbInbound || + syncedProps.clientIndex !== clientIndex || + syncedProps.nodeAddress !== nodeAddress || + syncedProps.subSettings !== subSettings || + syncedProps.ipLimitEnable !== ipLimitEnable) + ) { + setSyncedProps({ dbInbound, clientIndex, nodeAddress, subSettings, ipLimitEnable }); const info = buildInboundInfo(dbInbound); setInbound(info); setActiveTab(info.clients.length > 0 ? 'client' : 'inbound'); @@ -189,7 +207,16 @@ export default function InboundInfoModal({ } }); } - }, [open, dbInbound, clientIndex, nodeAddress, subSettings, ipLimitEnable, t]); + } + + // The expiry tag colours against the current time; a state-backed clock keeps + // render pure and still refreshes the tag while the modal stays open. + const [now, setNow] = useState(() => Date.now()); + useEffect(() => { + if (!open) return; + const id = window.setInterval(() => setNow(Date.now()), 60_000); + return () => window.clearInterval(id); + }, [open]); const isEnable = useMemo(() => { if (clientSettings) return !!clientSettings.enable; @@ -202,9 +229,9 @@ export default function InboundInfoModal({ const used = (clientStats.up ?? 0) + (clientStats.down ?? 0); if (total > 0 && used >= total) return true; const expiry = clientSettings.expiryTime ?? 0; - if (expiry > 0 && Date.now() >= expiry) return true; + if (expiry > 0 && now >= expiry) return true; return false; - }, [clientStats, clientSettings]); + }, [clientStats, clientSettings, now]); const remainingStats = useMemo(() => { if (!clientStats || !clientSettings) return '-'; @@ -212,10 +239,12 @@ export default function InboundInfoModal({ return remained > 0 ? SizeFormatter.sizeFormat(remained) : '-'; }, [clientStats, clientSettings]); - const wgPubKey = useMemo(() => { - if (!dbInbound?.isWireguard || !inbound?.settings?.secretKey) return ''; - return Wireguard.generateKeypair(inbound.settings.secretKey as string).publicKey; - }, [dbInbound?.isWireguard, inbound?.settings?.secretKey]); + const isWireguard = !!dbInbound?.isWireguard; + const wgSecretKey = inbound?.settings?.secretKey as string | undefined; + const wgPubKey = useMemo( + () => (isWireguard && wgSecretKey ? Wireguard.generateKeypair(wgSecretKey).publicKey : ''), + [isWireguard, wgSecretKey], + ); const formatLastOnline = useCallback( (email: string) => { @@ -438,9 +467,7 @@ export default function InboundInfoModal({ {(clientSettings?.expiryTime ?? 0) > 0 ? ( - + {IntlUtil.formatDate(clientSettings!.expiryTime!, datepicker)} ) : (clientSettings?.expiryTime ?? 0) < 0 ? ( diff --git a/frontend/src/pages/inbounds/list/InboundStatsModal.tsx b/frontend/src/pages/inbounds/list/InboundStatsModal.tsx index 9576f9fae..27f638262 100644 --- a/frontend/src/pages/inbounds/list/InboundStatsModal.tsx +++ b/frontend/src/pages/inbounds/list/InboundStatsModal.tsx @@ -1,3 +1,4 @@ +import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Modal, Tag } from 'antd'; @@ -40,6 +41,15 @@ export default function InboundStatsModal({ onClose, }: InboundStatsModalProps) { const { t } = useTranslation(); + // The expiry tag colours against the current time; a state-backed clock keeps + // render pure and still refreshes the tag while the modal stays open. + const [now, setNow] = useState(() => Date.now()); + useEffect(() => { + if (!open) return; + const id = window.setInterval(() => setNow(Date.now()), 60_000); + return () => window.clearInterval(id); + }, [open]); + return ( {t('pages.inbounds.expireDate')} {record.expiryTime > 0 ? ( - + {IntlUtil.formatRelativeTime(record.expiryTime)} ) : ( diff --git a/frontend/src/pages/inbounds/qr/QrCodeModal.tsx b/frontend/src/pages/inbounds/qr/QrCodeModal.tsx index aed641c04..2834eeb46 100644 --- a/frontend/src/pages/inbounds/qr/QrCodeModal.tsx +++ b/frontend/src/pages/inbounds/qr/QrCodeModal.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Collapse, Modal } from 'antd'; import type { CollapseProps } from 'antd'; @@ -54,8 +54,24 @@ export default function QrCodeModal({ const [subJsonLink, setSubJsonLink] = useState(''); const [activeKey, setActiveKey] = useState([]); - useEffect(() => { - if (!open || !dbInbound) return; + // Building the links is a pure function of the props, so it runs during + // render; an effect would paint the previous inbound's QR first. + const [syncedProps, setSyncedProps] = useState<{ + dbInbound: typeof dbInbound; + client: typeof client; + nodeAddress: typeof nodeAddress; + subSettings: typeof subSettings; + } | null>(null); + if ( + open && + dbInbound && + (syncedProps === null || + syncedProps.dbInbound !== dbInbound || + syncedProps.client !== client || + syncedProps.nodeAddress !== nodeAddress || + syncedProps.subSettings !== subSettings) + ) { + setSyncedProps({ dbInbound, client, nodeAddress, subSettings }); const inbound = inboundFromDb(dbInbound); const fallbackHostname = preferPublicHost( window.location.hostname, @@ -105,7 +121,7 @@ export default function QrCodeModal({ } setSubLink(nextSub); setSubJsonLink(nextSubJson); - }, [open, dbInbound, client, nodeAddress, subSettings]); + } const qrItems = useMemo(() => { const items: QrItem[] = []; @@ -158,13 +174,12 @@ export default function QrCodeModal({ [qrItems], ); - useEffect(() => { - if (!open) { - setActiveKey([]); - return; - } - setActiveKey(qrItems.length > 0 ? [qrItems[0].key] : []); - }, [open, qrItems]); + const firstKey = open && qrItems.length > 0 ? qrItems[0].key : null; + const [syncedFirstKey, setSyncedFirstKey] = useState(null); + if (firstKey !== syncedFirstKey) { + setSyncedFirstKey(firstKey); + setActiveKey(firstKey ? [firstKey] : []); + } return ( activateOnKey(copyImage)(event)} > ([]); const dbInboundsRef = useRef([]); - dbInboundsRef.current = dbInbounds; - - const [clientCount, setClientCount] = useState>({}); - const [statsVersion, setStatsVersion] = useState(0); + useEffect(() => { + dbInboundsRef.current = dbInbounds; + }); const [inboundSpeed, setInboundSpeed] = useState>(() => Date.now() - inboundSpeedCache.at < SPEED_CACHE_TTL_MS ? inboundSpeedCache.data : {}, @@ -226,20 +220,18 @@ export function useInbounds() { }, [inboundSpeed]); const [onlineClients, setOnlineClients] = useState([]); - const onlineClientsRef = useRef([]); - onlineClientsRef.current = onlineClients; // Online emails keyed by the hosting node's panelGuid. The rollup reads this // so each inbound only counts clients online on the node that physically // hosts it, attributing a sub-node's clients to that sub-node (#4983). - const onlineByGuidRef = useRef>>(new Map()); + const [onlineByGuid, setOnlineByGuid] = useState>>(() => new Map()); // Recently-active inbound tags keyed by the hosting node's panelGuid. A GUID // missing from this map means "no per-inbound activity reported" (e.g. remote // nodes), so the rollup leaves that node's inbounds ungated and falls back to // the email signal. A present GUID gates: a client only counts online on an // inbound whose tag carried traffic this window. - const activeByGuidRef = useRef>>(new Map()); + const [activeByGuid, setActiveByGuid] = useState>>(() => new Map()); const [lastOnlineMap, setLastOnlineMap] = useState>({}); @@ -276,12 +268,12 @@ export function useInbounds() { // the master-local synthetic id for an old-build node without one (#4983). const guid = dbInbound.originNodeGuid || (dbInbound.nodeId != null ? `node:${dbInbound.nodeId}` : ''); - const nodeOnline = onlineByGuidRef.current.get(guid); + const nodeOnline = onlineByGuid.get(guid); // A node absent from the active map reports no per-inbound activity, so // leave its inbounds ungated. When present, only mark a client online on // this inbound if its tag actually carried traffic — that's what stops a // multi-inbound client lighting up every inbound it's attached to. - const activeForNode = activeByGuidRef.current.get(guid); + const activeForNode = activeByGuid.get(guid); const inboundActive = activeForNode === undefined || !dbInbound.tag || activeForNode.has(dbInbound.tag); @@ -312,8 +304,8 @@ export function useInbounds() { if (inboundActive && nodeOnline?.has(client.email)) online.push(client.email); if (stats) { const expiringSoon = - (stats.expiryTime > 0 && stats.expiryTime - now < expireDiffRef.current) || - (stats.total > 0 && stats.total - (stats.up + stats.down) < trafficDiffRef.current); + (stats.expiryTime > 0 && stats.expiryTime - now < expireDiff) || + (stats.total > 0 && stats.total - (stats.up + stats.down) < trafficDiff); if (expiringSoon) expiring.push(client.email); } } @@ -333,12 +325,14 @@ export function useInbounds() { comments, }; }, - [], + [onlineByGuid, activeByGuid, expireDiff, trafficDiff], ); - const rebuildClientCount = useCallback(() => { + // Every write to a DBInbound row also replaces the dbInbounds array, so this + // recomputes on both a refetch and a WS-merged stats update. + const clientCount = useMemo(() => { const counts: Record = {}; - for (const dbInbound of dbInboundsRef.current) { + for (const dbInbound of dbInbounds) { const protocol = dbInbound.protocol; if (!TRACKED_PROTOCOLS.includes(protocol)) continue; const settings = coerceInboundJsonField(dbInbound.settings) as { @@ -348,60 +342,44 @@ export function useInbounds() { if (protocol === Protocols.SHADOWSOCKS && !isSSMultiUser({ protocol, settings })) continue; counts[dbInbound.id] = rollupClients(dbInbound, { clients: settings.clients }); } - setClientCount(counts); - }, [rollupClients]); + return counts; + }, [dbInbounds, rollupClients]); - // Seed dbInbounds + clientCount from the slim query. Runs on first fetch and - // again every time the query refetches (e.g. invalidate from WS bridge). - useEffect(() => { - if (!slimQuery.data) return; - const next: DBInboundInstance[] = []; - const counts: Record = {}; - for (const row of slimQuery.data as { protocol: string; id: number }[]) { - const dbInbound = new DBInbound(row) as DBInboundInstance; - next.push(dbInbound); - if (TRACKED_PROTOCOLS.includes(row.protocol)) { - const settings = coerceInboundJsonField(dbInbound.settings) as { - method?: string; - clients?: Array<{ email?: string; enable?: boolean; comment?: string }>; - }; - if ( - row.protocol === Protocols.SHADOWSOCKS && - !isSSMultiUser({ protocol: row.protocol, settings }) - ) - continue; - counts[row.id] = rollupClients(dbInbound, { clients: settings.clients }); - } - } - dbInboundsRef.current = next; - setDbInbounds(next); - setClientCount(counts); - }, [slimQuery.data, rollupClients]); + // Adopting fetched data during render (rather than in an effect) keeps the + // list from painting one frame of the previous data after a refetch. + const [syncedSlim, setSyncedSlim] = useState(); + if (slimQuery.data && slimQuery.data !== syncedSlim) { + setSyncedSlim(slimQuery.data); + setDbInbounds( + (slimQuery.data as { protocol: string; id: number }[]).map( + (row) => new DBInbound(row) as DBInboundInstance, + ), + ); + } - useEffect(() => { - if (onlinesQuery.data) { - onlineClientsRef.current = onlinesQuery.data; - setOnlineClients(onlinesQuery.data); - } - }, [onlinesQuery.data]); + const [syncedOnlines, setSyncedOnlines] = useState(); + if (onlinesQuery.data && onlinesQuery.data !== syncedOnlines) { + setSyncedOnlines(onlinesQuery.data); + setOnlineClients(onlinesQuery.data); + } - useEffect(() => { - if (onlinesByGuidQuery.data) { - onlineByGuidRef.current = toGuidOnlineMap(onlinesByGuidQuery.data); - rebuildClientCount(); - } - }, [onlinesByGuidQuery.data, rebuildClientCount]); + const [syncedOnlinesByGuid, setSyncedOnlinesByGuid] = useState(); + if (onlinesByGuidQuery.data && onlinesByGuidQuery.data !== syncedOnlinesByGuid) { + setSyncedOnlinesByGuid(onlinesByGuidQuery.data); + setOnlineByGuid(toGuidOnlineMap(onlinesByGuidQuery.data)); + } - useEffect(() => { - if (activeInboundsQuery.data) { - activeByGuidRef.current = toGuidOnlineMap(activeInboundsQuery.data); - rebuildClientCount(); - } - }, [activeInboundsQuery.data, rebuildClientCount]); + const [syncedActiveInbounds, setSyncedActiveInbounds] = useState(); + if (activeInboundsQuery.data && activeInboundsQuery.data !== syncedActiveInbounds) { + setSyncedActiveInbounds(activeInboundsQuery.data); + setActiveByGuid(toGuidOnlineMap(activeInboundsQuery.data)); + } - useEffect(() => { - if (lastOnlineQuery.data) setLastOnlineMap(lastOnlineQuery.data); - }, [lastOnlineQuery.data]); + const [syncedLastOnline, setSyncedLastOnline] = useState(); + if (lastOnlineQuery.data && lastOnlineQuery.data !== syncedLastOnline) { + setSyncedLastOnline(lastOnlineQuery.data); + setLastOnlineMap(lastOnlineQuery.data); + } const fetched = (slimQuery.data !== undefined || slimQuery.isError) && @@ -430,185 +408,161 @@ export function useInbounds() { // uuid/password/flow/etc.) and swaps it into the cached list. Use this // before opening edit / info / qr / export / clone flows — refresh() loads // the slim list which doesn't carry per-client secrets. - const hydrateInbound = useCallback( - async (id: number) => { - const msg = await HttpUtil.get(`/panel/api/inbounds/get/${id}`); - if (!msg?.success || !msg.obj) return null; - const validated = parseMsg(msg, InboundDetailSchema, `inbounds/get/${id}`); - if (!validated.obj) return null; - const dbInbound = new DBInbound(validated.obj) as DBInboundInstance; - setDbInbounds((prev) => { - const next = prev.map((row) => - (row as unknown as { id: number }).id === id ? dbInbound : row, - ); - dbInboundsRef.current = next; + const hydrateInbound = useCallback(async (id: number) => { + const msg = await HttpUtil.get(`/panel/api/inbounds/get/${id}`); + if (!msg?.success || !msg.obj) return null; + const validated = parseMsg(msg, InboundDetailSchema, `inbounds/get/${id}`); + if (!validated.obj) return null; + const dbInbound = new DBInbound(validated.obj) as DBInboundInstance; + setDbInbounds((prev) => { + const next = prev.map((row) => + (row as unknown as { id: number }).id === id ? dbInbound : row, + ); + dbInboundsRef.current = next; + return next; + }); + return dbInbound; + }, []); + + const applyTrafficEvent = useCallback((payload: unknown) => { + if (!payload || typeof payload !== 'object') return; + const p = payload as { + traffics?: TrafficDelta[]; + nodeTraffics?: TrafficDelta[]; + onlineClients?: string[]; + onlineByGuid?: Record; + activeInbounds?: Record; + lastOnlineMap?: Record; + }; + if (Array.isArray(p.onlineClients)) { + setOnlineClients(p.onlineClients); + } + if (p.onlineByGuid && typeof p.onlineByGuid === 'object') { + setOnlineByGuid(toGuidOnlineMap(p.onlineByGuid)); + } + if (p.activeInbounds && typeof p.activeInbounds === 'object') { + setActiveByGuid(toGuidOnlineMap(p.activeInbounds)); + } + if (p.lastOnlineMap && typeof p.lastOnlineMap === 'object') { + setLastOnlineMap((prev) => ({ ...prev, ...p.lastOnlineMap! })); + } + // Speed arrives from two independent 5s polls: the local Xray poll sends + // `traffics` (local inbounds) and the node sync sends `nodeTraffics` (node + // inbounds). Each replaces speed only within its own scope so the two don't + // clobber each other; an idle in-scope inbound — absent from its payload — + // clears instead of showing a stale value. + const applyTraffics = ( + traffics: TrafficDelta[], + inScope: (ib: DBInboundInstance) => boolean, + ) => { + const byTag = new Map(); + for (const tr of traffics) { + if (!tr || typeof tr.Tag !== 'string') continue; + if (tr.IsInbound === false) continue; + byTag.set(tr.Tag, tr); + } + setInboundSpeed((prev) => { + const next = { ...prev }; + for (const ib of dbInboundsRef.current) { + if (!inScope(ib)) continue; + const delta = byTag.get(ib.tag); + if (delta) { + next[ib.id] = { + up: (delta.Up || 0) / TRAFFIC_POLL_INTERVAL_S, + down: (delta.Down || 0) / TRAFFIC_POLL_INTERVAL_S, + }; + } else { + delete next[ib.id]; + } + } return next; }); - rebuildClientCount(); - return dbInbound; - }, - [rebuildClientCount], - ); + }; + if (Array.isArray(p.traffics)) applyTraffics(p.traffics, (ib) => ib.nodeId == null); + if (Array.isArray(p.nodeTraffics)) applyTraffics(p.nodeTraffics, (ib) => ib.nodeId != null); + }, []); - const applyTrafficEvent = useCallback( - (payload: unknown) => { - if (!payload || typeof payload !== 'object') return; - const p = payload as { - traffics?: TrafficDelta[]; - nodeTraffics?: TrafficDelta[]; - onlineClients?: string[]; - onlineByGuid?: Record; - activeInbounds?: Record; - lastOnlineMap?: Record; - }; - if (Array.isArray(p.onlineClients)) { - onlineClientsRef.current = p.onlineClients; - setOnlineClients(p.onlineClients); - } - if (p.onlineByGuid && typeof p.onlineByGuid === 'object') { - onlineByGuidRef.current = toGuidOnlineMap(p.onlineByGuid); - } - if (p.activeInbounds && typeof p.activeInbounds === 'object') { - activeByGuidRef.current = toGuidOnlineMap(p.activeInbounds); - } - if (p.lastOnlineMap && typeof p.lastOnlineMap === 'object') { - setLastOnlineMap((prev) => ({ ...prev, ...p.lastOnlineMap! })); - } - // Speed arrives from two independent 5s polls: the local Xray poll sends - // `traffics` (local inbounds) and the node sync sends `nodeTraffics` (node - // inbounds). Each replaces speed only within its own scope so the two don't - // clobber each other; an idle in-scope inbound — absent from its payload — - // clears instead of showing a stale value. - const applyTraffics = ( - traffics: TrafficDelta[], - inScope: (ib: DBInboundInstance) => boolean, - ) => { - const byTag = new Map(); - for (const tr of traffics) { - if (!tr || typeof tr.Tag !== 'string') continue; - if (tr.IsInbound === false) continue; - byTag.set(tr.Tag, tr); - } - setInboundSpeed((prev) => { - const next = { ...prev }; - for (const ib of dbInboundsRef.current) { - if (!inScope(ib)) continue; - const delta = byTag.get(ib.tag); - if (delta) { - next[ib.id] = { - up: (delta.Up || 0) / TRAFFIC_POLL_INTERVAL_S, - down: (delta.Down || 0) / TRAFFIC_POLL_INTERVAL_S, - }; - } else { - delete next[ib.id]; - } - } - return next; - }); - }; - if (Array.isArray(p.traffics)) applyTraffics(p.traffics, (ib) => ib.nodeId == null); - if (Array.isArray(p.nodeTraffics)) applyTraffics(p.nodeTraffics, (ib) => ib.nodeId != null); - rebuildClientCount(); - }, - [rebuildClientCount], - ); + const applyClientStatsEvent = useCallback((payload: unknown) => { + if (!payload || typeof payload !== 'object') return; + const p = payload as { + inbounds?: { id: number; up?: number; down?: number; total?: number; enable?: boolean }[]; + clients?: { + email: string; + up?: number; + down?: number; + total?: number; + expiryTime?: number; + enable?: boolean; + }[]; + }; - const applyClientStatsEvent = useCallback( - (payload: unknown) => { - if (!payload || typeof payload !== 'object') return; - const p = payload as { - inbounds?: { id: number; up?: number; down?: number; total?: number; enable?: boolean }[]; - clients?: { - email: string; - up?: number; - down?: number; - total?: number; - expiryTime?: number; - enable?: boolean; - }[]; - }; - let touched = false; - - if (Array.isArray(p.inbounds) && p.inbounds.length > 0) { - const byId = new Map< - number, - { id: number; up?: number; down?: number; total?: number; enable?: boolean } - >(); - for (const row of p.inbounds) { - if (row && row.id != null) byId.set(row.id, row); - } - for (const ib of dbInboundsRef.current) { - const upd = byId.get((ib as unknown as { id: number }).id); - if (!upd) continue; - const ibRec = ib as unknown as { - up: number; - down: number; - total: number; - enable: boolean; - }; - if (typeof upd.up === 'number') ibRec.up = upd.up; - if (typeof upd.down === 'number') ibRec.down = upd.down; - if (typeof upd.total === 'number') ibRec.total = upd.total; - if (typeof upd.enable === 'boolean') ibRec.enable = upd.enable; - touched = true; - } + const byId = new Map< + number, + { id: number; up?: number; down?: number; total?: number; enable?: boolean } + >(); + if (Array.isArray(p.inbounds)) { + for (const row of p.inbounds) { + if (row && row.id != null) byId.set(row.id, row); } - - if (Array.isArray(p.clients) && p.clients.length > 0) { - const byEmail = new Map< - string, - { - email: string; - up?: number; - down?: number; - total?: number; - expiryTime?: number; - enable?: boolean; - } - >(); - for (const row of p.clients) { - if (row && row.email) byEmail.set(row.email, row); - } - for (const ib of dbInboundsRef.current) { - const stats = ( - ib as unknown as { - clientStats: { - email: string; - up: number; - down: number; - total: number; - expiryTime: number; - enable: boolean; - }[]; - } - ).clientStats; - if (!Array.isArray(stats)) continue; - for (let i = 0; i < stats.length; i++) { - const stat = stats[i]; - const upd = byEmail.get(stat.email); - if (!upd) continue; - if (typeof upd.up === 'number') stat.up = upd.up; - if (typeof upd.down === 'number') stat.down = upd.down; - if (typeof upd.total === 'number') stat.total = upd.total; - if (typeof upd.expiryTime === 'number') stat.expiryTime = upd.expiryTime; - if (typeof upd.enable === 'boolean') stat.enable = upd.enable; - touched = true; - } - } + } + const byEmail = new Map< + string, + { + email: string; + up?: number; + down?: number; + total?: number; + expiryTime?: number; + enable?: boolean; } - - if (touched) { - setStatsVersion((v) => v + 1); - setDbInbounds((prev) => { - const next = [...prev]; - dbInboundsRef.current = next; - return next; - }); - rebuildClientCount(); + >(); + if (Array.isArray(p.clients)) { + for (const row of p.clients) { + if (row && row.email) byEmail.set(row.email, row); } - }, - [rebuildClientCount], - ); + } + if (byId.size === 0 && byEmail.size === 0) return; + + // Rows carrying an update are rebuilt rather than patched in place: the + // derived clientCount only recomputes when a row's identity changes. + let touched = false; + const next = dbInboundsRef.current.map((ib) => { + const upd = byId.get(ib.id); + const stats = Array.isArray(ib.clientStats) ? ib.clientStats : null; + let statsTouched = false; + const nextStats = + stats && byEmail.size > 0 + ? stats.map((stat) => { + const su = byEmail.get(stat.email); + if (!su) return stat; + statsTouched = true; + return { + ...stat, + up: typeof su.up === 'number' ? su.up : stat.up, + down: typeof su.down === 'number' ? su.down : stat.down, + total: typeof su.total === 'number' ? su.total : stat.total, + expiryTime: typeof su.expiryTime === 'number' ? su.expiryTime : stat.expiryTime, + enable: typeof su.enable === 'boolean' ? su.enable : stat.enable, + } as ClientStats; + }) + : null; + if (!upd && !statsTouched) return ib; + touched = true; + const row = new DBInbound(ib as DBInboundInit) as DBInboundInstance; + if (upd) { + if (typeof upd.up === 'number') row.up = upd.up; + if (typeof upd.down === 'number') row.down = upd.down; + if (typeof upd.total === 'number') row.total = upd.total; + if (typeof upd.enable === 'boolean') row.enable = upd.enable; + } + if (statsTouched && nextStats) row.clientStats = nextStats; + return row; + }); + + if (!touched) return; + dbInboundsRef.current = next; + setDbInbounds(next); + }, []); const totals = useMemo(() => { let up = 0; @@ -629,7 +583,6 @@ export function useInbounds() { onlineClients, lastOnlineMap, inboundSpeed, - statsVersion, totals, expireDiff, trafficDiff, diff --git a/frontend/src/pages/index/GeodataSection.tsx b/frontend/src/pages/index/GeodataSection.tsx index 186a5f2b5..26839ad1e 100644 --- a/frontend/src/pages/index/GeodataSection.tsx +++ b/frontend/src/pages/index/GeodataSection.tsx @@ -38,21 +38,20 @@ export default function GeodataSection({ active, onBusy, onClose }: GeodataSecti const [outbound, setOutbound] = useState(undefined); const [rows, setRows] = useState([]); const [outboundTags, setOutboundTags] = useState([]); - const templateRef = useRef | null>(null); + const [template, setTemplate] = useState | null>(null); const outboundTestUrlRef = useRef(''); const load = useCallback(async () => { - setLoading(true); try { const msg = await HttpUtil.post('/panel/api/xray/', undefined, { silent: true }); if (!msg?.success || typeof msg.obj !== 'string') return; const payload = JSON.parse(msg.obj) as Record; - const template = (payload.xraySetting || {}) as Record; - templateRef.current = template; + const next = (payload.xraySetting || {}) as Record; + setTemplate(next); outboundTestUrlRef.current = typeof payload.outboundTestUrl === 'string' ? payload.outboundTestUrl : ''; - const geodata = (template.geodata || {}) as Record; + const geodata = (next.geodata || {}) as Record; const assets = Array.isArray(geodata.assets) ? geodata.assets : []; setRows( assets @@ -67,7 +66,7 @@ export default function GeodataSection({ active, onBusy, onClose }: GeodataSecti // Download outbound candidates: template outbounds + subscription outbounds. // Skip blackhole outbounds — routing a download through one just drops it. const tags = new Set(); - const outbounds = Array.isArray(template.outbounds) ? template.outbounds : []; + const outbounds = Array.isArray(next.outbounds) ? next.outbounds : []; for (const o of outbounds) { if (!o || typeof o !== 'object') continue; const rec = o as Record; @@ -87,8 +86,14 @@ export default function GeodataSection({ active, onBusy, onClose }: GeodataSecti } }, []); + const [wasActive, setWasActive] = useState(false); + if (active !== wasActive) { + setWasActive(active); + if (active) setLoading(true); + } + useEffect(() => { - if (active) load(); + if (active) void load(); }, [active, load]); function setRow(index: number, patch: Partial) { @@ -102,7 +107,6 @@ export default function GeodataSection({ active, onBusy, onClose }: GeodataSecti } function save() { - const template = templateRef.current; if (!template) return; const assets = rows .map((r) => ({ url: r.url.trim(), file: r.file.trim() })) @@ -213,7 +217,7 @@ export default function GeodataSection({ active, onBusy, onClose }: GeodataSecti > {t('pages.index.geodataAddFile')} - diff --git a/frontend/src/pages/index/LogModal.tsx b/frontend/src/pages/index/LogModal.tsx index be446190a..4ebb0f854 100644 --- a/frontend/src/pages/index/LogModal.tsx +++ b/frontend/src/pages/index/LogModal.tsx @@ -25,10 +25,8 @@ export default function LogModal({ open, onClose }: LogModalProps) { const [autoUpdate, setAutoUpdate] = useState(false); const [loading, setLoading] = useState(false); const [logs, setLogs] = useState([]); - const openRef = useRef(open); - const refresh = useCallback(async () => { - setLoading(true); + const runRefresh = useCallback(async () => { try { const msg = await HttpUtil.post(`/panel/api/server/logs/${rows}`, { level, @@ -43,19 +41,28 @@ export default function LogModal({ open, onClose }: LogModalProps) { } }, [rows, level, syslog]); + const refresh = useCallback(() => { + setLoading(true); + void runRefresh(); + }, [runRefresh]); + const refreshRef = useRef(refresh); useEffect(() => { refreshRef.current = refresh; - }, [refresh]); + }); + + // The spinner is raised during render so the fetch effect stays side-effect + // free until its response lands. + const refreshKey = open ? `${rows}\u0000${level}\u0000${syslog}` : null; + const [loadingKey, setLoadingKey] = useState(null); + if (refreshKey !== loadingKey) { + setLoadingKey(refreshKey); + if (refreshKey) setLoading(true); + } useEffect(() => { - openRef.current = open; - if (open) refresh(); - }, [open, refresh]); - - useEffect(() => { - if (openRef.current) refresh(); - }, [rows, level, syslog, refresh]); + if (open) void runRefresh(); + }, [open, runRefresh]); useEffect(() => { if (!open || !autoUpdate) return; diff --git a/frontend/src/pages/index/SystemHistoryModal.tsx b/frontend/src/pages/index/SystemHistoryModal.tsx index 2fa8eb7db..bcc4b4465 100644 --- a/frontend/src/pages/index/SystemHistoryModal.tsx +++ b/frontend/src/pages/index/SystemHistoryModal.tsx @@ -200,16 +200,74 @@ function formatFullTimestamp(unixSec: number): string { return `${MM}-${DD} ${time}`; } +interface HistoryChart { + points: number[]; + points2: number[]; + points3: number[]; + labels: string[]; + timestamps: number[]; +} + +const EMPTY_CHART: HistoryChart = { + points: [], + points2: [], + points3: [], + labels: [], + timestamps: [], +}; + +async function loadBucket(metric: (typeof METRICS)[number], bucket: number): Promise { + try { + const msg = await HttpUtil.get(`/panel/api/server/history/${metric.key}/${bucket}`); + if (!msg?.success || !Array.isArray(msg.obj)) return EMPTY_CHART; + const points: number[] = []; + const labels: string[] = []; + const timestamps: number[] = []; + for (const p of msg.obj) { + const d = new Date(p.t * 1000); + const MM = String(d.getMonth() + 1).padStart(2, '0'); + const DD = String(d.getDate()).padStart(2, '0'); + const hh = String(d.getHours()).padStart(2, '0'); + const mm = String(d.getMinutes()).padStart(2, '0'); + const ss = String(d.getSeconds()).padStart(2, '0'); + labels.push( + bucket >= 2880 + ? `${MM}-${DD} ${hh}:${mm}` + : bucket >= 60 + ? `${hh}:${mm}` + : `${hh}:${mm}:${ss}`, + ); + points.push(Number(p.v) || 0); + timestamps.push(Number(p.t) || 0); + } + const fetchAligned = async (key?: string): Promise => { + if (!key) return []; + const m = await HttpUtil.get(`/panel/api/server/history/${key}/${bucket}`); + if (!m?.success || !Array.isArray(m.obj)) return []; + const byTs = new Map(); + for (const p of m.obj) byTs.set(Number(p.t) || 0, Number(p.v) || 0); + return timestamps.map((ts) => byTs.get(ts) ?? 0); + }; + return { + labels, + points, + timestamps, + points2: await fetchAligned(metric.key2), + points3: await fetchAligned(metric.key3), + }; + } catch (e) { + console.error('Failed to fetch history bucket', e); + return EMPTY_CHART; + } +} + export default function SystemHistoryModal({ open, status, onClose }: SystemHistoryModalProps) { const { t } = useTranslation(); const { isMobile } = useMediaQuery(); const [activeKey, setActiveKey] = useState('cpu'); const [bucket, setBucket] = useState(2); - const [points, setPoints] = useState([]); - const [points2, setPoints2] = useState([]); - const [points3, setPoints3] = useState([]); - const [labels, setLabels] = useState([]); - const [timestamps, setTimestamps] = useState([]); + const [{ points, points2, points3, labels, timestamps }, setChart] = + useState(EMPTY_CHART); const activeMetric = useMemo(() => METRICS.find((m) => m.key === activeKey), [activeKey]); const trName = (n?: string) => (n && n.startsWith('pages.') ? t(n) : n); @@ -237,70 +295,27 @@ export default function SystemHistoryModal({ open, status, onClose }: SystemHist const fetchBucket = useCallback(async () => { if (!activeMetric) return; - try { - const url = `/panel/api/server/history/${activeMetric.key}/${bucket}`; - const msg = await HttpUtil.get(url); - if (msg?.success && Array.isArray(msg.obj)) { - const vals: number[] = []; - const labs: string[] = []; - const tss: number[] = []; - for (const p of msg.obj) { - const d = new Date(p.t * 1000); - const MM = String(d.getMonth() + 1).padStart(2, '0'); - const DD = String(d.getDate()).padStart(2, '0'); - const hh = String(d.getHours()).padStart(2, '0'); - const mm = String(d.getMinutes()).padStart(2, '0'); - const ss = String(d.getSeconds()).padStart(2, '0'); - const lab = - bucket >= 2880 - ? `${MM}-${DD} ${hh}:${mm}` - : bucket >= 60 - ? `${hh}:${mm}` - : `${hh}:${mm}:${ss}`; - labs.push(lab); - vals.push(Number(p.v) || 0); - tss.push(Number(p.t) || 0); - } - setLabels(labs); - setPoints(vals); - setTimestamps(tss); - - const fetchAligned = async (key?: string): Promise => { - if (!key) return []; - const m = await HttpUtil.get(`/panel/api/server/history/${key}/${bucket}`); - if (m?.success && Array.isArray(m.obj)) { - const byTs = new Map(); - for (const p of m.obj) byTs.set(Number(p.t) || 0, Number(p.v) || 0); - return tss.map((ts) => byTs.get(ts) ?? 0); - } - return []; - }; - setPoints2(await fetchAligned(activeMetric.key2)); - setPoints3(await fetchAligned(activeMetric.key3)); - } else { - setLabels([]); - setPoints([]); - setPoints2([]); - setPoints3([]); - setTimestamps([]); - } - } catch (e) { - console.error('Failed to fetch history bucket', e); - setLabels([]); - setPoints([]); - setPoints2([]); - setPoints3([]); - setTimestamps([]); - } + const next = await loadBucket(activeMetric, bucket); + setChart(next); }, [activeMetric, bucket]); - useEffect(() => { + const [wasOpen, setWasOpen] = useState(false); + if (open !== wasOpen) { + setWasOpen(open); if (open) setActiveKey('cpu'); - }, [open]); + } useEffect(() => { - if (open) fetchBucket(); - }, [open, activeKey, bucket, fetchBucket]); + if (!open || !activeMetric) return; + let cancelled = false; + void (async () => { + const next = await loadBucket(activeMetric, bucket); + if (!cancelled) setChart(next); + })(); + return () => { + cancelled = true; + }; + }, [open, activeMetric, bucket]); useEffect(() => { if (!open) return undefined; diff --git a/frontend/src/pages/index/VersionModal.tsx b/frontend/src/pages/index/VersionModal.tsx index fb85f8e61..a7b6e9ded 100644 --- a/frontend/src/pages/index/VersionModal.tsx +++ b/frontend/src/pages/index/VersionModal.tsx @@ -38,7 +38,6 @@ export default function VersionModal({ open, status, onClose, onBusy }: VersionM const [loading, setLoading] = useState(false); const fetchVersions = useCallback(async () => { - setLoading(true); try { const msg = await HttpUtil.get('/panel/api/server/getXrayVersion'); if (msg?.success) setVersions(msg.obj || []); @@ -47,8 +46,14 @@ export default function VersionModal({ open, status, onClose, onBusy }: VersionM } }, []); + const [wasOpen, setWasOpen] = useState(false); + if (open !== wasOpen) { + setWasOpen(open); + if (open) setLoading(true); + } + useEffect(() => { - if (open) fetchVersions(); + if (open) void fetchVersions(); }, [open, fetchVersions]); function switchXrayVersion(version: string) { diff --git a/frontend/src/pages/index/XrayLogModal.tsx b/frontend/src/pages/index/XrayLogModal.tsx index e4534fc90..bda8b36ac 100644 --- a/frontend/src/pages/index/XrayLogModal.tsx +++ b/frontend/src/pages/index/XrayLogModal.tsx @@ -73,12 +73,10 @@ export default function XrayLogModal({ open, onClose }: XrayLogModalProps) { const [autoUpdate, setAutoUpdate] = useState(false); const [loading, setLoading] = useState(false); const [logs, setLogs] = useState([]); - const openRef = useRef(open); const orderedLogs = useMemo(() => [...logs].reverse(), [logs]); - const refresh = useCallback(async () => { - setLoading(true); + const runRefresh = useCallback(async () => { try { const msg = await HttpUtil.post(`/panel/api/server/xraylogs/${rows}`, { filter, @@ -93,19 +91,30 @@ export default function XrayLogModal({ open, onClose }: XrayLogModalProps) { } }, [rows, filter, showDirect, showBlocked, showProxy]); + const refresh = useCallback(() => { + setLoading(true); + void runRefresh(); + }, [runRefresh]); + const refreshRef = useRef(refresh); useEffect(() => { refreshRef.current = refresh; - }, [refresh]); + }); + + // The spinner is raised during render so the fetch effect stays side-effect + // free until its response lands. + const refreshKey = open + ? `${rows}\u0000${showDirect}\u0000${showBlocked}\u0000${showProxy}` + : null; + const [loadingKey, setLoadingKey] = useState(null); + if (refreshKey !== loadingKey) { + setLoadingKey(refreshKey); + if (refreshKey) setLoading(true); + } useEffect(() => { - openRef.current = open; - if (open) refresh(); - }, [open, refresh]); - - useEffect(() => { - if (openRef.current) refresh(); - }, [rows, showDirect, showBlocked, showProxy, refresh]); + if (open) void runRefresh(); + }, [open, rows, showDirect, showBlocked, showProxy, runRefresh]); useEffect(() => { if (!open || !autoUpdate) return; diff --git a/frontend/src/pages/index/XrayMetricsModal.tsx b/frontend/src/pages/index/XrayMetricsModal.tsx index 423d9ef60..c297a711b 100644 --- a/frontend/src/pages/index/XrayMetricsModal.tsx +++ b/frontend/src/pages/index/XrayMetricsModal.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import type { ReactNode } from 'react'; import { useTranslation } from 'react-i18next'; import { Alert, Modal, Select, Tabs, Tag } from 'antd'; @@ -147,19 +147,71 @@ function formatFullTimestamp(unixSec: number): string { return `${MM}-${DD} ${time}`; } +interface MetricsChart { + points: number[]; + labels: string[]; + timestamps: number[]; +} + +const EMPTY_CHART: MetricsChart = { points: [], labels: [], timestamps: [] }; + +function toChart(msg: Msg<{ t: number; v: number }[]> | null | undefined, bucket: number) { + if (!msg?.success || !Array.isArray(msg.obj)) return EMPTY_CHART; + const points: number[] = []; + const labels: string[] = []; + const timestamps: number[] = []; + for (const p of msg.obj) { + const d = new Date(p.t * 1000); + const hh = String(d.getHours()).padStart(2, '0'); + const mm = String(d.getMinutes()).padStart(2, '0'); + const ss = String(d.getSeconds()).padStart(2, '0'); + labels.push(bucket >= 60 ? `${hh}:${mm}` : `${hh}:${mm}:${ss}`); + points.push(Number(p.v) || 0); + timestamps.push(Number(p.t) || 0); + } + return { points, labels, timestamps }; +} + +async function loadHistory(url: string | null, bucket: number): Promise { + if (!url) return EMPTY_CHART; + try { + return toChart(await HttpUtil.get<{ t: number; v: number }[]>(url), bucket); + } catch (e) { + console.error('Failed to fetch xray metrics bucket', e); + return EMPTY_CHART; + } +} + +async function loadState(): Promise { + try { + const msg = await HttpUtil.get('/panel/api/server/xrayMetricsState'); + return msg?.success && msg.obj ? msg.obj : null; + } catch (e) { + console.error('Failed to fetch xray metrics state', e); + return null; + } +} + +async function loadObservatory(): Promise { + try { + const msg = await HttpUtil.get('/panel/api/server/xrayObservatory'); + return msg?.success && Array.isArray(msg.obj) ? msg.obj : []; + } catch (e) { + console.error('Failed to fetch observatory snapshot', e); + return []; + } +} + export default function XrayMetricsModal({ open, onClose }: XrayMetricsModalProps) { const { t } = useTranslation(); const { isMobile } = useMediaQuery(); const [activeKey, setActiveKey] = useState('xrAlloc'); const [bucket, setBucket] = useState(2); - const [points, setPoints] = useState([]); - const [labels, setLabels] = useState([]); - const [timestamps, setTimestamps] = useState([]); + const [{ points, labels, timestamps }, setChart] = useState(EMPTY_CHART); const [state, setState] = useState({ enabled: false, listen: '', reason: '' }); const [obsTags, setObsTags] = useState([]); const [obsActiveTag, setObsActiveTag] = useState(''); - const obsTimerRef = useRef(null); - const openRef = useRef(open); + const [obsTick, setObsTick] = useState(0); const activeMetric = useMemo(() => METRICS.find((m) => m.key === activeKey), [activeKey]); const isObservatory = activeKey === OBS_KEY; @@ -184,151 +236,63 @@ export default function XrayMetricsModal({ open, onClose }: XrayMetricsModalProp [tsLookup], ); - const applyHistory = useCallback( - (msg: Msg<{ t: number; v: number }[]> | null | undefined, currentBucket: number) => { - if (msg?.success && Array.isArray(msg.obj)) { - const vals: number[] = []; - const labs: string[] = []; - const tss: number[] = []; - for (const p of msg.obj) { - const d = new Date(p.t * 1000); - const hh = String(d.getHours()).padStart(2, '0'); - const mm = String(d.getMinutes()).padStart(2, '0'); - const ss = String(d.getSeconds()).padStart(2, '0'); - labs.push(currentBucket >= 60 ? `${hh}:${mm}` : `${hh}:${mm}:${ss}`); - vals.push(Number(p.v) || 0); - tss.push(Number(p.t) || 0); - } - setLabels(labs); - setPoints(vals); - setTimestamps(tss); - } else { - setLabels([]); - setPoints([]); - setTimestamps([]); - } - }, - [], - ); - - const fetchState = useCallback(async () => { - try { - const msg = await HttpUtil.get('/panel/api/server/xrayMetricsState'); - if (msg?.success && msg.obj) setState(msg.obj); - } catch (e) { - console.error('Failed to fetch xray metrics state', e); - } - }, []); - - const fetchObservatory = useCallback(async () => { - try { - const msg = await HttpUtil.get('/panel/api/server/xrayObservatory'); - if (msg?.success && Array.isArray(msg.obj)) { - const tags = msg.obj; - setObsTags(tags); - setObsActiveTag((prev) => { - if (tags.find((tg) => tg.tag === prev)) return prev; - return tags[0]?.tag || ''; - }); - } else { - setObsTags([]); - } - } catch (e) { - console.error('Failed to fetch observatory snapshot', e); - setObsTags([]); - } - }, []); - - const fetchMetricBucket = useCallback(async () => { - if (!activeMetric) return; - try { - const url = `/panel/api/server/xrayMetricsHistory/${activeMetric.key}/${bucket}`; - const msg = await HttpUtil.get<{ t: number; v: number }[]>(url); - applyHistory(msg, bucket); - } catch (e) { - console.error('Failed to fetch xray metrics bucket', e); - setLabels([]); - setPoints([]); - setTimestamps([]); - } - }, [activeMetric, bucket, applyHistory]); - - const fetchObsBucket = useCallback(async () => { - if (!obsActiveTag) { - setLabels([]); - setPoints([]); - setTimestamps([]); - return; - } - try { - const url = `/panel/api/server/xrayObservatoryHistory/${encodeURIComponent(obsActiveTag)}/${bucket}`; - const msg = await HttpUtil.get<{ t: number; v: number }[]>(url); - applyHistory(msg, bucket); - } catch (e) { - console.error('Failed to fetch observatory bucket', e); - setLabels([]); - setPoints([]); - setTimestamps([]); - } - }, [obsActiveTag, bucket, applyHistory]); - - const stopObsPolling = useCallback(() => { - if (obsTimerRef.current != null) { - window.clearInterval(obsTimerRef.current); - obsTimerRef.current = null; - } - }, []); - - useEffect(() => { - openRef.current = open; - if (open) { - setActiveKey('xrAlloc'); - fetchState(); - } else { - stopObsPolling(); - } - }, [open, fetchState, stopObsPolling]); + const [wasOpen, setWasOpen] = useState(false); + if (open !== wasOpen) { + setWasOpen(open); + if (open) setActiveKey('xrAlloc'); + } useEffect(() => { if (!open) return; - if (isObservatory) { - fetchObservatory(); - fetchObsBucket(); - stopObsPolling(); - obsTimerRef.current = window.setInterval(async () => { - if (!openRef.current || !isObservatory) return; - await fetchObservatory(); - fetchObsBucket(); - }, 2000); - } else { - stopObsPolling(); - fetchMetricBucket(); - } + let cancelled = false; + void (async () => { + const next = await loadState(); + if (!cancelled && next) setState(next); + })(); return () => { - stopObsPolling(); + cancelled = true; }; - }, [ - open, - activeKey, - isObservatory, - fetchObservatory, - fetchObsBucket, - fetchMetricBucket, - stopObsPolling, - ]); + }, [open]); + + // The observatory snapshot is a live view, so it re-polls; obsTick then pulls + // the chart along with it. + useEffect(() => { + if (!open || !isObservatory) return; + let cancelled = false; + const tick = async () => { + const tags = await loadObservatory(); + if (cancelled) return; + setObsTags(tags); + setObsActiveTag((prev) => (tags.find((tg) => tg.tag === prev) ? prev : tags[0]?.tag || '')); + setObsTick((n) => n + 1); + }; + void tick(); + const id = window.setInterval(() => void tick(), 2000); + return () => { + cancelled = true; + window.clearInterval(id); + }; + }, [open, isObservatory]); + + const historyUrl = isObservatory + ? obsActiveTag + ? `/panel/api/server/xrayObservatoryHistory/${encodeURIComponent(obsActiveTag)}/${bucket}` + : null + : activeMetric + ? `/panel/api/server/xrayMetricsHistory/${activeMetric.key}/${bucket}` + : null; useEffect(() => { if (!open) return; - if (isObservatory) { - fetchObsBucket(); - } else { - fetchMetricBucket(); - } - }, [open, bucket, isObservatory, fetchObsBucket, fetchMetricBucket]); - - useEffect(() => { - if (open && isObservatory) fetchObsBucket(); - }, [open, obsActiveTag, isObservatory, fetchObsBucket]); + let cancelled = false; + void (async () => { + const next = await loadHistory(historyUrl, bucket); + if (!cancelled) setChart(next); + })(); + return () => { + cancelled = true; + }; + }, [open, historyUrl, bucket, obsTick]); return ( { - if (!hasData) return; + // Each polled status is appended during render; an effect would show the + // chart one sample behind the numbers beside it. + const [sampledStatus, setSampledStatus] = useState(null); + if (hasData && status !== sampledStatus) { + setSampledStatus(status); setTrend((prev) => { const point = sampleOf(status); const next = emptyWindow(); @@ -139,7 +142,7 @@ export function useOverviewHistory(status: Status, hasData: boolean): OverviewHi } return next; }); - }, [status, hasData]); + } const labels = useMemo(() => trend.times.map(TimeFormatter.formatClock), [trend.times]); diff --git a/frontend/src/pages/nodes/NodeFormModal.tsx b/frontend/src/pages/nodes/NodeFormModal.tsx index 00c62f70f..2d80f5ff7 100644 --- a/frontend/src/pages/nodes/NodeFormModal.tsx +++ b/frontend/src/pages/nodes/NodeFormModal.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Alert, @@ -103,8 +103,12 @@ export default function NodeFormModal({ ]; }, [outboundGroups, t]); - useEffect(() => { - if (!open) return; + // Reset during render, not in an effect, so the first frame is already clean. + const [synced, setSynced] = useState<{ mode: string; node: NodeRecord | null } | null>(null); + if (!open) { + if (synced) setSynced(null); + } else if (!synced || synced.mode !== mode || synced.node !== (node ?? null)) { + setSynced({ mode, node: node ?? null }); const base = defaultValues(); const next: NodeFormValues = mode === 'edit' && node @@ -123,7 +127,7 @@ export default function NodeFormModal({ methods.reset(next); setInboundOptions((next.inboundTags || []).map((tag) => ({ tag }))); setTestResult(null); - }, [open, mode, node, methods]); + } const title = useMemo( () => (mode === 'edit' ? t('pages.nodes.editNode') : t('pages.nodes.addNode')), diff --git a/frontend/src/pages/settings/SecurityTab.tsx b/frontend/src/pages/settings/SecurityTab.tsx index 52f62da70..eb25b51dd 100644 --- a/frontend/src/pages/settings/SecurityTab.tsx +++ b/frontend/src/pages/settings/SecurityTab.tsx @@ -73,7 +73,7 @@ export default function SecurityTab({ allSetting, updateSetting, saveSetting }: const [updating, setUpdating] = useState(false); const [apiTokens, setApiTokens] = useState([]); - const [apiTokensLoading, setApiTokensLoading] = useState(false); + const [apiTokensLoading, setApiTokensLoading] = useState(true); const [createOpen, setCreateOpen] = useState(false); const [createName, setCreateName] = useState(''); const [creating, setCreating] = useState(false); @@ -130,8 +130,7 @@ export default function SecurityTab({ allSetting, updateSetting, saveSetting }: } } - const loadApiTokens = useCallback(async () => { - setApiTokensLoading(true); + const fetchApiTokens = useCallback(async () => { try { const msg = (await HttpUtil.get('/panel/api/setting/apiTokens')) as ApiMsg; if (msg?.success) setApiTokens(Array.isArray(msg.obj) ? msg.obj : []); @@ -140,9 +139,14 @@ export default function SecurityTab({ allSetting, updateSetting, saveSetting }: } }, []); + const loadApiTokens = useCallback(async () => { + setApiTokensLoading(true); + await fetchApiTokens(); + }, [fetchApiTokens]); + useEffect(() => { - loadApiTokens(); - }, [loadApiTokens]); + void fetchApiTokens(); + }, [fetchApiTokens]); async function copyToken(token: string) { if (!token) return; diff --git a/frontend/src/pages/settings/SettingsPage.tsx b/frontend/src/pages/settings/SettingsPage.tsx index 8926cd7a6..f862d6e46 100644 --- a/frontend/src/pages/settings/SettingsPage.tsx +++ b/frontend/src/pages/settings/SettingsPage.tsx @@ -86,16 +86,9 @@ export default function SettingsPage() { savePayload, } = useAllSettings(); - const [entryHost, setEntryHost] = useState(''); - const [entryPort, setEntryPort] = useState(''); - const [entryIsIP, setEntryIsIP] = useState(false); - - useEffect(() => { - const host = window.location.hostname; - setEntryHost(host); - setEntryPort(window.location.port); - setEntryIsIP(isIp(host)); - }, []); + const [entryHost] = useState(() => window.location.hostname); + const [entryPort] = useState(() => window.location.port); + const [entryIsIP] = useState(() => isIp(window.location.hostname)); const [alertVisible, setAlertVisible] = useState(true); const location = useLocation(); diff --git a/frontend/src/pages/settings/SubJsonFinalMaskForm.tsx b/frontend/src/pages/settings/SubJsonFinalMaskForm.tsx index 09f588a7a..a6e3eb3e5 100644 --- a/frontend/src/pages/settings/SubJsonFinalMaskForm.tsx +++ b/frontend/src/pages/settings/SubJsonFinalMaskForm.tsx @@ -30,7 +30,9 @@ export default function SubJsonFinalMaskForm({ value, onChange }: SubJsonFinalMa const [form] = Form.useForm(); const [initial] = useState(() => parseFinalMask(value)); const onChangeRef = useRef(onChange); - onChangeRef.current = onChange; + useEffect(() => { + onChangeRef.current = onChange; + }); const finalmask = Form.useWatch('finalmask', form) as FinalMaskStreamSettings | undefined; diff --git a/frontend/src/pages/settings/TwoFactorModal.tsx b/frontend/src/pages/settings/TwoFactorModal.tsx index 567de0900..2f2f24485 100644 --- a/frontend/src/pages/settings/TwoFactorModal.tsx +++ b/frontend/src/pages/settings/TwoFactorModal.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from 'react'; +import { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Button, Divider, Input, Modal, QRCode, message } from 'antd'; import * as OTPAuth from 'otpauth'; @@ -32,28 +32,25 @@ export default function TwoFactorModal({ const { t } = useTranslation(); const [messageApi, messageContextHolder] = message.useMessage(); const [enteredCode, setEnteredCode] = useState(''); - const [qrValue, setQrValue] = useState(''); - const totpRef = useRef(null); - useEffect(() => { - if (!open) return; - - setEnteredCode(''); - totpRef.current = null; - setQrValue(''); - if (token) { - const totp = new OTPAuth.TOTP({ - issuer: '3x-ui', - label: 'Administrator', - algorithm: 'SHA1', - digits: 6, - period: 30, - secret: token, - }); - totpRef.current = totp; - setQrValue(totp.toString()); - } + const totp = useMemo(() => { + if (!open || !token) return null; + return new OTPAuth.TOTP({ + issuer: '3x-ui', + label: 'Administrator', + algorithm: 'SHA1', + digits: 6, + period: 30, + secret: token, + }); }, [open, token]); + const qrValue = totp ? totp.toString() : ''; + + const [wasOpen, setWasOpen] = useState(false); + if (open !== wasOpen) { + setWasOpen(open); + if (open) setEnteredCode(''); + } function close(success: boolean, code = '') { onConfirm(success, code); @@ -73,8 +70,8 @@ export default function TwoFactorModal({ close(true, codeOk.data); return; } - if (!totpRef.current) return; - if (totpRef.current.generate() === codeOk.data) { + if (!totp) return; + if (totp.generate() === codeOk.data) { close(true); } else { messageApi.error(t('pages.settings.security.twoFactorModalError')); diff --git a/frontend/src/pages/xray/balancers/BalancerFormModal.tsx b/frontend/src/pages/xray/balancers/BalancerFormModal.tsx index c91df29e4..93ac4ec89 100644 --- a/frontend/src/pages/xray/balancers/BalancerFormModal.tsx +++ b/frontend/src/pages/xray/balancers/BalancerFormModal.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useMemo, useState } from 'react'; import type { ReactNode } from 'react'; import { useTranslation } from 'react-i18next'; import { Alert, Button, Form, Input, InputNumber, Modal, Select, Space, Switch, Tag } from 'antd'; @@ -72,12 +72,15 @@ export default function BalancerFormModal({ const [submitAttempted, setSubmitAttempted] = useState(false); const isEdit = balancer != null; - useEffect(() => { + const openBalancer = open ? (balancer ?? null) : undefined; + const [syncedBalancer, setSyncedBalancer] = useState(undefined); + if (openBalancer !== syncedBalancer) { + setSyncedBalancer(openBalancer); if (open) { methods.reset(initialState(balancer)); setSubmitAttempted(false); } - }, [open, balancer, methods]); + } const strategy = useWatch({ control: methods.control, name: 'strategy' }); const baselines = useWatch({ control: methods.control, name: 'settings.baselines' }) ?? []; diff --git a/frontend/src/pages/xray/balancers/BalancersTab.tsx b/frontend/src/pages/xray/balancers/BalancersTab.tsx index fb766509d..767d5537d 100644 --- a/frontend/src/pages/xray/balancers/BalancersTab.tsx +++ b/frontend/src/pages/xray/balancers/BalancersTab.tsx @@ -190,7 +190,14 @@ export default function BalancersTab({ }, [liveTags]); useEffect(() => { - refreshLive(); + let cancelled = false; + void (async () => { + await refreshLive(); + if (cancelled) return; + })(); + return () => { + cancelled = true; + }; }, [refreshLive]); async function setOverride(tag: string, target: string) { diff --git a/frontend/src/pages/xray/overrides/NordModal.tsx b/frontend/src/pages/xray/overrides/NordModal.tsx index b1cfa2fd1..631f9c8ab 100644 --- a/frontend/src/pages/xray/overrides/NordModal.tsx +++ b/frontend/src/pages/xray/overrides/NordModal.tsx @@ -128,7 +128,15 @@ export default function NordModal({ }, [fetchCountries]); useEffect(() => { - if (open) fetchData(); + if (!open) return; + let cancelled = false; + void (async () => { + await fetchData(); + if (cancelled) return; + })(); + return () => { + cancelled = true; + }; }, [open, fetchData]); async function login() { diff --git a/frontend/src/pages/xray/overrides/WarpModal.tsx b/frontend/src/pages/xray/overrides/WarpModal.tsx index 9352c335e..b1ad6ffc2 100644 --- a/frontend/src/pages/xray/overrides/WarpModal.tsx +++ b/frontend/src/pages/xray/overrides/WarpModal.tsx @@ -174,12 +174,26 @@ export default function WarpModal({ } }, [methods]); + const [wasOpen, setWasOpen] = useState(false); + if (open !== wasOpen) { + setWasOpen(open); + if (open) { + setWarpConfig(null); + setStagedOutbound(null); + setLicenseError(''); + } + } + useEffect(() => { if (!open) return; - setWarpConfig(null); - setStagedOutbound(null); - setLicenseError(''); - fetchData(); + let cancelled = false; + void (async () => { + await fetchData(); + if (cancelled) return; + })(); + return () => { + cancelled = true; + }; }, [open, fetchData]); async function register() { diff --git a/frontend/src/pages/xray/routing/RoutingTab.tsx b/frontend/src/pages/xray/routing/RoutingTab.tsx index b79865993..b828dc603 100644 --- a/frontend/src/pages/xray/routing/RoutingTab.tsx +++ b/frontend/src/pages/xray/routing/RoutingTab.tsx @@ -1,4 +1,4 @@ -import { useCallback, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Button, Dropdown, Modal, Space, Table, Tabs, message } from 'antd'; import { @@ -68,7 +68,6 @@ export default function RoutingTab({ [templateSettings?.routing?.rules], ); const rulesRef = useRef(rules); - rulesRef.current = rules; const rowsRef = useRef([]); const rows: RuleRow[] = useMemo( @@ -100,7 +99,11 @@ export default function RoutingTab({ }), [rules], ); - rowsRef.current = rows; + + useEffect(() => { + rulesRef.current = rules; + rowsRef.current = rows; + }); const mutate = useCallback( (mutator: (next: XraySettingsValue) => void) => {