[WEB-4488] feat: brand revamp (#7544)

* chore: empty state asset and theme improvement (#7542)

* chore: empty state asset and theme improvement

* chore: upgrade modal improvement and code refactor

* feat: onboarding revamp and theme changes (#7541)

* 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

* chore: brand logo added

* chore:  terms and conditions refactor

* chore: auth form refactor

* chore: oauth enhancements and refactor

* chore: plane new logos added

* chore: auth input form field added to ui package

* chore: password input component added

* chore: web auth refactor

* chore: update brand colors and remove onboarding-specific styles

* chore: clean up unused assets

* chore: profile menu text overflow

* chore: theme related changes

* chore: logo spinner updated

* chore: onboarding constant and types updated

* chore: theme changes and code refactor

* feat: onboarding flow revamp

* fix:  build error and code refactoring

* chore: code refactor

* fix: build error

* chore: consent option added to onboarding and code refactor

* fix: build fix

* chore: code refactor

* chore: auth screen revamp and code refactor

* chore: onboarding enhancements

* chore: code refactor

* chore: onboarding logic improvement

* chore: code refactor

* fix: onboarding pre release improvements

* chore: color token updated

* chore: color token updated

* chore: auth screen line height and size improvements

* chore: input height updated

* chore: n-progress theme updated

* chore: theme and logo enhancements

* chore: space auth and code refactor

* chore: update new brand empty states (#7543)

* [WEB-4585]chore: branding updates (#7540)

* chore: updated logo, og image, and loaders

* chore: updated branding colors

* chore: tour modal logo

* chore: updated logo spinner size

* chore: updated email templates logos and colors

* chore: code refactor

* fix: removed conditional hook render

* fix: space app loader

---------

Co-authored-by: Vamsi Krishna <46787868+vamsikrishnamathala@users.noreply.github.com>
Co-authored-by: vamsikrishnamathala <matalav55@gmail.com>
This commit is contained in:
Anmol Singh Bhatia 2025-08-06 22:24:47 +05:30 committed by GitHub
parent 6450793d72
commit 51e146f8ca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
345 changed files with 5158 additions and 2515 deletions

View file

@ -0,0 +1,77 @@
import React, { useState } from "react";
import { cn } from "@plane/utils";
import { AuthInput } from "./auth-input";
export interface AuthConfirmPasswordInputProps
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "autoComplete"> {
password: string;
label?: string;
error?: string;
showPasswordToggle?: boolean;
containerClassName?: string;
labelClassName?: string;
errorClassName?: string;
autoComplete?: "on" | "off";
onPasswordMatchChange?: (matches: boolean) => void;
}
export const AuthConfirmPasswordInput: React.FC<AuthConfirmPasswordInputProps> = ({
password,
label = "Confirm Password",
error,
showPasswordToggle = true,
containerClassName = "",
errorClassName = "",
className = "",
value = "",
onChange,
onPasswordMatchChange,
...props
}) => {
const [isFocused, setIsFocused] = useState(false);
const confirmPassword = value as string;
const passwordsMatch = password === confirmPassword && password.length > 0;
const showMatchError =
confirmPassword.length > 0 && !passwordsMatch && (!isFocused || confirmPassword.length >= password.length);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newConfirmPassword = e.target.value;
onChange?.(e);
onPasswordMatchChange?.(password === newConfirmPassword && password.length > 0);
};
const handleFocus = () => {
setIsFocused(true);
};
const handleBlur = () => {
setIsFocused(false);
};
const getError = () => {
if (error) return error;
if (showMatchError) return "Passwords don't match";
return "";
};
return (
<div className={cn("space-y-2", containerClassName)}>
<AuthInput
{...props}
type="password"
label={label}
error={getError()}
showPasswordToggle={showPasswordToggle}
errorClassName={errorClassName}
className={className}
value={value}
onChange={handleChange}
onFocus={handleFocus}
onBlur={handleBlur}
autoComplete="on"
/>
{confirmPassword && passwordsMatch && <p className="text-sm text-green-500">Passwords match</p>}
</div>
);
};

View file

@ -0,0 +1,41 @@
import React from "react";
import { cn } from "../../helpers";
export interface AuthForgotPasswordProps {
onForgotPassword?: () => void;
className?: string;
text?: string;
disabled?: boolean;
}
export const AuthForgotPassword: React.FC<AuthForgotPasswordProps> = ({
onForgotPassword,
className = "",
text = "Forgot your password?",
disabled = false,
}) => {
const handleClick = (e: React.MouseEvent) => {
e.preventDefault();
if (!disabled && onForgotPassword) {
onForgotPassword();
}
};
return (
<button
type="button"
onClick={handleClick}
disabled={disabled}
className={cn(
"text-sm text-custom-primary-100 hover:text-custom-primary-200 transition-colors duration-200",
{
"opacity-50 cursor-not-allowed": disabled,
"cursor-pointer": !disabled,
},
className
)}
>
{text}
</button>
);
};

View file

@ -0,0 +1,207 @@
import React, { useState, useMemo } from "react";
import { E_PASSWORD_STRENGTH } from "@plane/constants";
import { cn } from "../../helpers";
import { Button } from "../button/button";
import { Spinner } from "../spinners/circular-spinner";
import { AuthConfirmPasswordInput } from "./auth-confirm-password-input";
import { AuthForgotPassword } from "./auth-forgot-password";
import { AuthInput } from "./auth-input";
import { AuthPasswordInput } from "./auth-password-input";
export type AuthMode = "sign-in" | "sign-up";
export interface AuthFormData {
email: string;
password: string;
confirmPassword?: string;
}
export interface AuthFormProps {
mode: AuthMode;
initialData?: Partial<AuthFormData>;
onSubmit?: (data: AuthFormData) => void;
onForgotPassword?: () => void;
onModeChange?: (mode: AuthMode) => void;
loading?: boolean;
disabled?: boolean;
className?: string;
showForgotPassword?: boolean;
showPasswordStrength?: boolean;
emailError?: string;
passwordError?: string;
confirmPasswordError?: string;
submitButtonText?: string;
alternateModeText?: string;
alternateModeButtonText?: string;
}
export const AuthForm: React.FC<AuthFormProps> = ({
mode,
initialData = {},
onSubmit,
onForgotPassword,
onModeChange,
loading = false,
disabled = false,
className = "",
showForgotPassword = true,
showPasswordStrength = true,
emailError,
passwordError,
confirmPasswordError,
submitButtonText,
alternateModeText,
alternateModeButtonText,
}) => {
const [formData, setFormData] = useState<AuthFormData>({
email: initialData.email || "",
password: initialData.password || "",
confirmPassword: initialData.confirmPassword || "",
});
const [passwordStrength, setPasswordStrength] = useState<E_PASSWORD_STRENGTH>(E_PASSWORD_STRENGTH.EMPTY);
const [passwordsMatch, setPasswordsMatch] = useState(false);
const handleInputChange = (field: keyof AuthFormData) => (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData((prev) => ({
...prev,
[field]: e.target.value,
}));
};
const handlePasswordChange = (password: string) => {
setFormData((prev) => ({
...prev,
password,
}));
};
const handlePasswordStrengthChange = (strength: E_PASSWORD_STRENGTH) => {
setPasswordStrength(strength);
};
const handleConfirmPasswordChange = (matches: boolean) => {
setPasswordsMatch(matches);
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (onSubmit && isFormValid) {
onSubmit(formData);
}
};
const handleModeChange = () => {
const newMode = mode === "sign-in" ? "sign-up" : "sign-in";
onModeChange?.(newMode);
};
const isFormValid = useMemo(() => {
const hasEmail = formData.email.length > 0;
const hasPassword = formData.password.length > 0;
if (mode === "sign-in") {
return hasEmail && hasPassword && !loading && !disabled;
} else {
const isPasswordStrong = passwordStrength === E_PASSWORD_STRENGTH.STRENGTH_VALID;
const passwordsMatch = formData.password === formData.confirmPassword && formData.password.length > 0;
return hasEmail && hasPassword && isPasswordStrong && passwordsMatch && !loading && !disabled;
}
}, [mode, formData, passwordStrength, loading, disabled]);
const getSubmitButtonText = () => {
if (submitButtonText) return submitButtonText;
return mode === "sign-in" ? "Sign In" : "Create Account";
};
const getAlternateModeText = () => {
if (alternateModeText) return alternateModeText;
return mode === "sign-in" ? "Don't have an account?" : "Already have an account?";
};
const getAlternateModeButtonText = () => {
if (alternateModeButtonText) return alternateModeButtonText;
return mode === "sign-in" ? "Sign Up" : "Sign In";
};
return (
<form onSubmit={handleSubmit} className={cn("space-y-4", className)}>
{/* Email Input */}
<AuthInput
id="email"
name="email"
type="email"
label="Email"
value={formData.email}
onChange={handleInputChange("email")}
placeholder="name@company.com"
error={emailError}
disabled={disabled}
// autoComplete="email"
required
/>
{/* Password Input */}
<AuthPasswordInput
id="password"
name="password"
label={mode === "sign-in" ? "Password" : "Set a password"}
value={formData.password}
onChange={handleInputChange("password")}
onPasswordChange={handlePasswordChange}
onPasswordStrengthChange={handlePasswordStrengthChange}
placeholder="Enter password"
error={passwordError}
showPasswordStrength={showPasswordStrength && mode === "sign-up"}
disabled={disabled}
// autoComplete={mode === "sign-in" ? "current-password" : "new-password"}
required
/>
{/* Confirm Password Input (Sign Up Only) */}
{mode === "sign-up" && (
<AuthConfirmPasswordInput
id="confirmPassword"
name="confirmPassword"
password={formData.password}
value={formData.confirmPassword}
onChange={handleInputChange("confirmPassword")}
onPasswordMatchChange={handleConfirmPasswordChange}
error={confirmPasswordError}
disabled={disabled}
// autoComplete="new-password"
required
/>
)}
{/* Forgot Password Link (Sign In Only) */}
{mode === "sign-in" && showForgotPassword && (
<div className="flex justify-end">
<AuthForgotPassword onForgotPassword={onForgotPassword} disabled={disabled} />
</div>
)}
{/* Submit Button */}
<div className="space-y-2.5">
<Button type="submit" variant="primary" className="w-full" size="lg" disabled={!isFormValid} loading={loading}>
{loading ? <Spinner height="20px" width="20px" /> : getSubmitButtonText()}
</Button>
{/* Alternate Mode Button */}
{onModeChange && (
<div className="text-center">
<span className="text-sm text-custom-text-300">{getAlternateModeText()}</span>
<button
type="button"
onClick={handleModeChange}
className="ml-1 text-sm text-custom-primary-100 hover:text-custom-primary-200 transition-colors duration-200"
disabled={disabled}
>
{getAlternateModeButtonText()}
</button>
</div>
)}
</div>
</form>
);
};

View file

@ -0,0 +1,66 @@
import { Eye, EyeOff } from "lucide-react";
import React, { useState } from "react";
import { cn } from "../../helpers";
import { Input } from "../form-fields/input";
export interface AuthInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "autoComplete"> {
label?: string;
error?: string;
showPasswordToggle?: boolean;
errorClassName?: string;
autoComplete?: "on" | "off";
}
const baseContainerClassName = "flex flex-col gap-1.5";
export const AuthInput: React.FC<AuthInputProps> = ({
label,
error,
showPasswordToggle = false,
errorClassName = "",
className = "",
type = "text",
...props
}) => {
const { id } = props;
const [showPassword, setShowPassword] = useState(false);
const isPasswordType = type === "password";
const inputType = isPasswordType && showPasswordToggle && showPassword ? "text" : type;
return (
<div className={cn(baseContainerClassName)}>
{label && (
<label htmlFor={id} className={cn("text-sm font-semibold text-custom-text-300")}>
{label}
</label>
)}
<div
className={cn("relative flex items-center rounded-md border border-custom-border-300 py-2 px-3 transition-all")}
>
<Input
{...props}
type={inputType}
className={cn(
"rounded-md disable-autofill-style h-6 w-full placeholder:text-base placeholder:text-custom-text-400 p-0 border-none",
{
"border-red-500": error,
},
className
)}
/>
{showPasswordToggle && isPasswordType && (
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
>
{showPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
</button>
)}
</div>
{error && <p className={cn("text-sm text-red-500", errorClassName)}>{error}</p>}
</div>
);
};

View file

@ -0,0 +1,77 @@
import React, { useState } from "react";
import { E_PASSWORD_STRENGTH } from "@plane/constants";
import { cn, getPasswordStrength } from "@plane/utils";
import { PasswordStrengthIndicator } from "../form-fields/password/indicator";
import { AuthInput } from "./auth-input";
export interface AuthPasswordInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "autoComplete"> {
label?: string;
error?: string;
showPasswordStrength?: boolean;
showPasswordToggle?: boolean;
containerClassName?: string;
errorClassName?: string;
autoComplete?: "on" | "off";
onPasswordChange?: (password: string) => void;
onPasswordStrengthChange?: (strength: E_PASSWORD_STRENGTH) => void;
}
export const AuthPasswordInput: React.FC<AuthPasswordInputProps> = ({
label = "Password",
error,
showPasswordStrength = true,
showPasswordToggle = true,
containerClassName = "",
errorClassName = "",
className = "",
value = "",
onChange,
onPasswordChange,
onPasswordStrengthChange,
...props
}) => {
const [isFocused, setIsFocused] = useState(false);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newPassword = e.target.value;
onChange?.(e);
onPasswordChange?.(newPassword);
};
const handleFocus = () => {
setIsFocused(true);
};
const handleBlur = () => {
setIsFocused(false);
};
const passwordStrength = getPasswordStrength(value as string);
// Notify parent of strength change
React.useEffect(() => {
onPasswordStrengthChange?.(passwordStrength);
}, [passwordStrength, onPasswordStrengthChange]);
return (
<div className={cn("space-y-2", containerClassName)}>
<AuthInput
{...props}
type="password"
label={label}
error={error}
showPasswordToggle={showPasswordToggle}
errorClassName={errorClassName}
className={className}
value={value}
onChange={handleChange}
onFocus={handleFocus}
onBlur={handleBlur}
autoComplete="on"
/>
{showPasswordStrength && value && isFocused && (
<PasswordStrengthIndicator password={value as string} showCriteria />
)}
</div>
);
};

View file

@ -0,0 +1,11 @@
export { AuthForm } from "./auth-form";
export { AuthInput } from "./auth-input";
export { AuthPasswordInput } from "./auth-password-input";
export { AuthConfirmPasswordInput } from "./auth-confirm-password-input";
export { AuthForgotPassword } from "./auth-forgot-password";
export type { AuthFormProps, AuthFormData, AuthMode } from "./auth-form";
export type { AuthInputProps } from "./auth-input";
export type { AuthPasswordInputProps } from "./auth-password-input";
export type { AuthConfirmPasswordInputProps } from "./auth-confirm-password-input";
export type { AuthForgotPasswordProps } from "./auth-forgot-password";