@@ -20,6 +20,7 @@ export function Onboarding() {
2020 const [ acceptedPrivacy , setAcceptedPrivacy ] = useState ( false )
2121 const [ submitting , setSubmitting ] = useState ( false )
2222 const [ error , setError ] = useState < string | null > ( null )
23+ const [ pwFocused , setPwFocused ] = useState ( false )
2324
2425 async function onSubmit ( e : React . FormEvent ) {
2526 e . preventDefault ( )
@@ -143,8 +144,13 @@ export function Onboarding() {
143144 placeholder = "••••••••"
144145 value = { password }
145146 onChange = { ( e ) => setPassword ( e . target . value ) }
147+ onFocus = { ( ) => setPwFocused ( true ) }
148+ onBlur = { ( ) => setPwFocused ( false ) }
146149 className = "w-full bg-transparent text-sm outline-none placeholder:text-[var(--color-stone-soft)]"
147150 />
151+ { mode === 'signup' && ( pwFocused || password . length > 0 ) ? (
152+ < PasswordStrength password = { password } />
153+ ) : null }
148154 </ Field >
149155 </ div >
150156
@@ -219,6 +225,40 @@ function Field({ label, hint, children }: { label: React.ReactNode; hint?: strin
219225 )
220226}
221227
228+ function scorePassword ( pw : string ) : { score : number ; label : string ; color : string } {
229+ let score = 0
230+ if ( pw . length >= 8 ) score ++
231+ if ( pw . length >= 12 ) score ++
232+ if ( / [ A - Z ] / . test ( pw ) ) score ++
233+ if ( / [ 0 - 9 ] / . test ( pw ) ) score ++
234+ if ( / [ ^ A - Z a - z 0 - 9 ] / . test ( pw ) ) score ++
235+ if ( score <= 1 ) return { score, label : 'Weak' , color : 'bg-[var(--color-coral)]' }
236+ if ( score <= 2 ) return { score, label : 'Fair' , color : 'bg-amber-400' }
237+ if ( score <= 3 ) return { score, label : 'Good' , color : 'bg-yellow-400' }
238+ if ( score <= 4 ) return { score, label : 'Strong' , color : 'bg-emerald-400' }
239+ return { score, label : 'Great' , color : 'bg-emerald-500' }
240+ }
241+
242+ function PasswordStrength ( { password } : { password : string } ) {
243+ const { score, label, color } = scorePassword ( password )
244+ const filled = Math . max ( 1 , score )
245+ return (
246+ < div className = "mt-3 space-y-1.5" >
247+ < div className = "flex gap-1" >
248+ { Array . from ( { length : 5 } ) . map ( ( _ , i ) => (
249+ < div
250+ key = { i }
251+ className = { `h-1 flex-1 rounded-full transition-colors duration-300 ${
252+ i < filled ? color : 'bg-[var(--color-hairline)]'
253+ } `}
254+ />
255+ ) ) }
256+ </ div >
257+ < p className = "text-[10px] text-[var(--color-stone)]" > { label } </ p >
258+ </ div >
259+ )
260+ }
261+
222262function humanizeError ( code : string ) : string {
223263 switch ( code ) {
224264 case 'email_taken' :
0 commit comments