@insymetri/styleguide 0.1.26 → 0.1.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ declare const LoginFlowBareAlt: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type LoginFlowBareAlt = ReturnType<typeof LoginFlowBareAlt>;
3
+ export default LoginFlowBareAlt;
@@ -13,9 +13,13 @@
13
13
  stepIndicator?: Snippet
14
14
  /** Hide the built-in step indicator (when managed externally) */
15
15
  hideStepIndicator?: boolean
16
+ /** When true, content above step indicator fades out */
17
+ transitioning?: boolean
18
+ /** Hide the illustration banner on mobile/tablet */
19
+ hideBanner?: boolean
16
20
  }
17
21
 
18
- let {onContinue, illustration, stepIndicator, hideStepIndicator = false}: Props = $props()
22
+ let {onContinue, illustration, stepIndicator, hideStepIndicator = false, transitioning = false, hideBanner = false}: Props = $props()
19
23
 
20
24
  let email = $state('')
21
25
  let showError = $state(false)
@@ -23,61 +27,67 @@
23
27
  </script>
24
28
 
25
29
  <!-- Outer wrapper: grey bg on tablet+, full bleed on mobile -->
26
- <div class="min-h-screen md:bg-background md:flex md:items-start md:justify-center md:pt-[18vh] md:p-24">
30
+ <div class="flex h-screen flex-col md:h-auto md:min-h-screen md:items-center md:justify-center md:bg-background md:p-24">
27
31
  <!-- Card on tablet+, split on desktop -->
28
32
  <DensityProvider density="mobile">
29
- <div class="flex flex-col lg:flex-row lg:w-[860px] md:w-[440px] md:rounded-16 md:bg-white md:shadow-card md:overflow-hidden">
33
+ <div class="flex min-w-0 flex-1 flex-col bg-white md:flex-initial md:h-[694px] lg:h-[595px] lg:flex-row lg:w-[860px] md:w-[440px] md:rounded-16 md:shadow-card md:overflow-hidden">
30
34
  <!-- Illustration banner — mobile & tablet only -->
31
- <div class="lg:hidden relative h-[100px] md:h-[90px] overflow-hidden shrink-0" style="background: linear-gradient(135deg, var(--ii-primary), var(--ii-primary-hover))">
32
- <svg class="absolute inset-0 w-full h-full" viewBox="0 0 600 120" preserveAspectRatio="xMidYMid slice" xmlns="http://www.w3.org/2000/svg">
33
- <!-- Background texture -->
34
- {#each Array(2) as _, row}
35
- {#each Array(12) as _, col}
36
- <circle cx={30 + col * 50} cy={25 + row * 50} r="1.5" fill="white" opacity="0.1" />
35
+ {#if !hideBanner}
36
+ <div class="lg:hidden relative h-[100px] md:h-[90px] overflow-hidden shrink-0" style="background: linear-gradient(135deg, var(--ii-primary), var(--ii-primary-hover))">
37
+ <svg class="absolute inset-0 w-full h-full" viewBox="0 0 600 120" preserveAspectRatio="xMidYMid slice" xmlns="http://www.w3.org/2000/svg">
38
+ <!-- Background texture -->
39
+ {#each Array(2) as _, row}
40
+ {#each Array(12) as _, col}
41
+ <circle cx={30 + col * 50} cy={25 + row * 50} r="1.5" fill="white" opacity="0.1" />
42
+ {/each}
37
43
  {/each}
38
- {/each}
39
- <!-- Steps path -->
40
- <line x1="120" y1="60" x2="240" y2="60" stroke="white" stroke-width="2" opacity="0.3" />
41
- <line x1="240" y1="60" x2="360" y2="60" stroke="white" stroke-width="2" opacity="0.15" />
42
- <line x1="360" y1="60" x2="480" y2="60" stroke="white" stroke-width="2" opacity="0.08" />
43
- <!-- Step nodes -->
44
- <circle cx="120" cy="60" r="12" fill="white" opacity="0.3" />
45
- <circle cx="120" cy="60" r="6" fill="white" opacity="0.4" />
46
- <circle cx="240" cy="60" r="12" fill="white" opacity="0.15" />
47
- <circle cx="240" cy="60" r="4" fill="white" opacity="0.3" />
48
- <circle cx="360" cy="60" r="10" fill="white" opacity="0.12" />
49
- <circle cx="480" cy="60" r="8" fill="white" opacity="0.08" />
50
- </svg>
51
- </div>
44
+ <!-- Steps path -->
45
+ <line x1="120" y1="60" x2="240" y2="60" stroke="white" stroke-width="2" opacity="0.3" />
46
+ <line x1="240" y1="60" x2="360" y2="60" stroke="white" stroke-width="2" opacity="0.15" />
47
+ <line x1="360" y1="60" x2="480" y2="60" stroke="white" stroke-width="2" opacity="0.08" />
48
+ <!-- Step nodes -->
49
+ <circle cx="120" cy="60" r="12" fill="white" opacity="0.3" />
50
+ <circle cx="120" cy="60" r="6" fill="white" opacity="0.4" />
51
+ <circle cx="240" cy="60" r="12" fill="white" opacity="0.15" />
52
+ <circle cx="240" cy="60" r="4" fill="white" opacity="0.3" />
53
+ <circle cx="360" cy="60" r="10" fill="white" opacity="0.12" />
54
+ <circle cx="480" cy="60" r="8" fill="white" opacity="0.08" />
55
+ </svg>
56
+ </div>
57
+ {/if}
52
58
 
53
59
  <!-- Form side -->
54
- <div class="flex flex-col h-[calc(100vh-100px)] md:h-auto md:min-h-0 md:flex-1 px-24 md:p-40">
55
- <!-- Logo -->
56
- <div class="flex items-center gap-10 pt-24 md:pt-0">
57
- <svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
60
+ <div class="flex w-full min-w-0 flex-1 flex-col px-24 md:p-40">
61
+ <!-- Content that fades between steps -->
62
+ <div class="step-content flex flex-1 flex-col" class:fade-out={transitioning}>
63
+ <!-- Row 1: Logo — fixed height so headers align between steps -->
64
+ <div class="flex h-56 shrink-0 items-center justify-center gap-8 pt-24 md:pt-0">
65
+ <svg width="28" height="28" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
58
66
  <path d="M16 2L4 9v14l12 7 12-7V9L16 2z" fill="var(--ii-primary)" />
59
67
  <path d="M16 6l-8 4.5v9L16 24l8-4.5v-9L16 6z" fill="white" />
60
68
  <path d="M16 10l-4 2.25v4.5L16 19l4-2.25v-4.5L16 10z" fill="var(--ii-primary)" />
61
69
  </svg>
62
- <span class="text-display-sm text-body">Meridian Finance</span>
70
+ <span class="text-h3 text-body">Meridian Finance</span>
63
71
  </div>
64
72
 
65
- <!-- Header -->
73
+ <!-- Row 2: Header -->
66
74
  <div class="pt-24 pb-48">
67
- <h1 class="text-h2 text-body m-0">Your Loan Application</h1>
75
+ <div class="text-h2 text-body m-0">Your Loan Application</div>
68
76
  <p class="text-m3-emphasis text-tertiary mt-4 m-0">
69
77
  Get started or continue your application
70
78
  </p>
71
79
  </div>
72
80
 
73
- <!-- Form -->
74
- <div class="flex flex-col gap-16">
81
+ <!-- Row 3: Input -->
82
+ <form id="login-email-form" class="flex flex-col" onsubmit={(e) => { e.preventDefault(); loading = true; setTimeout(() => { loading = false; onContinue?.() }, 800) }}>
75
83
  {#if showError}
76
- <IIAlert variant="error" dismissible onDismiss={() => (showError = false)}>
77
- {#snippet children()}
78
- We couldn't find an account with that email address.
79
- {/snippet}
80
- </IIAlert>
84
+ <div class="mb-16">
85
+ <IIAlert variant="error" dismissible onDismiss={() => (showError = false)}>
86
+ {#snippet children()}
87
+ We couldn't find an account with that email address.
88
+ {/snippet}
89
+ </IIAlert>
90
+ </div>
81
91
  {/if}
82
92
 
83
93
  <IIInput
@@ -89,35 +99,37 @@
89
99
  autocomplete="email"
90
100
  error={showError}
91
101
  />
102
+ </form>
103
+
104
+ <!-- Info section — pushed to bottom on mobile -->
105
+ <div class="mb-24 mt-auto">
106
+ <p class="text-emphasis text-body m-0 mb-12">Here's what happens next:</p>
107
+ <ul class="m-0 list-none p-0 flex flex-col gap-8">
108
+ <li class="relative pl-20 text-default text-secondary">
109
+ <span class="absolute left-0 text-accent">&#x2713;</span>
110
+ New here? We'll help you get started
111
+ </li>
112
+ <li class="relative pl-20 text-default text-secondary">
113
+ <span class="absolute left-0 text-accent">&#x2713;</span>
114
+ Already applied? Pick up right where you left off
115
+ </li>
116
+ <li class="relative pl-20 text-default text-secondary">
117
+ <span class="absolute left-0 text-accent">&#x2713;</span>
118
+ All done? Check your loan status
119
+ </li>
120
+ </ul>
121
+ </div>
92
122
  </div>
93
123
 
94
- <!-- CTA pinned to bottom on mobile, inline on tablet+ -->
95
- <div class="mt-auto md:mt-48 pb-24 md:pb-0">
96
- <!-- Info Section -->
97
- <div class="mb-24">
98
- <p class="text-emphasis text-body m-0 mb-12">Here's what happens next:</p>
99
- <ul class="m-0 list-none p-0 flex flex-col gap-8">
100
- <li class="relative pl-20 text-default text-secondary">
101
- <span class="absolute left-0 text-accent">&#x2713;</span>
102
- New here? We'll help you get started
103
- </li>
104
- <li class="relative pl-20 text-default text-secondary">
105
- <span class="absolute left-0 text-accent">&#x2713;</span>
106
- Already applied? Pick up right where you left off
107
- </li>
108
- <li class="relative pl-20 text-default text-secondary">
109
- <span class="absolute left-0 text-accent">&#x2713;</span>
110
- All done? Check your loan status
111
- </li>
112
- </ul>
113
- </div>
124
+ <!-- Persistent bottom area step indicator + button (doesn't fade) -->
125
+ <div class="pb-24 md:pb-0">
114
126
  {#if stepIndicator}
115
- <div class="mt-56 mb-24">
127
+ <div class="mb-24 mt-24">
116
128
  {@render stepIndicator()}
117
129
  </div>
118
130
  {:else if !hideStepIndicator}
119
131
  <!-- Step indicator -->
120
- <div class="flex items-center justify-center gap-8 mt-56 mb-24">
132
+ <div class="flex items-center justify-center gap-8 mb-24 mt-24">
121
133
  <div class="h-6 rounded-full" style="background: var(--ii-primary); width: 36px; transition: width 0.4s ease, background-color 0.4s ease"></div>
122
134
  <div class="h-6 rounded-full" style="background: var(--ii-gray-300); width: 16px; transition: width 0.4s ease, background-color 0.4s ease"></div>
123
135
  </div>
@@ -125,15 +137,9 @@
125
137
  <IIButton
126
138
  variant="primary"
127
139
  class="w-full"
140
+ type="submit"
141
+ form="login-email-form"
128
142
  {loading}
129
- disabled={!email}
130
- onclick={() => {
131
- loading = true
132
- setTimeout(() => {
133
- loading = false
134
- onContinue?.()
135
- }, 800)
136
- }}
137
143
  >
138
144
  {#snippet children()}
139
145
  Continue
@@ -198,3 +204,14 @@
198
204
  </div>
199
205
  </DensityProvider>
200
206
  </div>
207
+
208
+ <style>
209
+ .step-content {
210
+ opacity: 1;
211
+ transition: opacity 200ms ease;
212
+ }
213
+
214
+ .step-content.fade-out {
215
+ opacity: 0;
216
+ }
217
+ </style>
@@ -7,6 +7,10 @@ type Props = {
7
7
  stepIndicator?: Snippet;
8
8
  /** Hide the built-in step indicator (when managed externally) */
9
9
  hideStepIndicator?: boolean;
10
+ /** When true, content above step indicator fades out */
11
+ transitioning?: boolean;
12
+ /** Hide the illustration banner on mobile/tablet */
13
+ hideBanner?: boolean;
10
14
  };
11
15
  declare const LoginScreenBare: import("svelte").Component<Props, {}, "">;
12
16
  type LoginScreenBare = ReturnType<typeof LoginScreenBare>;
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import DensityProvider from '../DensityProvider/DensityProvider.svelte'
3
3
  import IIButton from '../IIButton/IIButton.svelte'
4
+ import IIAlert from '../IIAlert/IIAlert.svelte'
4
5
 
5
6
  import type {Snippet} from 'svelte'
6
7
 
@@ -9,28 +10,36 @@
9
10
  onBack?: () => void
10
11
  stepIndicator?: Snippet
11
12
  hideStepIndicator?: boolean
13
+ /** When true, content above step indicator fades out */
14
+ transitioning?: boolean
15
+ /** Hide the illustration banner on mobile/tablet */
16
+ hideBanner?: boolean
12
17
  }
13
18
 
14
- let {onContinue, onBack, stepIndicator, hideStepIndicator = false}: Props = $props()
19
+ let {onContinue, onBack, stepIndicator, hideStepIndicator = false, transitioning = false, hideBanner = false}: Props = $props()
15
20
 
16
21
  let digits = $state<string[]>(['', '', '', '', '', ''])
17
22
  let inputs: HTMLInputElement[] = []
18
23
  let showError = $state(false)
19
- let resendTimer = $state(30)
20
24
  let verifying = $state(false)
25
+ let isResending = $state(false)
21
26
 
22
- let deliveryStatus = $state<'sending' | 'sent' | 'delivered'>('delivered')
27
+ let deliveryStatus = $state<'idle' | 'sending' | 'sent' | 'delivered' | 'bounced' | 'failed'>('delivered')
28
+ let deliveryError = $state('')
23
29
 
24
30
  const code = $derived(digits.join(''))
25
31
  const isComplete = $derived(code.length === 6 && digits.every((d) => d !== ''))
32
+ const hasFailed = $derived(deliveryStatus === 'bounced' || deliveryStatus === 'failed')
33
+ const sentComplete = $derived(deliveryStatus !== 'idle')
34
+ const deliveredComplete = $derived(deliveryStatus === 'delivered')
35
+ const isInFlight = $derived(deliveryStatus === 'sent' || deliveryStatus === 'sending')
26
36
 
27
37
  function handleInput(index: number, event: Event) {
28
38
  const target = event.target as HTMLInputElement
29
- const value = target.value
30
- if (value.length > 1) {
31
- digits[index] = value.slice(-1)
32
- }
33
- if (value && index < 5) {
39
+ const val = target.value.replace(/[^a-zA-Z0-9]/g, '').toUpperCase()
40
+ digits[index] = val.slice(-1)
41
+ target.value = digits[index]
42
+ if (digits[index] && index < 5) {
34
43
  inputs[index + 1]?.focus()
35
44
  }
36
45
  }
@@ -43,7 +52,11 @@
43
52
 
44
53
  function handlePaste(event: ClipboardEvent) {
45
54
  event.preventDefault()
46
- const paste = event.clipboardData?.getData('text')?.replace(/\D/g, '')?.slice(0, 6)
55
+ const paste = event.clipboardData
56
+ ?.getData('text')
57
+ ?.replace(/[^a-zA-Z0-9]/g, '')
58
+ ?.toUpperCase()
59
+ ?.slice(0, 6)
47
60
  if (paste) {
48
61
  for (let i = 0; i < 6; i++) {
49
62
  digits[i] = paste[i] || ''
@@ -52,20 +65,11 @@
52
65
  inputs[nextEmpty]?.focus()
53
66
  }
54
67
  }
55
-
56
- $effect(() => {
57
- if (resendTimer > 0) {
58
- const interval = setInterval(() => {
59
- resendTimer--
60
- }, 1000)
61
- return () => clearInterval(interval)
62
- }
63
- })
64
68
  </script>
65
69
 
66
- <div class="min-h-screen md:bg-background md:flex md:items-start md:justify-center md:pt-[18vh] md:p-24">
70
+ <div class="flex h-screen flex-col md:h-auto md:min-h-screen md:items-center md:justify-center md:bg-background md:p-24">
67
71
  <DensityProvider density="mobile">
68
- <div class="flex flex-col lg:flex-row lg:w-[860px] md:w-[440px] md:rounded-16 md:bg-white md:shadow-card md:overflow-hidden">
72
+ <div class="flex min-w-0 flex-1 flex-col bg-white md:flex-initial md:h-[694px] lg:h-[595px] lg:flex-row lg:w-[860px] md:w-[440px] md:rounded-16 md:shadow-card md:overflow-hidden">
69
73
  <!-- Illustration banner — mobile & tablet only -->
70
74
  <div class="lg:hidden relative h-[100px] md:h-[90px] overflow-hidden shrink-0" style="background: linear-gradient(135deg, var(--ii-primary), var(--ii-primary-hover))">
71
75
  <svg class="absolute inset-0 w-full h-full" viewBox="0 0 600 120" preserveAspectRatio="xMidYMid slice" xmlns="http://www.w3.org/2000/svg">
@@ -88,9 +92,11 @@
88
92
  </div>
89
93
 
90
94
  <!-- Form side -->
91
- <div class="flex flex-col h-[calc(100vh-100px)] md:h-auto md:min-h-0 md:flex-1 px-24 md:p-40">
92
- <!-- Back link -->
93
- <div class="pt-24 md:pt-0 mb-24">
95
+ <div class="flex w-full min-w-0 flex-1 flex-col px-24 md:p-40">
96
+ <!-- Content that fades between steps -->
97
+ <div class="step-content flex flex-1 flex-col" class:fade-out={transitioning}>
98
+ <!-- Row 1: Back button — fixed height so headers align with email screen -->
99
+ <div class="flex h-56 shrink-0 items-center gap-10 pt-24 md:pt-0">
94
100
  <button
95
101
  class="text-small text-secondary bg-transparent border-0 cursor-default p-0 hover:text-body"
96
102
  onclick={onBack}
@@ -99,137 +105,191 @@
99
105
  </button>
100
106
  </div>
101
107
 
102
- <!-- Header -->
103
- <div class="mb-32">
104
- <h1 class="text-h2 text-body m-0">Enter verification code</h1>
108
+ <!-- Row 2: Header -->
109
+ <div class="pt-24 pb-48">
110
+ <div class="text-h2 text-body m-0">Enter verification code</div>
105
111
  <p class="text-m3-emphasis text-tertiary mt-4 m-0">
106
112
  We sent a 6-digit code to <strong class="text-body">john@example.com</strong>
107
113
  </p>
108
114
  </div>
109
115
 
110
- <!-- Delivery Status -->
111
- <div class="flex items-center gap-0 mb-32" role="status" aria-label="Code delivery status">
112
- <div class="flex items-center gap-0">
113
- <div
114
- class="w-24 h-24 rounded-full flex items-center justify-center shrink-0 {deliveryStatus === 'sending'
115
- ? 'bg-primary text-inverse animate-pulse'
116
- : 'bg-primary text-inverse'}"
117
- >
118
- {#if deliveryStatus !== 'sending'}
119
- <svg width="12" height="12" viewBox="0 0 256 256" fill="currentColor">
120
- <path d="m229.66 77.66l-128 128a8 8 0 0 1-11.32 0l-56-56a8 8 0 0 1 11.32-11.32L96 188.69L218.34 66.34a8 8 0 0 1 11.32 11.32" />
121
- </svg>
122
- {/if}
116
+ <!-- Row 3: OTP Input — pt-20 matches the email input's label height -->
117
+ <form id="login-otp-form" class="flex flex-col pt-20" onsubmit={(e) => { e.preventDefault(); verifying = true; setTimeout(() => { verifying = false; onContinue?.() }, 800) }}>
118
+ {#if showError}
119
+ <div class="mb-16">
120
+ <IIAlert variant="error" dismissible onDismiss={() => (showError = false)}>
121
+ {#snippet children()}
122
+ Invalid verification code. Please try again.
123
+ {/snippet}
124
+ </IIAlert>
123
125
  </div>
124
- <span class="text-[10px] text-secondary ml-4 mr-8">Sent</span>
125
- </div>
126
- <div class="h-[2px] w-48 {deliveryStatus === 'delivered' ? 'bg-primary' : 'bg-muted'} transition-colors duration-500"></div>
127
- <div class="flex items-center gap-0">
128
- <span class="text-[10px] text-secondary mr-4 ml-8">Delivered</span>
129
- <div
130
- class="w-24 h-24 rounded-full flex items-center justify-center shrink-0 {deliveryStatus === 'delivered'
131
- ? 'bg-primary text-inverse'
132
- : deliveryStatus === 'sent'
133
- ? 'bg-primary text-inverse animate-pulse'
134
- : 'bg-muted text-secondary'}"
135
- >
136
- {#if deliveryStatus === 'delivered'}
137
- <svg width="12" height="12" viewBox="0 0 256 256" fill="currentColor">
138
- <path d="m229.66 77.66l-128 128a8 8 0 0 1-11.32 0l-56-56a8 8 0 0 1 11.32-11.32L96 188.69L218.34 66.34a8 8 0 0 1 11.32 11.32" />
139
- </svg>
140
- {/if}
126
+ {/if}
127
+
128
+ <div class="mb-16">
129
+ <div class="flex justify-center gap-8" role="group" aria-label="Verification code">
130
+ {#each digits as digit, i}
131
+ <input
132
+ bind:this={inputs[i]}
133
+ bind:value={digits[i]}
134
+ type="text"
135
+ inputmode="text"
136
+ autocomplete={i === 0 ? 'one-time-code' : 'off'}
137
+ maxlength="1"
138
+ pattern="[a-zA-Z0-9]"
139
+ class="w-48 h-48 text-center !text-m1 font-medium text-body bg-input-bg border-2 rounded-8 outline-none transition-all duration-fast
140
+ {showError
141
+ ? 'border-error'
142
+ : digit
143
+ ? 'border-primary'
144
+ : 'border-input-border'}
145
+ focus:border-primary focus:ring-2 focus:ring-primary/20"
146
+ oninput={(e) => handleInput(i, e)}
147
+ onkeydown={(e) => handleKeydown(i, e)}
148
+ onpaste={handlePaste}
149
+ aria-label="Digit {i + 1}"
150
+ />
151
+ {/each}
141
152
  </div>
142
153
  </div>
143
- </div>
144
154
 
145
- <!-- OTP Input -->
146
- <div class="flex justify-center gap-8 mb-8" role="group" aria-label="Verification code">
147
- {#each digits as digit, i}
148
- <input
149
- bind:this={inputs[i]}
150
- bind:value={digits[i]}
151
- type="text"
152
- inputmode="numeric"
153
- autocomplete={i === 0 ? 'one-time-code' : 'off'}
154
- maxlength="1"
155
- pattern="[0-9]"
156
- class="w-48 h-56 text-center text-h3 font-semibold text-body bg-input-bg border-2 rounded-8 outline-none transition-all duration-fast
157
- {showError
158
- ? 'border-error'
159
- : digit
160
- ? 'border-primary'
161
- : 'border-input-border'}
162
- focus:border-primary focus:ring-2 focus:ring-primary/20"
163
- oninput={(e) => handleInput(i, e)}
164
- onkeydown={(e) => handleKeydown(i, e)}
165
- onpaste={handlePaste}
166
- aria-label="Digit {i + 1}"
167
- />
168
- {/each}
169
- </div>
155
+ <!-- Delivery Status -->
156
+ <div class="mb-16 flex justify-center">
157
+ <div class="flex justify-center gap-0" role="status" aria-label="Code delivery status">
158
+ <!-- Sent -->
159
+ <div class="flex flex-col items-center" style="min-width: 56px">
160
+ <div
161
+ class="w-24 h-24 rounded-full flex items-center justify-center shrink-0
162
+ {hasFailed
163
+ ? 'bg-error text-inverse'
164
+ : sentComplete
165
+ ? 'bg-primary text-inverse'
166
+ : 'bg-muted text-secondary'}
167
+ {deliveryStatus === 'sending' ? ' animate-pulse' : ''}"
168
+ >
169
+ {#if sentComplete && !hasFailed}
170
+ <svg width="12" height="12" viewBox="0 0 256 256" fill="currentColor">
171
+ <path d="m229.66 77.66l-128 128a8 8 0 0 1-11.32 0l-56-56a8 8 0 0 1 11.32-11.32L96 188.69L218.34 66.34a8 8 0 0 1 11.32 11.32" />
172
+ </svg>
173
+ {:else if hasFailed}
174
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3">
175
+ <line x1="18" y1="6" x2="6" y2="18"></line>
176
+ <line x1="6" y1="6" x2="18" y2="18"></line>
177
+ </svg>
178
+ {/if}
179
+ </div>
180
+ <span class="mt-4 text-[10px] text-secondary">Sent</span>
181
+ </div>
170
182
 
171
- {#if showError}
172
- <p class="text-tiny text-error text-center mb-16">Invalid code. Please try again.</p>
173
- {/if}
183
+ <!-- Connector -->
184
+ <div class="flex items-start pt-12">
185
+ <div
186
+ class="mx-6 h-[2px] w-80 transition-colors duration-500
187
+ {hasFailed ? 'bg-error' : deliveredComplete ? 'bg-primary' : 'bg-muted'}"
188
+ ></div>
189
+ </div>
174
190
 
175
- <!-- Resend -->
176
- <div class="mt-20">
177
- {#if resendTimer > 0}
178
- <p class="text-small text-secondary m-0">
179
- Resend code in <span class="text-body font-medium">{resendTimer}s</span>
180
- </p>
181
- {:else}
191
+ <!-- Delivered -->
192
+ <div class="flex flex-col items-center" style="min-width: 56px">
193
+ <div
194
+ class="w-24 h-24 rounded-full flex items-center justify-center shrink-0
195
+ {hasFailed
196
+ ? 'bg-error text-inverse'
197
+ : deliveredComplete
198
+ ? 'bg-primary text-inverse'
199
+ : isInFlight
200
+ ? 'bg-primary text-inverse animate-pulse'
201
+ : 'bg-muted text-secondary'}"
202
+ >
203
+ {#if deliveredComplete}
204
+ <svg width="12" height="12" viewBox="0 0 256 256" fill="currentColor">
205
+ <path d="m229.66 77.66l-128 128a8 8 0 0 1-11.32 0l-56-56a8 8 0 0 1 11.32-11.32L96 188.69L218.34 66.34a8 8 0 0 1 11.32 11.32" />
206
+ </svg>
207
+ {:else if hasFailed}
208
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3">
209
+ <line x1="18" y1="6" x2="6" y2="18"></line>
210
+ <line x1="6" y1="6" x2="18" y2="18"></line>
211
+ </svg>
212
+ {/if}
213
+ </div>
214
+ <span class="mt-4 text-[10px] text-secondary">
215
+ {#if hasFailed}Failed{:else}Delivered{/if}
216
+ </span>
217
+ </div>
218
+ </div>
219
+ </div>
220
+
221
+ {#if deliveryError}
222
+ <p class="mt-0 mb-16 p-0 text-center text-default text-error">{deliveryError}</p>
223
+ {/if}
224
+ </form>
225
+
226
+ <!-- Help text — pushed to bottom on mobile -->
227
+ <div class="mb-24 mt-auto">
228
+ <p class="m-0 text-tiny text-tertiary">
229
+ Didn't receive the code?
182
230
  <button
183
- class="text-small text-accent font-medium bg-transparent border-0 cursor-default p-0 underline"
231
+ type="button"
232
+ class="cursor-default border-0 bg-transparent p-0 text-tiny text-accent underline"
233
+ disabled={isResending}
184
234
  onclick={() => {
185
- resendTimer = 30
235
+ isResending = true
186
236
  deliveryStatus = 'sending'
187
237
  setTimeout(() => (deliveryStatus = 'sent'), 800)
188
- setTimeout(() => (deliveryStatus = 'delivered'), 2000)
238
+ setTimeout(() => { deliveryStatus = 'delivered'; isResending = false }, 2000)
189
239
  }}
190
240
  >
191
- Resend code
241
+ {isResending ? 'Sending...' : 'Resend code'}
192
242
  </button>
193
- {/if}
243
+ or
244
+ <button
245
+ type="button"
246
+ class="cursor-default border-0 bg-transparent p-0 text-tiny text-accent underline"
247
+ onclick={onBack}
248
+ >
249
+ try a different email
250
+ </button>
251
+ </p>
252
+ </div>
194
253
  </div>
195
254
 
196
- <!-- CTA pinned to bottom on mobile, inline on tablet+ -->
197
- <div class="mt-auto md:mt-48 pb-24 md:pb-0">
198
- <!-- Help text -->
199
- <div class="mb-24">
200
- <p class="text-tiny text-tertiary m-0">
201
- Didn't receive the code? Check your spam folder or
202
- <button class="text-accent bg-transparent border-0 cursor-default text-tiny p-0 underline">try a different email</button>
203
- </p>
204
- </div>
255
+ <!-- Persistent bottom area step indicator + button (doesn't fade) -->
256
+ <div class="pb-24 md:pb-0">
205
257
  {#if stepIndicator}
206
- <div class="mt-56 mb-24">
258
+ <div class="mb-24 mt-24">
207
259
  {@render stepIndicator()}
208
260
  </div>
209
261
  {:else if !hideStepIndicator}
210
262
  <!-- Step indicator -->
211
- <div class="flex items-center justify-center gap-8 mt-56 mb-24">
263
+ <div class="flex items-center justify-center gap-8 mb-24 mt-24">
212
264
  <div class="h-6 rounded-full" style="background: var(--ii-gray-300); width: 16px; transition: width 0.4s ease, background-color 0.4s ease"></div>
213
265
  <div class="h-6 rounded-full" style="background: var(--ii-primary); width: 36px; transition: width 0.4s ease, background-color 0.4s ease"></div>
214
266
  </div>
215
267
  {/if}
216
- <IIButton
217
- variant="primary"
218
- class="w-full"
219
- disabled={!isComplete}
220
- loading={verifying}
221
- onclick={() => {
222
- verifying = true
223
- setTimeout(() => {
224
- verifying = false
225
- onContinue?.()
226
- }, 800)
227
- }}
228
- >
229
- {#snippet children()}
230
- Verify
231
- {/snippet}
232
- </IIButton>
268
+ {#if hasFailed}
269
+ <IIButton
270
+ variant="primary"
271
+ class="w-full"
272
+ type="button"
273
+ onclick={onBack}
274
+ >
275
+ {#snippet children()}
276
+ Use Different Email
277
+ {/snippet}
278
+ </IIButton>
279
+ {:else}
280
+ <IIButton
281
+ variant="primary"
282
+ class="w-full"
283
+ type="submit"
284
+ form="login-otp-form"
285
+ disabled={!isComplete || verifying}
286
+ loading={verifying}
287
+ >
288
+ {#snippet children()}
289
+ Verify
290
+ {/snippet}
291
+ </IIButton>
292
+ {/if}
233
293
  </div>
234
294
  </div>
235
295
 
@@ -283,3 +343,14 @@
283
343
  </div>
284
344
  </DensityProvider>
285
345
  </div>
346
+
347
+ <style>
348
+ .step-content {
349
+ opacity: 1;
350
+ transition: opacity 200ms ease;
351
+ }
352
+
353
+ .step-content.fade-out {
354
+ opacity: 0;
355
+ }
356
+ </style>