395395 color : # 2c2c2c ;
396396 font-weight : 600 ;
397397 }
398+
399+ /* Password Strength Styles */
400+ .password-strength-container {
401+ margin-top : 1rem ;
402+ padding : 0 0.2rem ;
403+ transition : all 0.3s ease;
404+ }
405+
406+ .password-strength-bar {
407+ height : 6px ;
408+ background : rgba (255 , 255 , 255 , 0.1 );
409+ border-radius : 4px ;
410+ overflow : hidden;
411+ margin-bottom : 0.75rem ;
412+ box-shadow : inset 0 1px 2px rgba (0 , 0 , 0 , 0.2 );
413+ }
414+
415+ .password-strength-fill {
416+ height : 100% ;
417+ width : 0% ;
418+ transition : width 0.3s ease, background-color 0.3s ease;
419+ }
420+
421+ .password-criteria {
422+ list-style : none;
423+ padding : 0 ;
424+ margin : 0 ;
425+ display : grid;
426+ gap : 0.5rem ;
427+ }
428+
429+ .password-criteria li {
430+ font-size : 0.85rem ;
431+ color : var (--text-muted );
432+ display : flex;
433+ align-items : center;
434+ gap : 0.5rem ;
435+ transition : color 0.2s ease;
436+ }
437+
438+ .password-criteria li i {
439+ font-size : 0.95rem ;
440+ width : 16px ;
441+ text-align : center;
442+ }
443+
444+ .criterion-met {
445+ color : # 4ade80 !important ;
446+ }
447+ .criterion-met i {
448+ color : # 4ade80 ;
449+ }
450+ .criterion-unmet {
451+ color : # f87171 !important ;
452+ }
453+ .criterion-unmet i {
454+ color : # f87171 ;
455+ }
456+
457+ .strength-weak { background-color : # f87171 ; box-shadow : 0 0 8px rgba (248 , 113 , 113 , 0.4 ); }
458+ .strength-fair { background-color : # fbbf24 ; box-shadow : 0 0 8px rgba (251 , 191 , 36 , 0.4 ); }
459+ .strength-good { background-color : # a3e635 ; box-shadow : 0 0 8px rgba (163 , 230 , 53 , 0.4 ); }
460+ .strength-strong { background-color : # 4ade80 ; box-shadow : 0 0 8px rgba (74 , 222 , 128 , 0.4 ); }
461+
462+ @keyframes inputShake {
463+ 0% , 100% { transform : translateX (0 ); }
464+ 20% , 60% { transform : translateX (-5px ); border-color : # f87171 ; }
465+ 40% , 80% { transform : translateX (5px ); border-color : # f87171 ; }
466+ }
398467 </ style >
399468 < link rel ="icon " href ="../assets/images/biblioDrift_favicon.png " type ="image/png ">
400469 <!-- PWA -->
@@ -487,6 +556,18 @@ <h2 id="authTitle">Welcome Back</h2>
487556 value ="demo123 " required autocomplete ="current-password ">
488557 < i class ="fa-solid fa-eye password-toggle-icon " id ="togglePassword " title ="Show Password "
489558 aria-label ="Show password " role ="button " tabindex ="0 "> </ i >
559+
560+ <!-- Client-side Password Strength Validation UI -->
561+ < div id ="passwordStrengthContainer " style ="display: none; " class ="password-strength-container ">
562+ < div class ="password-strength-bar ">
563+ < div class ="password-strength-fill " id ="passwordStrengthFill "> </ div >
564+ </ div >
565+ < ul class ="password-criteria " id ="passwordCriteria ">
566+ < li id ="criterion-length " class ="criterion-unmet "> < i class ="fa-solid fa-circle-xmark "> </ i > At least 8 characters</ li >
567+ < li id ="criterion-number " class ="criterion-unmet "> < i class ="fa-solid fa-circle-xmark "> </ i > At least 1 number</ li >
568+ < li id ="criterion-special " class ="criterion-unmet "> < i class ="fa-solid fa-circle-xmark "> </ i > At least 1 special character</ li >
569+ </ ul >
570+ </ div >
490571 </ div >
491572
492573 <!--
@@ -704,6 +785,13 @@ <h3 style="color: var(--text-main) !important;">Ambient Sanctuary</h3>
704785 googleAuthBtn . querySelector ( 'span' ) . textContent = 'Sign up with Google' ;
705786 toggleText . textContent = 'Already have an account? Sign in.' ;
706787 passwordInput . autocomplete = 'new-password' ;
788+ passwordInput . value = '' ; // Reset password field on switch to register
789+ if ( document . getElementById ( 'passwordStrengthContainer' ) ) {
790+ document . getElementById ( 'passwordStrengthContainer' ) . style . display = 'block' ;
791+ if ( typeof updatePasswordStrengthMeter === 'function' ) {
792+ updatePasswordStrengthMeter ( ) ;
793+ }
794+ }
707795 } else {
708796 authForm . dataset . mode = 'login' ;
709797 authTitle . textContent = 'Welcome Back' ;
@@ -712,6 +800,9 @@ <h3 style="color: var(--text-main) !important;">Ambient Sanctuary</h3>
712800 googleAuthBtn . querySelector ( 'span' ) . textContent = 'Sign in with Google' ;
713801 toggleText . textContent = 'No account? Create one.' ;
714802 passwordInput . autocomplete = 'current-password' ;
803+ if ( document . getElementById ( 'passwordStrengthContainer' ) ) {
804+ document . getElementById ( 'passwordStrengthContainer' ) . style . display = 'none' ;
805+ }
715806 }
716807 showAuthPanel ( 'signin' ) ;
717808 } ) ;
@@ -746,6 +837,104 @@ <h3 style="color: var(--text-main) !important;">Ambient Sanctuary</h3>
746837 } ) ;
747838 }
748839
840+ // ==============================================================================
841+ // PASSWORD STRENGTH VALIDATION LOGIC
842+ // ==============================================================================
843+ const passwordStrengthContainer = document . getElementById ( 'passwordStrengthContainer' ) ;
844+ const passwordStrengthFill = document . getElementById ( 'passwordStrengthFill' ) ;
845+ const criteriaNodes = {
846+ length : document . getElementById ( 'criterion-length' ) ,
847+ number : document . getElementById ( 'criterion-number' ) ,
848+ special : document . getElementById ( 'criterion-special' )
849+ } ;
850+
851+ function validatePasswordCriteria ( password ) {
852+ const hasLength = password . length >= 8 ;
853+ const hasNumber = / \d / . test ( password ) ;
854+ const hasSpecial = / [ ! @ # $ % ^ & * ( ) , . ? " : { } | < > \- _ \+ = \/ \[ \] \\ ' ; ` ~ ] / . test ( password ) ;
855+
856+ return {
857+ length : hasLength ,
858+ number : hasNumber ,
859+ special : hasSpecial ,
860+ score : ( hasLength ? 1 : 0 ) + ( hasNumber ? 1 : 0 ) + ( hasSpecial ? 1 : 0 )
861+ } ;
862+ }
863+
864+ function updateCriterionNode ( node , isMet ) {
865+ if ( ! node ) return ;
866+ const icon = node . querySelector ( 'i' ) ;
867+ if ( isMet ) {
868+ node . classList . add ( 'criterion-met' ) ;
869+ node . classList . remove ( 'criterion-unmet' ) ;
870+ icon . className = 'fa-solid fa-circle-check' ;
871+ } else {
872+ node . classList . remove ( 'criterion-met' ) ;
873+ node . classList . add ( 'criterion-unmet' ) ;
874+ icon . className = 'fa-solid fa-circle-xmark' ;
875+ }
876+ }
877+
878+ function updatePasswordStrengthMeter ( ) {
879+ if ( ! passwordInput || authForm . dataset . mode !== 'register' ) return ;
880+
881+ const password = passwordInput . value ;
882+ const validation = validatePasswordCriteria ( password ) ;
883+
884+ updateCriterionNode ( criteriaNodes . length , validation . length ) ;
885+ updateCriterionNode ( criteriaNodes . number , validation . number ) ;
886+ updateCriterionNode ( criteriaNodes . special , validation . special ) ;
887+
888+ let fillWidth = '0%' ;
889+ let fillClass = 'strength-weak' ;
890+
891+ if ( password . length > 0 ) {
892+ if ( validation . score === 1 ) {
893+ fillWidth = '33%' ;
894+ fillClass = 'strength-weak' ;
895+ } else if ( validation . score === 2 ) {
896+ fillWidth = '66%' ;
897+ fillClass = 'strength-fair' ;
898+ } else if ( validation . score === 3 ) {
899+ fillWidth = '100%' ;
900+ fillClass = password . length >= 12 ? 'strength-strong' : 'strength-good' ;
901+ }
902+ }
903+
904+ if ( passwordStrengthFill ) {
905+ passwordStrengthFill . style . width = fillWidth ;
906+ passwordStrengthFill . className = `password-strength-fill ${ fillClass } ` ;
907+ }
908+ }
909+
910+ if ( passwordInput ) {
911+ passwordInput . addEventListener ( 'input' , updatePasswordStrengthMeter ) ;
912+ }
913+
914+ // Intercept form submission to enforce validation on register
915+ if ( authForm ) {
916+ const originalOnSubmit = authForm . onsubmit ;
917+ authForm . onsubmit = function ( event ) {
918+ if ( authForm . dataset . mode === 'register' ) {
919+ const validation = validatePasswordCriteria ( passwordInput . value ) ;
920+ if ( validation . score < 3 ) {
921+ event . preventDefault ( ) ;
922+
923+ passwordInput . style . animation = 'inputShake 0.4s ease-in-out 0s 1' ;
924+ setTimeout ( ( ) => {
925+ passwordInput . style . animation = 'none' ;
926+ } , 400 ) ;
927+
928+ passwordInput . focus ( ) ;
929+ return false ; // Stop submission
930+ }
931+ }
932+ if ( originalOnSubmit ) {
933+ return originalOnSubmit . call ( this , event ) ;
934+ }
935+ } ;
936+ }
937+
749938 const resetToken = new URLSearchParams ( window . location . search ) . get ( 'token' ) ;
750939 if ( resetToken ) {
751940 showAuthPanel ( 'reset' ) ;
0 commit comments