From a375d2912b3dbcfb28507bda4017ed17452f3303 Mon Sep 17 00:00:00 2001 From: owlsua Date: Mon, 27 Apr 2026 23:17:01 +0200 Subject: [PATCH 01/67] infra: add TypeScript and CSS Modules support Configure TypeScript compilation, CSS module type declarations, webpack loaders, babel and jest for .tsx files. --- .gitignore | 3 + babel.config.js | 1 + jest.config.js | 3 + package-lock.json | 315 ++++++++++++++++++++++++++++++++++++++++- package.json | 4 + src/css.d.ts | 5 + src/types/modules.d.ts | 7 + src/types/svg.d.ts | 6 + tsconfig.json | 40 ++++++ webpack.config.js | 8 +- 10 files changed, 384 insertions(+), 8 deletions(-) create mode 100644 src/css.d.ts create mode 100644 src/types/modules.d.ts create mode 100644 src/types/svg.d.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore index 6da3defb..ec11ca26 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,6 @@ yarn-error.log* /playwright-report/ /blob-report/ /playwright/.cache/ + +# llm +.claude diff --git a/babel.config.js b/babel.config.js index 6183f4ea..aa1b3db6 100644 --- a/babel.config.js +++ b/babel.config.js @@ -2,5 +2,6 @@ module.exports = { presets: [ ['@babel/preset-env', { targets: { node: 'current' } }], ['@babel/preset-react', { runtime: 'automatic' }], + '@babel/preset-typescript', ], } diff --git a/jest.config.js b/jest.config.js index f4749e5e..aedc5619 100644 --- a/jest.config.js +++ b/jest.config.js @@ -4,6 +4,9 @@ module.exports = { testEnvironment: 'jsdom', setupFilesAfterEnv: ['/src/setupTests.js'], testPathIgnorePatterns: ['/node_modules/', '/tests/', 'src/pages'], + transform: { + '\\.[jt]sx?$': 'babel-jest', + }, transformIgnorePatterns: [ 'node_modules/(?!(react-router|react-router-dom|@remix-run|date-fns|konva|react-konva|@mintlayer)/)', ], diff --git a/package-lock.json b/package-lock.json index e77c29f5..a7011203 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,6 +35,7 @@ "devDependencies": { "@babel/preset-env": "^7.29.0", "@babel/preset-react": "^7.28.5", + "@babel/preset-typescript": "^7.28.5", "@eslint/js": "^9.39.2", "@playwright/test": "^1.58.2", "@svgr/webpack": "^8.1.0", @@ -42,7 +43,10 @@ "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", + "@types/d3": "^7.4.3", "@types/node": "^25.2.3", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", "@types/tiny-secp256k1": "^2.0.1", "babel-jest": "^30.2.0", "babel-loader": "^10.0.0", @@ -5490,6 +5494,290 @@ "@types/node": "*" } }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/eslint": { "version": "8.56.12", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.12.tgz", @@ -5558,6 +5846,13 @@ "@types/send": "*" } }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/html-minifier-terser": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", @@ -5660,15 +5955,24 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.2.13", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.13.tgz", - "integrity": "sha512-KkiJeU6VbYbUOp5ITMIc7kBfqlYkKA5KhEHVrGMmUUMt7NeaZg65ojdPk+FtNrBAOXNVM5QM72jnADjM+XVRAQ==", + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, "node_modules/@types/react-reconciler": { "version": "0.32.3", "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.32.3.tgz", @@ -8236,8 +8540,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/d3": { "version": "7.9.0", diff --git a/package.json b/package.json index ac588341..396c0be7 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "devDependencies": { "@babel/preset-env": "^7.29.0", "@babel/preset-react": "^7.28.5", + "@babel/preset-typescript": "^7.28.5", "@eslint/js": "^9.39.2", "@playwright/test": "^1.58.2", "@svgr/webpack": "^8.1.0", @@ -67,7 +68,10 @@ "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", + "@types/d3": "^7.4.3", "@types/node": "^25.2.3", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", "@types/tiny-secp256k1": "^2.0.1", "babel-jest": "^30.2.0", "babel-loader": "^10.0.0", diff --git a/src/css.d.ts b/src/css.d.ts new file mode 100644 index 00000000..cfe824ef --- /dev/null +++ b/src/css.d.ts @@ -0,0 +1,5 @@ +declare module '*.css' +declare module '*.module.css' { + const classes: { [key: string]: string } + export default classes +} diff --git a/src/types/modules.d.ts b/src/types/modules.d.ts new file mode 100644 index 00000000..dcdb1e5f --- /dev/null +++ b/src/types/modules.d.ts @@ -0,0 +1,7 @@ +declare module '*.png' { + const src: string + export default src +} + +declare const browser: any +declare const chrome: any diff --git a/src/types/svg.d.ts b/src/types/svg.d.ts new file mode 100644 index 00000000..5584ba01 --- /dev/null +++ b/src/types/svg.d.ts @@ -0,0 +1,6 @@ +declare module '*.svg' { + import React from 'react' + export const ReactComponent: React.FC> + const src: string + export default src +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..ec539e51 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,40 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "allowJs": true, + "checkJs": false, + "noEmit": true, + "isolatedModules": true, + "resolveJsonModule": true, + "ignoreDeprecations": "6.0", + "baseUrl": ".", + "paths": { + "@BasicComponents": ["./src/components/basic/index.js"], + "@ComposedComponents": ["./src/components/composed/index.js"], + "@LayoutComponents": ["./src/components/layouts/index.js"], + "@ContainerComponents": ["./src/components/containers/index.js"], + "@Assets/*": ["./src/assets/*"], + "@Contexts": ["./src/contexts/index.js"], + "@Hooks": ["./src/hooks/index.js"], + "@Pages": ["./src/pages/index.js"], + "@APIs": ["./src/services/API/index.js"], + "@Cryptos": ["./src/services/Crypto/index.js"], + "@Databases": ["./src/services/Database/index.js"], + "@Entities": ["./src/services/Entity/index.js"], + "@Helpers": ["./src/utils/Helpers/index.js"], + "@Constants": ["./src/utils/Constants/index.js"], + "@TestData": ["./src/utils/TestData/index.js"], + "@Storage": ["./src/services/Storage/index.js"], + "@Version": ["./src/version/version.js"] + } + }, + "include": ["src"], + "exclude": ["node_modules", "build"] +} diff --git a/webpack.config.js b/webpack.config.js index 3cd61a57..cf485429 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -93,7 +93,7 @@ module.exports = { }, resolve: { - extensions: ['.js', '.jsx', '.json', '.mjs', '.wasm'], + extensions: ['.ts', '.tsx', '.js', '.jsx', '.json', '.mjs', '.wasm'], alias: aliases, fallback: { stream: require.resolve('stream-browserify'), @@ -108,7 +108,7 @@ module.exports = { rules: [ // JavaScript/JSX { - test: /\.(js|jsx|mjs)$/, + test: /\.(js|jsx|mjs|ts|tsx)$/, exclude: /node_modules/, use: { loader: 'babel-loader', @@ -119,6 +119,7 @@ module.exports = { { targets: { browsers: ['last 2 versions'] } }, ], ['@babel/preset-react', { runtime: 'automatic' }], + '@babel/preset-typescript', ], cacheDirectory: true, }, @@ -143,6 +144,9 @@ module.exports = { modules: { namedExport: false, exportLocalsConvention: 'asIs', + localIdentName: isDevelopment + ? '[name]__[local]--[hash:base64:5]' + : '[hash:base64:8]', }, }, }, From a1c7fa88e4712fbf0cf9ab2f9c7d2d8b19c9c820 Mon Sep 17 00:00:00 2001 From: owlsua Date: Mon, 27 Apr 2026 23:17:38 +0200 Subject: [PATCH 02/67] assets: update and add SVG icons Add new icons (eye, lock, logout, settings, background-home). Update existing icons with new design. Remove unused SVGs. --- src/assets/images/background-home.svg | 41 +++++++++++++++++++++++++++ src/assets/images/btc-logo.svg | 2 +- src/assets/images/icon-arrow-left.svg | 5 ++-- src/assets/images/icon-expand.svg | 4 ++- src/assets/images/icon-eye-slash.svg | 3 ++ src/assets/images/icon-eye.svg | 4 +++ src/assets/images/icon-hamburger.svg | 7 ++--- src/assets/images/icon-home.svg | 7 ++--- src/assets/images/icon-lock.svg | 3 ++ src/assets/images/icon-logout.svg | 3 ++ src/assets/images/icon-settings.svg | 4 +++ src/assets/images/icon-shield.svg | 2 +- src/assets/images/icon-upload.svg | 3 ++ src/assets/images/logout.svg | 3 -- src/assets/images/settings.svg | 4 --- 15 files changed, 75 insertions(+), 20 deletions(-) create mode 100644 src/assets/images/background-home.svg create mode 100644 src/assets/images/icon-eye-slash.svg create mode 100644 src/assets/images/icon-eye.svg create mode 100644 src/assets/images/icon-lock.svg create mode 100644 src/assets/images/icon-logout.svg create mode 100644 src/assets/images/icon-settings.svg create mode 100644 src/assets/images/icon-upload.svg delete mode 100644 src/assets/images/logout.svg delete mode 100644 src/assets/images/settings.svg diff --git a/src/assets/images/background-home.svg b/src/assets/images/background-home.svg new file mode 100644 index 00000000..38ab383e --- /dev/null +++ b/src/assets/images/background-home.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/images/btc-logo.svg b/src/assets/images/btc-logo.svg index 2249e495..68908070 100644 --- a/src/assets/images/btc-logo.svg +++ b/src/assets/images/btc-logo.svg @@ -1,4 +1,4 @@ -
+ + {inputElement} + +
+ ) + } + + return inputElement +} + +export default Input From 1617fdd15c932d4cf8ad9047f116fa50a70ea82d Mon Sep 17 00:00:00 2001 From: owlsua Date: Mon, 27 Apr 2026 23:19:24 +0200 Subject: [PATCH 06/67] Error: migrate to TypeScript and CSS Modules --- src/components/basic/Error/Error.css | 10 ---- src/components/basic/Error/Error.js | 30 ---------- src/components/basic/Error/Error.module.css | 39 ++++++++++++ src/components/basic/Error/Error.tsx | 66 +++++++++++++++++++++ 4 files changed, 105 insertions(+), 40 deletions(-) delete mode 100644 src/components/basic/Error/Error.css delete mode 100644 src/components/basic/Error/Error.js create mode 100644 src/components/basic/Error/Error.module.css create mode 100644 src/components/basic/Error/Error.tsx diff --git a/src/components/basic/Error/Error.css b/src/components/basic/Error/Error.css deleted file mode 100644 index 8687bcb9..00000000 --- a/src/components/basic/Error/Error.css +++ /dev/null @@ -1,10 +0,0 @@ -.errorMessage { - margin: 2px 0 0 0; -} - -.errorMessage, -.errorMessage * { - color: rgb(var(--color-red)); - text-align: center; - font-weight: 600; -} diff --git a/src/components/basic/Error/Error.js b/src/components/basic/Error/Error.js deleted file mode 100644 index 8d999c40..00000000 --- a/src/components/basic/Error/Error.js +++ /dev/null @@ -1,30 +0,0 @@ -import './Error.css' - -const Error = ({ error }) => { - return ( -
- {Array.isArray(error) ? ( - error.map((message) => ( -

- {message} -

- )) - ) : ( -

- {error} -

- )} -
- ) -} - -export default Error diff --git a/src/components/basic/Error/Error.module.css b/src/components/basic/Error/Error.module.css new file mode 100644 index 00000000..8907c6ce --- /dev/null +++ b/src/components/basic/Error/Error.module.css @@ -0,0 +1,39 @@ +.errorMessage { + margin: 2px 0 0 8px; + animation: slideIn 0.25s ease-out; + text-align: left; +} + +.errorMessage, +.errorMessage p { + color: rgb(var(--color-red)); + font-size: 14px; + font-weight: 600; + text-align: left; +} + +.errorMessageHiding { + animation: slideOut 0.2s ease-in forwards; +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(-4px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes slideOut { + from { + opacity: 1; + transform: translateY(0); + } + to { + opacity: 0; + transform: translateY(-4px); + } +} diff --git a/src/components/basic/Error/Error.tsx b/src/components/basic/Error/Error.tsx new file mode 100644 index 00000000..64d11c25 --- /dev/null +++ b/src/components/basic/Error/Error.tsx @@ -0,0 +1,66 @@ +import { useState, useEffect, useRef } from 'react' + +import styles from './Error.module.css' + +interface ErrorProps { + error: string | string[] | null +} + +const Error = ({ error }: ErrorProps) => { + const [visible, setVisible] = useState(!!error) + const [hiding, setHiding] = useState(false) + const lastError = useRef(error) + + useEffect(() => { + if (error) { + lastError.current = error + setHiding(false) + setVisible(true) + } else if (visible) { + setHiding(true) + } + }, [error]) + + const handleAnimationEnd = () => { + if (hiding) { + setVisible(false) + setHiding(false) + } + } + + if (!visible) return null + + const displayError = error || lastError.current + + const className = [styles.errorMessage, hiding && styles.errorMessageHiding] + .filter(Boolean) + .join(' ') + + return ( +
+ {Array.isArray(displayError) ? ( + displayError.map((message) => ( +

+ {message} +

+ )) + ) : ( +

+ {displayError} +

+ )} +
+ ) +} + +export default Error From 8dd97b8599f94d5907c56a136490e66856d1cccc Mon Sep 17 00:00:00 2001 From: owlsua Date: Mon, 27 Apr 2026 23:19:53 +0200 Subject: [PATCH 07/67] BrandPanel: add new component --- .../basic/BrandPanel/BrandPanel.module.css | 59 +++++++++++++++++++ .../basic/BrandPanel/BrandPanel.tsx | 25 ++++++++ 2 files changed, 84 insertions(+) create mode 100644 src/components/basic/BrandPanel/BrandPanel.module.css create mode 100644 src/components/basic/BrandPanel/BrandPanel.tsx diff --git a/src/components/basic/BrandPanel/BrandPanel.module.css b/src/components/basic/BrandPanel/BrandPanel.module.css new file mode 100644 index 00000000..c1c32e4b --- /dev/null +++ b/src/components/basic/BrandPanel/BrandPanel.module.css @@ -0,0 +1,59 @@ +.brandPanel { + display: none; + + @media screen and (min-width: 801px) { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 38%; + background: rgb(var(--mojito-green)); + overflow: hidden; + flex-shrink: 0; + } +} + +.brandBg { + position: absolute; + inset: 0; + width: 100%; + height: 100%; +} + +.brandContent { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; +} + +.brandLogo { + width: 64px; + height: 64px; + filter: brightness(0) invert(1); +} + +.brandTitle { + display: flex; + align-items: center; + gap: 10px; + color: #fff; + font-size: 28px; + font-weight: 700; + letter-spacing: -0.5px; +} + +.brandTitleIcon { + width: 28px; + height: 28px; + filter: brightness(0) invert(1); +} + +.brandSubtitle { + font-size: 14px; + color: rgba(255, 255, 255, 0.75); + text-align: center; + line-height: 1.5; + margin: 0; +} diff --git a/src/components/basic/BrandPanel/BrandPanel.tsx b/src/components/basic/BrandPanel/BrandPanel.tsx new file mode 100644 index 00000000..97fdba7c --- /dev/null +++ b/src/components/basic/BrandPanel/BrandPanel.tsx @@ -0,0 +1,25 @@ +import { ReactComponent as LogoIcon } from '@Assets/images/logo.svg' +import { ReactComponent as BackgroundHome } from '@Assets/images/background-home.svg' + +import styles from './BrandPanel.module.css' + +const BrandPanel = () => { + return ( +
+ +
+ +
+ Mojito +
+

+ Your non-custodial +
+ Mintlayer wallet +

+
+
+ ) +} + +export default BrandPanel From de2e1626282812e9d5d0b29a782bd56267b1c743 Mon Sep 17 00:00:00 2001 From: owlsua Date: Mon, 27 Apr 2026 23:20:18 +0200 Subject: [PATCH 08/67] PageWrapper: add new component --- .../basic/PageWrapper/PageWrapper.module.css | 13 +++++++++++ .../basic/PageWrapper/PageWrapper.tsx | 22 +++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 src/components/basic/PageWrapper/PageWrapper.module.css create mode 100644 src/components/basic/PageWrapper/PageWrapper.tsx diff --git a/src/components/basic/PageWrapper/PageWrapper.module.css b/src/components/basic/PageWrapper/PageWrapper.module.css new file mode 100644 index 00000000..b70e9f3c --- /dev/null +++ b/src/components/basic/PageWrapper/PageWrapper.module.css @@ -0,0 +1,13 @@ +.wrapper { + position: relative; + display: flex; + flex-direction: column; + height: 530px; + padding: 1rem; + flex: 1; + + @media screen and (min-width: 801px) { + height: 93%; + padding: 2rem; + } +} diff --git a/src/components/basic/PageWrapper/PageWrapper.tsx b/src/components/basic/PageWrapper/PageWrapper.tsx new file mode 100644 index 00000000..5dfab98f --- /dev/null +++ b/src/components/basic/PageWrapper/PageWrapper.tsx @@ -0,0 +1,22 @@ +import { ReactNode, CSSProperties } from 'react' +import styles from './PageWrapper.module.css' + +interface PageWrapperProps { + children: ReactNode + style?: CSSProperties + className?: string +} + +const PageWrapper = ({ children, style, className }: PageWrapperProps) => { + const classNames = [styles.wrapper, className].filter(Boolean).join(' ') + return ( +
+ {children} +
+ ) +} + +export default PageWrapper From 7d7236391e7b0048a221af94291a6d902687ca19 Mon Sep 17 00:00:00 2001 From: owlsua Date: Mon, 27 Apr 2026 23:20:35 +0200 Subject: [PATCH 09/67] OptionCard: add new component --- .../basic/OptionCard/OptionCard.module.css | 67 +++++++++++++++++++ .../basic/OptionCard/OptionCard.tsx | 45 +++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 src/components/basic/OptionCard/OptionCard.module.css create mode 100644 src/components/basic/OptionCard/OptionCard.tsx diff --git a/src/components/basic/OptionCard/OptionCard.module.css b/src/components/basic/OptionCard/OptionCard.module.css new file mode 100644 index 00000000..fa6ac583 --- /dev/null +++ b/src/components/basic/OptionCard/OptionCard.module.css @@ -0,0 +1,67 @@ +.card { + display: flex; + flex-direction: column; + gap: 8px; + padding: 24px; + border-radius: 20px; + background: rgb(var(--color-white)); + border: 1px solid rgba(var(--color-light-gray), 0.4); + cursor: pointer; + transition: + border-color 0.2s ease, + box-shadow 0.2s ease; +} + +.card:hover { + border-color: rgba(var(--mojito-green), 0.5); + box-shadow: 0 2px 12px rgba(var(--mojito-green), 0.08); +} + +.icon { + display: flex; + align-items: center; + justify-content: center; + width: 48px; + height: 48px; + border-radius: 14px; + background: rgba(var(--mojito-green), 0.1); + margin-bottom: 4px; +} + +.icon svg { + width: 24px; + height: 24px; + color: rgb(var(--mojito-green)); +} + +.title { + font-size: 16px; + font-weight: 700; + color: rgb(var(--color-black)); +} + +.description { + font-size: 13px; + color: rgb(var(--color-dark-gray)); + line-height: 1.4; +} + +.link { + display: inline-flex; + align-items: center; + gap: 4px; + font-size: 14px; + font-weight: 600; + color: rgb(var(--mojito-green)); + margin-top: 4px; +} + +.link svg { + width: 14px; + height: 14px; + transition: transform 0.2s ease; +} + +.card:hover .link svg { + transform: translateX(3px); +} diff --git a/src/components/basic/OptionCard/OptionCard.tsx b/src/components/basic/OptionCard/OptionCard.tsx new file mode 100644 index 00000000..2a27e923 --- /dev/null +++ b/src/components/basic/OptionCard/OptionCard.tsx @@ -0,0 +1,45 @@ +import { ReactNode } from 'react' +import styles from './OptionCard.module.css' + +interface OptionCardProps { + icon: ReactNode + title: string + description: string + linkText?: string + onClick: () => void +} + +const OptionCard = ({ + icon, + title, + description, + linkText = 'Select', + onClick, +}: OptionCardProps) => ( +
+
{icon}
+ {title} + {description} + + {linkText} + + + + +
+) + +export default OptionCard From 66d31b76e6f11e2514415fe1caeb08258559111d Mon Sep 17 00:00:00 2001 From: owlsua Date: Mon, 27 Apr 2026 23:20:52 +0200 Subject: [PATCH 10/67] basic components: update Arc, Line, Logo, SkeletonLoader, Svg and index --- src/components/basic/Arc/Arc.js | 5 +++-- src/components/basic/Line/Line.js | 7 ++++--- src/components/basic/Logo/Logo.css | 18 ++++++++---------- .../basic/SkeletonLoader/SkeletonLoader.css | 15 +++++++++++++++ .../basic/SkeletonLoader/SkeletonLoader.js | 12 +++++++----- src/components/basic/Svg/Svg.js | 1 + src/components/basic/index.js | 12 +++++++++--- 7 files changed, 47 insertions(+), 23 deletions(-) diff --git a/src/components/basic/Arc/Arc.js b/src/components/basic/Arc/Arc.js index ee9fe501..b5e43afe 100644 --- a/src/components/basic/Arc/Arc.js +++ b/src/components/basic/Arc/Arc.js @@ -1,7 +1,7 @@ import * as d3 from 'd3' const createArcGenerator = () => - d3.arc().innerRadius(86).outerRadius(100).cornerRadius(10) + d3.arc().innerRadius(96).outerRadius(100).cornerRadius(3) const createPieGenerator = () => d3 @@ -10,6 +10,7 @@ const createPieGenerator = () => .endAngle(0.5 * Math.PI) .value((item) => item.value) .padAngle(0.02) + .sort((a, b) => b.value - a.value) const createTooltip = () => d3 @@ -61,7 +62,7 @@ const buildArc = ({ .data(pathData) .join('path') .attr('d', arcGenerator) - .attr('stroke', '#fff') + .attr('stroke', 'none') .attr('fill', (item) => item.data.color) .attr('data-testid', (item) => `arc-${item.data.asset}-container`) .on('mouseover', mouseMoveHandleBinded) diff --git a/src/components/basic/Line/Line.js b/src/components/basic/Line/Line.js index d42c5757..de4ebb7e 100644 --- a/src/components/basic/Line/Line.js +++ b/src/components/basic/Line/Line.js @@ -1,5 +1,5 @@ import React from 'react' -import { line, scaleLinear, curveNatural } from 'd3' +import { line, scaleLinear, curveMonotoneX } from 'd3' const Line = ({ points, @@ -11,13 +11,14 @@ const Line = ({ const min = Math.min(...points.map((item) => Number(item[1]))) const max = Math.max(...points.map((item) => Number(item[1]))) + const padding = parseInt(strokeWidth) / 2 const scale = scaleLinear() .domain([min, max]) - .range([0, parseInt(height)]) + .range([parseInt(height) - padding, padding]) const lineGenerator = line() .y((d) => scale(d[1]).toFixed(2)) - .curve(curveNatural) + .curve(curveMonotoneX) const pathData = lineGenerator(points) diff --git a/src/components/basic/Logo/Logo.css b/src/components/basic/Logo/Logo.css index fb6a4457..f9cfbe79 100644 --- a/src/components/basic/Logo/Logo.css +++ b/src/components/basic/Logo/Logo.css @@ -3,37 +3,35 @@ display: flex; justify-content: space-between; align-items: center; - line-height: 60px; - width: 190px; + width: 117px; } .logoContainer .logo { - width: 50px; - height: 50px; + width: 30px; + height: 30px; margin: 0; } .logoContainer .mojitoLettering { - font-size: 2.4rem; + font-size: 1.5rem; color: rgb(var(--color-black)); } .logoContainer .mojitoLettering .testnetMark { - font-size: 2.5rem; + font-size: 1.5rem; color: rgb(var(--color-orange)); } .testnetMessage { position: absolute; - bottom: -3px; - right: 0; + bottom: -2px; + right: -1px; display: flex; align-items: center; color: rgb(var(--color-orange)); border-radius: 20px; - font-size: 0.8rem; + font-size: 0.55rem; font-weight: 600; - height: 1.5rem; margin: 0; text-align: center; } diff --git a/src/components/basic/SkeletonLoader/SkeletonLoader.css b/src/components/basic/SkeletonLoader/SkeletonLoader.css index 29a06f51..346051f6 100644 --- a/src/components/basic/SkeletonLoader/SkeletonLoader.css +++ b/src/components/basic/SkeletonLoader/SkeletonLoader.css @@ -8,6 +8,14 @@ color: rgb(var(--color-white)); } +.card-compact { + padding: 8px 20px 8px 12px; + margin-bottom: 0; + border-radius: 35px; + max-height: 72px; + min-height: 72px; +} + .skeleton { animation: skeleton-loading 1s linear infinite alternate; } @@ -30,6 +38,13 @@ min-height: 72px; } +.cardHeader-compact { + height: 50px; + width: 50px; + min-width: 50px; + min-height: 50px; +} + .cardBodyWrapper { display: flex; align-items: center; diff --git a/src/components/basic/SkeletonLoader/SkeletonLoader.js b/src/components/basic/SkeletonLoader/SkeletonLoader.js index 1f3b9084..fd2b7f53 100644 --- a/src/components/basic/SkeletonLoader/SkeletonLoader.js +++ b/src/components/basic/SkeletonLoader/SkeletonLoader.js @@ -15,10 +15,12 @@ const SkeletonText = () => { ) } -const SkeletonLoader = () => { +const SkeletonLoader = ({ variant = 'default' }) => { + const isCompact = variant === 'compact' + return ( @@ -46,7 +48,7 @@ const SkeletonLoader = () => { className="cardBody cardBodyRight" data-testid="body-item" > - {Array.from({ length: 3 }).map((item, index) => ( + {Array.from({ length: isCompact ? 2 : 3 }).map((item, index) => ( ))} diff --git a/src/components/basic/Svg/Svg.js b/src/components/basic/Svg/Svg.js index faef6c9a..ad4bc69b 100644 --- a/src/components/basic/Svg/Svg.js +++ b/src/components/basic/Svg/Svg.js @@ -16,6 +16,7 @@ const Svg = ({ width={width} height={height} viewBox={`0 0 ${viewboxWidth} ${viewboxHeight}`} + preserveAspectRatio="none" data-testid="svg-container" > {children} diff --git a/src/components/basic/index.js b/src/components/basic/index.js index d203b9fe..1e3e0b91 100644 --- a/src/components/basic/index.js +++ b/src/components/basic/index.js @@ -1,12 +1,12 @@ import * as Arc from './Arc/Arc' -import Button from './Button/Button' -import Input from './Input/Input' +import Button from './Button/Button.tsx' +import Input from './Input/Input.tsx' import InputInteger from './Input/InputInteger' import InputFloat from './Input/InputFloat' import InputBTC from './Input/InputBTC' import Line from './Line/Line' import Svg from './Svg/Svg' -import Error from './Error/Error' +import Error from './Error/Error.tsx' import Toggle from './Toggle/Toggle' import Logo from './Logo/Logo' import LogoRound from './LogoRound/LogoRound' @@ -15,6 +15,9 @@ import Tooltip from './Tooltip/Tooltip' import Textarea from './Textarea/Textarea' import EmptyListMessage from './EmptyList/EmptyList' import SwapTokenLogo from './SwapTokenLogo/SwapTokenLogo' +import PageWrapper from './PageWrapper/PageWrapper.tsx' +import BrandPanel from './BrandPanel/BrandPanel' +import OptionCard from './OptionCard/OptionCard' export { Arc, @@ -34,4 +37,7 @@ export { Textarea, EmptyListMessage, SwapTokenLogo, + PageWrapper, + BrandPanel, + OptionCard, } From 91f67fafacb2b265af29090d440f1571b8e620bd Mon Sep 17 00:00:00 2001 From: owlsua Date: Mon, 27 Apr 2026 23:21:09 +0200 Subject: [PATCH 11/67] Header: migrate to TypeScript and CSS Modules --- src/components/composed/Header/Header.css | 108 ------------------ .../composed/Header/Header.module.css | 108 ++++++++++++++++++ src/components/composed/Header/Header.test.js | 2 +- .../composed/Header/{Header.js => Header.tsx} | 55 +++++---- 4 files changed, 144 insertions(+), 129 deletions(-) delete mode 100644 src/components/composed/Header/Header.css create mode 100644 src/components/composed/Header/Header.module.css rename src/components/composed/Header/{Header.js => Header.tsx} (65%) diff --git a/src/components/composed/Header/Header.css b/src/components/composed/Header/Header.css deleted file mode 100644 index 5a37bc1f..00000000 --- a/src/components/composed/Header/Header.css +++ /dev/null @@ -1,108 +0,0 @@ -.header-container { - display: flex; - justify-content: center; - min-height: 60px; -} - -header { - position: relative; - margin-bottom: 20px; - min-height: 60px; - - @media screen and (min-width: 801px) { - margin-bottom: 40px; - } -} - -.logo-wrapper { - display: flex; - justify-content: center; - align-items: center; -} - -.backButton { - height: 32px; - width: 32px; - padding: 0.2rem; - position: absolute; - top: 0.7rem; - left: 0; - background-color: transparent !important; -} - -.backButton svg { - width: 100%; - height: 100%; -} - -.backButton:hover svg path, -.backButton:focus svg path { - stroke: rgb(var(--color-black)); - opacity: 0.6; -} - -.logout { - height: 40px; - width: 40px; - padding: 0.2rem; - position: absolute; - top: 0.5rem; - right: 0; -} - -.header-menu-button { - height: 40px; - width: 40px; - padding: 0.2rem; - top: 0.5rem; - right: 45px; - outline: none; - background-color: transparent !important; -} - -.header-menu-button svg { - width: 24px; - height: 24px; -} - -.settings { - height: 40px; - width: 40px; - padding: 0.2rem; - top: 0.5rem; - right: 45px; - outline: none; -} - -.expand { - height: 40px; - width: 40px; - padding: 0.2rem; - top: 0.5rem; - right: 45px; - outline: none; -} - -.logout svg, -.settings svg, -.menu svg { - width: 100%; - height: 100%; -} - -.expand-wrapped { - position: absolute; - display: flex; - top: 8px; - right: 0; - overflow: visible; -} - -.expand-wrapped-unlocked { - right: 40px; -} - -.tooltipWrapper { - position: relative; - overflow: visible; -} diff --git a/src/components/composed/Header/Header.module.css b/src/components/composed/Header/Header.module.css new file mode 100644 index 00000000..8bb13544 --- /dev/null +++ b/src/components/composed/Header/Header.module.css @@ -0,0 +1,108 @@ +.header { + display: flex; + justify-content: center; + position: relative; + min-height: 70px; + background: rgb(var(--color-white)); + padding: 20px 31px; + border-bottom: 1px solid rgba(var(--color-black), 0.1); +} + +.logoWrapper { + display: flex; + justify-content: center; + align-items: center; +} + +.backButton { + height: 35px; + width: 35px; + padding: 0.2rem; + position: absolute; + top: 18px; + left: 27px; + border-radius: 10px; + background-color: transparent !important; +} + +.backButton:hover { + background-color: rgba(var(--color-black), 0.05) !important; +} + +.backButton svg { + width: 23px; + height: 23px; +} + +.backButton svg path, +.backButton svg path { + stroke: rgb(var(--color-black)); +} + +.backButton:hover svg path, +.backButton:focus svg path { + opacity: 0.6; +} + +.menuButton { + height: 40px; + width: 40px; + padding: 0.2rem; + top: 0.5rem; + right: 45px; + outline: none; + background-color: transparent !important; +} + +.menuButton svg { + width: 24px; + height: 24px; +} + +.menuButton svg path { + stroke: rgb(var(--color-black)); +} + +.expandWrapped { + position: absolute; + display: flex; + top: 16px; + right: 22px; + overflow: visible; + + @media screen and (min-width: 801px) { + display: none; + } +} + +.settingsExpand { + display: none; + position: absolute; + top: 16px; + right: 22px; + + @media screen and (min-width: 801px) { + display: flex; + } +} + +.settingsButton { + height: 35px; + width: 35px; + padding: 0.2rem; + border-radius: 10px; + background-color: transparent !important; +} + +.settingsButton:hover { + background-color: rgba(var(--color-black), 0.05) !important; +} + +.settingsButton svg { + width: 20px; + height: 20px; +} + +.settingsButton svg path { + stroke: rgb(var(--color-black)); +} diff --git a/src/components/composed/Header/Header.test.js b/src/components/composed/Header/Header.test.js index 4594a6e2..3aeda2fc 100644 --- a/src/components/composed/Header/Header.test.js +++ b/src/components/composed/Header/Header.test.js @@ -8,7 +8,7 @@ import { MintlayerProvider, BitcoinProvider, } from '@Contexts' -import Header from './Header' +import Header from './Header.tsx' global.AbortSignal = global.AbortSignal || {} diff --git a/src/components/composed/Header/Header.js b/src/components/composed/Header/Header.tsx similarity index 65% rename from src/components/composed/Header/Header.js rename to src/components/composed/Header/Header.tsx index bb856fa6..806968b9 100644 --- a/src/components/composed/Header/Header.js +++ b/src/components/composed/Header/Header.tsx @@ -1,21 +1,26 @@ -import React, { useContext, useEffect, useState } from 'react' +import { useContext, useEffect, useState } from 'react' import { useNavigate, useLocation } from 'react-router' import { ReactComponent as BackImg } from '@Assets/images/icon-arrow-left.svg' import { ReactComponent as MenuImg } from '@Assets/images/icon-hamburger.svg' +import { ReactComponent as SettingsImg } from '@Assets/images/icon-settings.svg' import { Button, Logo } from '@BasicComponents' import { UpdateButton, SliderMenu, Navigation } from '@ComposedComponents' import { AccountContext } from '@Contexts' -import './Header.css' +import styles from './Header.module.css' -const Header = ({ customBackAction }) => { +const Header = () => { const [unlocked, setUnlocked] = useState(false) const navigate = useNavigate() const location = useLocation() - const { isAccountUnlocked, sliderMenuOpen, setSliderMenuOpen } = - useContext(AccountContext) + const { + isAccountUnlocked, + sliderMenuOpen, + setSliderMenuOpen, + customBackAction, + } = useContext(AccountContext) const coinType = location.pathname.includes('/wallet/') ? location.pathname.split('/wallet/')[1].split('/')[0] @@ -27,8 +32,7 @@ const Header = ({ customBackAction }) => { const noBackButtonPages = ['/dashboard', '/'] const noBackButton = noBackButtonPages.includes(location.pathname) - - const hideWithoutCustomBack = ['/set-account', '/restore-account'] + const isCreateRestorePage = location.pathname === '/create-restore' useEffect(() => { const accountUnlocked = isAccountUnlocked() @@ -36,10 +40,6 @@ const Header = ({ customBackAction }) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [location.pathname]) - if (hideWithoutCustomBack.includes(location.pathname) && !customBackAction) { - return null - } - const goBack = () => { if (isWalletPage) { navigate('/dashboard') @@ -61,29 +61,44 @@ const Header = ({ customBackAction }) => { } return ( -
+
-
+
-
- - {unlocked && } -
+ {!isCreateRestorePage && ( +
+ + {unlocked && } +
+ )} + + {!unlocked && ( +
+ +
+ )} Date: Mon, 27 Apr 2026 23:21:28 +0200 Subject: [PATCH 12/67] Loading: migrate to TypeScript and CSS Modules --- src/components/composed/Loading/Loading.js | 18 ------------------ .../{Loading.css => Loading.module.css} | 8 ++++---- .../composed/Loading/Loading.test.js | 2 +- src/components/composed/Loading/Loading.tsx | 18 ++++++++++++++++++ 4 files changed, 23 insertions(+), 23 deletions(-) delete mode 100644 src/components/composed/Loading/Loading.js rename src/components/composed/Loading/{Loading.css => Loading.module.css} (75%) create mode 100644 src/components/composed/Loading/Loading.tsx diff --git a/src/components/composed/Loading/Loading.js b/src/components/composed/Loading/Loading.js deleted file mode 100644 index e809ccd7..00000000 --- a/src/components/composed/Loading/Loading.js +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react' -import { useStyleClasses } from '@Hooks' - -import './Loading.css' - -const Loading = ({ extraStyleClasses = [] }) => { - const classesList = ['lds-dual-ring', ...extraStyleClasses] - const { styleClasses } = useStyleClasses(classesList) - - return ( -
- ) -} - -export default Loading diff --git a/src/components/composed/Loading/Loading.css b/src/components/composed/Loading/Loading.module.css similarity index 75% rename from src/components/composed/Loading/Loading.css rename to src/components/composed/Loading/Loading.module.css index b6472b79..e7a2579b 100644 --- a/src/components/composed/Loading/Loading.css +++ b/src/components/composed/Loading/Loading.module.css @@ -1,10 +1,10 @@ -.lds-dual-ring { +.ldsDualRing { display: inline-block; width: 80px; height: 80px; } -.lds-dual-ring:after { +.ldsDualRing:after { content: ' '; display: block; width: 64px; @@ -13,10 +13,10 @@ border: 6px solid #fff; border-color: rgb(var(--color-main-green)) transparent rgb(var(--color-black)) transparent; - animation: lds-dual-ring 1.2s linear infinite; + animation: ldsDualRing 1.2s linear infinite; } -@keyframes lds-dual-ring { +@keyframes ldsDualRing { 0% { transform: rotate(0deg); } diff --git a/src/components/composed/Loading/Loading.test.js b/src/components/composed/Loading/Loading.test.js index d933b46c..6ddb6d70 100644 --- a/src/components/composed/Loading/Loading.test.js +++ b/src/components/composed/Loading/Loading.test.js @@ -1,5 +1,5 @@ import { render, screen } from '@testing-library/react' -import Loading from './Loading' +import Loading from './Loading.tsx' test('Render Loading component', () => { render() diff --git a/src/components/composed/Loading/Loading.tsx b/src/components/composed/Loading/Loading.tsx new file mode 100644 index 00000000..f0214fe0 --- /dev/null +++ b/src/components/composed/Loading/Loading.tsx @@ -0,0 +1,18 @@ +import styles from './Loading.module.css' + +interface LoadingProps { + extraStyleClasses?: string[] +} + +const Loading = ({ extraStyleClasses = [] }: LoadingProps) => { + const styleClasses = [styles.ldsDualRing, ...extraStyleClasses].join(' ') + + return ( +
+ ) +} + +export default Loading From 55b51ae5018f7ee9bfaaebd9f3e816bfee1eefed Mon Sep 17 00:00:00 2001 From: owlsua Date: Mon, 27 Apr 2026 23:21:46 +0200 Subject: [PATCH 13/67] Navigation: migrate to TypeScript and CSS Modules --- .../composed/Navigation/Navigation.css | 75 ---------- .../composed/Navigation/Navigation.module.css | 48 ++++++ .../composed/Navigation/Navigation.test.js | 2 +- .../{Navigation.js => Navigation.tsx} | 137 +++++++++--------- .../composed/Navigation/NestedNavigation.js | 56 ------- ...gation.css => NestedNavigation.module.css} | 22 +-- .../composed/Navigation/NestedNavigation.tsx | 90 ++++++++++++ 7 files changed, 220 insertions(+), 210 deletions(-) delete mode 100644 src/components/composed/Navigation/Navigation.css create mode 100644 src/components/composed/Navigation/Navigation.module.css rename src/components/composed/Navigation/{Navigation.js => Navigation.tsx} (62%) delete mode 100644 src/components/composed/Navigation/NestedNavigation.js rename src/components/composed/Navigation/{NestedNavigation.css => NestedNavigation.module.css} (77%) create mode 100644 src/components/composed/Navigation/NestedNavigation.tsx diff --git a/src/components/composed/Navigation/Navigation.css b/src/components/composed/Navigation/Navigation.css deleted file mode 100644 index 4975d549..00000000 --- a/src/components/composed/Navigation/Navigation.css +++ /dev/null @@ -1,75 +0,0 @@ -.bottom-menu-item { - display: flex; - align-items: center; - padding: 10px 5px; - border-radius: 4px; - cursor: pointer; - transition: all 0.2s ease; -} - -.navigation-item { - position: relative; - display: flex; - flex-direction: column; - justify-content: center; - border-radius: 4px; - cursor: pointer; - transition: all 0.2s ease; -} - -.bottom-menu-item:last-child, -.navigation-item:last-child { - border-bottom: none; -} - -.label-wrapper { - display: flex; - align-items: center; - padding: 10px 5px; - border-radius: 4px; -} - -.bottom-menu-item svg, -.navigation-item svg { - width: 21px; - height: 20px; - margin-right: 10px; -} - -.slider-version { - display: flex; - margin-top: 5px; - padding-left: 5px; - font-size: 0.9rem; -} - -.navigation-item-open { - background: rgba(var(--color-light-purple), 0.4); - padding: 5px; - transition: all 0.2s ease; -} - -.navigation-item .navigation-triangle { - position: absolute; - top: 16px; - right: 10px; - width: 8px; - height: 8px; - margin: 0; - transform: rotate(180deg); - transition: all 0.2s ease; -} - -.navigation-item .navigation-triangle g { - fill: rgb(var(--color-white)); -} - -.navigation-item .navigation-triangle-open { - transform: none; - top: 21px; -} - -.bottom-menu-item:hover, -.navigation-item:hover { - background-color: rgba(var(--color-light-purple), 0.4); -} diff --git a/src/components/composed/Navigation/Navigation.module.css b/src/components/composed/Navigation/Navigation.module.css new file mode 100644 index 00000000..ffb21e98 --- /dev/null +++ b/src/components/composed/Navigation/Navigation.module.css @@ -0,0 +1,48 @@ +.navigationList { + display: flex; + flex-direction: column; + flex: 1; + gap: 3px; +} + +.navigationItem, +.bottomMenuItem { + position: relative; + display: flex; + align-items: center; + border-radius: 16px; + padding: 10px 14px; + cursor: pointer; + transition: all 0.2s ease; + cursor: pointer; +} + +.labelWrapper { + display: flex; + align-items: center; + font-size: 14px; +} + +.bottomMenuItem svg, +.navigationItem svg { + width: 18px; + height: 18px; + margin-right: 10px; +} + +.sliderVersion { + display: flex; + margin-top: 5px; + padding-left: 15px; + font-size: 11px; +} + +.navigationItemActive { + background: rgb(var(--mojito-green-soft)); + color: rgb(var(--mojito-green)); +} + +.navigationItem:hover, +.bottomMenuItem:hover { + background: rgb(var(--mojito-green-soft)); +} diff --git a/src/components/composed/Navigation/Navigation.test.js b/src/components/composed/Navigation/Navigation.test.js index 194dc833..cd1044d1 100644 --- a/src/components/composed/Navigation/Navigation.test.js +++ b/src/components/composed/Navigation/Navigation.test.js @@ -1,7 +1,7 @@ import React from 'react' import { render, screen, fireEvent } from '@testing-library/react' import { MemoryRouter } from 'react-router' -import Navigation from './Navigation' +import Navigation from './Navigation.tsx' import { AccountContext, MintlayerContext } from '@Contexts' // Mock the chrome object diff --git a/src/components/composed/Navigation/Navigation.js b/src/components/composed/Navigation/Navigation.tsx similarity index 62% rename from src/components/composed/Navigation/Navigation.js rename to src/components/composed/Navigation/Navigation.tsx index 66858ec9..f0447098 100644 --- a/src/components/composed/Navigation/Navigation.js +++ b/src/components/composed/Navigation/Navigation.tsx @@ -1,29 +1,42 @@ /* eslint-disable no-undef */ -import React, { useContext, useEffect, useState } from 'react' +import { ReactNode, useContext, useEffect, useState } from 'react' import { useNavigate, useLocation } from 'react-router' -import { ReactComponent as LogoutImg } from '@Assets/images/logout.svg' +import { ReactComponent as LogoutImg } from '@Assets/images/icon-logout.svg' import { ReactComponent as ExpandImg } from '@Assets/images/icon-expand.svg' -import { ReactComponent as SettingsImg } from '@Assets/images/settings.svg' +import { ReactComponent as SettingsImg } from '@Assets/images/icon-settings.svg' import { ReactComponent as LoginImg } from '@Assets/images/icon-login.svg' import { ReactComponent as AddWalletImg } from '@Assets/images/icon-add-wallet.svg' import { ReactComponent as HomeImg } from '@Assets/images/icon-home.svg' -// import { ReactComponent as WalletIcon } from '@Assets/images/icon-wallet.svg' -import { ReactComponent as TriangleIcon } from '@Assets/images/icon-triangle.svg' +import { ReactComponent as BtcLogo } from '@Assets/images/btc-logo.svg' +import { ReactComponent as MlLogo } from '@Assets/images/logo.svg' import { APP_VERSION } from '@Version' import { AccountContext, MintlayerContext } from '@Contexts' -// import { AppInfo } from '@Constants' -import NestedNavigation from './NestedNavigation' +import styles from './Navigation.module.css' -import './Navigation.css' +interface NavigationItem { + id: number + label: string + icon?: ReactNode + link?: string + type?: string + content?: NavigationItem[] +} + +interface NavigationProps { + customNavigation?: NavigationItem[] + toggleMenu?: boolean +} -const Navigation = ({ customNavigation }) => { +const Navigation = ({ + customNavigation, + toggleMenu = true, +}: NavigationProps) => { const [unlocked, setUnlocked] = useState(false) - const [navigationItemID, setNavigationItemID] = useState(null) - const [nestedItemID, setNestedItemID] = useState(null) + const [navigationItemID, setNavigationItemID] = useState(null) const navigate = useNavigate() const location = useLocation() const { @@ -42,34 +55,23 @@ const Navigation = ({ customNavigation }) => { }, [location.pathname]) const toggleSliderMenu = () => { + if (!toggleMenu) return setSliderMenuOpen(!sliderMenuOpen) } - const onNavigationItemClick = (item) => { + const onNavigationItemClick = (item: NavigationItem) => { if (item.type !== 'menu') { - navigate(item.link) + navigate(item.link!) toggleSliderMenu() } else { setNavigationItemID(navigationItemID === item.id ? null : item.id) } - return - } - - const onNestedItemClick = (item) => { - if (item.type !== 'menu') { - navigate(item.link) - toggleSliderMenu() - } else { - setNestedItemID(nestedItemID === item.id ? null : item.id) - } - return } const expandHandler = () => { window.open( typeof browser !== 'undefined' - ? // eslint-disable-next-line no-undef - browser.runtime.getURL('popup.html') + ? browser.runtime.getURL('popup.html') : chrome.runtime.getURL('popup.html'), '_blank', ) @@ -82,22 +84,29 @@ const Navigation = ({ customNavigation }) => { toggleSliderMenu() } - const loggedNavigationList = [ + const loggedNavigationList: NavigationItem[] = [ { id: 1, label: 'Dashboard', icon: , link: '/dashboard', }, - // { - // id: 2, - // label: 'Wallets', - // icon: , - // type: 'menu', - // content: AppInfo.WALLETS_NAVIGATION, - // }, + + { + id: 2, + label: 'Bitcoin Wallet', + icon: , + link: '/wallet/Bitcoin', + }, { id: 3, + label: 'Mintlayer Wallet', + icon: , + link: '/wallet/Mintlayer', + }, + + { + id: 4, label: 'Settings', icon: , link: '/settings', @@ -105,7 +114,7 @@ const Navigation = ({ customNavigation }) => { ...(process.env.REACT_APP_CONFIG_NAME !== 'production' ? [ { - id: 4, + id: 5, label: 'Connection Page', icon: , link: '/connect', @@ -115,7 +124,7 @@ const Navigation = ({ customNavigation }) => { ...(process.env.REACT_APP_CONFIG_NAME !== 'production' ? [ { - id: 5, + id: 6, label: 'Test Sign Transaction', icon: , link: '/wallet/Mintlayer/sign-external-transaction', @@ -125,7 +134,7 @@ const Navigation = ({ customNavigation }) => { ...(process.env.REACT_APP_CONFIG_NAME !== 'production' ? [ { - id: 6, + id: 7, label: 'Test Sign Bitcoin Transaction', icon: , link: '/wallet/Bitcoin/sign-transaction', @@ -135,7 +144,7 @@ const Navigation = ({ customNavigation }) => { ...(process.env.REACT_APP_CONFIG_NAME !== 'production' ? [ { - id: 7, + id: 8, label: 'Test Sign Challenge', icon: , link: '/wallet/Mintlayer/sign-challenge', @@ -144,7 +153,7 @@ const Navigation = ({ customNavigation }) => { : []), ] - const navigationList = [ + const navigationList: NavigationItem[] = [ { id: 1, label: 'Login', @@ -168,6 +177,11 @@ const Navigation = ({ customNavigation }) => { }, ] + const isActive = (item: NavigationItem) => { + if (!item.link) return false + return location.pathname.startsWith(item.link) + } + const navList = customNavigation ? customNavigation : unlocked @@ -176,58 +190,47 @@ const Navigation = ({ customNavigation }) => { return ( <> -
    +
      {navList.map((item) => (
    • { + onNavigationItemClick(item) + }} > -
      { - onNavigationItemClick(item) - }} - > +
      {item.icon && item.icon} {item.label}
      - - {item.type === 'menu' && navigationItemID === item.id && ( - - )} - {item.type === 'menu' && ( - - )}
    • ))}
    -
      +
        {!isExtended && (
      • - Expand view +
        + Expand view +
      • )} {unlocked && (
      • - - Logout +
        + + Logout +
      • )} - v{APP_VERSION} + v{APP_VERSION}
      ) diff --git a/src/components/composed/Navigation/NestedNavigation.js b/src/components/composed/Navigation/NestedNavigation.js deleted file mode 100644 index 069b2d66..00000000 --- a/src/components/composed/Navigation/NestedNavigation.js +++ /dev/null @@ -1,56 +0,0 @@ -import React, { useContext } from 'react' -import { useNavigate } from 'react-router' -import { ReactComponent as TriangleIcon } from '@Assets/images/icon-triangle.svg' - -import { AccountContext } from '@Contexts' - -import './NestedNavigation.css' - -const NestedNavigation = ({ item, onNestedItemClick, nestedItemID }) => { - const { sliderMenuOpen, setSliderMenuOpen } = useContext(AccountContext) - const navigate = useNavigate() - const toggleSliderMenu = () => { - setSliderMenuOpen(!sliderMenuOpen) - } - return ( -
        - {item.content.map((nestedItem) => ( -
      • -
        { - onNestedItemClick(nestedItem) - }} - > - {nestedItem.icon && nestedItem.icon} - {nestedItem.label} -
        - - {nestedItem.type === 'menu' && nestedItemID === nestedItem.id && ( -
          - {nestedItem.actions.map((action) => ( -
        • { - navigate(action.link) - toggleSliderMenu() - }} - > - {action.name} -
        • - ))} -
        - )} -
      • - ))} -
      - ) -} - -export default NestedNavigation diff --git a/src/components/composed/Navigation/NestedNavigation.css b/src/components/composed/Navigation/NestedNavigation.module.css similarity index 77% rename from src/components/composed/Navigation/NestedNavigation.css rename to src/components/composed/Navigation/NestedNavigation.module.css index 195b4469..2f81df5d 100644 --- a/src/components/composed/Navigation/NestedNavigation.css +++ b/src/components/composed/Navigation/NestedNavigation.module.css @@ -1,9 +1,9 @@ -.nested-menu { +.nestedMenu { margin-top: 10px; transition: all 0.2s ease; } -.nested-menu-item { +.nestedMenuItem { position: relative; margin-bottom: 1px; border-radius: 4px; @@ -13,11 +13,11 @@ transition: all 0.2s ease; } -.nested-menu-item-open { +.nestedMenuItemOpen { padding: 5px; } -.nested-label-wrapper { +.nestedLabelWrapper { display: flex; align-items: center; padding: 10px 15px; @@ -25,23 +25,23 @@ border-radius: 4px; } -.nested-label-wrapper:hover { +.nestedLabelWrapper:hover { color: rgb(var(--color-white)); background: rgba(var(--color-purple), 0.7); transition: all 0.2s ease; } -.nested-menu-item-open .nested-label-wrapper { +.nestedMenuItemOpen .nestedLabelWrapper { color: rgb(var(--color-white)); background: rgba(var(--color-purple), 0.7); transition: all 0.2s ease; } -.nested-item-menu { +.nestedItemMenu { margin-top: 5px; } -.nested-item-menu-item { +.nestedItemMenuItem { display: flex; padding: 10px 15px; margin-bottom: 1px; @@ -52,12 +52,12 @@ transition: all 0.2s ease; } -.nested-item-menu-item:hover { +.nestedItemMenuItem:hover { color: rgb(var(--color-white)); background-color: rgba(153, 196, 153, 0.8); } -.nested-item-menu .navigation-triangle { +.navigationTriangle { position: absolute; top: 16px; right: 10px; @@ -68,7 +68,7 @@ transition: all 0.2s ease; } -.nested-item-menu .navigation-triangle-open { +.navigationTriangleOpen { transform: none; top: 21px; } diff --git a/src/components/composed/Navigation/NestedNavigation.tsx b/src/components/composed/Navigation/NestedNavigation.tsx new file mode 100644 index 00000000..656fdfe1 --- /dev/null +++ b/src/components/composed/Navigation/NestedNavigation.tsx @@ -0,0 +1,90 @@ +import { ReactNode, useContext } from 'react' +import { useNavigate } from 'react-router' +import { ReactComponent as TriangleIcon } from '@Assets/images/icon-triangle.svg' + +import { AccountContext } from '@Contexts' + +import styles from './NestedNavigation.module.css' + +interface NestedAction { + id: number + name: string + link: string +} + +interface NestedItem { + id: number + label: string + icon?: ReactNode + link?: string + type?: string + actions?: NestedAction[] +} + +interface NavigationItem { + id: number + label: string + icon?: ReactNode + link?: string + type?: string + content: NestedItem[] +} + +interface NestedNavigationProps { + item: NavigationItem + onNestedItemClick: (item: NestedItem) => void + nestedItemID: number | null +} + +const NestedNavigation = ({ + item, + onNestedItemClick, + nestedItemID, +}: NestedNavigationProps) => { + const { sliderMenuOpen, setSliderMenuOpen } = useContext(AccountContext) + const navigate = useNavigate() + const toggleSliderMenu = () => { + setSliderMenuOpen(!sliderMenuOpen) + } + return ( +
        + {item.content.map((nestedItem) => ( +
      • +
        { + onNestedItemClick(nestedItem) + }} + > + {nestedItem.icon && nestedItem.icon} + {nestedItem.label} +
        + + {nestedItem.type === 'menu' && nestedItemID === nestedItem.id && ( +
          + {nestedItem.actions?.map((action) => ( +
        • { + navigate(action.link) + toggleSliderMenu() + }} + > + {action.name} +
        • + ))} +
        + )} +
      • + ))} +
      + ) +} + +export default NestedNavigation From eecdd4557d58f94ae2ac6105d3713827b2213ae2 Mon Sep 17 00:00:00 2001 From: owlsua Date: Mon, 27 Apr 2026 23:22:41 +0200 Subject: [PATCH 14/67] TextField: migrate to TypeScript and CSS Modules --- .../{TextField.css => TextField.module.css} | 22 ++++++- .../composed/TextField/TextField.test.js | 2 +- .../TextField/{TextField.js => TextField.tsx} | 61 +++++++++++-------- 3 files changed, 57 insertions(+), 28 deletions(-) rename src/components/composed/TextField/{TextField.css => TextField.module.css} (57%) rename src/components/composed/TextField/{TextField.js => TextField.tsx} (54%) diff --git a/src/components/composed/TextField/TextField.css b/src/components/composed/TextField/TextField.module.css similarity index 57% rename from src/components/composed/TextField/TextField.css rename to src/components/composed/TextField/TextField.module.css index c78b7e35..8881944c 100644 --- a/src/components/composed/TextField/TextField.css +++ b/src/components/composed/TextField/TextField.module.css @@ -4,12 +4,11 @@ } .inputLabel.alternate { - font-size: 1.5rem; + font-size: 1rem; text-align: center; } .inputLabel.alternate strong { - font-size: 1.5rem; display: block; margin-top: 10px; color: rgb(var(--color-main-green)); @@ -22,3 +21,22 @@ .inputLabel.inputLabelRight { text-align: right; } + +.errorIcon { + display: none; +} + +.errorText { + line-height: 1.3; +} + +@keyframes errorSlideIn { + from { + opacity: 0; + transform: translateY(-4px); + } + to { + opacity: 1; + transform: translateY(0); + } +} diff --git a/src/components/composed/TextField/TextField.test.js b/src/components/composed/TextField/TextField.test.js index 83487313..13c0fecc 100644 --- a/src/components/composed/TextField/TextField.test.js +++ b/src/components/composed/TextField/TextField.test.js @@ -1,6 +1,6 @@ import { render, screen, waitFor } from '@testing-library/react' -import TextField from './TextField' +import TextField from './TextField.tsx' const ONCHANGEHANDLESAMPLE = () => {} diff --git a/src/components/composed/TextField/TextField.js b/src/components/composed/TextField/TextField.tsx similarity index 54% rename from src/components/composed/TextField/TextField.js rename to src/components/composed/TextField/TextField.tsx index 1f7a41d2..5d2c8992 100644 --- a/src/components/composed/TextField/TextField.js +++ b/src/components/composed/TextField/TextField.tsx @@ -1,10 +1,26 @@ -import React, { useId, useEffect, useState } from 'react' +import { ReactNode, useId, useEffect, useState, ChangeEvent } from 'react' import { Input, Error } from '@BasicComponents' import { VerticalGroup } from '@LayoutComponents' -import { useStyleClasses } from '@Hooks' -import './TextField.css' +import styles from './TextField.module.css' + +interface TextFieldProps { + label?: ReactNode + labelPosition?: 'center' | 'left' | 'right' + placeHolder?: string + alternate?: boolean + password?: boolean + value?: string + onChangeHandle?: (value: string) => void + validity?: boolean | null + pattern?: string + extraStyleClasses?: string[] + errorMessages?: string | null + pristinity?: boolean + focus?: boolean + bigGap?: boolean +} const TextField = ({ label, @@ -20,45 +36,38 @@ const TextField = ({ errorMessages, pristinity = true, focus = true, - bigGap = true, -}) => { + bigGap = false, +}: TextFieldProps) => { const inputId = useId() - - const { styleClasses, addStyleClass, removeStyleClass } = - useStyleClasses('inputLabel') const [isPristine, setIsPristine] = useState(true) - const [fieldValidity, setFieldValidity] = useState(null) - - useEffect(() => { - alternate ? addStyleClass('alternate') : removeStyleClass('alternate') - }, [alternate, addStyleClass, removeStyleClass]) + const [fieldValidity, setFieldValidity] = useState(null) useEffect(() => { - if (isPristine || validity === null) return + if (isPristine || validity === null || validity === undefined) return validity ? setFieldValidity('valid') : setFieldValidity('invalid') }, [validity, isPristine]) - useEffect(() => { - labelPosition === 'left' - ? addStyleClass('inputLabelLeft') - : removeStyleClass('inputLabelLeft') - labelPosition === 'right' - ? addStyleClass('inputLabelRight') - : removeStyleClass('inputLabelRight') - }, [labelPosition, addStyleClass, removeStyleClass]) - useEffect(() => { setIsPristine(pristinity) }, [pristinity]) const setPristineState = () => setIsPristine(false) + const labelClasses = [ + styles.inputLabel, + alternate && styles.alternate, + labelPosition === 'left' && styles.inputLabelLeft, + labelPosition === 'right' && styles.inputLabelRight, + ] + .filter(Boolean) + .join(' ') + return ( {label && (