diff --git a/.gitignore b/.gitignore index 30365a2..c6ad3fa 100644 --- a/.gitignore +++ b/.gitignore @@ -130,4 +130,16 @@ yarn-error.log* /export -.swc \ No newline at end of file +.swc + +# Playwright +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ + +.tests-examples/ +.playwright.config.ts +.reference/ +.rules +.debug_tools/ diff --git a/package.json b/package.json index ea994fe..129e432 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ }, "devDependencies": { "@antfu/eslint-config": "^2.22.2", + "@swc/core": "^1.13.4", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.4.8", "@testing-library/react": "^16.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 147c31c..d40f011 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -97,10 +97,10 @@ importers: version: 2.4.0 tailwindcss: specifier: ^3.4.4 - version: 3.4.4(ts-node@10.9.2(@types/node@18.19.39)(typescript@5.5.3)) + version: 3.4.4(ts-node@10.9.2(@swc/core@1.13.4)(@types/node@18.19.39)(typescript@5.5.3)) tailwindcss-animate: specifier: ^1.0.7 - version: 1.0.7(tailwindcss@3.4.4(ts-node@10.9.2(@types/node@18.19.39)(typescript@5.5.3))) + version: 1.0.7(tailwindcss@3.4.4(ts-node@10.9.2(@swc/core@1.13.4)(@types/node@18.19.39)(typescript@5.5.3))) typescript: specifier: ^5.5.0 version: 5.5.3 @@ -122,7 +122,10 @@ importers: devDependencies: '@antfu/eslint-config': specifier: ^2.22.2 - version: 2.22.2(@eslint-react/eslint-plugin@1.5.25(eslint@9.7.0)(typescript@5.5.3))(@vue/compiler-sfc@3.4.31)(eslint-plugin-react-hooks@5.1.0-rc-df5f2736-20240712(eslint@9.7.0))(eslint-plugin-react-refresh@0.4.8(eslint@9.7.0))(eslint@9.7.0)(typescript@5.5.3)(vitest@2.0.5(@types/node@18.19.39)(@vitest/ui@2.0.5)(happy-dom@14.12.3)(jsdom@24.1.1)) + version: 2.22.2(@eslint-react/eslint-plugin@1.5.25(eslint@9.7.0)(typescript@5.5.3))(@vue/compiler-sfc@3.4.31)(eslint-plugin-react-hooks@5.1.0-rc-df5f2736-20240712(eslint@9.7.0))(eslint-plugin-react-refresh@0.4.8(eslint@9.7.0))(eslint@9.7.0)(typescript@5.5.3)(vitest@2.0.5) + '@swc/core': + specifier: ^1.13.4 + version: 1.13.4 '@testing-library/dom': specifier: ^10.4.0 version: 10.4.0 @@ -146,7 +149,7 @@ importers: version: 2.2.9 '@vitejs/plugin-react-swc': specifier: ^3.7.0 - version: 3.7.0(@swc/helpers@0.5.12)(vite@5.3.3(@types/node@18.19.39)) + version: 3.7.0(vite@5.3.3(@types/node@18.19.39)) '@vitest/ui': specifier: ^2.0.5 version: 2.0.5(vitest@2.0.5) @@ -176,7 +179,7 @@ importers: version: 2.0.5(@types/node@18.19.39)(@vitest/ui@2.0.5)(happy-dom@14.12.3)(jsdom@24.1.1) vitest-canvas-mock: specifier: ^0.3.3 - version: 0.3.3(vitest@2.0.5(@types/node@18.19.39)(@vitest/ui@2.0.5)(happy-dom@14.12.3)(jsdom@24.1.1)) + version: 0.3.3(vitest@2.0.5) packages: @@ -464,6 +467,7 @@ packages: '@eslint-react/tools@1.5.25': resolution: {integrity: sha512-i036JyZesrpBcLzREFP9Cknuq4y3FXGZhvm7eswVhI19/SiMnn6+XtMc9hQdVYqlSTwXSauRWeLaQJTEyrl2SQ==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. '@eslint-react/types@1.5.25': resolution: {integrity: sha512-MqlyjJyjiR+njDUeizcjnRL1Ar6wG5jz58o8JJuwnsLaU+m6ttIih7c+KhEN8pAEUofex7sfqdcejYO6yO+cMQ==} @@ -1114,71 +1118,71 @@ packages: peerDependencies: eslint: '>=8.40.0' - '@swc/core-darwin-arm64@1.6.13': - resolution: {integrity: sha512-SOF4buAis72K22BGJ3N8y88mLNfxLNprTuJUpzikyMGrvkuBFNcxYtMhmomO0XHsgLDzOJ+hWzcgjRNzjMsUcQ==} + '@swc/core-darwin-arm64@1.13.4': + resolution: {integrity: sha512-CGbTu9dGBwgklUj+NAQAYyPjBuoHaNRWK4QXJRv1QNIkhtE27aY7QA9uEON14SODxsio3t8+Pjjl2Mzx1Pxf+g==} engines: {node: '>=10'} cpu: [arm64] os: [darwin] - '@swc/core-darwin-x64@1.6.13': - resolution: {integrity: sha512-AW8akFSC+tmPE6YQQvK9S2A1B8pjnXEINg+gGgw0KRUUXunvu1/OEOeC5L2Co1wAwhD7bhnaefi06Qi9AiwOag==} + '@swc/core-darwin-x64@1.13.4': + resolution: {integrity: sha512-qLFwYmLrqHNCf+JO9YLJT6IP/f9LfbXILTaqyfluFLW1GCfJyvUrSt3CWaL2lwwyT1EbBh6BVaAAecXiJIo3vg==} engines: {node: '>=10'} cpu: [x64] os: [darwin] - '@swc/core-linux-arm-gnueabihf@1.6.13': - resolution: {integrity: sha512-f4gxxvDXVUm2HLYXRd311mSrmbpQF2MZ4Ja6XCQz1hWAxXdhRl1gpnZ+LH/xIfGSwQChrtLLVrkxdYUCVuIjFg==} + '@swc/core-linux-arm-gnueabihf@1.13.4': + resolution: {integrity: sha512-y7SeNIA9em3+smNMpr781idKuNwJNAqewiotv+pIR5FpXdXXNjHWW+jORbqQYd61k6YirA5WQv+Af4UzqEX17g==} engines: {node: '>=10'} cpu: [arm] os: [linux] - '@swc/core-linux-arm64-gnu@1.6.13': - resolution: {integrity: sha512-Nf/eoW2CbG8s+9JoLtjl9FByBXyQ5cjdBsA4efO7Zw4p+YSuXDgc8HRPC+E2+ns0praDpKNZtLvDtmF2lL+2Gg==} + '@swc/core-linux-arm64-gnu@1.13.4': + resolution: {integrity: sha512-u0c51VdzRmXaphLgghY9+B2Frzler6nIv+J788nqIh6I0ah3MmMW8LTJKZfdaJa3oFxzGNKXsJiaU2OFexNkug==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - '@swc/core-linux-arm64-musl@1.6.13': - resolution: {integrity: sha512-2OysYSYtdw79prJYuKIiux/Gj0iaGEbpS2QZWCIY4X9sGoETJ5iMg+lY+YCrIxdkkNYd7OhIbXdYFyGs/w5LDg==} + '@swc/core-linux-arm64-musl@1.13.4': + resolution: {integrity: sha512-Z92GJ98x8yQHn4I/NPqwAQyHNkkMslrccNVgFcnY1msrb6iGSw5uFg2H2YpvQ5u2/Yt6CRpLIUVVh8SGg1+gFA==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - '@swc/core-linux-x64-gnu@1.6.13': - resolution: {integrity: sha512-PkR4CZYJNk5hcd2+tMWBpnisnmYsUzazI1O5X7VkIGFcGePTqJ/bWlfUIVVExWxvAI33PQFzLbzmN5scyIUyGQ==} + '@swc/core-linux-x64-gnu@1.13.4': + resolution: {integrity: sha512-rSUcxgpFF0L8Fk1CbUf946XCX1CRp6eaHfKqplqFNWCHv8HyqAtSFvgCHhT+bXru6Ca/p3sLC775SUeSWhsJ9w==} engines: {node: '>=10'} cpu: [x64] os: [linux] - '@swc/core-linux-x64-musl@1.6.13': - resolution: {integrity: sha512-OdsY7wryTxCKwGQcwW9jwWg3cxaHBkTTHi91+5nm7hFPpmZMz1HivJrWAMwVE7iXFw+M4l6ugB/wCvpYrUAAjA==} + '@swc/core-linux-x64-musl@1.13.4': + resolution: {integrity: sha512-qY77eFUvmdXNSmTW+I1fsz4enDuB0I2fE7gy6l9O4koSfjcCxkXw2X8x0lmKLm3FRiINS1XvZSg2G+q4NNQCRQ==} engines: {node: '>=10'} cpu: [x64] os: [linux] - '@swc/core-win32-arm64-msvc@1.6.13': - resolution: {integrity: sha512-ap6uNmYjwk9M/+bFEuWRNl3hq4VqgQ/Lk+ID/F5WGqczNr0L7vEf+pOsRAn0F6EV+o/nyb3ePt8rLhE/wjHpPg==} + '@swc/core-win32-arm64-msvc@1.13.4': + resolution: {integrity: sha512-xjPeDrOf6elCokxuyxwoskM00JJFQMTT2hTQZE24okjG3JiXzSFV+TmzYSp+LWNxPpnufnUUy/9Ee8+AcpslGw==} engines: {node: '>=10'} cpu: [arm64] os: [win32] - '@swc/core-win32-ia32-msvc@1.6.13': - resolution: {integrity: sha512-IJ8KH4yIUHTnS/U1jwQmtbfQals7zWPG0a9hbEfIr4zI0yKzjd83lmtS09lm2Q24QBWOCFGEEbuZxR4tIlvfzA==} + '@swc/core-win32-ia32-msvc@1.13.4': + resolution: {integrity: sha512-Ta+Bblc9tE9X9vQlpa3r3+mVnHYdKn09QsZ6qQHvuXGKWSS99DiyxKTYX2vxwMuoTObR0BHvnhNbaGZSV1VwNA==} engines: {node: '>=10'} cpu: [ia32] os: [win32] - '@swc/core-win32-x64-msvc@1.6.13': - resolution: {integrity: sha512-f6/sx6LMuEnbuxtiSL/EkR0Y6qUHFw1XVrh6rwzKXptTipUdOY+nXpKoh+1UsBm/r7H0/5DtOdrn3q5ZHbFZjQ==} + '@swc/core-win32-x64-msvc@1.13.4': + resolution: {integrity: sha512-pHnb4QwGiuWs4Z9ePSgJ48HP3NZIno6l75SB8YLCiPVDiLhvCLKEjz/caPRsFsmet9BEP8e3bAf2MV8MXgaTSg==} engines: {node: '>=10'} cpu: [x64] os: [win32] - '@swc/core@1.6.13': - resolution: {integrity: sha512-eailUYex6fkfaQTev4Oa3mwn0/e3mQU4H8y1WPuImYQESOQDtVrowwUGDSc19evpBbHpKtwM+hw8nLlhIsF+Tw==} + '@swc/core@1.13.4': + resolution: {integrity: sha512-bCq2GCuKV16DSOOEdaRqHMm1Ok4YEoLoNdgdzp8BS/Hxxr/0NVCHBUgRLLRy/TlJGv20Idx+djd5FIDvsnqMaw==} engines: {node: '>=10'} peerDependencies: - '@swc/helpers': '*' + '@swc/helpers': '>=0.5.17' peerDependenciesMeta: '@swc/helpers': optional: true @@ -1186,11 +1190,8 @@ packages: '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} - '@swc/helpers@0.5.12': - resolution: {integrity: sha512-KMZNXiGibsW9kvZAO1Pam2JPTDBm+KSHMMHWdsyI/1DbIZjT2A6Gy3hblVXUMEDvUAKq+e0vL0X0o54owWji7g==} - - '@swc/types@0.1.9': - resolution: {integrity: sha512-qKnCno++jzcJ4lM4NTfYifm1EFSCeIfKiAHAfkENZAV5Kl9PjJIyd2yeeVv6c/2CckuLyv2NmRC5pv6pm2WQBg==} + '@swc/types@0.1.24': + resolution: {integrity: sha512-tjTMh3V4vAORHtdTprLlfoMptu1WfTZG9Rsca6yOKyNYsRr+MUXutKmliB17orgSZk5DpnDxs8GUdd/qwYxOng==} '@testing-library/dom@10.4.0': resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} @@ -3597,7 +3598,7 @@ snapshots: '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 - '@antfu/eslint-config@2.22.2(@eslint-react/eslint-plugin@1.5.25(eslint@9.7.0)(typescript@5.5.3))(@vue/compiler-sfc@3.4.31)(eslint-plugin-react-hooks@5.1.0-rc-df5f2736-20240712(eslint@9.7.0))(eslint-plugin-react-refresh@0.4.8(eslint@9.7.0))(eslint@9.7.0)(typescript@5.5.3)(vitest@2.0.5(@types/node@18.19.39)(@vitest/ui@2.0.5)(happy-dom@14.12.3)(jsdom@24.1.1))': + '@antfu/eslint-config@2.22.2(@eslint-react/eslint-plugin@1.5.25(eslint@9.7.0)(typescript@5.5.3))(@vue/compiler-sfc@3.4.31)(eslint-plugin-react-hooks@5.1.0-rc-df5f2736-20240712(eslint@9.7.0))(eslint-plugin-react-refresh@0.4.8(eslint@9.7.0))(eslint@9.7.0)(typescript@5.5.3)(vitest@2.0.5)': dependencies: '@antfu/install-pkg': 0.3.3 '@clack/prompts': 0.7.0 @@ -3622,7 +3623,7 @@ snapshots: eslint-plugin-toml: 0.11.1(eslint@9.7.0) eslint-plugin-unicorn: 54.0.0(eslint@9.7.0) eslint-plugin-unused-imports: 4.0.0(@typescript-eslint/eslint-plugin@8.0.0-alpha.40(@typescript-eslint/parser@8.0.0-alpha.40(eslint@9.7.0)(typescript@5.5.3))(eslint@9.7.0)(typescript@5.5.3))(eslint@9.7.0) - eslint-plugin-vitest: 0.5.4(@typescript-eslint/eslint-plugin@8.0.0-alpha.40(@typescript-eslint/parser@8.0.0-alpha.40(eslint@9.7.0)(typescript@5.5.3))(eslint@9.7.0)(typescript@5.5.3))(eslint@9.7.0)(typescript@5.5.3)(vitest@2.0.5(@types/node@18.19.39)(@vitest/ui@2.0.5)(happy-dom@14.12.3)(jsdom@24.1.1)) + eslint-plugin-vitest: 0.5.4(@typescript-eslint/eslint-plugin@8.0.0-alpha.40(@typescript-eslint/parser@8.0.0-alpha.40(eslint@9.7.0)(typescript@5.5.3))(eslint@9.7.0)(typescript@5.5.3))(eslint@9.7.0)(typescript@5.5.3)(vitest@2.0.5) eslint-plugin-vue: 9.27.0(eslint@9.7.0) eslint-plugin-yml: 1.14.0(eslint@9.7.0) eslint-processor-vue-blocks: 0.1.2(@vue/compiler-sfc@3.4.31)(eslint@9.7.0) @@ -4551,61 +4552,55 @@ snapshots: - supports-color - typescript - '@swc/core-darwin-arm64@1.6.13': + '@swc/core-darwin-arm64@1.13.4': optional: true - '@swc/core-darwin-x64@1.6.13': + '@swc/core-darwin-x64@1.13.4': optional: true - '@swc/core-linux-arm-gnueabihf@1.6.13': + '@swc/core-linux-arm-gnueabihf@1.13.4': optional: true - '@swc/core-linux-arm64-gnu@1.6.13': + '@swc/core-linux-arm64-gnu@1.13.4': optional: true - '@swc/core-linux-arm64-musl@1.6.13': + '@swc/core-linux-arm64-musl@1.13.4': optional: true - '@swc/core-linux-x64-gnu@1.6.13': + '@swc/core-linux-x64-gnu@1.13.4': optional: true - '@swc/core-linux-x64-musl@1.6.13': + '@swc/core-linux-x64-musl@1.13.4': optional: true - '@swc/core-win32-arm64-msvc@1.6.13': + '@swc/core-win32-arm64-msvc@1.13.4': optional: true - '@swc/core-win32-ia32-msvc@1.6.13': + '@swc/core-win32-ia32-msvc@1.13.4': optional: true - '@swc/core-win32-x64-msvc@1.6.13': + '@swc/core-win32-x64-msvc@1.13.4': optional: true - '@swc/core@1.6.13(@swc/helpers@0.5.12)': + '@swc/core@1.13.4': dependencies: '@swc/counter': 0.1.3 - '@swc/types': 0.1.9 + '@swc/types': 0.1.24 optionalDependencies: - '@swc/core-darwin-arm64': 1.6.13 - '@swc/core-darwin-x64': 1.6.13 - '@swc/core-linux-arm-gnueabihf': 1.6.13 - '@swc/core-linux-arm64-gnu': 1.6.13 - '@swc/core-linux-arm64-musl': 1.6.13 - '@swc/core-linux-x64-gnu': 1.6.13 - '@swc/core-linux-x64-musl': 1.6.13 - '@swc/core-win32-arm64-msvc': 1.6.13 - '@swc/core-win32-ia32-msvc': 1.6.13 - '@swc/core-win32-x64-msvc': 1.6.13 - '@swc/helpers': 0.5.12 + '@swc/core-darwin-arm64': 1.13.4 + '@swc/core-darwin-x64': 1.13.4 + '@swc/core-linux-arm-gnueabihf': 1.13.4 + '@swc/core-linux-arm64-gnu': 1.13.4 + '@swc/core-linux-arm64-musl': 1.13.4 + '@swc/core-linux-x64-gnu': 1.13.4 + '@swc/core-linux-x64-musl': 1.13.4 + '@swc/core-win32-arm64-msvc': 1.13.4 + '@swc/core-win32-ia32-msvc': 1.13.4 + '@swc/core-win32-x64-msvc': 1.13.4 '@swc/counter@0.1.3': {} - '@swc/helpers@0.5.12': - dependencies: - tslib: 2.6.3 - optional: true - - '@swc/types@0.1.9': + '@swc/types@0.1.24': dependencies: '@swc/counter': 0.1.3 @@ -4869,9 +4864,9 @@ snapshots: optionalDependencies: react: 18.3.1 - '@vitejs/plugin-react-swc@3.7.0(@swc/helpers@0.5.12)(vite@5.3.3(@types/node@18.19.39))': + '@vitejs/plugin-react-swc@3.7.0(vite@5.3.3(@types/node@18.19.39))': dependencies: - '@swc/core': 1.6.13(@swc/helpers@0.5.12) + '@swc/core': 1.13.4 vite: 5.3.3(@types/node@18.19.39) transitivePeerDependencies: - '@swc/helpers' @@ -5596,7 +5591,7 @@ snapshots: optionalDependencies: '@typescript-eslint/eslint-plugin': 8.0.0-alpha.40(@typescript-eslint/parser@8.0.0-alpha.40(eslint@9.7.0)(typescript@5.5.3))(eslint@9.7.0)(typescript@5.5.3) - eslint-plugin-vitest@0.5.4(@typescript-eslint/eslint-plugin@8.0.0-alpha.40(@typescript-eslint/parser@8.0.0-alpha.40(eslint@9.7.0)(typescript@5.5.3))(eslint@9.7.0)(typescript@5.5.3))(eslint@9.7.0)(typescript@5.5.3)(vitest@2.0.5(@types/node@18.19.39)(@vitest/ui@2.0.5)(happy-dom@14.12.3)(jsdom@24.1.1)): + eslint-plugin-vitest@0.5.4(@typescript-eslint/eslint-plugin@8.0.0-alpha.40(@typescript-eslint/parser@8.0.0-alpha.40(eslint@9.7.0)(typescript@5.5.3))(eslint@9.7.0)(typescript@5.5.3))(eslint@9.7.0)(typescript@5.5.3)(vitest@2.0.5): dependencies: '@typescript-eslint/utils': 7.16.0(eslint@9.7.0)(typescript@5.5.3) eslint: 9.7.0 @@ -6397,13 +6392,13 @@ snapshots: camelcase-css: 2.0.1 postcss: 8.4.39 - postcss-load-config@4.0.2(postcss@8.4.39)(ts-node@10.9.2(@types/node@18.19.39)(typescript@5.5.3)): + postcss-load-config@4.0.2(postcss@8.4.39)(ts-node@10.9.2(@swc/core@1.13.4)(@types/node@18.19.39)(typescript@5.5.3)): dependencies: lilconfig: 3.1.2 yaml: 2.4.5 optionalDependencies: postcss: 8.4.39 - ts-node: 10.9.2(@types/node@18.19.39)(typescript@5.5.3) + ts-node: 10.9.2(@swc/core@1.13.4)(@types/node@18.19.39)(typescript@5.5.3) postcss-nested@6.0.1(postcss@8.4.39): dependencies: @@ -6785,11 +6780,11 @@ snapshots: tailwind-merge@2.4.0: {} - tailwindcss-animate@1.0.7(tailwindcss@3.4.4(ts-node@10.9.2(@types/node@18.19.39)(typescript@5.5.3))): + tailwindcss-animate@1.0.7(tailwindcss@3.4.4(ts-node@10.9.2(@swc/core@1.13.4)(@types/node@18.19.39)(typescript@5.5.3))): dependencies: - tailwindcss: 3.4.4(ts-node@10.9.2(@types/node@18.19.39)(typescript@5.5.3)) + tailwindcss: 3.4.4(ts-node@10.9.2(@swc/core@1.13.4)(@types/node@18.19.39)(typescript@5.5.3)) - tailwindcss@3.4.4(ts-node@10.9.2(@types/node@18.19.39)(typescript@5.5.3)): + tailwindcss@3.4.4(ts-node@10.9.2(@swc/core@1.13.4)(@types/node@18.19.39)(typescript@5.5.3)): dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -6808,7 +6803,7 @@ snapshots: postcss: 8.4.39 postcss-import: 15.1.0(postcss@8.4.39) postcss-js: 4.0.1(postcss@8.4.39) - postcss-load-config: 4.0.2(postcss@8.4.39)(ts-node@10.9.2(@types/node@18.19.39)(typescript@5.5.3)) + postcss-load-config: 4.0.2(postcss@8.4.39)(ts-node@10.9.2(@swc/core@1.13.4)(@types/node@18.19.39)(typescript@5.5.3)) postcss-nested: 6.0.1(postcss@8.4.39) postcss-selector-parser: 6.1.1 resolve: 1.22.8 @@ -6879,7 +6874,7 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-node@10.9.2(@types/node@18.19.39)(typescript@5.5.3): + ts-node@10.9.2(@swc/core@1.13.4)(@types/node@18.19.39)(typescript@5.5.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 @@ -6896,6 +6891,8 @@ snapshots: typescript: 5.5.3 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 + optionalDependencies: + '@swc/core': 1.13.4 optional: true tsconfck@3.1.1(typescript@5.5.3): @@ -7020,7 +7017,7 @@ snapshots: '@types/node': 18.19.39 fsevents: 2.3.3 - vitest-canvas-mock@0.3.3(vitest@2.0.5(@types/node@18.19.39)(@vitest/ui@2.0.5)(happy-dom@14.12.3)(jsdom@24.1.1)): + vitest-canvas-mock@0.3.3(vitest@2.0.5): dependencies: jest-canvas-mock: 2.5.2 vitest: 2.0.5(@types/node@18.19.39)(@vitest/ui@2.0.5)(happy-dom@14.12.3)(jsdom@24.1.1) diff --git a/public/locales/cn/translation.json b/public/locales/cn/translation.json index d2406f1..784364b 100644 --- a/public/locales/cn/translation.json +++ b/public/locales/cn/translation.json @@ -48,6 +48,11 @@ "Parallel": "插入或", "After": "向后插入", "show more": "显示更多", + "Export": "导出", + "Export as SVG": "导出为SVG", + "No graph to export": "没有可导出的图形", + "Export failed": "导出失败", + "Graph exported as SVG": "图形已导出为SVG", "show less": "显示常用", "Expression": "表达式", "Content": "内容", @@ -106,6 +111,48 @@ "3. Whole + Decimal Numbers": "3. 整数 + 小数", "4. Negative, Positive Whole + Decimal Numbers": "4. 正负 整数 + 小数", "6. Date Format YYYY-MM-dd": "6. 日期格式 YYYY-MM-dd", + "Numbers": "数字", + "URLs": "网址", + "Dates": "日期", + "Phone Numbers": "电话号码", + "IP Addresses": "IP地址", + "HTML Tags": "HTML标签", + "Email Addresses": "邮箱地址", + "Passwords": "密码", + "ID Numbers": "证件号码", + "All Categories": "所有分类", + "Whole Numbers": "整数", + "Decimal Numbers": "小数", + "Whole + Decimal Numbers": "整数和小数", + "Negative, Positive Whole + Decimal Numbers": "正负整数和小数", + "Currency Amount": "货币金额", + "Percentage": "百分比", + "Basic URL": "基本URL", + "Domain Name": "域名", + "FTP URL": "FTP地址", + "URL with Port": "带端口的URL", + "Date Format YYYY-MM-DD": "日期格式 YYYY-MM-DD", + "Date Format DD/MM/YYYY": "日期格式 DD/MM/YYYY", + "Date Format MM/DD/YYYY": "日期格式 MM/DD/YYYY", + "Time Format HH:MM": "时间格式 HH:MM", + "DateTime ISO 8601": "ISO 8601日期时间", + "Chinese Mobile Phone": "中国手机号", + "US Phone Number": "美国电话号码", + "International Phone": "国际电话号码", + "Chinese Landline": "中国固定电话", + "IPv4 Address": "IPv4地址", + "IPv6 Address": "IPv6地址", + "MAC Address": "MAC地址", + "HTML Tag": "HTML标签", + "HTML Tag with Attributes": "带属性的HTML标签", + "HTML Comment": "HTML注释", + "HTML Image Tag": "HTML图片标签", + "Basic Email": "基本邮箱", + "Strict Email RFC 5322": "严格邮箱格式 RFC 5322", + "Strong Password": "强密码", + "Medium Password": "中等强度密码", + "Chinese ID Card": "中国身份证", + "Credit Card Number": "信用卡号码", "Flags: ": "标志: ", "Allows . to match newline": "允许 . 匹配换行符", "Settings: ": "设置: ", @@ -113,5 +160,39 @@ "Copy permalink": "复制链接", "Permalink copied.": "链接已复制", "Empty": "空", - "Group's name": "组名" + "Group's name": "组名", + "Regex Samples": "正则表达式样例", + "No regex samples available for this category": "此分类下没有可用的正则表达式样例", + "Matches one or more consecutive digits from start to end of string": "从字符串开始到结束匹配一个或多个连续数字", + "Matches numbers with decimal point, allowing optional digits before decimal": "匹配带小数点的数字,小数点前可选数字", + "Matches both integers and decimals using optional decimal group": "使用可选小数组匹配整数和小数", + "Includes optional minus sign for negative numbers with decimal support": "包含可选负号,支持负数和小数", + "Validates currency format with optional dollar sign, comma separators, and cents": "验证货币格式,可选美元符号、逗号分隔符和分", + "Matches percentage values from 0-999% with up to 2 decimal places": "匹配0-999%的百分比值,最多2位小数", + "Validates HTTP/HTTPS URLs with optional www prefix and query parameters": "验证HTTP/HTTPS URL,可选www前缀和查询参数", + "Matches valid domain names following RFC standards with length limits": "匹配符合RFC标准的有效域名,有长度限制", + "Validates FTP protocol URLs with optional port and path components": "验证FTP协议URL,可选端口和路径组件", + "Matches URLs with optional port numbers and path segments": "匹配带可选端口号和路径段的URL", + "Validates ISO date format with proper month and day ranges": "验证ISO日期格式,正确的月份和日期范围", + "European date format with day-first ordering and slash separators": "欧洲日期格式,日期在前,斜杠分隔", + "American date format with month-first ordering and validation": "美国日期格式,月份在前并验证", + "Validates 24-hour time format with proper hour and minute ranges": "验证24小时时间格式,正确的小时和分钟范围", + "Matches ISO 8601 datetime with optional milliseconds and timezone": "匹配ISO 8601日期时间,可选毫秒和时区", + "Validates 11-digit Chinese mobile numbers starting with 13-19": "验证以13-19开头的11位中国手机号", + "Matches US phone format with optional parentheses and separators": "匹配美国电话格式,可选括号和分隔符", + "Validates international phone numbers following E.164 standard": "验证符合E.164标准的国际电话号码", + "Matches Chinese landline format with area code and optional dash": "匹配中国固定电话格式,带区号和可选破折号", + "Validates IPv4 addresses with proper octet range validation (0-255)": "验证IPv4地址,正确的八位字节范围验证(0-255)", + "Matches full IPv6 addresses with 8 hexadecimal groups separated by colons": "匹配完整IPv6地址,8个十六进制组用冒号分隔", + "Validates MAC addresses with colon or hyphen separators between hex pairs": "验证MAC地址,十六进制对之间用冒号或连字符分隔", + "Matches opening and closing HTML tags with optional attributes": "匹配开始和结束HTML标签,可选属性", + "Captures complete HTML tag pairs with content using backreferences": "使用反向引用捕获完整的HTML标签对及内容", + "Matches HTML comments including multiline content with non-greedy matching": "匹配HTML注释,包括多行内容,非贪婪匹配", + "Extracts src attribute value from img tags with flexible attribute ordering": "从img标签中提取src属性值,灵活的属性顺序", + "Validates common email format with alphanumeric characters and standard symbols": "验证常见邮箱格式,字母数字字符和标准符号", + "Comprehensive RFC 5322 compliant email validation with full character set support": "全面的RFC 5322兼容邮箱验证,支持完整字符集", + "Strong password validation using positive lookaheads for complexity requirements": "使用正向前瞻的强密码验证,满足复杂性要求", + "Medium strength password requiring letters and numbers with minimum length": "中等强度密码,需要字母和数字,最小长度", + "Validates Chinese national ID format with birth date and checksum validation": "验证中国身份证格式,包含出生日期和校验位验证", + "Matches major credit card formats including Visa, MasterCard, AmEx, and Discover": "匹配主要信用卡格式,包括Visa、万事达、美国运通和Discover" } diff --git a/public/locales/jp/translation.json b/public/locales/jp/translation.json index 1a6ab12..e0c4183 100644 --- a/public/locales/jp/translation.json +++ b/public/locales/jp/translation.json @@ -114,6 +114,48 @@ "3. Whole + Decimal Numbers": "3. 整数と小数", "4. Negative, Positive Whole + Decimal Numbers": "4. 正負の整数と小数", "6. Date Format YYYY-MM-dd": "6. 日付形式 YYYY-MM-dd", + "Numbers": "数字", + "URLs": "URL", + "Dates": "日付", + "Phone Numbers": "電話番号", + "IP Addresses": "IPアドレス", + "HTML Tags": "HTMLタグ", + "Email Addresses": "メールアドレス", + "Passwords": "パスワード", + "ID Numbers": "ID番号", + "All Categories": "すべてのカテゴリ", + "Whole Numbers": "整数", + "Decimal Numbers": "小数", + "Whole + Decimal Numbers": "整数と小数", + "Negative, Positive Whole + Decimal Numbers": "正負の整数と小数", + "Currency Amount": "通貨金額", + "Percentage": "パーセンテージ", + "Basic URL": "基本URL", + "Domain Name": "ドメイン名", + "FTP URL": "FTP URL", + "URL with Port": "ポート付きURL", + "Date Format YYYY-MM-DD": "日付形式 YYYY-MM-DD", + "Date Format DD/MM/YYYY": "日付形式 DD/MM/YYYY", + "Date Format MM/DD/YYYY": "日付形式 MM/DD/YYYY", + "Time Format HH:MM": "時刻形式 HH:MM", + "DateTime ISO 8601": "ISO 8601日時", + "Chinese Mobile Phone": "中国携帯電話", + "US Phone Number": "米国電話番号", + "International Phone": "国際電話", + "Chinese Landline": "中国固定電話", + "IPv4 Address": "IPv4アドレス", + "IPv6 Address": "IPv6アドレス", + "MAC Address": "MACアドレス", + "HTML Tag": "HTMLタグ", + "HTML Tag with Attributes": "属性付きHTMLタグ", + "HTML Comment": "HTMLコメント", + "HTML Image Tag": "HTML画像タグ", + "Basic Email": "基本メール", + "Strict Email RFC 5322": "厳密メール RFC 5322", + "Strong Password": "強力パスワード", + "Medium Password": "中程度パスワード", + "Chinese ID Card": "中国身分証", + "Credit Card Number": "クレジットカード番号", "Flags: ": "フラグ: ", "Flag: ": "フラグ: ", "Allows . to match newline": "ドットが改行に一致することを許可", @@ -122,5 +164,49 @@ "Copy permalink": "パーマリンクをコピー", "Permalink copied.": "パーマリンクがコピーされました", "Empty": "空", - "Group's name": "グループ名" + "Group's name": "グループ名", + "Regex Samples": "正規表現サンプル", + "All Categories": "全カテゴリ", + "Numbers": "数字", + "URLs": "URL", + "Dates": "日付", + "Phone Numbers": "電話番号", + "IP Addresses": "IPアドレス", + "HTML Tags": "HTMLタグ", + "Email Addresses": "メールアドレス", + "Passwords": "パスワード", + "ID Numbers": "ID番号", + "No regex samples available for this category": "このカテゴリには利用可能な正規表現サンプルがありません", + "Matches one or more consecutive digits from start to end of string": "文字列の開始から終了まで1つ以上の連続する数字にマッチ", + "Matches numbers with decimal point, allowing optional digits before decimal": "小数点のある数字にマッチ、小数点前の数字は任意", + "Matches both integers and decimals using optional decimal group": "オプションの小数グループを使用して整数と小数の両方にマッチ", + "Includes optional minus sign for negative numbers with decimal support": "負の数のオプションのマイナス記号を含み、小数をサポート", + "Validates currency format with optional dollar sign, comma separators, and cents": "オプションのドル記号、カンマ区切り、セントを含む通貨形式を検証", + "Matches percentage values from 0-999% with up to 2 decimal places": "最大2桁の小数点以下を持つ0-999%のパーセンテージ値にマッチ", + "Validates HTTP/HTTPS URLs with optional www prefix and query parameters": "オプションのwwwプレフィックスとクエリパラメータを持つHTTP/HTTPS URLを検証", + "Matches valid domain names following RFC standards with length limits": "長さ制限のあるRFC標準に従った有効なドメイン名にマッチ", + "Validates FTP protocol URLs with optional port and path components": "オプションのポートとパスコンポーネントを持つFTPプロトコルURLを検証", + "Matches URLs with optional port numbers and path segments": "オプションのポート番号とパスセグメントを持つURLにマッチ", + "Validates ISO date format with proper month and day ranges": "適切な月と日の範囲を持つISO日付形式を検証", + "European date format with day-first ordering and slash separators": "日付優先順序とスラッシュ区切りのヨーロッパ日付形式", + "American date format with month-first ordering and validation": "月優先順序と検証のアメリカ日付形式", + "Validates 24-hour time format with proper hour and minute ranges": "適切な時間と分の範囲を持つ24時間時刻形式を検証", + "Matches ISO 8601 datetime with optional milliseconds and timezone": "オプションのミリ秒とタイムゾーンを持つISO 8601日時にマッチ", + "Validates 11-digit Chinese mobile numbers starting with 13-19": "13-19で始まる11桁の中国携帯電話番号を検証", + "Matches US phone format with optional parentheses and separators": "オプションの括弧と区切り文字を持つアメリカ電話形式にマッチ", + "Validates international phone numbers following E.164 standard": "E.164標準に従った国際電話番号を検証", + "Matches Chinese landline format with area code and optional dash": "市外局番とオプションのダッシュを持つ中国固定電話形式にマッチ", + "Validates IPv4 addresses with proper octet range validation (0-255)": "適切なオクテット範囲検証(0-255)を持つIPv4アドレスを検証", + "Matches full IPv6 addresses with 8 hexadecimal groups separated by colons": "コロンで区切られた8つの16進グループを持つ完全なIPv6アドレスにマッチ", + "Validates MAC addresses with colon or hyphen separators between hex pairs": "16進ペア間のコロンまたはハイフン区切りを持つMACアドレスを検証", + "Matches opening and closing HTML tags with optional attributes": "オプションの属性を持つ開始と終了HTMLタグにマッチ", + "Captures complete HTML tag pairs with content using backreferences": "後方参照を使用してコンテンツを持つ完全なHTMLタグペアをキャプチャ", + "Matches HTML comments including multiline content with non-greedy matching": "非貪欲マッチングで複数行コンテンツを含むHTMLコメントにマッチ", + "Extracts src attribute value from img tags with flexible attribute ordering": "柔軟な属性順序でimgタグからsrc属性値を抽出", + "Validates common email format with alphanumeric characters and standard symbols": "英数字と標準記号を持つ一般的なメール形式を検証", + "Comprehensive RFC 5322 compliant email validation with full character set support": "完全な文字セットサポートを持つ包括的なRFC 5322準拠メール検証", + "Strong password validation using positive lookaheads for complexity requirements": "複雑性要件のための正の先読みを使用した強力なパスワード検証", + "Medium strength password requiring letters and numbers with minimum length": "最小長を持つ文字と数字を必要とする中程度の強度のパスワード", + "Validates Chinese national ID format with birth date and checksum validation": "生年月日とチェックサム検証を持つ中国国民ID形式を検証", + "Matches major credit card formats including Visa, MasterCard, AmEx, and Discover": "Visa、MasterCard、AmEx、Discoverを含む主要なクレジットカード形式にマッチ" } diff --git a/public/locales/ru/translation.json b/public/locales/ru/translation.json index 0d41d6e..ea07665 100644 --- a/public/locales/ru/translation.json +++ b/public/locales/ru/translation.json @@ -114,6 +114,48 @@ "3. Whole + Decimal Numbers": "3. Неотрицательные целые и действительные числа", "4. Negative, Positive Whole + Decimal Numbers": "4. Действительные числа", "6. Date Format YYYY-MM-dd": "6. Дата в формате YYYY-MM-dd", + "Numbers": "Числа", + "URLs": "URL-адреса", + "Dates": "Даты", + "Phone Numbers": "Номера телефонов", + "IP Addresses": "IP-адреса", + "HTML Tags": "HTML-теги", + "Email Addresses": "Адреса электронной почты", + "Passwords": "Пароли", + "ID Numbers": "Идентификационные номера", + "All Categories": "Все категории", + "Whole Numbers": "Целые числа", + "Decimal Numbers": "Десятичные числа", + "Whole + Decimal Numbers": "Целые и десятичные числа", + "Negative, Positive Whole + Decimal Numbers": "Положительные и отрицательные числа", + "Currency Amount": "Денежная сумма", + "Percentage": "Проценты", + "Basic URL": "Базовый URL", + "Domain Name": "Доменное имя", + "FTP URL": "FTP URL", + "URL with Port": "URL с портом", + "Date Format YYYY-MM-DD": "Формат даты YYYY-MM-DD", + "Date Format DD/MM/YYYY": "Формат даты DD/MM/YYYY", + "Date Format MM/DD/YYYY": "Формат даты MM/DD/YYYY", + "Time Format HH:MM": "Формат времени HH:MM", + "DateTime ISO 8601": "Дата и время ISO 8601", + "Chinese Mobile Phone": "Китайский мобильный телефон", + "US Phone Number": "Американский номер телефона", + "International Phone": "Международный телефон", + "Chinese Landline": "Китайский стационарный телефон", + "IPv4 Address": "IPv4 адрес", + "IPv6 Address": "IPv6 адрес", + "MAC Address": "MAC адрес", + "HTML Tag": "HTML тег", + "HTML Tag with Attributes": "HTML тег с атрибутами", + "HTML Comment": "HTML комментарий", + "HTML Image Tag": "HTML тег изображения", + "Basic Email": "Базовый email", + "Strict Email RFC 5322": "Строгий email RFC 5322", + "Strong Password": "Сильный пароль", + "Medium Password": "Средний пароль", + "Chinese ID Card": "Китайское удостоверение личности", + "Credit Card Number": "Номер кредитной карты", "Flag: ": "Флаги: ", "Allows . to match newline": "Считать . символом перевода строки", "Settings: ": "Настройки: ", @@ -121,5 +163,49 @@ "Copy permalink": "Скопировать ссылку", "Permalink copied.": "Ссылка скопирована", "Empty": "Пустой узел", - "Group's name": "Имя группы" + "Group's name": "Имя группы", + "Regex Samples": "Примеры регулярных выражений", + "All Categories": "Все категории", + "Numbers": "Числа", + "URLs": "URL-адреса", + "Dates": "Даты", + "Phone Numbers": "Телефонные номера", + "IP Addresses": "IP-адреса", + "HTML Tags": "HTML-теги", + "Email Addresses": "Адреса электронной почты", + "Passwords": "Пароли", + "ID Numbers": "Номера удостоверений", + "No regex samples available for this category": "Нет доступных примеров регулярных выражений для этой категории", + "Matches one or more consecutive digits from start to end of string": "Соответствует одной или нескольким последовательным цифрам от начала до конца строки", + "Matches numbers with decimal point, allowing optional digits before decimal": "Соответствует числам с десятичной точкой, допуская необязательные цифры перед десятичной", + "Matches both integers and decimals using optional decimal group": "Соответствует как целым числам, так и десятичным, используя необязательную десятичную группу", + "Includes optional minus sign for negative numbers with decimal support": "Включает необязательный знак минус для отрицательных чисел с поддержкой десятичных", + "Validates currency format with optional dollar sign, comma separators, and cents": "Проверяет формат валюты с необязательным знаком доллара, запятыми-разделителями и центами", + "Matches percentage values from 0-999% with up to 2 decimal places": "Соответствует процентным значениям от 0-999% с до 2 десятичных знаков", + "Validates HTTP/HTTPS URLs with optional www prefix and query parameters": "Проверяет HTTP/HTTPS URL с необязательным префиксом www и параметрами запроса", + "Matches valid domain names following RFC standards with length limits": "Соответствует действительным доменным именам, следующим стандартам RFC с ограничениями длины", + "Validates FTP protocol URLs with optional port and path components": "Проверяет URL протокола FTP с необязательными компонентами порта и пути", + "Matches URLs with optional port numbers and path segments": "Соответствует URL с необязательными номерами портов и сегментами пути", + "Validates ISO date format with proper month and day ranges": "Проверяет формат даты ISO с правильными диапазонами месяцев и дней", + "European date format with day-first ordering and slash separators": "Европейский формат даты с порядком день-первый и разделителями слэш", + "American date format with month-first ordering and validation": "Американский формат даты с порядком месяц-первый и проверкой", + "Validates 24-hour time format with proper hour and minute ranges": "Проверяет 24-часовой формат времени с правильными диапазонами часов и минут", + "Matches ISO 8601 datetime with optional milliseconds and timezone": "Соответствует дате-времени ISO 8601 с необязательными миллисекундами и часовым поясом", + "Validates 11-digit Chinese mobile numbers starting with 13-19": "Проверяет 11-значные китайские мобильные номера, начинающиеся с 13-19", + "Matches US phone format with optional parentheses and separators": "Соответствует формату телефона США с необязательными скобками и разделителями", + "Validates international phone numbers following E.164 standard": "Проверяет международные телефонные номера, следующие стандарту E.164", + "Matches Chinese landline format with area code and optional dash": "Соответствует формату китайского стационарного телефона с кодом области и необязательным тире", + "Validates IPv4 addresses with proper octet range validation (0-255)": "Проверяет адреса IPv4 с правильной проверкой диапазона октетов (0-255)", + "Matches full IPv6 addresses with 8 hexadecimal groups separated by colons": "Соответствует полным адресам IPv6 с 8 шестнадцатеричными группами, разделенными двоеточиями", + "Validates MAC addresses with colon or hyphen separators between hex pairs": "Проверяет MAC-адреса с разделителями двоеточие или дефис между шестнадцатеричными парами", + "Matches opening and closing HTML tags with optional attributes": "Соответствует открывающим и закрывающим HTML-тегам с необязательными атрибутами", + "Captures complete HTML tag pairs with content using backreferences": "Захватывает полные пары HTML-тегов с содержимым, используя обратные ссылки", + "Matches HTML comments including multiline content with non-greedy matching": "Соответствует HTML-комментариям, включая многострочное содержимое с нежадным сопоставлением", + "Extracts src attribute value from img tags with flexible attribute ordering": "Извлекает значение атрибута src из тегов img с гибким порядком атрибутов", + "Validates common email format with alphanumeric characters and standard symbols": "Проверяет общий формат электронной почты с буквенно-цифровыми символами и стандартными символами", + "Comprehensive RFC 5322 compliant email validation with full character set support": "Всеобъемлющая проверка электронной почты, соответствующая RFC 5322, с поддержкой полного набора символов", + "Strong password validation using positive lookaheads for complexity requirements": "Проверка надежного пароля с использованием положительных просмотров вперед для требований сложности", + "Medium strength password requiring letters and numbers with minimum length": "Пароль средней силы, требующий букв и цифр с минимальной длиной", + "Validates Chinese national ID format with birth date and checksum validation": "Проверяет формат китайского национального удостоверения личности с проверкой даты рождения и контрольной суммы", + "Matches major credit card formats including Visa, MasterCard, AmEx, and Discover": "Соответствует основным форматам кредитных карт, включая Visa, MasterCard, AmEx и Discover" } diff --git a/src/global.css b/src/global.css index e7ae7e4..5711014 100644 --- a/src/global.css +++ b/src/global.css @@ -61,7 +61,7 @@ --chart-5: 340 75% 55%; --graph: #d4d4d8; --graph-group: #52525b; - --graph-bg: #111111; + --graph-bg: #000000; } } diff --git a/src/modules/export/dropdown.tsx b/src/modules/export/dropdown.tsx new file mode 100644 index 0000000..f1a78d0 --- /dev/null +++ b/src/modules/export/dropdown.tsx @@ -0,0 +1,53 @@ +import React from 'react' +import { DownloadIcon } from '@radix-ui/react-icons' +import { useTranslation } from 'react-i18next' +import { Button } from '@/components/ui/button' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu' + +export type ExportFormat = 'svg' + +interface ExportDropdownProps { + onExport: (format: ExportFormat) => void + disabled?: boolean + className?: string +} + +const ExportDropdown: React.FC = ({ + onExport, + disabled = false, + className, +}) => { + const { t } = useTranslation() + + const handleExport = (format: ExportFormat) => { + onExport(format) + } + + return ( + + + + + + handleExport('svg')}> + {t('Export as SVG')} + + + + ) +} + +export default ExportDropdown \ No newline at end of file diff --git a/src/modules/export/index.ts b/src/modules/export/index.ts new file mode 100644 index 0000000..fe513c6 --- /dev/null +++ b/src/modules/export/index.ts @@ -0,0 +1,5 @@ +// Export functionality module unified entry point +export { default as ExportDropdown } from './dropdown' +export type { ExportFormat } from './dropdown' +export { exportGraph, exportSVG } from './utils' +export type { ExportFormat as UtilsExportFormat } from './utils' \ No newline at end of file diff --git a/src/modules/export/utils.ts b/src/modules/export/utils.ts new file mode 100644 index 0000000..8d82280 --- /dev/null +++ b/src/modules/export/utils.ts @@ -0,0 +1,264 @@ +export type ExportFormat = 'svg' + +// Export configuration constants +const EXPORT_CONFIG = { + DEFAULT_FILENAME: 'regex-graph', + SVG_NAMESPACE: 'http://www.w3.org/2000/svg', + XLINK_NAMESPACE: 'http://www.w3.org/1999/xlink', + DEFAULT_FONT_SIZE: '15', + DEFAULT_ICON_WIDTH: 12, + DEFAULT_ICON_HEIGHT: 18, + + DEFAULT_COLORS: { + BLACK: '#000000', + WHITE: '#ffffff', + DARK_TEXT: '#111827' + }, + SVG_PATHS: { + // Phosphor Icons Infinity path + INFINITY: 'M248,128a56,56,0,0,1-96,39.6L83.33,96.17A40,40,0,1,0,83.33,159.83L152,231.6A56,56,0,1,1,152,24.4L83.33,96.17a40,40,0,1,1,0,63.66L152,231.6A56.09,56.09,0,0,1,248,128Z', + // Quantifier repetition arrow paths + QUANTIFIER_PATHS: [ + 'M17 1l4 4-4 4', + 'M3 11V9a4 4 0 014-4h14M21 13v2a4 4 0 01-4 4H3', + 'M7 23l-4-4 4-4' + ] + } +} as const + +/** + * Export SVG format + * @param svgElement SVG element + * @param filename File name + */ +export const exportSVG = (svgElement: SVGElement, filename: string = EXPORT_CONFIG.DEFAULT_FILENAME) => { + try { + // Clone the SVG element to avoid modifying the original + const clonedSvg = svgElement.cloneNode(true) as SVGElement + + // Remove rect elements that only have 'fill-transparent' class to avoid black borders in export + const fillTransparentElements = clonedSvg.querySelectorAll('rect.fill-transparent') + fillTransparentElements.forEach(element => { + // Only remove elements that have exactly 'fill-transparent' class and no other classes + const svgElement = element as SVGElement + if (svgElement.className.baseVal === 'fill-transparent') { + element.remove() + } + }) + + // Set SVG xmlns attributes to ensure independence + clonedSvg.setAttribute('xmlns', EXPORT_CONFIG.SVG_NAMESPACE) + clonedSvg.setAttribute('xmlns:xlink', EXPORT_CONFIG.XLINK_NAMESPACE) + + // Modify icon dimensions before serialization + const quantifierIcons = clonedSvg.querySelectorAll('svg[viewBox="0 0 24 24"]') + quantifierIcons.forEach(icon => { + icon.setAttribute('width', '18') + icon.setAttribute('height', '10') + }) + + const infinityIcons = clonedSvg.querySelectorAll('svg[viewBox="0 0 256 256"]') + infinityIcons.forEach(icon => { + icon.setAttribute('width', '18') + icon.setAttribute('height', '10') + }) + + // Get computed styles and inline them into SVG + const styleElement = document.createElementNS('http://www.w3.org/2000/svg', 'style') + const computedStyles = getComputedStylesForSVG(svgElement) + styleElement.textContent = computedStyles + clonedSvg.insertBefore(styleElement, clonedSvg.firstChild) + + // Serialize SVG + const serializer = new XMLSerializer() + let svgString = serializer.serializeToString(clonedSvg) + + // Replace icon placeholders with actual SVG elements + svgString = svgString.replace(/{{INFINITY_ICON}}/g, + ``+ + ``+ + `` + ) + + svgString = svgString.replace(/{{QUANTIFIER_ICON}}/g, + ``+ + EXPORT_CONFIG.SVG_PATHS.QUANTIFIER_PATHS.map(path => ``).join('') + + `` + ) + + // Create Blob and download + const blob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' }) + downloadBlob(blob, `${filename}.svg`) + } catch (error) { + console.error('SVG export failed:', error) + throw new Error('SVG export failed') + } +} + +/** + * Extract text content from element and replace icons with placeholders + * @param element - DOM element to extract text from + * @returns Extracted text content with icon placeholders + */ +function extractTextWithIcons(element: Element): string { + let content = '' + + // Process child nodes to identify icons and text + Array.from(element.childNodes).forEach(node => { + if (node.nodeType === Node.ELEMENT_NODE) { + const el = node as Element + if (el.tagName.toLowerCase() === 'svg') { + const viewBox = el.getAttribute('viewBox') + if (viewBox === '0 0 256 256') { + content += '{{INFINITY_ICON}}' + } else if (viewBox === '0 0 24 24') { + content += '{{QUANTIFIER_ICON}}' + } + } else { + content += el.textContent || '' + } + } else if (node.nodeType === Node.TEXT_NODE) { + content += node.textContent || '' + } + }) + + return content +} + +/** + * Get current theme color values + * @returns Theme color object + */ +function getCurrentThemeColors() { + const rootStyles = getComputedStyle(document.documentElement) + + // Get CSS variable values + const graphColor = rootStyles.getPropertyValue('--graph').trim() || EXPORT_CONFIG.DEFAULT_COLORS.BLACK + const foregroundColor = rootStyles.getPropertyValue('--foreground').trim() + const backgroundColor = rootStyles.getPropertyValue('--background').trim() + + // Convert HSL to hex (if needed) + const convertHslToHex = (hsl: string): string => { + if (hsl.startsWith('#')) return hsl + if (!hsl) return EXPORT_CONFIG.DEFAULT_COLORS.BLACK + + // Simple HSL to RGB conversion (for common HSL formats) + const hslMatch = hsl.match(/([\d.]+)\s+([\d.]+)%\s+([\d.]+)%/) + if (hslMatch) { + const h = parseFloat(hslMatch[1]) / 360 + const s = parseFloat(hslMatch[2]) / 100 + const l = parseFloat(hslMatch[3]) / 100 + + const hue2rgb = (p: number, q: number, t: number) => { + if (t < 0) t += 1 + if (t > 1) t -= 1 + if (t < 1/6) return p + (q - p) * 6 * t + if (t < 1/2) return q + if (t < 2/3) return p + (q - p) * (2/3 - t) * 6 + return p + } + + const q = l < 0.5 ? l * (1 + s) : l + s - l * s + const p = 2 * l - q + const r = Math.round(hue2rgb(p, q, h + 1/3) * 255) + const g = Math.round(hue2rgb(p, q, h) * 255) + const b = Math.round(hue2rgb(p, q, h - 1/3) * 255) + + return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}` + } + + return EXPORT_CONFIG.DEFAULT_COLORS.BLACK + } + + return { + graph: graphColor.startsWith('#') ? graphColor : convertHslToHex(foregroundColor) || EXPORT_CONFIG.DEFAULT_COLORS.BLACK, + foreground: convertHslToHex(foregroundColor) || EXPORT_CONFIG.DEFAULT_COLORS.BLACK, + background: convertHslToHex(backgroundColor) || EXPORT_CONFIG.DEFAULT_COLORS.WHITE + } +} + +/** + * Get computed styles for SVG + * @param svgElement SVG element + * @returns CSS style string + */ +function getComputedStylesForSVG(svgElement: SVGElement): string { + const colors = getCurrentThemeColors() + const styles: string[] = [] + + // Add basic styles, use black as export color + styles.push(` + .stroke-graph { stroke: ${EXPORT_CONFIG.DEFAULT_COLORS.BLACK} !important; stroke-width: 1; } + .fill-transparent { fill: transparent !important; } + /* Ensure circle elements with fill-transparent still show their stroke */ + circle.fill-transparent { stroke: ${EXPORT_CONFIG.DEFAULT_COLORS.BLACK} !important; } + .text-foreground { fill: ${EXPORT_CONFIG.DEFAULT_COLORS.BLACK} !important; } + .rounded-lg { rx: 8; ry: 8; } + .border { stroke: ${EXPORT_CONFIG.DEFAULT_COLORS.BLACK} !important; stroke-width: 1; } + .font-mono { font-family: ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace; } + .text-center { text-anchor: middle; } + .whitespace-nowrap { white-space: nowrap; } + .leading-normal { line-height: 1.5; } + .pointer-events-none { pointer-events: none; } + text { fill: ${EXPORT_CONFIG.DEFAULT_COLORS.BLACK} !important; dominant-baseline: central; alignment-baseline: middle; } + path { stroke: ${EXPORT_CONFIG.DEFAULT_COLORS.BLACK} !important; } + circle { stroke: ${EXPORT_CONFIG.DEFAULT_COLORS.BLACK} !important; } + line { stroke: ${EXPORT_CONFIG.DEFAULT_COLORS.BLACK} !important; } + /* Preserve negative lookaround assertion red dashed style */ + rect.stroke-red-500 { stroke: #ef4444 !important; stroke-dasharray: 4 2 !important; } + rect[class*="stroke-red-500"][class*="stroke-dasharray"] { stroke: #ef4444 !important; stroke-dasharray: 4 2 !important; } + rect[class*="stroke-red-500"] { stroke: #ef4444 !important; } + rect[class*="stroke-dasharray"] { stroke-dasharray: 4 2 !important; } + rect:not([class*="stroke-red-500"]) { stroke: ${EXPORT_CONFIG.DEFAULT_COLORS.BLACK} !important; } + `) + + return styles.join('\n') +} + +/** + * Download Blob file + * @param blob Blob object + * @param filename File name + */ +function downloadBlob(blob: Blob, filename: string) { + const url = URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + link.download = filename + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + URL.revokeObjectURL(url) +} + +/** + * Unified export function + * @param format Export format + * @param element Element to export + * @param filename File name + * @param options Export options + */ +export const exportGraph = async ( + format: ExportFormat, + element: HTMLElement | SVGElement, + filename: string = EXPORT_CONFIG.DEFAULT_FILENAME, + options: any = {} +) => { + switch (format) { + case 'svg': + if (element instanceof SVGElement) { + exportSVG(element, filename) + } else { + // If the passed element is not an SVG element, try to find SVG child element + const svgElement = element.querySelector('svg') + if (svgElement) { + exportSVG(svgElement, filename) + } else { + throw new Error('SVG element not found') + } + } + break + default: + throw new Error(`Unsupported export format: ${format}`) + } +} \ No newline at end of file diff --git a/src/modules/home/index.tsx b/src/modules/home/index.tsx index 0e5883d..823dc7e 100644 --- a/src/modules/home/index.tsx +++ b/src/modules/home/index.tsx @@ -28,6 +28,8 @@ import { import { useToast } from '@/components/ui/use-toast' import { Toggle } from '@/components/ui/toggle' import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area' +import { ExportDropdown, exportGraph } from '@/modules/export' +import type { ExportFormat } from '@/modules/export' function Home() { const [searchParams, setSearchParams] = useSearchParams() @@ -128,6 +130,36 @@ function Home() { toast({ description: t('Permalink copied.') }) } + const handleExport = async (format: ExportFormat) => { + try { + // Find graph container element + // Get the parent container of SVG, not the SVG element itself + const svgElement = document.querySelector('[data-testid="graph"]') as SVGElement + const graphElement = svgElement?.parentElement || svgElement + if (!graphElement) { + toast({ + description: t('No graph to export'), + variant: 'destructive' + }) + return + } + + // Generate filename + const timestamp = new Date().toISOString().slice(0, 19).replace(/[:.]/g, '-') + const filename = `regex-graph-${timestamp}` + + await exportGraph(format, graphElement, filename) + toast({ + description: t(`Graph exported as ${format.toUpperCase()}`) + }) + } catch (error) { + toast({ + description: t('Export failed'), + variant: 'destructive' + }) + } + } + const graphShow = regex !== '' || (ast.body.length > 0 && !errorMsg) return (
- setEditorCollapsed(!pressed)} - > - - +
+ {graphShow && ( + + )} + setEditorCollapsed(!pressed)} + > + + +
{regex !== null && } diff --git a/src/modules/samples/data.ts b/src/modules/samples/data.ts new file mode 100644 index 0000000..676cdaa --- /dev/null +++ b/src/modules/samples/data.ts @@ -0,0 +1,289 @@ +// Regex sample data +export interface RegexSample { + desc: string + label: string + regex: string + explanation?: string +} + +export interface RegexCategory { + id: string + name: string + icon: string + samples: RegexSample[] +} + +// Regex category data +export const regexCategories: RegexCategory[] = [ + { + id: 'numbers', + name: 'Numbers', + icon: '🔢', + samples: [ + { + desc: 'Whole Numbers', + label: '/^\\d+$/', + regex: '^\\d+$', + explanation: 'Matches one or more consecutive digits from start to end of string' + }, + { + desc: 'Decimal Numbers', + label: '/^\\d*\\.\\d+$/', + regex: '^\\d*\\.\\d+$', + explanation: 'Matches numbers with decimal point, allowing optional digits before decimal' + }, + { + desc: 'Whole + Decimal Numbers', + label: '/^\\d*(\\.\\d+)?$/', + regex: '^\\d*(\\.\\d+)?$', + explanation: 'Matches both integers and decimals using optional decimal group' + }, + { + desc: 'Negative, Positive Whole + Decimal Numbers', + label: '/^-?\\d*(\\.\\d+)?$/', + regex: '^-?\\d*(\\.\\d+)?$', + explanation: 'Includes optional minus sign for negative numbers with decimal support' + }, + { + desc: 'Currency Amount', + label: '/^\\$?\\d{1,3}(,\\d{3})*(\\.\\d{2})?$/', + regex: '^\\$?\\d{1,3}(,\\d{3})*(\\.\\d{2})?$', + explanation: 'Validates currency format with optional dollar sign, comma separators, and cents' + }, + { + desc: 'Percentage', + label: '/^\\d{1,3}(\\.\\d{1,2})?%$/', + regex: '^\\d{1,3}(\\.\\d{1,2})?%$', + explanation: 'Matches percentage values from 0-999% with up to 2 decimal places' + } + ] + }, + { + id: 'urls', + name: 'URLs', + icon: '🌐', + samples: [ + { + desc: 'Basic URL', + label: '/^https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\\+.~#()?&//=]*)$/', + regex: '^https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\\+.~#()?&//=]*)$', + explanation: 'Validates HTTP/HTTPS URLs with optional www prefix and query parameters' + }, + { + desc: 'Domain Name', + label: '/^[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?\\.[a-zA-Z]{2,}$/', + regex: '^[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?\\.[a-zA-Z]{2,}$', + explanation: 'Matches valid domain names following RFC standards with length limits' + }, + { + desc: 'FTP URL', + label: '/^ftp:\\/\\/[\\w\\.-]+\\.[a-zA-Z]{2,}(:\\d+)?(\\/.*)?$/', + regex: '^ftp:\\/\\/[\\w\\.-]+\\.[a-zA-Z]{2,}(:\\d+)?(\\/.*)?$', + explanation: 'Validates FTP protocol URLs with optional port and path components' + }, + { + desc: 'URL with Port', + label: '/^https?:\\/\\/[\\w\\.-]+(:\\d+)?(\\/.*)?$/', + regex: '^https?:\\/\\/[\\w\\.-]+(:\\d+)?(\\/.*)?$', + explanation: 'Matches URLs with optional port numbers and path segments' + } + ] + }, + { + id: 'dates', + name: 'Dates', + icon: '📅', + samples: [ + { + desc: 'Date Format YYYY-MM-DD', + label: '/^[12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])$/', + regex: '^[12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])$', + explanation: 'Validates ISO date format with proper month and day ranges' + }, + { + desc: 'Date Format DD/MM/YYYY', + label: '/^(0[1-9]|[12]\\d|3[01])\\/(0[1-9]|1[0-2])\\/[12]\\d{3}$/', + regex: '^(0[1-9]|[12]\\d|3[01])\\/(0[1-9]|1[0-2])\\/[12]\\d{3}$', + explanation: 'European date format with day-first ordering and slash separators' + }, + { + desc: 'Date Format MM/DD/YYYY', + label: '/^(0[1-9]|1[0-2])\\/(0[1-9]|[12]\\d|3[01])\\/[12]\\d{3}$/', + regex: '^(0[1-9]|1[0-2])\\/(0[1-9]|[12]\\d|3[01])\\/[12]\\d{3}$', + explanation: 'American date format with month-first ordering and validation' + }, + { + desc: 'Time Format HH:MM', + label: '/^([01]?\\d|2[0-3]):[0-5]\\d$/', + regex: '^([01]?\\d|2[0-3]):[0-5]\\d$', + explanation: 'Validates 24-hour time format with proper hour and minute ranges' + }, + { + desc: 'DateTime ISO 8601', + label: '/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{3})?Z?$/', + regex: '^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{3})?Z?$', + explanation: 'Matches ISO 8601 datetime with optional milliseconds and timezone' + } + ] + }, + { + id: 'phones', + name: 'Phone Numbers', + icon: '📞', + samples: [ + { + desc: 'Chinese Mobile Phone', + label: '/^1[3-9]\\d{9}$/', + regex: '^1[3-9]\\d{9}$', + explanation: 'Validates 11-digit Chinese mobile numbers starting with 13-19' + }, + { + desc: 'US Phone Number', + label: '/^\\(?([0-9]{3})\\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$/', + regex: '^\\(?([0-9]{3})\\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$', + explanation: 'Matches US phone format with optional parentheses and separators' + }, + { + desc: 'International Phone', + label: '/^\\+?[1-9]\\d{1,14}$/', + regex: '^\\+?[1-9]\\d{1,14}$', + explanation: 'Validates international phone numbers following E.164 standard' + }, + { + desc: 'Chinese Landline', + label: '/^0\\d{2,3}-?\\d{7,8}$/', + regex: '^0\\d{2,3}-?\\d{7,8}$', + explanation: 'Matches Chinese landline format with area code and optional dash' + } + ] + }, + { + id: 'ips', + name: 'IP Addresses', + icon: '🌍', + samples: [ + { + desc: 'IPv4 Address', + label: '/^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/', + regex: '^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$', + explanation: 'Validates IPv4 addresses with proper octet range validation (0-255)' + }, + { + desc: 'IPv6 Address', + label: '/^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/', + regex: '^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$', + explanation: 'Matches full IPv6 addresses with 8 hexadecimal groups separated by colons' + }, + { + desc: 'MAC Address', + label: '/^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/', + regex: '^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$', + explanation: 'Validates MAC addresses with colon or hyphen separators between hex pairs' + } + ] + }, + { + id: 'html', + name: 'HTML Tags', + icon: '🏷️', + samples: [ + { + desc: 'HTML Tag', + label: '/<\\/?[a-zA-Z][a-zA-Z0-9]*[^<>]*>/', + regex: '<\\/?[a-zA-Z][a-zA-Z0-9]*[^<>]*>', + explanation: 'Matches opening and closing HTML tags with optional attributes' + }, + { + desc: 'HTML Tag with Attributes', + label: '/<([a-zA-Z][a-zA-Z0-9]*)\\b[^>]*>(.*?)<\\/\\1>/', + regex: '<([a-zA-Z][a-zA-Z0-9]*)\\b[^>]*>(.*?)<\\/\\1>', + explanation: 'Captures complete HTML tag pairs with content using backreferences' + }, + { + desc: 'HTML Comment', + label: '//', + regex: '', + explanation: 'Matches HTML comments including multiline content with non-greedy matching' + }, + { + desc: 'HTML Image Tag', + label: '/]*src\\s*=\\s*["\']([^"\'>]+)["\'][^>]*>/', + regex: ']*src\\s*=\\s*["\']([^"\'>]+)["\'][^>]*>', + explanation: 'Extracts src attribute value from img tags with flexible attribute ordering' + } + ] + }, + { + id: 'emails', + name: 'Email Addresses', + icon: '📧', + samples: [ + { + desc: 'Basic Email', + label: '/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/', + regex: '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$', + explanation: 'Validates common email format with alphanumeric characters and standard symbols' + }, + { + desc: 'Strict Email RFC 5322', + label: '/^[a-zA-Z0-9!#$%&\'*+\/=?^_`{|}~-]+(?:\\.[a-zA-Z0-9!#$%&\'*+\/=?^_`{|}~-]+)*@(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$/', + regex: '^[a-zA-Z0-9!#$%&\'*+\/=?^_`{|}~-]+(?:\\.[a-zA-Z0-9!#$%&\'*+\/=?^_`{|}~-]+)*@(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$', + explanation: 'Comprehensive RFC 5322 compliant email validation with full character set support' + } + ] + }, + { + id: 'passwords', + name: 'Passwords', + icon: '🔒', + samples: [ + { + desc: 'Strong Password', + label: '/^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,}$/', + regex: '^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,}$', + explanation: 'Strong password validation using positive lookaheads for complexity requirements' + }, + { + desc: 'Medium Password', + label: '/^(?=.*[a-zA-Z])(?=.*\\d)[a-zA-Z\\d@$!%*?&]{6,}$/', + regex: '^(?=.*[a-zA-Z])(?=.*\\d)[a-zA-Z\\d@$!%*?&]{6,}$', + explanation: 'Medium strength password requiring letters and numbers with minimum length' + } + ] + }, + { + id: 'identifiers', + name: 'ID Numbers', + icon: '🆔', + samples: [ + { + desc: 'Chinese ID Card', + label: '/^[1-9]\\d{5}(18|19|20)\\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$/', + regex: '^[1-9]\\d{5}(18|19|20)\\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$', + explanation: 'Validates Chinese national ID format with birth date and checksum validation' + }, + { + desc: 'Credit Card Number', + label: '/^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|3[47][0-9]{13}|3[0-9]{13}|6(?:011|5[0-9]{2})[0-9]{12})$/', + regex: '^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|3[47][0-9]{13}|3[0-9]{13}|6(?:011|5[0-9]{2})[0-9]{12})$', + explanation: 'Matches major credit card formats including Visa, MasterCard, AmEx, and Discover' + } + ] + } +] + +// Get flattened list of all samples +export function getAllSamples(): RegexSample[] { + return regexCategories.flatMap(category => category.samples) +} + +// Get samples by category ID +export function getSamplesByCategory(categoryId: string): RegexSample[] { + const category = regexCategories.find(cat => cat.id === categoryId) + return category ? category.samples : [] +} + +// Get category information +export function getCategoryById(categoryId: string): RegexCategory | undefined { + return regexCategories.find(cat => cat.id === categoryId) +} \ No newline at end of file diff --git a/src/modules/samples/index.tsx b/src/modules/samples/index.tsx index 6971d8b..c4dab77 100644 --- a/src/modules/samples/index.tsx +++ b/src/modules/samples/index.tsx @@ -1,65 +1,127 @@ +import { useState } from 'react' import { Link } from 'react-router-dom' import { useTranslation } from 'react-i18next' import SimpleGraph from '@/modules/graph/simple-graph' import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area' +import { Button } from '@/components/ui/button' +import { regexCategories, getAllSamples, getSamplesByCategory } from './data' +import type { RegexSample } from './data' -const samples = [ - { desc: '1. Whole Numbers', label: '/^\\d+$/', regex: '^\\d+$' }, - { - desc: '2. Decimal Numbers', - label: '/^\\d*\\.\\d+$/', - regex: '^\\d*\\.\\d+$', - }, - { - desc: '3. Whole + Decimal Numbers', - label: '/^\\d*(\\.\\d+)?$/', - regex: '^\\d*(\\.\\d+)?$', - }, - { - desc: '4. Negative, Positive Whole + Decimal Numbers', - label: '/^-?\\d*(\\.\\d+)?$/', - regex: '^-?\\d*(\\.\\d+)?$', - }, - { - desc: '5. Url', - label: - '/^https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\\+.~#()?&//=]*)$/', - regex: - '^https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\\+.~#()?&//=]*)$', - }, - { - desc: '6. Date Format YYYY-MM-dd', - label: '/^[12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])$/', - regex: '^[12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])$', - }, -] function Samples() { const { t } = useTranslation() + const [selectedCategory, setSelectedCategory] = useState('all') + + // Get samples for currently selected category + const currentSamples: RegexSample[] = selectedCategory === 'all' + ? getAllSamples() + : getSamplesByCategory(selectedCategory) + return ( - -
-
- {samples.map(({ desc, label, regex }) => { - const linkTo = `/?r=${encodeURIComponent(`/${regex}/`)}` - return ( -
- - {t(desc)} - : - {label} - - - - - - - +
+ {/* Left sidebar */} +
+
+

+ {t('Regex Samples')} +

+
+ {/* All samples button */} + + + {/* Category buttons */} + {regexCategories.map((category) => ( + + ))} +
- + + {/* Main content area */} +
+ +
+
+ {/* Category header */} + + + {/* Sample list */} +
+ {currentSamples.map((sample, index) => { + const linkTo = `/?r=${encodeURIComponent(`/${sample.regex}/`)}` + return ( +
+ {/* Sample title and description */} +
+ +

+ {t(sample.desc)} +

+

+ {sample.explanation ? t(sample.explanation) : ''} +

+ +
+ + {/* Regular expression */} +
+ + + {sample.label} + + +
+ + {/* Visualization graph */} +
+ + +
+ +
+ + +
+
+
+ ) + })} +
+ + {/* Empty state */} + {currentSamples.length === 0 && ( +
+
📝
+

+ {t('No regex samples available for this category')} +

+
+ )} +
+
+
+
+
) }