From 73a01f41754e1f0b6d49c531d410ddaed692b483 Mon Sep 17 00:00:00 2001 From: codevory Date: Mon, 29 Dec 2025 14:33:00 +0530 Subject: [PATCH 01/30] added a sliding ui effect for theme switcher button --- package-lock.json | 30 +++++++-------------- src/components/ThemeSwitch.jsx | 5 ++-- src/components/ThemeSwitch.module.css | 38 ++++++++++++++++++++++++--- 3 files changed, 46 insertions(+), 27 deletions(-) diff --git a/package-lock.json b/package-lock.json index 83d6c0b67..53d276ccf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -145,7 +145,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -463,7 +462,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -510,7 +508,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -2520,7 +2517,8 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/chai": { "version": "5.2.3", @@ -2574,7 +2572,6 @@ "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -2585,7 +2582,6 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -2766,7 +2762,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2995,7 +2990,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -3738,7 +3732,8 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/dot-prop": { "version": "5.3.0", @@ -3966,7 +3961,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5048,7 +5042,6 @@ "integrity": "sha512-GtldT42B8+jefDUC4yUKAvsaOrH7PDHmZxZXNgF2xMmymjUbRYJvpAybZAKEmXDGTM0mCsz8duOa4vTm5AY2Kg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@acemir/cssom": "^0.9.28", "@asamuzakjp/dom-selector": "^6.7.6", @@ -5319,6 +5312,7 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -6038,7 +6032,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -6080,6 +6073,7 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -6095,6 +6089,7 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -6107,7 +6102,8 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/process-nextick-args": { "version": "2.0.1", @@ -6185,7 +6181,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -6195,7 +6190,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -6574,7 +6568,6 @@ "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -7263,7 +7256,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -7479,7 +7471,6 @@ "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -7594,7 +7585,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -7608,7 +7598,6 @@ "integrity": "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/expect": "4.0.16", "@vitest/mocker": "4.0.16", @@ -7943,7 +7932,6 @@ "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/components/ThemeSwitch.jsx b/src/components/ThemeSwitch.jsx index f294f3091..d41673a1f 100644 --- a/src/components/ThemeSwitch.jsx +++ b/src/components/ThemeSwitch.jsx @@ -18,9 +18,10 @@ export default function ThemeSwitch() { return ( - ); diff --git a/src/components/ThemeSwitch.module.css b/src/components/ThemeSwitch.module.css index 2b9c83341..ded231566 100644 --- a/src/components/ThemeSwitch.module.css +++ b/src/components/ThemeSwitch.module.css @@ -1,22 +1,52 @@ -.themeButton { +.themeButtonLight{ + width:60px; + height:30px; border-radius: 9999px; padding: 0.5rem 0.75rem; transition: all 0.2s ease-in-out; border: none; margin: 0px 0.875rem; cursor: pointer; + position:relative; + background-color: #1a001f; +} + +.themeButtonDark{ + width:60px; + height:30px; + border-radius: 9999px; + padding: 0.5rem 0.75rem; + transition: all 0.2s ease-in-out; + border: none; + margin: 0px 0.875rem; + cursor: pointer; + position:relative; + background-color: #fed0d0;; } .themeButton:hover { .icon { color: var(--color-secondary); } - transform: scale(1.05); + transform: scale(1.02); } .icon { - height: 1.25rem; - width: 1.25rem; + position:absolute; + top:0; + left:0; + height: 1.8rem; + width: 1.8rem; color: var(--color-primary); transition: color 0.2s ease-in-out; } +.icondark{ + position:absolute; + top:0; + left:0; + height: 1.8rem; + width: 1.8rem; + color: var(--color-primary); + transition: all 0.5s ease-in; + transform:translateX(31px) +} \ No newline at end of file From 5dba35bf72726867925343abbe770551d4b7f05f Mon Sep 17 00:00:00 2001 From: codevory Date: Mon, 29 Dec 2025 16:05:01 +0530 Subject: [PATCH 02/30] refactor(theme-switch): address review feedback --- src/components/ThemeSwitch.jsx | 2 +- src/components/ThemeSwitch.module.css | 80 ++++++++++++++++----------- 2 files changed, 50 insertions(+), 32 deletions(-) diff --git a/src/components/ThemeSwitch.jsx b/src/components/ThemeSwitch.jsx index d41673a1f..a34265c6c 100644 --- a/src/components/ThemeSwitch.jsx +++ b/src/components/ThemeSwitch.jsx @@ -19,7 +19,7 @@ export default function ThemeSwitch() { return ( {/* Added sliding toggle effect to change theme mode which enhances ui */} - diff --git a/src/components/ThemeSwitch.module.css b/src/components/ThemeSwitch.module.css index ded231566..57d0c30e0 100644 --- a/src/components/ThemeSwitch.module.css +++ b/src/components/ThemeSwitch.module.css @@ -1,52 +1,70 @@ -.themeButtonLight{ +.themeButton{ width:60px; height:30px; border-radius: 9999px; - padding: 0.5rem 0.75rem; - transition: all 0.2s ease-in-out; + padding-inline: 8px; + transition: transform 0.2s ease-in-out; border: none; margin: 0px 0.875rem; - cursor: pointer; position:relative; + display: flex; + align-items: center; + justify-content: center; +} + +.themeButtonLight{ background-color: #1a001f; } .themeButtonDark{ - width:60px; - height:30px; - border-radius: 9999px; - padding: 0.5rem 0.75rem; - transition: all 0.2s ease-in-out; - border: none; - margin: 0px 0.875rem; - cursor: pointer; - position:relative; - background-color: #fed0d0;; + background-color: #ddd9d9; +} + +.themeButton:hover .icondark { + color: rgb(220, 220, 220); +} + +.themeButton:hover .icon { + color: #1a001f; } .themeButton:hover { - .icon { - color: var(--color-secondary); - } + cursor: pointer; transform: scale(1.02); } + .icon { - position:absolute; - top:0; - left:0; - height: 1.8rem; - width: 1.8rem; + height: 1.3rem; + width: 1.3rem; color: var(--color-primary); - transition: color 0.2s ease-in-out; + transition: transform 0.2s ease-in-out, color 0.2s ease-in-out; } .icondark{ - position:absolute; - top:0; - left:0; - height: 1.8rem; - width: 1.8rem; + height: 1.3rem; + width: 1.3rem; color: var(--color-primary); - transition: all 0.5s ease-in; - transform:translateX(31px) -} \ No newline at end of file + transition: transform 0.2s ease-in-out, color 0.2s ease-in-out; +} + +.themeButtonLight .icon{ + transform: translateX(0); +} +.themeButtonLight .icondark{ +transform: translateX(100%); +} + +.themeButtonDark .icon{ + transform: translateX(-100%); +} +.themeButtonDark .icondark{ + transform: translateX(0); +} + +@media (prefers-reduced-motion: reduce) { + .icon, + .icondark, + .themeButton { + transition: none; + } +} From 932e8ca9bb5333d29479f527e8ff22d872859964 Mon Sep 17 00:00:00 2001 From: codevory Date: Mon, 29 Dec 2025 16:26:21 +0530 Subject: [PATCH 03/30] fix(theme-switch): render both icons to enable sliding animation --- src/components/ThemeSwitch.jsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/components/ThemeSwitch.jsx b/src/components/ThemeSwitch.jsx index a34265c6c..1d349877c 100644 --- a/src/components/ThemeSwitch.jsx +++ b/src/components/ThemeSwitch.jsx @@ -19,10 +19,14 @@ export default function ThemeSwitch() { return ( {/* Added sliding toggle effect to change theme mode which enhances ui */} - + + ); } From 482289c4be7481d57830a7a9fba653530d8da140 Mon Sep 17 00:00:00 2001 From: codevory Date: Tue, 30 Dec 2025 13:29:32 +0530 Subject: [PATCH 04/30] refactor(glass-switch): apply DRY styles and finalize reusable switch --- package-lock.json | 30 ++++++------------ src/components/GlassSwitch.jsx | 18 +++++++++++ src/components/GlassSwitch.module.css | 27 ++++++++++++++++ src/components/GlassSwitch.test.jsx | 44 +++++++++++++++++++++++++++ src/components/NavBar.jsx | 8 ++++- 5 files changed, 105 insertions(+), 22 deletions(-) create mode 100644 src/components/GlassSwitch.jsx create mode 100644 src/components/GlassSwitch.module.css create mode 100644 src/components/GlassSwitch.test.jsx diff --git a/package-lock.json b/package-lock.json index faaa820c1..3b4b91a64 100644 --- a/package-lock.json +++ b/package-lock.json @@ -145,7 +145,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -463,7 +462,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -510,7 +508,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -2538,7 +2535,8 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/chai": { "version": "5.2.3", @@ -2592,7 +2590,6 @@ "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -2603,7 +2600,6 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -2784,7 +2780,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3013,7 +3008,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -3756,7 +3750,8 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/dot-prop": { "version": "5.3.0", @@ -3984,7 +3979,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5053,7 +5047,6 @@ "integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@acemir/cssom": "^0.9.28", "@asamuzakjp/dom-selector": "^6.7.6", @@ -5324,6 +5317,7 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -6043,7 +6037,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -6085,6 +6078,7 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -6100,6 +6094,7 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -6112,7 +6107,8 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/process-nextick-args": { "version": "2.0.1", @@ -6190,7 +6186,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -6200,7 +6195,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -6579,7 +6573,6 @@ "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -7261,7 +7254,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -7477,7 +7469,6 @@ "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -7592,7 +7583,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -7606,7 +7596,6 @@ "integrity": "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/expect": "4.0.16", "@vitest/mocker": "4.0.16", @@ -7928,7 +7917,6 @@ "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/components/GlassSwitch.jsx b/src/components/GlassSwitch.jsx new file mode 100644 index 000000000..1fc4f96d0 --- /dev/null +++ b/src/components/GlassSwitch.jsx @@ -0,0 +1,18 @@ +import styles from './GlassSwitch.module.css' +import Tooltip from '@components/Tooltip'; + +const GlassSwitch = ({onChange ,checked,ariaLabel}) => { + return ( + + + + ) +} + +export default GlassSwitch diff --git a/src/components/GlassSwitch.module.css b/src/components/GlassSwitch.module.css new file mode 100644 index 000000000..9d18356b2 --- /dev/null +++ b/src/components/GlassSwitch.module.css @@ -0,0 +1,27 @@ +.switch{ + display: flex; + align-items: center; + width:64px; + height:32px; + border-radius: 30px; + padding: 0; + margin-right:20px; + cursor: pointer; +} +.unChecked{ + justify-content: start; +} +.checked{ + justify-content: flex-end; + background-color: rgb(26, 194, 26); +} + +.thumb{ +width:30px; +height:30px; +border-radius: 100%; +transition:color 0.3s ease; +background-color: rgba(255, 255, 255, 0.867); +cursor: pointer; +} + diff --git a/src/components/GlassSwitch.test.jsx b/src/components/GlassSwitch.test.jsx new file mode 100644 index 000000000..f960cb767 --- /dev/null +++ b/src/components/GlassSwitch.test.jsx @@ -0,0 +1,44 @@ +import { describe, it, expect, vi } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import GlassSwitch from './GlassSwitch'; + +describe('GlassSwitch', () => { + it('renders a switch button', () => { + render( {}} ariaLabel="Toggle" />); + expect(screen.getByRole('switch')).toBeInTheDocument(); + }); + + it('sets aria-checked to true when checked', () => { + render( {}} ariaLabel="Toggle" />); + expect(screen.getByRole('switch')).toHaveAttribute('aria-checked', 'true'); + }); + + it('sets aria-checked to false when unchecked', () => { + render( {}} ariaLabel="Toggle" />); + expect(screen.getByRole('switch')).toHaveAttribute('aria-checked', 'false'); + }); + + it('calls onChange when clicked', async () => { + const user = userEvent.setup(); + const onChange = vi.fn(); + + render(); + await user.click(screen.getByRole('switch')); + + expect(onChange).toHaveBeenCalledOnce(); + }); + + it('is keyboard accessible', async () => { + const user = userEvent.setup(); + const onChange = vi.fn(); + + render(); + const button = screen.getByRole('switch'); + + button.focus(); + await user.keyboard('{Enter}'); + + expect(onChange).toHaveBeenCalled(); + }); +}); diff --git a/src/components/NavBar.jsx b/src/components/NavBar.jsx index f8dc3f18d..910d3980f 100644 --- a/src/components/NavBar.jsx +++ b/src/components/NavBar.jsx @@ -5,10 +5,15 @@ import styles from './NavBar.module.css'; import GlassCard from '@components/GlassCard'; import ThemeSwitch from '@components/ThemeSwitch'; import Tooltip from '@components/Tooltip'; +import GlassSwitch from './GlassSwitch'; +import { useTheme } from '@hooks/useTheme'; + export default function NavBar() { const [isOpen, setIsOpen] = useState(false); const location = useLocation(); + const {theme , toggleTheme} = useTheme() +const isDark = theme === 'dark'; const links = [ { path: '/', label: 'Home', tooltip: 'Go to the home page' }, @@ -55,7 +60,8 @@ export default function NavBar() {
- + + - ) } diff --git a/src/components/GlassSwitch.module.css b/src/components/GlassSwitch.module.css index 9d18356b2..46fd471b0 100644 --- a/src/components/GlassSwitch.module.css +++ b/src/components/GlassSwitch.module.css @@ -12,7 +12,7 @@ justify-content: start; } .checked{ - justify-content: flex-end; + justify-content: end; background-color: rgb(26, 194, 26); } @@ -21,7 +21,6 @@ width:30px; height:30px; border-radius: 100%; transition:color 0.3s ease; -background-color: rgba(255, 255, 255, 0.867); -cursor: pointer; +background-color: rgba(255, 255, 255, 0.956); } diff --git a/src/components/NavBar.jsx b/src/components/NavBar.jsx index 910d3980f..1737b81e4 100644 --- a/src/components/NavBar.jsx +++ b/src/components/NavBar.jsx @@ -13,7 +13,7 @@ export default function NavBar() { const [isOpen, setIsOpen] = useState(false); const location = useLocation(); const {theme , toggleTheme} = useTheme() -const isDark = theme === 'dark'; + const isDark = theme === 'dark'; const links = [ { path: '/', label: 'Home', tooltip: 'Go to the home page' }, @@ -60,7 +60,7 @@ const isDark = theme === 'dark';
- + - + ); -} +} \ No newline at end of file diff --git a/src/components/ThemeSwitch.module.css b/src/components/ThemeSwitch.module.css index 57d0c30e0..9bfaf0511 100644 --- a/src/components/ThemeSwitch.module.css +++ b/src/components/ThemeSwitch.module.css @@ -1,70 +1,22 @@ -.themeButton{ - width:60px; - height:30px; +.themeButton { border-radius: 9999px; - padding-inline: 8px; - transition: transform 0.2s ease-in-out; + padding: 0.5rem 0.75rem; + transition: all 0.2s ease-in-out; border: none; margin: 0px 0.875rem; - position:relative; - display: flex; - align-items: center; - justify-content: center; -} - -.themeButtonLight{ - background-color: #1a001f; -} - -.themeButtonDark{ - background-color: #ddd9d9; -} - -.themeButton:hover .icondark { - color: rgb(220, 220, 220); -} - -.themeButton:hover .icon { - color: #1a001f; + cursor: pointer; } .themeButton:hover { - cursor: pointer; - transform: scale(1.02); + .icon { + color: var(--color-secondary); + } + transform: scale(1.05); } - .icon { - height: 1.3rem; - width: 1.3rem; + height: 1.25rem; + width: 1.25rem; color: var(--color-primary); - transition: transform 0.2s ease-in-out, color 0.2s ease-in-out; -} -.icondark{ - height: 1.3rem; - width: 1.3rem; - color: var(--color-primary); - transition: transform 0.2s ease-in-out, color 0.2s ease-in-out; -} - -.themeButtonLight .icon{ - transform: translateX(0); -} -.themeButtonLight .icondark{ -transform: translateX(100%); -} - -.themeButtonDark .icon{ - transform: translateX(-100%); -} -.themeButtonDark .icondark{ - transform: translateX(0); -} - -@media (prefers-reduced-motion: reduce) { - .icon, - .icondark, - .themeButton { - transition: none; - } -} + transition: color 0.2s ease-in-out; +} \ No newline at end of file diff --git a/src/components/ThemeSwitch.test.jsx b/src/components/ThemeSwitch.test.jsx index f109fc151..3f392d4dd 100644 --- a/src/components/ThemeSwitch.test.jsx +++ b/src/components/ThemeSwitch.test.jsx @@ -114,4 +114,4 @@ describe('ThemeSwitch component', () => { fireEvent.click(btn); expect(customToggle).toHaveBeenCalledTimes(1); }); -}); +}); \ No newline at end of file From 1c19a89449b1ca4b00947a281fe807c34582c21f Mon Sep 17 00:00:00 2001 From: codevory Date: Tue, 30 Dec 2025 14:54:08 +0530 Subject: [PATCH 06/30] fixed some issues & added sliding effect --- src/components/GlassSwitch.jsx | 7 +++++-- src/components/GlassSwitch.module.css | 13 ++++++++----- src/components/NavBar.jsx | 3 +-- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/components/GlassSwitch.jsx b/src/components/GlassSwitch.jsx index bbad817c6..2d0e5626f 100644 --- a/src/components/GlassSwitch.jsx +++ b/src/components/GlassSwitch.jsx @@ -1,12 +1,15 @@ import styles from './GlassSwitch.module.css' +import Tooltip from '@components/Tooltip'; const GlassSwitch = ({onChange ,checked,ariaLabel}) => { return ( + + ) } diff --git a/src/components/GlassSwitch.module.css b/src/components/GlassSwitch.module.css index 46fd471b0..c562ff7e5 100644 --- a/src/components/GlassSwitch.module.css +++ b/src/components/GlassSwitch.module.css @@ -7,20 +7,23 @@ padding: 0; margin-right:20px; cursor: pointer; + position:relative; } -.unChecked{ - justify-content: start; -} + .checked{ - justify-content: end; background-color: rgb(26, 194, 26); } .thumb{ +position:absolute; +left:2px; width:30px; height:30px; border-radius: 100%; -transition:color 0.3s ease; +transition:transform 0.3s ease; background-color: rgba(255, 255, 255, 0.956); } +.checked .thumb{ + transform: translateX(100%); +} diff --git a/src/components/NavBar.jsx b/src/components/NavBar.jsx index 1737b81e4..e42d9dcb8 100644 --- a/src/components/NavBar.jsx +++ b/src/components/NavBar.jsx @@ -60,8 +60,7 @@ export default function NavBar() {
- - + + - ) -} + ); +}; + +GlassSwitch.propTypes = { + isOn: PropTypes.bool, + onChange: PropTypes.func.isRequired, + ariaLabel: PropTypes.string.isRequired, + thumbContent: PropTypes.node, +}; -export default GlassSwitch +export default GlassSwitch; diff --git a/src/components/GlassSwitch.module.css b/src/components/GlassSwitch.module.css index c562ff7e5..9430dc0e7 100644 --- a/src/components/GlassSwitch.module.css +++ b/src/components/GlassSwitch.module.css @@ -1,29 +1,28 @@ -.switch{ - display: flex; - align-items: center; - width:64px; - height:32px; - border-radius: 30px; - padding: 0; - margin-right:20px; - cursor: pointer; - position:relative; +.switch { + --size:32px; + display: flex; + align-items: center; + width: calc(var(--size) * 2); + height: var(--size); + padding: 0; + border-radius: 9999px; + position: relative; + cursor: pointer; + margin-right:15px; } -.checked{ - background-color: rgb(26, 194, 26); +.thumb { + position: absolute; + left:0; + bottom: 0; + width: calc(var(--size) - 4px); + height: calc(var(--size) - 4px); + border-radius: 50%; + background: var(--color-surface); + color: var(--color-text); + transition: transform 0.3s ease; } -.thumb{ -position:absolute; -left:2px; -width:30px; -height:30px; -border-radius: 100%; -transition:transform 0.3s ease; -background-color: rgba(255, 255, 255, 0.956); -} - -.checked .thumb{ - transform: translateX(100%); +.checked .thumb { + transform: translateX(var(--size)); } diff --git a/src/components/GlassSwitch.test.jsx b/src/components/GlassSwitch.test.jsx index f960cb767..2a63a880f 100644 --- a/src/components/GlassSwitch.test.jsx +++ b/src/components/GlassSwitch.test.jsx @@ -5,17 +5,17 @@ import GlassSwitch from './GlassSwitch'; describe('GlassSwitch', () => { it('renders a switch button', () => { - render( {}} ariaLabel="Toggle" />); + render( {}} ariaLabel="Toggle" />); expect(screen.getByRole('switch')).toBeInTheDocument(); }); it('sets aria-checked to true when checked', () => { - render( {}} ariaLabel="Toggle" />); + render( {}} ariaLabel="Toggle" />); expect(screen.getByRole('switch')).toHaveAttribute('aria-checked', 'true'); }); it('sets aria-checked to false when unchecked', () => { - render( {}} ariaLabel="Toggle" />); + render( {}} ariaLabel="Toggle" />); expect(screen.getByRole('switch')).toHaveAttribute('aria-checked', 'false'); }); @@ -23,7 +23,7 @@ describe('GlassSwitch', () => { const user = userEvent.setup(); const onChange = vi.fn(); - render(); + render(); await user.click(screen.getByRole('switch')); expect(onChange).toHaveBeenCalledOnce(); @@ -33,7 +33,7 @@ describe('GlassSwitch', () => { const user = userEvent.setup(); const onChange = vi.fn(); - render(); + render(); const button = screen.getByRole('switch'); button.focus(); diff --git a/src/components/NavBar.jsx b/src/components/NavBar.jsx index e42d9dcb8..e255e5b85 100644 --- a/src/components/NavBar.jsx +++ b/src/components/NavBar.jsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { SquareArrowOutUpRight } from 'lucide-react'; +import { Moon, SquareArrowOutUpRight, Sun } from 'lucide-react'; import { Link, useLocation } from 'react-router-dom'; import styles from './NavBar.module.css'; import GlassCard from '@components/GlassCard'; @@ -60,7 +60,7 @@ export default function NavBar() {
- + : } ariaLabel={`switch to ${isDark ? 'light' : 'dark'} mode`} /> - + : } + ariaLabel={`switch to ${isDark ? 'light' : 'dark'} mode`} + /> ); } \ No newline at end of file From 5aa17826a3d738105c31031fe8b789b666323f0b Mon Sep 17 00:00:00 2001 From: codevory Date: Wed, 31 Dec 2025 12:59:45 +0530 Subject: [PATCH 11/30] removed unused code & rendered themeswitch only --- src/components/NavBar.jsx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/components/NavBar.jsx b/src/components/NavBar.jsx index e255e5b85..258004b15 100644 --- a/src/components/NavBar.jsx +++ b/src/components/NavBar.jsx @@ -1,19 +1,16 @@ import { useState } from 'react'; -import { Moon, SquareArrowOutUpRight, Sun } from 'lucide-react'; +import { SquareArrowOutUpRight } from 'lucide-react'; import { Link, useLocation } from 'react-router-dom'; import styles from './NavBar.module.css'; import GlassCard from '@components/GlassCard'; import ThemeSwitch from '@components/ThemeSwitch'; import Tooltip from '@components/Tooltip'; -import GlassSwitch from './GlassSwitch'; -import { useTheme } from '@hooks/useTheme'; export default function NavBar() { const [isOpen, setIsOpen] = useState(false); const location = useLocation(); - const {theme , toggleTheme} = useTheme() - const isDark = theme === 'dark'; + const links = [ { path: '/', label: 'Home', tooltip: 'Go to the home page' }, @@ -60,7 +57,7 @@ export default function NavBar() {
- : } ariaLabel={`switch to ${isDark ? 'light' : 'dark'} mode`} /> + @@ -25,6 +26,8 @@ GlassSwitch.propTypes = { onChange: PropTypes.func.isRequired, ariaLabel: PropTypes.string.isRequired, thumbContent: PropTypes.node, + disabled:PropTypes.bool, + size:PropTypes.string, }; export default GlassSwitch; From 57d911fde3c007ecc5637c379f3e79c24cd11b82 Mon Sep 17 00:00:00 2001 From: codevory Date: Wed, 31 Dec 2025 13:27:37 +0530 Subject: [PATCH 15/30] improved documentation for GlassSwitch resuable component --- docs/docs/components/glass-switch.mdx | 39 ++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/docs/docs/components/glass-switch.mdx b/docs/docs/components/glass-switch.mdx index 9b8fb3a2d..a9d760c1d 100644 --- a/docs/docs/components/glass-switch.mdx +++ b/docs/docs/components/glass-switch.mdx @@ -18,11 +18,24 @@ This makes it suitable for use cases such as: --- +## Props + +| Prop | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `isOn` | `boolean` | Yes | — | Whether the switch is toggled on | +| `onChange` | `function` | Yes | — | Callback function fired when the switch is clicked | +| `ariaLabel` | `string` | Yes | — | Accessibility label for the switch | +| `thumbContent` | `ReactNode` | No | `undefined` | Content to display inside the switch thumb | +| `disabled` | `boolean` | No | `false` | Whether the switch is disabled | +| `size` | `string` | No | `'medium'` | Size of the switch (`'small'`, `'medium'`, `'large'`) | + +--- + ## Basic Usage ```jsx import { useState } from 'react'; -+import { Moon, Sun } from 'lucide-react'; +import { Moon, Sun } from 'lucide-react'; import GlassSwitch from '@/components/GlassSwitch'; const Example = () => { @@ -37,3 +50,27 @@ const Example = () => { /> ); }; +``` + +--- + +## With Optional Props + +```jsx +import { useState } from 'react'; +import GlassSwitch from '@/components/GlassSwitch'; + +const ExampleWithOptions = () => { + const [isOn, setIsOn] = useState(false); + + return ( + setIsOn(prev => !prev)} + ariaLabel="Large switch" + size="large" + disabled={false} + /> + ); +}; +``` From db1b19106ccc89606aff453f5f077e7c65c0a5a2 Mon Sep 17 00:00:00 2001 From: codevory Date: Fri, 2 Jan 2026 11:29:10 +0530 Subject: [PATCH 16/30] Added documentation for GlassSwitch component including it's tests --- .../components/GlassSwitch/_category_.json | 8 + .../react/components/GlassSwitch/index.md | 387 ++++++++++++++++++ .../react/components/GlassSwitch/tests.md | 171 ++++++++ 3 files changed, 566 insertions(+) create mode 100644 docs/docs/reference/react/components/GlassSwitch/_category_.json create mode 100644 docs/docs/reference/react/components/GlassSwitch/index.md create mode 100644 docs/docs/reference/react/components/GlassSwitch/tests.md diff --git a/docs/docs/reference/react/components/GlassSwitch/_category_.json b/docs/docs/reference/react/components/GlassSwitch/_category_.json new file mode 100644 index 000000000..037e4a6c0 --- /dev/null +++ b/docs/docs/reference/react/components/GlassSwitch/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "GlassSwitch", + "position": 4, + "link": { + "type": "generated-index", + "description": "Documentation for the GlassSwitch component" + } +} diff --git a/docs/docs/reference/react/components/GlassSwitch/index.md b/docs/docs/reference/react/components/GlassSwitch/index.md new file mode 100644 index 000000000..11da39da8 --- /dev/null +++ b/docs/docs/reference/react/components/GlassSwitch/index.md @@ -0,0 +1,387 @@ +--- +title: GlassSwitch +description: A customizable glass-morphism toggle switch component with accessibility support +--- + +## Overview + +`GlassSwitch` is a reusable, accessible toggle switch component with a modern glass-morphism design. It provides smooth animations, flexible customization options, and full keyboard navigation support. + +### Key Features + +- ✨ **Glass-morphism design** - Beautiful translucent styling +- 🎨 **Customizable thumb content** - Use icons, text, or any React element +- ♿ **Fully accessible** - WCAG compliant with proper ARIA attributes +- ⌨️ **Keyboard navigation** - Tab to focus, Enter/Space to toggle +- 🎭 **Smooth animations** - 0.3s ease transitions +- 🛡️ **Type-safe** - PropTypes validation included + +:::tip Best Use Cases +Perfect for: +- **Theme toggles** - Light/dark mode switching +- **Feature flags** - Enable/disable features +- **Settings switches** - User preferences and configurations +- **Boolean states** - Any on/off toggle requirement + +See the [Examples](#examples) section for practical implementations. +::: + +## Installation & Dependencies + +This component is part of the Img2Num component library. It depends on: + +| Dependency | Purpose | +|------------|---------| +| `@components/Tooltip` | Displays helpful tooltips on hover | +| `prop-types` | Runtime prop type validation | +| `lucide-react` | (Optional) For icon support in thumb content | + +## Quick Start + +### Basic Usage + +The simplest implementation requires just three props: `isOn`, `onChange`, and `ariaLabel`. + +```jsx +import GlassSwitch from '@components/GlassSwitch'; +import { useState } from 'react'; + +export default function Settings() { + const [isEnabled, setIsEnabled] = useState(false); + + return ( +
+ +
+ ); +} +``` + +### With Custom Icons + +Add visual indicators using custom thumb content (icons, emojis, or any React element). + +```jsx +import GlassSwitch from '@components/GlassSwitch'; +import { Bell, BellOff } from 'lucide-react'; +import { useState } from 'react'; + +export default function NotificationToggle() { + const [notificationsOn, setNotificationsOn] = useState(false); + + return ( + setNotificationsOn(!notificationsOn)} + ariaLabel="Toggle notifications" + thumbContent={notificationsOn ? : } + /> + ); +} +``` + +## API Reference + +### Props + +| Prop | Type | Required | Default | Description | +| -------------- | ------------- | -------- | ---------- | ---------------------------------------------------- | +| `isOn` | `boolean` | ✅ Yes | - | Controls the switch state (true = on, false = off) | +| `onChange` | `function` | ✅ Yes | - | Callback fired when the switch is toggled | +| `ariaLabel` | `string` | ✅ Yes | - | Accessible label for screen readers and tooltips | +| `thumbContent` | `React.node` | No | fallback | Custom content inside the thumb (icons, text, etc.) | +| `size` | `string` | No | `'medium'` | Size variant (future feature, currently not active) | +| `disabled` | `boolean` | No | `false` | Disables the switch and prevents interaction | + +#### Prop Usage Guide + +**`isOn` (required)** - Boolean state controller +```jsx +const [isOn, setIsOn] = useState(false); +``` + +**`onChange` (required)** - Toggle handler +```jsx +onChange={() => setIsOn(!isOn)} +// or with custom logic +onChange={handleToggle} +``` + +**`ariaLabel` (required)** - Accessibility label (also shown in tooltip) +```jsx +ariaLabel="Toggle dark mode" +ariaLabel="Enable push notifications" +``` + +**`thumbContent` (optional)** - Custom thumb visuals +```jsx +// Icons +thumbContent={} + +// Emojis +thumbContent="🌙" + +// Conditional +thumbContent={isDark ? : } + +// Omit for CSS-based fallback (on/off colored thumb) +``` + +**`disabled` (optional)** - Prevent interaction +```jsx +disabled={!isPremiumUser} +disabled={isLoading} +``` + +### CSS Classes & Styling + +The component uses CSS modules for scoped styling: + +| Class | Applied To | Purpose | +| ------------------------ | ----------------- | ------------------------------------------------- | +| `switch` | Button element | Main switch container and glass effect | +| `thumb` | Thumb span | Sliding thumb element | +| `checked` | Button (when on) | Added when `isOn={true}` to trigger animation | +| `fallbackThumbContentOn` | Thumb (default) | On-state style (green tone) when no thumbContent | +| `fallbackThumbContentOff`| Thumb (default) | Off-state style (gray tone) when no thumbContent | + +**Global Classes:** +- `.glass` - Provides glass-morphism effects (backdrop blur, transparency) + +**CSS Custom Properties:** +- `--size: 32px` - Controls switch dimensions (width = 2× size) + +:::info Customization +To override styles, target these classes in your own CSS or use inline styles on the wrapper element. +::: + +## Accessibility + +GlassSwitch follows WCAG guidelines for accessible toggle switches: + +### Semantic HTML +- ✅ Uses `role="switch"` for proper assistive technology support +- ✅ Renders as ` + : } + ariaLabel={`switch to ${isDark ? 'light' : 'dark'} mode`} + /> ); } ``` ## Visual behavior -| Current Theme | Icon Displayed | Next Theme on Click | -| ------------- | -------------- | ------------------- | -| Light | 🌙 Moon | Dark | -| Dark | ☀️ Sun | Light | +| Current Theme | Icon Displayed | aria-label | Next Theme on Toggle | +| ------------- | -------------- | ---------------------------------- | ------------------- | +| Light | 🌙 Moon | "switch to dark mode" | Dark | +| Dark | ☀️ Sun | "switch to light mode" | Light | ## Testing diff --git a/docs/docs/reference/react/components/ThemeSwitch/tests.md b/docs/docs/reference/react/components/ThemeSwitch/tests.md index 4b022ee59..2912ff297 100644 --- a/docs/docs/reference/react/components/ThemeSwitch/tests.md +++ b/docs/docs/reference/react/components/ThemeSwitch/tests.md @@ -3,7 +3,7 @@ title: ThemeSwitch Tests --- -The ThemeSwitch component has 7 updated tests covering rendering, icon display, theme toggling, hook integration, and edge cases. +The ThemeSwitch component has 7 tests covering rendering, icon display, theme toggling, hook integration, and edge cases. ## Test file location @@ -26,25 +26,24 @@ npm test -- --watch ThemeSwitch.test.jsx ## Test organization -### 1. Rendering (2 tests) +### 1. Rendering & accessibility (2 tests) -* Renders a button with the correct aria-label based on the theme -* Button has type="button" attribute and CSS class `themeButton` +* Renders a switch with the correct aria-label based on the theme (light → "switch to dark mode", dark → "switch to light mode") +* Uses `type="button"` and `role="switch"` with proper `aria-checked` ### 2. Icon display based on theme (2 tests) * Shows Moon icon for light theme * Shows Sun icon for dark theme -* Applies `icon` CSS class correctly ### 3. Theme toggling functionality (2 tests) -* Calls `toggleTheme` when button is clicked +* Calls `toggleTheme` when clicked * Is keyboard accessible (focusable) ### 4. Integration with useTheme hook (1 test) -* Uses `toggleTheme` function from hook +* Uses the `toggleTheme` function provided by the hook ### 5. Edge cases (1 test) @@ -53,21 +52,18 @@ npm test -- --watch ThemeSwitch.test.jsx ## Mocking strategy * **useTheme hook**: mocked with `vi.spyOn` to control theme and toggle function -* **CSS modules**: mocked to check `className` usage -* **Lucide icons**: mocked with test spans for Moon/Sun icons -* **Tooltip**: mocked as a simple wrapper component +* **Lucide icons**: mocked with test spans for Moon/Sun icons (for easy querying) ## Example test snippets -### Rendering button with light theme +### Rendering switch with light theme ```javascript vi.spyOn(useThemeModule, 'useTheme').mockReturnValue({ theme: 'light', toggleTheme: mockToggleTheme }); render(); -const button = screen.getByRole('button', { name: 'Switch to Dark Mode' }); +const button = screen.getByRole('switch', { name: 'switch to dark mode' }); expect(button).toBeInTheDocument(); expect(button).toHaveAttribute('type', 'button'); -expect(button).toHaveClass('mocked-theme-button-class'); ``` ### Testing icon display @@ -75,14 +71,13 @@ expect(button).toHaveClass('mocked-theme-button-class'); ```javascript const moonIcon = screen.getByTestId('moon-icon'); expect(moonIcon).toBeInTheDocument(); -expect(moonIcon).toHaveClass('mocked-icon-class'); expect(screen.queryByTestId('sun-icon')).not.toBeInTheDocument(); ``` ### Testing toggleTheme ```javascript -const button = screen.getByRole('button', { name: 'Switch to Dark Mode' }); +const button = screen.getByRole('switch', { name: 'switch to dark mode' }); fireEvent.click(button); expect(mockToggleTheme).toHaveBeenCalledTimes(1); ``` From 77d21c45ae0da62419e5d25974bf6f38ca8405ee Mon Sep 17 00:00:00 2001 From: codevory Date: Fri, 2 Jan 2026 11:34:35 +0530 Subject: [PATCH 18/30] Added a fallback Content when no thumbContent is given to component --- src/components/GlassSwitch.jsx | 8 +++++--- src/components/GlassSwitch.module.css | 11 +++++++++-- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/components/GlassSwitch.jsx b/src/components/GlassSwitch.jsx index 0cda5de71..a95ab413f 100644 --- a/src/components/GlassSwitch.jsx +++ b/src/components/GlassSwitch.jsx @@ -2,8 +2,8 @@ import styles from './GlassSwitch.module.css'; import Tooltip from '@components/Tooltip'; import PropTypes from 'prop-types'; -const GlassSwitch = ({ onChange, isOn, ariaLabel, thumbContent , size='medium', disabled=false}) => { - +const GlassSwitch = ({ onChange, isOn, ariaLabel, thumbContent, size = 'medium', disabled = false }) => { + const fallbackContent = isOn ? styles.fallbackThumbContentOn : styles.fallbackThumbContentOff ; return ( ); diff --git a/src/components/GlassSwitch.module.css b/src/components/GlassSwitch.module.css index 9430dc0e7..78efe9913 100644 --- a/src/components/GlassSwitch.module.css +++ b/src/components/GlassSwitch.module.css @@ -1,5 +1,5 @@ .switch { - --size:32px; + --size:30px; display: flex; align-items: center; width: calc(var(--size) * 2); @@ -14,7 +14,7 @@ .thumb { position: absolute; left:0; - bottom: 0; + bottom: 1; width: calc(var(--size) - 4px); height: calc(var(--size) - 4px); border-radius: 50%; @@ -26,3 +26,10 @@ .checked .thumb { transform: translateX(var(--size)); } +.fallbackThumbContentOff{ + background-color: rgb(110, 110, 110); +} + +.fallbackThumbContentOn{ +background-color: rgb(26, 219, 32); +} \ No newline at end of file From f282c8f1e422a0a7bd2e92c8878a0c0b6c475202 Mon Sep 17 00:00:00 2001 From: codevory Date: Fri, 2 Jan 2026 11:36:15 +0530 Subject: [PATCH 19/30] Improved the tests for GlassSwitch component to address changes --- src/components/GlassSwitch.test.jsx | 72 +++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/src/components/GlassSwitch.test.jsx b/src/components/GlassSwitch.test.jsx index 2a63a880f..bb43c6375 100644 --- a/src/components/GlassSwitch.test.jsx +++ b/src/components/GlassSwitch.test.jsx @@ -3,6 +3,23 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import GlassSwitch from './GlassSwitch'; +// Mock the Tooltip component +vi.mock('@components/Tooltip', () => ({ + __esModule: true, + default: ({ children }) =>
{children}
, +})); + +// Mock the CSS module +vi.mock('./GlassSwitch.module.css', () => ({ + default: { + switch: 'mocked-switch-class', + thumb: 'mocked-thumb-class', + checked: 'mocked-checked-class', + fallbackThumbContentOn: 'mocked-fallback-on', + fallbackThumbContentOff: 'mocked-fallback-off', + }, +})); + describe('GlassSwitch', () => { it('renders a switch button', () => { render( {}} ariaLabel="Toggle" />); @@ -37,8 +54,63 @@ describe('GlassSwitch', () => { const button = screen.getByRole('switch'); button.focus(); + expect(button).toHaveFocus(); + await user.keyboard('{Enter}'); expect(onChange).toHaveBeenCalled(); }); + + it('applies correct CSS classes when checked', () => { + render( {}} ariaLabel="Toggle" />); + const switchButton = screen.getByRole('switch'); + + expect(switchButton).toHaveClass('mocked-switch-class'); + expect(switchButton).toHaveClass('mocked-checked-class'); + }); + + it('does not apply checked class when unchecked', () => { + render( {}} ariaLabel="Toggle" />); + const switchButton = screen.getByRole('switch'); + + expect(switchButton).toHaveClass('mocked-switch-class'); + expect(switchButton).not.toHaveClass('mocked-checked-class'); + }); + + it('renders with thumbContent when provided', () => { + const thumbContent = Custom; + render( {}} ariaLabel="Toggle" thumbContent={thumbContent} />); + + expect(screen.getByTestId('custom-thumb')).toBeInTheDocument(); + }); + + it('uses fallback off styling when no thumbContent and isOff', () => { + render( {}} ariaLabel="Toggle" />); + const thumb = screen.getByRole('switch').querySelector('span'); + expect(thumb).toHaveClass('mocked-thumb-class'); + expect(thumb).toHaveClass('mocked-fallback-off'); + expect(thumb?.textContent).toBe(''); + }); + + it('uses fallback on styling when no thumbContent and isOn', () => { + render( {}} ariaLabel="Toggle" />); + const thumb = screen.getByRole('switch').querySelector('span'); + expect(thumb).toHaveClass('mocked-thumb-class'); + expect(thumb).toHaveClass('mocked-fallback-on'); + expect(thumb?.textContent).toBe(''); + }); + + it('can be disabled', () => { + const onChange = vi.fn(); + render(); + const switchButton = screen.getByRole('switch'); + + expect(switchButton).toBeDisabled(); + }); + + it('sets correct aria-label', () => { + render( {}} ariaLabel="My Custom Label" />); + + expect(screen.getByLabelText('My Custom Label')).toBeInTheDocument(); + }); }); From 8b40a924009d0645a82c2f00474e304b9ee0f046 Mon Sep 17 00:00:00 2001 From: codevory Date: Fri, 2 Jan 2026 11:37:12 +0530 Subject: [PATCH 20/30] Added useful comments back --- src/components/ThemeSwitch.jsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/components/ThemeSwitch.jsx b/src/components/ThemeSwitch.jsx index 955e3fe82..d4a54d727 100644 --- a/src/components/ThemeSwitch.jsx +++ b/src/components/ThemeSwitch.jsx @@ -2,7 +2,17 @@ import { Moon, Sun } from 'lucide-react'; import GlassSwitch from './GlassSwitch'; import { useTheme } from '@hooks/useTheme'; + /* ThemeSwitch component + + A toggle button that allows users to switch between light and dark themes. + The button displays a moon icon in light mode and a sun icon in dark mode. + Theme preference is persisted in localStorage via the useTheme hook. + + @returns {JSX.Element} A button that toggles between light and dark themes + */ + export default function ThemeSwitch() { + // Get current theme and toggle function from the theme hook const { theme, toggleTheme } = useTheme(); const isDark = theme === 'dark'; @@ -10,7 +20,7 @@ export default function ThemeSwitch() { : } + thumbContent={isDark ? : } ariaLabel={`switch to ${isDark ? 'light' : 'dark'} mode`} /> ); From ca45667b570f7b0980dd2745f0b98afbf7b751a1 Mon Sep 17 00:00:00 2001 From: codevory Date: Fri, 2 Jan 2026 11:38:16 +0530 Subject: [PATCH 21/30] Improved tests for ThemeSwitch component & Adresed the required changes as per it's new usage --- src/components/ThemeSwitch.test.jsx | 30 +++++++++-------------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/src/components/ThemeSwitch.test.jsx b/src/components/ThemeSwitch.test.jsx index 3f392d4dd..fdd9c785e 100644 --- a/src/components/ThemeSwitch.test.jsx +++ b/src/components/ThemeSwitch.test.jsx @@ -1,20 +1,12 @@ import React from 'react'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { render, screen, fireEvent } from '@testing-library/react'; -import ThemeSwitch from './ThemeSwitch'; // adjust path if needed +import ThemeSwitch from './ThemeSwitch'; import * as useThemeModule from '@hooks/useTheme'; // Mock the useTheme hook module vi.mock('@hooks/useTheme'); -// Mock CSS module so `className` checks work -vi.mock('./ThemeSwitch.module.css', () => ({ - default: { - themeButton: 'mocked-theme-button-class', - icon: 'mocked-icon-class', - }, -})); - // Mock lucide icons (Sun / Moon) vi.mock('lucide-react', () => ({ Moon: ({ className }) => Moon Icon, @@ -38,40 +30,36 @@ describe('ThemeSwitch component', () => { vi.clearAllMocks(); }); - it('renders a button with the correct aria-label when theme is light', () => { + it('renders a switch with the correct aria-label when theme is light', () => { vi.spyOn(useThemeModule, 'useTheme').mockReturnValue({ theme: 'light', toggleTheme: mockToggleTheme }); render(); - const button = screen.getByRole('button', { name: 'Switch to Dark Mode' }); + const button = screen.getByRole('switch', { name: 'switch to dark mode' }); expect(button).toBeInTheDocument(); expect(button).toHaveAttribute('type', 'button'); - expect(button).toHaveClass('mocked-theme-button-class'); }); - it('shows Moon icon for light theme and applies icon class', () => { + it('shows Moon icon for light theme', () => { vi.spyOn(useThemeModule, 'useTheme').mockReturnValue({ theme: 'light', toggleTheme: mockToggleTheme }); render(); const moonIcon = screen.getByTestId('moon-icon'); expect(moonIcon).toBeInTheDocument(); - expect(moonIcon).toHaveClass('mocked-icon-class'); expect(screen.queryByTestId('sun-icon')).not.toBeInTheDocument(); }); - it('shows Sun icon for dark theme and applies icon class', () => { + it('shows Sun icon for dark theme and updates aria-label accordingly', () => { vi.spyOn(useThemeModule, 'useTheme').mockReturnValue({ theme: 'dark', toggleTheme: mockToggleTheme }); render(); const sunIcon = screen.getByTestId('sun-icon'); expect(sunIcon).toBeInTheDocument(); - expect(sunIcon).toHaveClass('mocked-icon-class'); expect(screen.queryByTestId('moon-icon')).not.toBeInTheDocument(); - // aria-label should match dark case - const button = screen.getByRole('button', { name: 'Switch to Light Mode' }); + const button = screen.getByRole('switch', { name: 'switch to light mode' }); expect(button).toBeInTheDocument(); }); @@ -80,7 +68,7 @@ describe('ThemeSwitch component', () => { render(); - const button = screen.getByRole('button', { name: 'Switch to Dark Mode' }); + const button = screen.getByRole('switch', { name: 'switch to dark mode' }); fireEvent.click(button); expect(mockToggleTheme).toHaveBeenCalledTimes(1); @@ -91,7 +79,7 @@ describe('ThemeSwitch component', () => { render(); - const button = screen.getByRole('button', { name: 'Switch to Dark Mode' }); + const button = screen.getByRole('switch', { name: 'switch to dark mode' }); button.focus(); expect(button).toHaveFocus(); }); @@ -110,7 +98,7 @@ describe('ThemeSwitch component', () => { render(); - const btn = screen.getByRole('button', { name: 'Switch to Dark Mode' }); + const btn = screen.getByRole('switch', { name: 'switch to dark mode' }); fireEvent.click(btn); expect(customToggle).toHaveBeenCalledTimes(1); }); From bc713b294de5175038ebed379977ea62394bd040 Mon Sep 17 00:00:00 2001 From: codevory Date: Fri, 2 Jan 2026 11:54:39 +0530 Subject: [PATCH 22/30] modified documentation to ease the usage --- docs/docs/reference/react/components/ThemeSwitch/index.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/docs/reference/react/components/ThemeSwitch/index.md b/docs/docs/reference/react/components/ThemeSwitch/index.md index 817d1831f..c8b170f72 100644 --- a/docs/docs/reference/react/components/ThemeSwitch/index.md +++ b/docs/docs/reference/react/components/ThemeSwitch/index.md @@ -54,7 +54,7 @@ ThemeSwitch uses `GlassSwitch`, which implements accessible switch semantics: | ARIA label | `aria-label="switch to dark mode"` or `"switch to light mode"` | | Button type | `type="button"` to avoid form submission | | Keyboard support | Tab to focus; Enter/Space toggles | -| Focus indicator | Inherits focus ring from `GlassSwitch` styles | +| Focus indicator | Visible focus ring via `:focus-visible` with 2px outline and box-shadow (WCAG 2.4.7) | | Icon semantics | Icons are decorative; meaning conveyed through the aria-label | ## Examples @@ -120,8 +120,8 @@ export default function ThemeControl() { The component: 1. Calls `useTheme()` to get the current theme and toggle function -2. Renders a button with an onClick handler that calls `toggleTheme()` -3. Conditionally displays a Sun icon (dark mode) or Moon icon (light mode) +2. Renders a ` diff --git a/src/components/GlassSwitch.module.css b/src/components/GlassSwitch.module.css index 93698ab35..721444404 100644 --- a/src/components/GlassSwitch.module.css +++ b/src/components/GlassSwitch.module.css @@ -15,7 +15,7 @@ .switch:focus-visible { outline: 2px solid var(--color-primary, #4f46e5); outline-offset: 2px; - box-shadow: 0 0 0 4px var(--color-focus-ring, rgba(79, 70, 229, 0.1)); + box-shadow: 0 0 0 4px rgba(79, 70, 229, 0.1); } /* Fallback for browsers that don't support :focus-visible */ @@ -50,5 +50,5 @@ } .fallbackThumbContentOn{ -background-color: rgb(26, 219, 32); -} \ No newline at end of file + background-color: var(--color-success); +} From eaefe457aa81097f1172a1622bad8ec0dfd17a97 Mon Sep 17 00:00:00 2001 From: codevory Date: Sat, 3 Jan 2026 10:42:13 +0530 Subject: [PATCH 26/30] removed size props as is not being used --- docs/docs/components/glass-switch.mdx | 76 ------------------- .../react/components/GlassSwitch/index.md | 3 +- src/components/GlassSwitch.jsx | 3 +- src/components/ThemeSwitch.module.css | 22 ------ 4 files changed, 2 insertions(+), 102 deletions(-) delete mode 100644 docs/docs/components/glass-switch.mdx delete mode 100644 src/components/ThemeSwitch.module.css diff --git a/docs/docs/components/glass-switch.mdx b/docs/docs/components/glass-switch.mdx deleted file mode 100644 index a9d760c1d..000000000 --- a/docs/docs/components/glass-switch.mdx +++ /dev/null @@ -1,76 +0,0 @@ ---- -title: GlassSwitch -description: A reusable, accessible glass-style toggle switch component. ---- - -## GlassSwitch - -`GlassSwitch` is a reusable, accessible toggle switch component built using the global `glass` styles. - -It is a **controlled component**, meaning the parent component is responsible for managing its state. -`GlassSwitch` focuses purely on presentation and interaction and does not contain any business or theme logic. - -This makes it suitable for use cases such as: -- Theme toggles -- Feature flags -- Settings switches -- Any boolean on/off state - ---- - -## Props - -| Prop | Type | Required | Default | Description | -|------|------|----------|---------|-------------| -| `isOn` | `boolean` | Yes | — | Whether the switch is toggled on | -| `onChange` | `function` | Yes | — | Callback function fired when the switch is clicked | -| `ariaLabel` | `string` | Yes | — | Accessibility label for the switch | -| `thumbContent` | `ReactNode` | No | `undefined` | Content to display inside the switch thumb | -| `disabled` | `boolean` | No | `false` | Whether the switch is disabled | -| `size` | `string` | No | `'medium'` | Size of the switch (`'small'`, `'medium'`, `'large'`) | - ---- - -## Basic Usage - -```jsx -import { useState } from 'react'; -import { Moon, Sun } from 'lucide-react'; -import GlassSwitch from '@/components/GlassSwitch'; - -const Example = () => { - const [isOn, setIsOn] = useState(false); - - return ( - setIsOn(prev => !prev)} - thumbContent={isOn ? : } - ariaLabel="Toggle feature" - /> - ); -}; -``` - ---- - -## With Optional Props - -```jsx -import { useState } from 'react'; -import GlassSwitch from '@/components/GlassSwitch'; - -const ExampleWithOptions = () => { - const [isOn, setIsOn] = useState(false); - - return ( - setIsOn(prev => !prev)} - ariaLabel="Large switch" - size="large" - disabled={false} - /> - ); -}; -``` diff --git a/docs/docs/reference/react/components/GlassSwitch/index.md b/docs/docs/reference/react/components/GlassSwitch/index.md index 11da39da8..133a31ddd 100644 --- a/docs/docs/reference/react/components/GlassSwitch/index.md +++ b/docs/docs/reference/react/components/GlassSwitch/index.md @@ -96,8 +96,7 @@ export default function NotificationToggle() { | `isOn` | `boolean` | ✅ Yes | - | Controls the switch state (true = on, false = off) | | `onChange` | `function` | ✅ Yes | - | Callback fired when the switch is toggled | | `ariaLabel` | `string` | ✅ Yes | - | Accessible label for screen readers and tooltips | -| `thumbContent` | `React.node` | No | fallback | Custom content inside the thumb (icons, text, etc.) | -| `size` | `string` | No | `'medium'` | Size variant (future feature, currently not active) | +| `thumbContent` | `React.node` | No | fallback | Custom content inside the thumb (icons, text, etc.). | `disabled` | `boolean` | No | `false` | Disables the switch and prevents interaction | #### Prop Usage Guide diff --git a/src/components/GlassSwitch.jsx b/src/components/GlassSwitch.jsx index a95ab413f..23d0e6d27 100644 --- a/src/components/GlassSwitch.jsx +++ b/src/components/GlassSwitch.jsx @@ -2,7 +2,7 @@ import styles from './GlassSwitch.module.css'; import Tooltip from '@components/Tooltip'; import PropTypes from 'prop-types'; -const GlassSwitch = ({ onChange, isOn, ariaLabel, thumbContent, size = 'medium', disabled = false }) => { +const GlassSwitch = ({ onChange, isOn, ariaLabel, thumbContent, disabled = false }) => { const fallbackContent = isOn ? styles.fallbackThumbContentOn : styles.fallbackThumbContentOff ; return ( @@ -29,7 +29,6 @@ GlassSwitch.propTypes = { ariaLabel: PropTypes.string.isRequired, thumbContent: PropTypes.node, disabled:PropTypes.bool, - size:PropTypes.string, }; export default GlassSwitch; diff --git a/src/components/ThemeSwitch.module.css b/src/components/ThemeSwitch.module.css deleted file mode 100644 index 9bfaf0511..000000000 --- a/src/components/ThemeSwitch.module.css +++ /dev/null @@ -1,22 +0,0 @@ -.themeButton { - border-radius: 9999px; - padding: 0.5rem 0.75rem; - transition: all 0.2s ease-in-out; - border: none; - margin: 0px 0.875rem; - cursor: pointer; -} - -.themeButton:hover { - .icon { - color: var(--color-secondary); - } - transform: scale(1.05); -} - -.icon { - height: 1.25rem; - width: 1.25rem; - color: var(--color-primary); - transition: color 0.2s ease-in-out; -} \ No newline at end of file From 856c8c30d544d58f615bb1cdb4845b74a3eb50e4 Mon Sep 17 00:00:00 2001 From: codevory Date: Sat, 3 Jan 2026 11:07:26 +0530 Subject: [PATCH 27/30] fixed table formating issue --- docs/docs/reference/react/components/GlassSwitch/index.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/docs/reference/react/components/GlassSwitch/index.md b/docs/docs/reference/react/components/GlassSwitch/index.md index 133a31ddd..0c29057f7 100644 --- a/docs/docs/reference/react/components/GlassSwitch/index.md +++ b/docs/docs/reference/react/components/GlassSwitch/index.md @@ -96,7 +96,7 @@ export default function NotificationToggle() { | `isOn` | `boolean` | ✅ Yes | - | Controls the switch state (true = on, false = off) | | `onChange` | `function` | ✅ Yes | - | Callback fired when the switch is toggled | | `ariaLabel` | `string` | ✅ Yes | - | Accessible label for screen readers and tooltips | -| `thumbContent` | `React.node` | No | fallback | Custom content inside the thumb (icons, text, etc.). +| `thumbContent` | `React.node` | No | fallback | Custom content inside the thumb (icons, text, etc.) | | `disabled` | `boolean` | No | `false` | Disables the switch and prevents interaction | #### Prop Usage Guide @@ -280,6 +280,7 @@ export default function SettingsPanel() { ```jsx import GlassSwitch from '@components/GlassSwitch'; import { Crown } from 'lucide-react'; +import { useState } from 'react'; export default function PremiumFeature({ isPremium }) { const [enabled, setEnabled] = useState(false); From 07dbafb9e48ce972b0b632d8461dec9049e8fccb Mon Sep 17 00:00:00 2001 From: codevory Date: Sat, 3 Jan 2026 11:08:41 +0530 Subject: [PATCH 28/30] Fixed react node issue in classname --- src/components/GlassSwitch.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/GlassSwitch.jsx b/src/components/GlassSwitch.jsx index cdc8bd509..512a12dfa 100644 --- a/src/components/GlassSwitch.jsx +++ b/src/components/GlassSwitch.jsx @@ -3,7 +3,7 @@ import Tooltip from '@components/Tooltip'; import PropTypes from 'prop-types'; const GlassSwitch = ({ onChange, isOn, ariaLabel, thumbContent, disabled = false }) => { - const fallbackContent = isOn ? styles.fallbackThumbContentOn : styles.fallbackThumbContentOff ; + const fallbackContent = isOn ? styles.fallbackThumbContentOn : styles.fallbackThumbContentOff; return ( @@ -28,7 +28,7 @@ GlassSwitch.propTypes = { onChange: PropTypes.func.isRequired, ariaLabel: PropTypes.string.isRequired, thumbContent: PropTypes.node, - disabled:PropTypes.bool, + disabled: PropTypes.bool, }; export default GlassSwitch; From fb0b19f762763d4101c4e79feed6bd50f093ef69 Mon Sep 17 00:00:00 2001 From: Ryan-Millard Date: Sun, 4 Jan 2026 19:08:19 +0200 Subject: [PATCH 29/30] style(react: GlassSwitch, ThemeSwitch): format GlassSwitch and ThemeSwitch components, tests, and docs --- .../react/components/GlassSwitch/index.md | 69 +++++++++++-------- .../react/components/GlassSwitch/tests.md | 48 ++++++------- .../react/components/ThemeSwitch/index.md | 22 +++--- .../react/components/ThemeSwitch/tests.md | 19 +++-- src/components/GlassSwitch.jsx | 7 +- src/components/GlassSwitch.module.css | 10 +-- src/components/GlassSwitch.test.jsx | 12 ++-- src/components/ThemeSwitch.jsx | 22 +++--- src/components/ThemeSwitch.test.jsx | 2 +- 9 files changed, 109 insertions(+), 102 deletions(-) diff --git a/docs/docs/reference/react/components/GlassSwitch/index.md b/docs/docs/reference/react/components/GlassSwitch/index.md index 0c29057f7..c92a42281 100644 --- a/docs/docs/reference/react/components/GlassSwitch/index.md +++ b/docs/docs/reference/react/components/GlassSwitch/index.md @@ -18,6 +18,7 @@ description: A customizable glass-morphism toggle switch component with accessib :::tip Best Use Cases Perfect for: + - **Theme toggles** - Light/dark mode switching - **Feature flags** - Enable/disable features - **Settings switches** - User preferences and configurations @@ -30,11 +31,11 @@ See the [Examples](#examples) section for practical implementations. This component is part of the Img2Num component library. It depends on: -| Dependency | Purpose | -|------------|---------| -| `@components/Tooltip` | Displays helpful tooltips on hover | -| `prop-types` | Runtime prop type validation | -| `lucide-react` | (Optional) For icon support in thumb content | +| Dependency | Purpose | +| --------------------- | -------------------------------------------- | +| `@components/Tooltip` | Displays helpful tooltips on hover | +| `prop-types` | Runtime prop type validation | +| `lucide-react` | (Optional) For icon support in thumb content | ## Quick Start @@ -53,11 +54,7 @@ export default function Settings() {
); @@ -91,22 +88,24 @@ export default function NotificationToggle() { ### Props -| Prop | Type | Required | Default | Description | -| -------------- | ------------- | -------- | ---------- | ---------------------------------------------------- | -| `isOn` | `boolean` | ✅ Yes | - | Controls the switch state (true = on, false = off) | -| `onChange` | `function` | ✅ Yes | - | Callback fired when the switch is toggled | -| `ariaLabel` | `string` | ✅ Yes | - | Accessible label for screen readers and tooltips | -| `thumbContent` | `React.node` | No | fallback | Custom content inside the thumb (icons, text, etc.) | -| `disabled` | `boolean` | No | `false` | Disables the switch and prevents interaction | +| Prop | Type | Required | Default | Description | +| -------------- | ------------ | -------- | -------- | --------------------------------------------------- | +| `isOn` | `boolean` | ✅ Yes | - | Controls the switch state (true = on, false = off) | +| `onChange` | `function` | ✅ Yes | - | Callback fired when the switch is toggled | +| `ariaLabel` | `string` | ✅ Yes | - | Accessible label for screen readers and tooltips | +| `thumbContent` | `React.node` | No | fallback | Custom content inside the thumb (icons, text, etc.) | +| `disabled` | `boolean` | No | `false` | Disables the switch and prevents interaction | #### Prop Usage Guide **`isOn` (required)** - Boolean state controller + ```jsx const [isOn, setIsOn] = useState(false); ``` **`onChange` (required)** - Toggle handler + ```jsx onChange={() => setIsOn(!isOn)} // or with custom logic @@ -114,12 +113,14 @@ onChange={handleToggle} ``` **`ariaLabel` (required)** - Accessibility label (also shown in tooltip) + ```jsx -ariaLabel="Toggle dark mode" -ariaLabel="Enable push notifications" +ariaLabel = 'Toggle dark mode'; +ariaLabel = 'Enable push notifications'; ``` **`thumbContent` (optional)** - Custom thumb visuals + ```jsx // Icons thumbContent={} @@ -134,6 +135,7 @@ thumbContent={isDark ? : } ``` **`disabled` (optional)** - Prevent interaction + ```jsx disabled={!isPremiumUser} disabled={isLoading} @@ -143,18 +145,20 @@ disabled={isLoading} The component uses CSS modules for scoped styling: -| Class | Applied To | Purpose | -| ------------------------ | ----------------- | ------------------------------------------------- | -| `switch` | Button element | Main switch container and glass effect | -| `thumb` | Thumb span | Sliding thumb element | -| `checked` | Button (when on) | Added when `isOn={true}` to trigger animation | -| `fallbackThumbContentOn` | Thumb (default) | On-state style (green tone) when no thumbContent | -| `fallbackThumbContentOff`| Thumb (default) | Off-state style (gray tone) when no thumbContent | +| Class | Applied To | Purpose | +| ------------------------- | ---------------- | ------------------------------------------------ | +| `switch` | Button element | Main switch container and glass effect | +| `thumb` | Thumb span | Sliding thumb element | +| `checked` | Button (when on) | Added when `isOn={true}` to trigger animation | +| `fallbackThumbContentOn` | Thumb (default) | On-state style (green tone) when no thumbContent | +| `fallbackThumbContentOff` | Thumb (default) | Off-state style (gray tone) when no thumbContent | **Global Classes:** + - `.glass` - Provides glass-morphism effects (backdrop blur, transparency) **CSS Custom Properties:** + - `--size: 32px` - Controls switch dimensions (width = 2× size) :::info Customization @@ -166,20 +170,24 @@ To override styles, target these classes in your own CSS or use inline styles on GlassSwitch follows WCAG guidelines for accessible toggle switches: ### Semantic HTML + - ✅ Uses `role="switch"` for proper assistive technology support - ✅ Renders as `
); diff --git a/src/components/GlassSwitch.module.css b/src/components/GlassSwitch.module.css index 721444404..91b035763 100644 --- a/src/components/GlassSwitch.module.css +++ b/src/components/GlassSwitch.module.css @@ -1,5 +1,5 @@ .switch { - --size:30px; + --size: 30px; display: flex; align-items: center; width: calc(var(--size) * 2); @@ -8,7 +8,7 @@ border-radius: 9999px; position: relative; cursor: pointer; - margin-right:15px; + margin-right: 15px; } /* WCAG 2.4.7 Compliant Focus Indicator */ @@ -32,7 +32,7 @@ .thumb { position: absolute; - left:0; + left: 0; bottom: 1; width: calc(var(--size) - 4px); height: calc(var(--size) - 4px); @@ -45,10 +45,10 @@ .checked .thumb { transform: translateX(var(--size)); } -.fallbackThumbContentOff{ +.fallbackThumbContentOff { background-color: rgb(110, 110, 110); } -.fallbackThumbContentOn{ +.fallbackThumbContentOn { background-color: var(--color-success); } diff --git a/src/components/GlassSwitch.test.jsx b/src/components/GlassSwitch.test.jsx index bb43c6375..e0cc84ed7 100644 --- a/src/components/GlassSwitch.test.jsx +++ b/src/components/GlassSwitch.test.jsx @@ -55,7 +55,7 @@ describe('GlassSwitch', () => { button.focus(); expect(button).toHaveFocus(); - + await user.keyboard('{Enter}'); expect(onChange).toHaveBeenCalled(); @@ -64,7 +64,7 @@ describe('GlassSwitch', () => { it('applies correct CSS classes when checked', () => { render( {}} ariaLabel="Toggle" />); const switchButton = screen.getByRole('switch'); - + expect(switchButton).toHaveClass('mocked-switch-class'); expect(switchButton).toHaveClass('mocked-checked-class'); }); @@ -72,7 +72,7 @@ describe('GlassSwitch', () => { it('does not apply checked class when unchecked', () => { render( {}} ariaLabel="Toggle" />); const switchButton = screen.getByRole('switch'); - + expect(switchButton).toHaveClass('mocked-switch-class'); expect(switchButton).not.toHaveClass('mocked-checked-class'); }); @@ -80,7 +80,7 @@ describe('GlassSwitch', () => { it('renders with thumbContent when provided', () => { const thumbContent = Custom; render( {}} ariaLabel="Toggle" thumbContent={thumbContent} />); - + expect(screen.getByTestId('custom-thumb')).toBeInTheDocument(); }); @@ -104,13 +104,13 @@ describe('GlassSwitch', () => { const onChange = vi.fn(); render(); const switchButton = screen.getByRole('switch'); - + expect(switchButton).toBeDisabled(); }); it('sets correct aria-label', () => { render( {}} ariaLabel="My Custom Label" />); - + expect(screen.getByLabelText('My Custom Label')).toBeInTheDocument(); }); }); diff --git a/src/components/ThemeSwitch.jsx b/src/components/ThemeSwitch.jsx index 09643ac9b..3c6b9f69f 100644 --- a/src/components/ThemeSwitch.jsx +++ b/src/components/ThemeSwitch.jsx @@ -2,17 +2,17 @@ import { Moon, Sun } from 'lucide-react'; import GlassSwitch from './GlassSwitch'; import { useTheme } from '@hooks/useTheme'; -/** ThemeSwitch component - - A toggle button that allows users to switch between light and dark themes. - The button displays a moon icon in light mode and a sun icon in dark mode. - Theme preference is persisted in localStorage via the useTheme hook. - - @returns {JSX.Element} A button that toggles between light and dark themes -*/ - +/* + * ThemeSwitch + * + * A toggle button that allows users to switch between light and dark themes. + * The button displays a moon icon in light mode and a sun icon in dark mode. + * Theme preference is persisted in localStorage via the useTheme hook. + * + * @returns {JSX.Element} A button that toggles between light and dark themes + */ export default function ThemeSwitch() { -// Get current theme and toggle function from the theme hook + // Get current theme and toggle function from the theme hook const { theme, toggleTheme } = useTheme(); const isDark = theme === 'dark'; @@ -24,4 +24,4 @@ export default function ThemeSwitch() { ariaLabel={`switch to ${isDark ? 'light' : 'dark'} mode`} /> ); -} \ No newline at end of file +} diff --git a/src/components/ThemeSwitch.test.jsx b/src/components/ThemeSwitch.test.jsx index a499b1c39..c7ede0758 100644 --- a/src/components/ThemeSwitch.test.jsx +++ b/src/components/ThemeSwitch.test.jsx @@ -110,4 +110,4 @@ describe('ThemeSwitch component', () => { fireEvent.click(btn); expect(customToggle).toHaveBeenCalledTimes(1); }); -}); \ No newline at end of file +}); From 0de229d3900cc652247b87534c661dca21d75faf Mon Sep 17 00:00:00 2001 From: Ryan-Millard Date: Sun, 4 Jan 2026 20:43:58 +0200 Subject: [PATCH 30/30] docs(react: GlassSwitch): fix wording --- .../react/components/GlassSwitch/tests.md | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/docs/docs/reference/react/components/GlassSwitch/tests.md b/docs/docs/reference/react/components/GlassSwitch/tests.md index 54a378ad2..16b85f213 100644 --- a/docs/docs/reference/react/components/GlassSwitch/tests.md +++ b/docs/docs/reference/react/components/GlassSwitch/tests.md @@ -2,7 +2,7 @@ title: GlassSwitch Tests --- -The GlassSwitch component has a comprehensive test suite with 11 tests covering rendering, state management, accessibility, styling, and component props. +The GlassSwitch component has a comprehensive test suite that covers rendering, state management, accessibility, styling, and component props. ## Test file location @@ -25,31 +25,33 @@ npm test -- --watch GlassSwitch.test.jsx ## Test organization -### 1. Basic rendering (1 test) +### 1. Basic rendering - Renders a switch button with the correct role -### 2. Checked state management (2 tests) +### 2. Checked state management - Sets `aria-checked` to "true" when `isOn={true}` - Sets `aria-checked` to "false" when `isOn={false}` -### 3. User interaction (2 tests) +### 3. User interaction - Calls `onChange` callback when clicked - Is keyboard accessible (focusable and responds to Enter key) -### 4. CSS class application (2 tests) +### 4. CSS class application - Applies correct CSS classes when checked (includes `checked` class) - Does not apply checked class when unchecked -### 5. Thumb content (2 tests) +### 5. Thumb content - Renders custom `thumbContent` when provided -- Renders fallback thumb content when `thumbContent` is not provided +- Renders fallback thumb content + - **Off styling:** when `thumbContent` is not provided and isOff + - **On styling:** when `thumbContent` is not provided and isOn -### 6. Component props (2 tests) +### 6. Component props - Can be disabled via `disabled` prop - Sets correct `aria-label` for accessibility @@ -159,11 +161,11 @@ it('sets correct aria-label', () => { The test suite ensures: -1. ✅ **Accessibility**: ARIA attributes, keyboard navigation, labels -2. ✅ **Functionality**: Click handlers, state management -3. ✅ **Styling**: CSS class application based on props -4. ✅ **Props**: All component props work correctly -5. ✅ **Edge cases**: Fallback content, disabled state +1. **Accessibility**: ARIA attributes, keyboard navigation, labels +2. **Functionality**: Click handlers, state management +3. **Styling**: CSS class application based on props +4. **Props**: All component props work correctly +5. **Edge cases**: Fallback content, disabled state ## Related documentation