Skip to content

Commit 48d0f46

Browse files
committed
feat(experimental): password reveal, fixed layout, dialog override, input hint
1 parent f64a313 commit 48d0f46

2 files changed

Lines changed: 122 additions & 17 deletions

File tree

apps/login-app/src/routes/(app)/+layout.svelte

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,19 @@
8686
color: white;
8787
}
8888
}
89+
90+
/**
91+
* Demo override — removes the widget dialog/container chrome so custom
92+
* stages render directly on the page background without a grey card wrapper.
93+
*/
94+
dialog,
95+
.tw_containing-box,
96+
.tw_containing-box_dark,
97+
[class*='tw_containing-box'] {
98+
background-color: transparent !important;
99+
box-shadow: none !important;
100+
border: none !important;
101+
}
89102
</style>
90103
</svelte:head>
91104

experimental/custom/stages/username-avatar/username-avatar.svelte

Lines changed: 109 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)