[WEB-4513] refactor: consolidate password strength meter into shared ui package (#7462)

* refactor: consolidate password strength indicator into shared UI package

* chore: remove old password strength meter implementations

* chore: update package dependencies for password strength refactor

* chore: code refactor

* fix: lock file

---------

Co-authored-by: sriramveeraghanta <veeraghanta.sriram@gmail.com>
This commit is contained in:
Anmol Singh Bhatia 2025-07-25 16:56:46 +05:30 committed by GitHub
parent 63d025cbf4
commit a5f3bd15b1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 310 additions and 458 deletions

View file

@ -2,3 +2,4 @@ export * from "./input";
export * from "./textarea";
export * from "./input-color-picker";
export * from "./checkbox";
export * from "./password";

View file

@ -0,0 +1,65 @@
import { E_PASSWORD_STRENGTH } from "@plane/constants";
export interface StrengthInfo {
message: string;
textColor: string;
activeFragments: number;
}
/**
* Get strength information including message, color, and active fragments
*/
export const getStrengthInfo = (strength: E_PASSWORD_STRENGTH): StrengthInfo => {
switch (strength) {
case E_PASSWORD_STRENGTH.EMPTY:
return {
message: "Please enter your password",
textColor: "text-custom-text-100",
activeFragments: 0,
};
case E_PASSWORD_STRENGTH.LENGTH_NOT_VALID:
return {
message: "Password is too short",
textColor: "text-red-500",
activeFragments: 1,
};
case E_PASSWORD_STRENGTH.STRENGTH_NOT_VALID:
return {
message: "Password is weak",
textColor: "text-orange-500",
activeFragments: 2,
};
case E_PASSWORD_STRENGTH.STRENGTH_VALID:
return {
message: "Password is strong",
textColor: "text-green-500",
activeFragments: 3,
};
default:
return {
message: "Please enter your password",
textColor: "text-custom-text-100",
activeFragments: 0,
};
}
};
/**
* Get fragment color based on position and active state
*/
export const getFragmentColor = (fragmentIndex: number, activeFragments: number): string => {
if (fragmentIndex >= activeFragments) {
return "bg-custom-background-90";
}
switch (activeFragments) {
case 1:
return "bg-red-500";
case 2:
return "bg-orange-500";
case 3:
return "bg-green-500";
default:
return "bg-custom-background-90";
}
};

View file

@ -0,0 +1,2 @@
export * from "./indicator";
export * from "./helper";

View file

@ -0,0 +1,75 @@
import { CircleCheck } from "lucide-react";
import React from "react";
import { E_PASSWORD_STRENGTH } from "@plane/constants";
import { cn, getPasswordStrength, getPasswordCriteria } from "@plane/utils";
import { getStrengthInfo, getFragmentColor } from "./helper";
export interface PasswordStrengthIndicatorProps {
password: string;
showCriteria?: boolean;
isFocused?: boolean;
}
export const PasswordStrengthIndicator: React.FC<PasswordStrengthIndicatorProps> = ({
password,
showCriteria = true,
isFocused = false,
}) => {
const strength = getPasswordStrength(password);
const criteria = getPasswordCriteria(password);
const strengthInfo = getStrengthInfo(strength);
const isPasswordMeterVisible = isFocused ? true : strength === E_PASSWORD_STRENGTH.STRENGTH_VALID ? false : true;
if ((!password && !showCriteria) || !isPasswordMeterVisible) {
return null;
}
return (
<div className={cn("space-y-3")}>
{/* Strength Indicator */}
<div className="space-y-2">
<div className="flex gap-1 w-full transition-all duration-300 ease-linear">
{[0, 1, 2].map((fragmentIndex) => (
<div
key={fragmentIndex}
className={cn(
"h-1 flex-1 rounded-sm transition-all duration-300 ease-in-out",
getFragmentColor(fragmentIndex, strengthInfo.activeFragments)
)}
/>
))}
</div>
{/* Strength Message */}
{password && <p className={cn("text-sm font-medium", strengthInfo.textColor)}>{strengthInfo.message}</p>}
</div>
{/* Criteria list */}
{showCriteria && (
<div className="flex flex-wrap gap-2">
{criteria.map((criterion) => (
<div key={criterion.key} className="flex items-center gap-1.5">
<div className="flex items-center justify-center p-0.5">
<CircleCheck
className={cn("h-3 w-3 flex-shrink-0", {
"text-green-500": criterion.isValid,
"text-custom-text-100": !criterion.isValid,
})}
/>
</div>
<span
className={cn("text-xs", {
"text-green-500": criterion.isValid,
"text-custom-text-100": !criterion.isValid,
})}
>
{criterion.label}
</span>
</div>
))}
</div>
)}
</div>
);
};