@@ -74,6 +74,7 @@ username is typed and reveals it with a circular iris animation.
7474 let matchedUsername = ' ' ;
7575 let matchedGreeting = ' ' ;
7676 let emailError: string | null = null ;
77+ let passwordFilled = false ;
7778
7879 function determineSubmission() {
7980 if (metadata ?.step ?.derived .isStepSelfSubmittable ()) {
@@ -99,6 +100,8 @@ username is typed and reveals it with a circular iris animation.
99100 let inputObserver: MutationObserver | null = null ;
100101 let inputEl: HTMLInputElement | null = null ;
101102 let inputListener: ((e : Event ) => void ) | null = null ;
103+ let passwordEl: HTMLInputElement | null = null ;
104+ let passwordListener: ((e : Event ) => void ) | null = null ;
102105
103106 function watchUsernameInput() {
104107 // Poll briefly for the input rendered by CallbackMapper, then attach listener.
@@ -122,11 +125,29 @@ username is typed and reveals it with a circular iris animation.
122125 if (inputEl && inputListener ) {
123126 inputEl .removeEventListener (' input' , inputListener );
124127 }
128+ if (passwordEl && passwordListener ) {
129+ passwordEl .removeEventListener (' input' , passwordListener );
130+ }
125131 if (inputObserver ) {
126132 inputObserver .disconnect ();
127133 }
128134 }
129135
136+ function watchPasswordInput() {
137+ const poll = setInterval (() => {
138+ const found = document .querySelector <HTMLInputElement >(' input[type="password"]' );
139+ if (found ) {
140+ clearInterval (poll );
141+ passwordEl = found ;
142+ passwordListener = (e : Event ) => {
143+ passwordFilled = !! (e .target as HTMLInputElement ).value ;
144+ };
145+ passwordEl .addEventListener (' input' , passwordListener );
146+ }
147+ }, 50 );
148+ setTimeout (() => clearInterval (poll ), 3000 );
149+ }
150+
130151 async function handleUsernameInput(value : string ) {
131152 const username = value .trim ().toLowerCase ();
132153
@@ -140,6 +161,7 @@ username is typed and reveals it with a circular iris animation.
140161 avatarLoading = false ;
141162 matchedUsername = ' ' ;
142163 matchedGreeting = ' ' ;
164+ passwordFilled = false ;
143165 emailError = null ;
144166 return ;
145167 }
@@ -150,6 +172,7 @@ username is typed and reveals it with a circular iris animation.
150172 avatarVisible = false ;
151173 avatarLoading = false ;
152174 matchedUsername = ' ' ;
175+ passwordFilled = false ;
153176 emailError = ' Please enter a valid email address.' ;
154177 return ;
155178 }
@@ -165,6 +188,7 @@ username is typed and reveals it with a circular iris animation.
165188 avatarLoading = false ;
166189 matchedUsername = ' ' ;
167190 matchedGreeting = ' ' ;
191+ passwordFilled = false ;
168192 return ;
169193 }
170194
@@ -205,6 +229,12 @@ username is typed and reveals it with a circular iris animation.
205229 $ : {
206230 formMessageKey = convertStringToKey (form ?.message );
207231 }
232+
233+ $ : if (avatarVisible ) {
234+ // Password field just mounted — start watching it.
235+ passwordFilled = false ;
236+ watchPasswordInput ();
237+ }
208238 </script >
209239
210240<Form bind:formEl ariaDescribedBy ="avatarFormFailureAlert" onSubmitWhenValid ={form ?.submit }>
@@ -262,26 +292,50 @@ username is typed and reveals it with a circular iris animation.
262292 </Alert >
263293 {/if }
264294
265- <div class =" callbacks-section" >
266- {#each step ?.callbacks as callback , idx }
267- <CallbackMapper
268- props ={{
269- callback ,
270- callbackMetadata : metadata ?.callbacks [idx ],
271- selfSubmitFunction : determineSubmission ,
272- stepMetadata : metadata ?.step && { ... metadata .step },
273- style : currentStyle ,
274- }}
275- />
276- {/each }
277- </div >
295+ <div class =" dynamic-section" >
296+ <div class =" callbacks-section" >
297+ {#each step ?.callbacks as callback , idx }
298+ {#if callback .getType () === ' PasswordCallback' }
299+ {#if avatarVisible }
300+ <div class =" password-reveal" >
301+ <CallbackMapper
302+ props ={{
303+ callback ,
304+ callbackMetadata : metadata ?.callbacks [idx ],
305+ selfSubmitFunction : determineSubmission ,
306+ stepMetadata : metadata ?.step && { ... metadata .step },
307+ style : currentStyle ,
308+ }}
309+ />
310+ </div >
311+ {/if }
312+ {:else }
313+ <CallbackMapper
314+ props ={{
315+ callback ,
316+ callbackMetadata : metadata ?.callbacks [idx ],
317+ selfSubmitFunction : determineSubmission ,
318+ stepMetadata : metadata ?.step && { ... metadata .step },
319+ style : currentStyle ,
320+ }}
321+ />
322+ {/if }
323+ {/each }
324+ </div >
278325
279- {#if emailError }
280- <p class ="email-error" role ="alert" >{emailError }</p >
281- {/if }
326+ {#if emailError }
327+ <p class ="email-error" role ="alert" >{emailError }</p >
328+ {:else if ! avatarVisible && ! avatarLoading }
329+ <p class =" input-hint" >Enter your email to continue</p >
330+ {/if }
331+ </div >
282332
283333 {#if metadata ?.step ?.derived .isUserInputOptional || ! metadata ?.step ?.derived .isStepSelfSubmittable ()}
284- <button class ="next-button" type ="submit" disabled ={journey ?.loading }>
334+ <button
335+ class =" next-button"
336+ type =" submit"
337+ disabled ={journey ?.loading || ! avatarVisible || ! passwordFilled }
338+ >
285339 {#if journey ?.loading }
286340 <span class =" btn-spinner" aria-hidden =" true" />
287341 {:else }
@@ -478,13 +532,51 @@ username is typed and reveals it with a circular iris animation.
478532 margin : 0 ;
479533 }
480534
535+ /* ── Dynamic section (fixed height prevents layout shift) ── */
536+ .dynamic-section {
537+ height : 130px ;
538+ display : flex ;
539+ flex-direction : column ;
540+ justify-content : flex-start ;
541+ gap : 0.5rem ;
542+ overflow : hidden ;
543+ }
544+
481545 /* ── Callbacks ── */
482546 .callbacks-section {
483547 display : flex ;
484548 flex-direction : column ;
485549 gap : 0.75rem ;
486550 }
487551
552+ /* ── Password reveal ── */
553+ .password-reveal {
554+ animation : slideDown 0.4s cubic-bezier (0.34 , 1.56 , 0.64 , 1 ) both ;
555+ overflow : hidden ;
556+ }
557+
558+ @keyframes slideDown {
559+ from {
560+ opacity : 0 ;
561+ transform : translateY (-12px );
562+ max-height : 0 ;
563+ }
564+ to {
565+ opacity : 1 ;
566+ transform : translateY (0 );
567+ max-height : 200px ;
568+ }
569+ }
570+
571+ /* ── Input hint ── */
572+ .input-hint {
573+ font-size : 0.8125rem ;
574+ color : rgba (255 , 255 , 255 , 0.3 );
575+ margin : 0 ;
576+ text-align : center ;
577+ font-style : italic ;
578+ }
579+
488580 /* ── Email validation error ── */
489581 .email-error {
490582 font-size : 0.8125rem ;
0 commit comments