diff --git a/.dockerignore b/.dockerignore index bdbb86c..653fc9b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -13,6 +13,9 @@ !public/** !docker/ !docker/** +!scripts/ +!scripts/minimatch-compat/ +!scripts/minimatch-compat/** !packages/ !packages/tokens/ !packages/tokens/package.json diff --git a/.gitea/workflows/verify.yml b/.gitea/workflows/verify.yml index bbef2a1..518324b 100644 --- a/.gitea/workflows/verify.yml +++ b/.gitea/workflows/verify.yml @@ -66,7 +66,7 @@ jobs: - name: Verify pre-commit secret blocking and redaction run: PATH=/tmp/gitleaks-bin:$PATH node --test scripts/test-pre-commit.mjs - name: Audit runtime and development dependencies - run: npm audit --include=dev --audit-level=low + run: npm audit --include=dev --audit-level=moderate verify: needs: [commits, security] diff --git a/CHANGELOG.md b/CHANGELOG.md index d0d8863..8dd80e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,36 @@ # Changelog +## [1.3.0](https://git.personal.ksv741.ru/WealthLedger/frontend/compare/v1.2.1...v1.3.0) (2026-11-01) + +### Features + +* **account:** add concentration tab with top-5 slices and chart (WealthLedger/main[#48](https://git.personal.ksv741.ru/WealthLedger/frontend/issues/48)) ([#127](https://git.personal.ksv741.ru/WealthLedger/frontend/issues/127)) ([70a2c33](https://git.personal.ksv741.ru/WealthLedger/frontend/commit/70a2c335fdddb8b6a99e3f609befe06aeb8cc45d)) +* group all-account rows by instrument and day (WealthLedger/main[#47](https://git.personal.ksv741.ru/WealthLedger/frontend/issues/47)) ([#126](https://git.personal.ksv741.ru/WealthLedger/frontend/issues/126)) ([71126c9](https://git.personal.ksv741.ru/WealthLedger/frontend/commit/71126c91f0b30f4d65a313ddc7d7f86963340455)) +* make brokerage account list compact ([#111](https://git.personal.ksv741.ru/WealthLedger/frontend/issues/111)) ([c24704a](https://git.personal.ksv741.ru/WealthLedger/frontend/commit/c24704affacbe7fbc08f5e2b61bb6e7ffa6053b9)) +* remove top header and rework mobile bottom navigation ([#109](https://git.personal.ksv741.ru/WealthLedger/frontend/issues/109)) ([1a92f02](https://git.personal.ksv741.ru/WealthLedger/frontend/commit/1a92f023c1c79f646424f72710ab74de63386a11)) +* replace account page accordion with tabs and show open position result ([#112](https://git.personal.ksv741.ru/WealthLedger/frontend/issues/112)) ([3075081](https://git.personal.ksv741.ru/WealthLedger/frontend/commit/3075081df188de0630d314ba15a34fe6efd1eea9)) +* show return, cash and structure on account cards ([#114](https://git.personal.ksv741.ru/WealthLedger/frontend/issues/114)) ([431559b](https://git.personal.ksv741.ru/WealthLedger/frontend/commit/431559be0fca9eb37f6a09d930d502f939884e39)) + +### Bug Fixes + +* round money to currency minor units without negative zero (WealthLedger/frontend[#118](https://git.personal.ksv741.ru/WealthLedger/frontend/issues/118)) ([#119](https://git.personal.ksv741.ru/WealthLedger/frontend/issues/119)) ([1d49904](https://git.personal.ksv741.ru/WealthLedger/frontend/commit/1d4990491f254e1ec6bafad8204872eaae2982d0)) +* show account source sync status (WealthLedger/frontend[#120](https://git.personal.ksv741.ru/WealthLedger/frontend/issues/120)) ([#121](https://git.personal.ksv741.ru/WealthLedger/frontend/issues/121)) ([a56edf2](https://git.personal.ksv741.ru/WealthLedger/frontend/commit/a56edf2b3757ef0b0f6c42ecbd422a1a563f6505)) +* show history loading alert only while loading (WealthLedger/frontend[#124](https://git.personal.ksv741.ru/WealthLedger/frontend/issues/124)) ([#125](https://git.personal.ksv741.ru/WealthLedger/frontend/issues/125)) ([6792f5b](https://git.personal.ksv741.ru/WealthLedger/frontend/commit/6792f5b70aa3ba90b2eb2df7e63d3ae2f669e295)) +* stabilize auth client registration (WealthLedger/frontend[#95](https://git.personal.ksv741.ru/WealthLedger/frontend/issues/95)) ([#122](https://git.personal.ksv741.ru/WealthLedger/frontend/issues/122)) ([5796d2d](https://git.personal.ksv741.ru/WealthLedger/frontend/commit/5796d2d789963e66a3a4bcd79eab44700f16bd31)) + +### Refactoring + +* derive api error codes from contract (WealthLedger/backend[#117](https://git.personal.ksv741.ru/WealthLedger/frontend/issues/117)) ([#123](https://git.personal.ksv741.ru/WealthLedger/frontend/issues/123)) ([32e8fe9](https://git.personal.ksv741.ru/WealthLedger/frontend/commit/32e8fe9402665ea0c350f5f247d87e328ffd44c8)) + +### Tests + +* pin date in all accounts events test ([#115](https://git.personal.ksv741.ru/WealthLedger/frontend/issues/115)) ([58b4e12](https://git.personal.ksv741.ru/WealthLedger/frontend/commit/58b4e126330d01c1d6865bd0e9dd066a028fba08)) + +### Maintenance + +* **deps:** resolve minimatch and glob audit advisories (WealthLedger/main[#49](https://git.personal.ksv741.ru/WealthLedger/frontend/issues/49)) ([#128](https://git.personal.ksv741.ru/WealthLedger/frontend/issues/128)) ([7242240](https://git.personal.ksv741.ru/WealthLedger/frontend/commit/7242240b2188e9d1ca1325e65d0e1a0a3655d882)) +* sync brokerage accounts contract (WealthLedger/backend[#138](https://git.personal.ksv741.ru/WealthLedger/frontend/issues/138)) ([0e4bbec](https://git.personal.ksv741.ru/WealthLedger/frontend/commit/0e4bbec2a7b814579095ea7ef6db68a70508fa29)) + ## [1.2.1](https://git.personal.ksv741.ru/WealthLedger/frontend/compare/v1.2.0...v1.2.1) (2026-10-24) ### Bug Fixes diff --git a/Dockerfile b/Dockerfile index 2795607..31c3f57 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,6 +5,7 @@ COPY package.json package-lock.json ./ COPY packages/tokens/package.json ./packages/tokens/ COPY packages/ui/package.json ./packages/ui/ COPY packages/icons/package.json ./packages/icons/ +COPY scripts/minimatch-compat ./scripts/minimatch-compat RUN npm ci FROM dependencies AS development diff --git a/README.md b/README.md index c136094..7ea5714 100644 --- a/README.md +++ b/README.md @@ -123,10 +123,12 @@ Chromium при ширинах 375 и 1280 px. Browser project не исполь Новый запуск отменяет устаревший для того же PR/ветки и типа события (требуется Gitea 1.26+). Ежедневно в 05:17 UTC выполняется только `security`. Job `security` проверяет полную доступную историю Gitleaks 8.30.1 и выполняет -`npm audit --include=dev --audit-level=low`. Любой advisory блокирует security job; -исключений frontend нет. Локально можно выполнить те же команды: +`npm audit --include=dev --audit-level=moderate`. Advisory уровня moderate и выше +блокирует security job; low не блокирует, т.к. для них часто нет исправленной версии +(например, GHSA-gcqx-w48m-w2h4 в build-only deepmerge). Других исключений нет. +Локально можно выполнить те же команды: `gitleaks git --log-opts=--all --redact=100 --no-banner` и -`npm audit --include=dev --audit-level=low`. Audit требует доступа к npm registry; +`npm audit --include=dev --audit-level=moderate`. Audit требует доступа к npm registry; проверка секретов выполняется локально и не выводит их значения. Имена jobs `commits`, `security`, `verify` сохранены, но Gitea включает имя diff --git a/index.html b/index.html index e33e6a4..42dc759 100644 --- a/index.html +++ b/index.html @@ -5,7 +5,7 @@ - + WealthLedger diff --git a/knip.json b/knip.json index 5f5b315..a31ed98 100644 --- a/knip.json +++ b/knip.json @@ -1,5 +1,5 @@ { - "ignoreDependencies": ["conventional-changelog-conventionalcommits"], + "ignoreDependencies": ["conventional-changelog-conventionalcommits", "minimatch"], "workspaces": { ".": { "project": ["src/**/*.{ts,tsx}", "!src/shared/api/generated/**"] diff --git a/package-lock.json b/package-lock.json index ff08565..d9dd952 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "wealth-ledger-frontend", - "version": "1.2.1", + "version": "1.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "wealth-ledger-frontend", - "version": "1.2.1", + "version": "1.3.0", "workspaces": [ "packages/*" ], @@ -80,6 +80,7 @@ "jsdom": "30.1.3", "knip": "6.43.0", "lint-staged": "17.6.0", + "minimatch": "file:scripts/minimatch-compat", "msw": "2.15.0", "openapi-typescript": "7.13.0", "playwright": "1.64.1", @@ -88,7 +89,7 @@ "typescript": "5.9.3", "typescript-eslint": "8.72.0", "vite": "8.3.5", - "vite-plugin-pwa": "1.3.0", + "vite-plugin-pwa": "2.0.0", "vitest": "5.0.5" }, "engines": { @@ -2511,14 +2512,12 @@ } } }, - "node_modules/@isaacs/cliui": { - "version": "9.0.0", - "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "node_modules/@isaacs/nonrecursive-glob-match": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@isaacs/nonrecursive-glob-match/-/nonrecursive-glob-match-1.0.3.tgz", + "integrity": "sha512-SI7kyKHq4XXDRozw+r9GlRZkia1ZkFrpLW98r2IC2Bpr8YkFGE3fvjzmxCG0x78A++69HmDNrDvSMYBZmHGpKA==", "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } + "license": "BlueOak-1.0.0" }, "node_modules/@joshwooding/vite-plugin-react-docgen-typescript": { "version": "0.7.0", @@ -3720,33 +3719,6 @@ "npm": ">=9.5.0" } }, - "node_modules/@redocly/openapi-core/node_modules/balanced-match": { - "version": "1.0.2", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@redocly/openapi-core/node_modules/brace-expansion": { - "version": "2.1.7", - "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@redocly/openapi-core/node_modules/minimatch": { - "version": "5.1.9", - "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@reduxjs/toolkit": { "version": "2.12.0", "integrity": "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==", @@ -6448,6 +6420,7 @@ }, "node_modules/balanced-match": { "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, "license": "MIT", @@ -6495,6 +6468,7 @@ }, "node_modules/brace-expansion": { "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "dev": true, "license": "MIT", @@ -9097,33 +9071,6 @@ "minimatch": "^5.0.1" } }, - "node_modules/filelist/node_modules/balanced-match": { - "version": "1.0.2", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.1.7", - "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.9", - "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/find-up": { "version": "5.0.0", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", @@ -9194,22 +9141,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/foreground-child": { - "version": "3.3.1", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/formatly": { "version": "0.11.4", "resolved": "https://registry.npmjs.org/formatly/-/formatly-0.11.4.tgz", @@ -9488,14 +9419,15 @@ } }, "node_modules/glob": { - "version": "13.0.6", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-14.0.3.tgz", + "integrity": "sha512-A1vjIwC3qpdCso45Iw127gnp7sPhqgY8lUQhDrArIs+GbmfaaVPFuoyy7WyLT9EPZCRZ7j7aLdwUhQmHWesEow==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "minimatch": "^10.2.2", + "minimatch": "^11.0.2", "minipass": "^7.1.3", - "path-scurry": "^2.0.2" + "path-scurry": "^3.0.0" }, "engines": { "node": "18 || 20 || >=22" @@ -10670,21 +10602,6 @@ "node": "^18.17 || >=20.6.1" } }, - "node_modules/jackspeak": { - "version": "4.2.3", - "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^9.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/jake": { "version": "10.9.4", "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", @@ -11473,12 +11390,19 @@ } }, "node_modules/minimatch": { - "version": "10.2.6", - "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "resolved": "scripts/minimatch-compat", + "link": true + }, + "node_modules/minimatch-11": { + "name": "minimatch", + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-11.0.2.tgz", + "integrity": "sha512-pWTgz5CN3tjqyZNSK2tePlSmu9IwM41q8yn8Usdqqwe9TIHh+9dEpLLjF4uApsfPEAZY71qtSiA2B2APEr+1Yw==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.8" + "@isaacs/nonrecursive-glob-match": "^1.0.2", + "brace-expansion": "^5.0.12" }, "engines": { "node": "18 || 20 || >=22" @@ -12212,12 +12136,6 @@ "quickjs-wasi": "^2.2.0" } }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, "node_modules/package-manager-detector": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.9.0.tgz", @@ -12314,8 +12232,9 @@ "license": "MIT" }, "node_modules/path-scurry": { - "version": "2.0.2", - "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-3.0.0.tgz", + "integrity": "sha512-1pTbQqH1wIwMi/eleQ5leFTTCkzFsPcgXNjwOA8YGGW/W4/kHT3dssE7Z6NH+zuqNLOzOB0uaD61yl7cAcVhgg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -15929,8 +15848,9 @@ } }, "node_modules/vite-plugin-pwa": { - "version": "1.3.0", - "integrity": "sha512-c5kMgN+ITrOtHXp8PAtk2uOIEea6XjP/unCGxOWWBzQ6qa65qj/awHg0wf+QF9E/2u9vh86LqxPwzEPNbM2r5A==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-2.0.0.tgz", + "integrity": "sha512-6G9dsPKT9QKYpeTuTLJM28zZL3HsXUTUM/QUZBEgQk2jbfrPJDq2i8gTewAKqi1c9Mt6MiidrL4SFI+HthiQMg==", "dev": true, "license": "MIT", "dependencies": { @@ -15941,13 +15861,13 @@ "workbox-window": "^7.4.1" }, "engines": { - "node": ">=16.0.0" + "node": ">=20.19.0" }, "funding": { "url": "https://github.com/sponsors/antfu" }, "peerDependencies": { - "@vite-pwa/assets-generator": "^1.0.0", + "@vite-pwa/assets-generator": "^1.0.0 || ^2.0.0", "vite": "^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", "workbox-build": "^7.4.1", "workbox-window": "^7.4.1" @@ -17642,30 +17562,6 @@ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/workbox-build/node_modules/glob": { - "version": "11.1.0", - "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "foreground-child": "^3.3.1", - "jackspeak": "^4.1.1", - "minimatch": "^10.1.1", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^2.0.0" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/workbox-build/node_modules/lru-cache": { "version": "5.1.1", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", @@ -18076,6 +17972,14 @@ "react": "19.3.1", "react-dom": "19.3.1" } + }, + "scripts/minimatch-compat": { + "name": "minimatch", + "version": "11.0.2-compat.0", + "dev": true, + "dependencies": { + "minimatch-11": "npm:minimatch@11.0.2" + } } } } diff --git a/package.json b/package.json index 168e6da..434f528 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "wealth-ledger-frontend", "private": true, - "version": "1.2.1", + "version": "1.3.0", "type": "module", "engines": { "node": "24.22.1" @@ -106,6 +106,7 @@ "jsdom": "30.1.3", "knip": "6.43.0", "lint-staged": "17.6.0", + "minimatch": "file:scripts/minimatch-compat", "msw": "2.15.0", "openapi-typescript": "7.13.0", "playwright": "1.64.1", @@ -114,7 +115,7 @@ "typescript": "5.9.3", "typescript-eslint": "8.72.0", "vite": "8.3.5", - "vite-plugin-pwa": "1.3.0", + "vite-plugin-pwa": "2.0.0", "vitest": "5.0.5" }, "overrides": { @@ -124,7 +125,9 @@ }, "@vitest/browser": "5.0.5", "@vitest/ui": "5.0.5", - "@vitest/mocker": "5.0.5" + "@vitest/mocker": "5.0.5", + "minimatch": "$minimatch", + "glob": "14.0.3" }, "allowScripts": { "esbuild@0.28.2": true, diff --git a/packages/ui/src/EntryCard/EntryCard.tsx b/packages/ui/src/EntryCard/EntryCard.tsx index 973c8ef..76e5f66 100644 --- a/packages/ui/src/EntryCard/EntryCard.tsx +++ b/packages/ui/src/EntryCard/EntryCard.tsx @@ -16,7 +16,7 @@ type EntryCardProps = { tone: "negative" | "neutral" | "positive"; - value: string; + value: ReactNode; valueLabel: string; wrap?: boolean; }; @@ -32,6 +32,7 @@ const EntryCard: FC = (props) => { const valueContainerClass = props.actions ? classes.valueFooter : undefined; const valueLabelClass = `${classes[props.tone]} ${props.actions ? classes.valueLabel : ""}`; + const valueComponent = typeof props.value === "string" ? undefined : "div"; return ( = (props) => { - + {props.value} diff --git a/scripts/minimatch-compat/index.cjs b/scripts/minimatch-compat/index.cjs new file mode 100644 index 0000000..f1e6908 --- /dev/null +++ b/scripts/minimatch-compat/index.cjs @@ -0,0 +1,9 @@ +// ponytail: CommonJS twin of index.js (import-x ships lib/index.cjs too). +const minimatch = require("minimatch-11"); + +module.exports = { + ...minimatch, + makeRe: (pattern, options = {}) => ({ + test: path => minimatch.minimatch(path, pattern, options), + }), +}; diff --git a/scripts/minimatch-compat/index.js b/scripts/minimatch-compat/index.js new file mode 100644 index 0000000..eeab204 --- /dev/null +++ b/scripts/minimatch-compat/index.js @@ -0,0 +1,11 @@ +// ponytail: shim for eslint-plugin-import-x@4.17.1, which still imports the +// makeRe export removed in minimatch 11. Its only caller (no-internal-modules) +// uses the result via .test(), so a matcher object is enough. Delete this +// package and the override once import-x supports minimatch 11. +import { minimatch } from "minimatch-11"; + +export * from "minimatch-11"; + +export const makeRe = (pattern, options = {}) => ({ + test: path => minimatch(path, pattern, options), +}); diff --git a/scripts/minimatch-compat/package.json b/scripts/minimatch-compat/package.json new file mode 100644 index 0000000..9e0832a --- /dev/null +++ b/scripts/minimatch-compat/package.json @@ -0,0 +1,14 @@ +{ + "name": "minimatch", + "version": "11.0.2-compat.0", + "private": true, + "description": "minimatch 11 plus the makeRe export eslint-plugin-import-x 4.17.1 still imports. Remove when import-x supports minimatch 11.", + "type": "module", + "exports": { + "import": "./index.js", + "require": "./index.cjs" + }, + "dependencies": { + "minimatch-11": "npm:minimatch@11.0.2" + } +} diff --git a/src/app/routes/AppRoutes/AllAccounts.test.tsx b/src/app/routes/AppRoutes/AllAccounts.test.tsx index a7dacf9..0363249 100644 --- a/src/app/routes/AppRoutes/AllAccounts.test.tsx +++ b/src/app/routes/AppRoutes/AllAccounts.test.tsx @@ -11,7 +11,7 @@ import { } from "@testing-library/react"; import { http, HttpResponse } from "msw"; import { MemoryRouter, useLocation } from "react-router"; -import { beforeEach, describe, expect, it } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { BrokerageOperation } from "@/entities/broker-operation"; import type { IncomeAnalytics } from "@/entities/income"; @@ -46,8 +46,10 @@ const position: Portfolio["positions"][number] displayName: "Сбербанк", figi: "SBER", isin: null, + issuer: null, logoName: null, quantity: "2", + sector: null, ticker: "SBER", type: "STOCK", }; @@ -145,6 +147,7 @@ function renderPage(path: string) { return client; } +afterEach(() => vi.useRealTimers()); beforeEach(() => { server.use( http.post("*/api/v1/auth/refresh", () => @@ -271,6 +274,8 @@ describe("All accounts routes", () => { ); it("restores applied dates when switching accounts in events", async () => { + vi.useFakeTimers({ toFake: ["Date"] }); + vi.setSystemTime(new Date("2026-10-15T12:00:00Z")); // The events calendar blocks past dates, so pick within selectable dates. const search = `?from=${defaultPeriod()[0]}&to=${defaultPeriod()[1]}`; renderPage(`/accounts/all/events${search}`); @@ -303,14 +308,16 @@ describe("All accounts routes", () => { "href", `/accounts/${account.id}`, ); - for (const suffix of ["operations", "income", "events"]) - expect( - screen - .getAllByRole("link") - .find( - link => link.getAttribute("href") === `/accounts/all/${suffix}`, - ), - ).toBeInTheDocument(); + + for (const [suffix, tab] of [ + ["operations", "Операции"], + ["income", "Доход"], + ["events", "События"], + ]) { + fireEvent.click(screen.getByRole("tab", { name: tab })); + expect(screen.getByRole("link", { name: "Подробнее" })) + .toHaveAttribute("href", `/accounts/all/${suffix}`); + } }); it("switches accounts within operations with applied filters and a fresh cursor", async () => { diff --git a/src/app/routes/AppRoutes/AppRoutes.test.tsx b/src/app/routes/AppRoutes/AppRoutes.test.tsx index ca97244..7707151 100644 --- a/src/app/routes/AppRoutes/AppRoutes.test.tsx +++ b/src/app/routes/AppRoutes/AppRoutes.test.tsx @@ -232,7 +232,8 @@ describe("AppRoutes", () => { }), ).toBeInTheDocument(); expect( - screen.getByRole("navigation", { name: "WealthLedger" }), - ).toBeInTheDocument(); + screen.getAllByRole("navigation", { name: "Основная навигация" }), + ).toHaveLength(2); + expect(screen.queryByRole("banner")).not.toBeInTheDocument(); }); }); diff --git a/src/app/ui/AppShell/AppShell.module.css b/src/app/ui/AppShell/AppShell.module.css index 2baaffc..6d463bf 100644 --- a/src/app/ui/AppShell/AppShell.module.css +++ b/src/app/ui/AppShell/AppShell.module.css @@ -1,12 +1,22 @@ +.root { + --bottom-nav-height: 56px; +} + .content { max-width: 1120px; margin-inline: auto; } -.header { +.brand { align-items: center; + border-bottom: 1px solid var(--mantine-color-gray-3); display: flex; - padding: 0 var(--mantine-spacing-md); + min-height: 60px; + padding-inline: var(--mantine-spacing-sm); +} + +.navList { + padding: var(--mantine-spacing-sm); } .bottomNav { @@ -52,29 +62,85 @@ .bottomNav { background: var(--mantine-color-white); border-top: 1px solid var(--mantine-color-gray-3); - bottom: 0; - display: flex; - justify-content: space-around; - left: 0; - padding: var(--mantine-spacing-xs) 2px; - gap: 2px; - font-size: 11px; - & a { - min-width: 0; - padding: 8px 2px; - text-align: center; - } + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + inset: auto 0 0; + min-height: calc(var(--bottom-nav-height) + env(safe-area-inset-bottom, 0px)); + padding-bottom: env(safe-area-inset-bottom, 0px); + padding-inline: + max(4px, env(safe-area-inset-left, 0px)) + max(4px, env(safe-area-inset-right, 0px)); position: fixed; - right: 0; - z-index: 200; + z-index: var(--mantine-z-index-app); + } + + .bottomNavItem { + align-items: center; + color: var(--mantine-color-gray-7); + display: flex; + flex-direction: column; + gap: 2px; + justify-content: center; + min-height: var(--bottom-nav-height); + min-width: 0; + text-decoration: none; + + &:active .iconPill { + background-color: var(--mantine-color-gray-1); + } + + &:focus-visible { + outline: 2px solid var(--mantine-primary-color-filled); + outline-offset: -2px; + } + } + + @media (hover: hover) { + .bottomNavItem:hover .iconPill { + background-color: var(--mantine-color-gray-1); + } + } + + .iconPill { + align-items: center; + border-radius: 14px; + display: inline-flex; + height: 28px; + justify-content: center; + width: 48px; + } + + .bottomNavLabel { + font-size: 12px; + font-weight: 500; + line-height: 16px; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + @media (prefers-reduced-motion: no-preference) { + .bottomNavItem, + .iconPill { + transition: + background-color 150ms ease-in-out, + color 150ms ease-in-out; + } } .active { color: var(--mantine-primary-color-filled); - font-weight: 600; + + & .iconPill { + background-color: var(--mantine-primary-color-light); + } } .main { - padding-bottom: 5rem; + padding-bottom: calc( + var(--bottom-nav-height) + env(safe-area-inset-bottom, 0px) + + var(--mantine-spacing-md) + ); } } diff --git a/src/app/ui/AppShell/AppShell.test.tsx b/src/app/ui/AppShell/AppShell.test.tsx new file mode 100644 index 0000000..307f2a0 --- /dev/null +++ b/src/app/ui/AppShell/AppShell.test.tsx @@ -0,0 +1,47 @@ +import { MantineProvider } from "@mantine/core"; +import { render, screen, within } from "@testing-library/react"; +import { MemoryRouter } from "react-router"; +import { describe, expect, it } from "vitest"; + +import { AppShell } from "./AppShell"; + +function renderShell(path: string) { + return render( + + + + + , + ); +} + +describe("AppShell", () => { + it("has no header banner", () => { + renderShell("/cards"); + + expect(screen.queryByRole("banner")).not.toBeInTheDocument(); + }); + + it("marks only the current bottom navigation item", () => { + renderShell("/cards"); + + const bottomNav = screen + .getAllByRole("navigation", { name: "Основная навигация" }) + .at(-1); + + if (!bottomNav) { + throw new Error("Bottom navigation is not rendered"); + } + + const links = within(bottomNav).getAllByRole("link"); + + expect(links).toHaveLength(5); + expect(within(bottomNav).getByRole("link", { name: "Карты" })).toHaveAttribute( + "aria-current", + "page", + ); + expect( + within(bottomNav).getByRole("link", { name: "Счета" }), + ).not.toHaveAttribute("aria-current"); + }); +}); diff --git a/src/app/ui/AppShell/AppShell.tsx b/src/app/ui/AppShell/AppShell.tsx index b498395..aee1778 100644 --- a/src/app/ui/AppShell/AppShell.tsx +++ b/src/app/ui/AppShell/AppShell.tsx @@ -17,6 +17,12 @@ type AppShellProps = { contentAboveOutlet?: ReactNode; }; +function getBottomNavItemClass({ isActive }: { isActive: boolean }) { + return isActive + ? `${classes.bottomNavItem} ${classes.active}` + : classes.bottomNavItem; +} + const AppShell: FC = (props) => { const { contentAboveOutlet } = props; const { t } = useTranslation(); @@ -24,31 +30,31 @@ const AppShell: FC = (props) => { const items = [ { - icon: , + Icon: PortfolioIcon, iconClassName: classes.portfolioIcon, label: t("navigation.accounts"), to: "/accounts", }, { - icon: , + Icon: CardsIcon, iconClassName: classes.cardsIcon, label: t("navigation.cards"), to: "/cards", }, { - icon: , + Icon: StocksIcon, iconClassName: classes.stocksIcon, label: t("navigation.stocks"), to: "/stocks", }, { - icon: , + Icon: BondsIcon, iconClassName: classes.bondsIcon, label: t("navigation.bonds"), to: "/bonds", }, { - icon: , + Icon: SettingsIcon, iconClassName: classes.settingsIcon, label: t("navigation.settings"), to: "/settings", @@ -57,32 +63,32 @@ const AppShell: FC = (props) => { return ( - - {t("appName")} - + +
+ {t("appName")} +
- - {items.map(item => ( - - {item.icon} - - )} - to={item.to} - /> - ))} +
+ {items.map(item => ( + + + + )} + to={item.to} + /> + ))} +
@@ -93,22 +99,23 @@ const AppShell: FC = (props) => { -