@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.
- package/dist/IIButton/IIButton.svelte +1 -1
- package/dist/IICombobox/IICombobox.svelte +7 -0
- package/dist/IICombobox/IICombobox.svelte.d.ts +1 -0
- package/dist/IIDateInput/IIDateInput.svelte +2 -2
- package/dist/IIIconButton/IIIconButton.svelte +1 -1
- package/dist/IIInput/IIInput.svelte +2 -2
- package/dist/IISegmentedControl/IISegmentedControl.svelte +73 -19
- package/dist/IISegmentedControl/IISegmentedControl.svelte.d.ts +0 -1
- package/dist/IISegmentedControl/IISegmentedControlStories.svelte +10 -26
- package/dist/IISwitch/IISwitch.svelte +1 -1
- package/dist/IIToggle/IIToggle.svelte +1 -1
- package/dist/MobileOnboarding/LoginFlowBare.svelte +13 -2
- package/dist/MobileOnboarding/LoginFlowBareAlt.svelte +968 -0
- package/dist/MobileOnboarding/LoginFlowBareAlt.svelte.d.ts +3 -0
- package/dist/MobileOnboarding/LoginScreenBare.svelte +84 -67
- package/dist/MobileOnboarding/LoginScreenBare.svelte.d.ts +4 -0
- package/dist/MobileOnboarding/OTPScreenBare.svelte +196 -125
- package/dist/MobileOnboarding/OTPScreenBare.svelte.d.ts +4 -0
- package/dist/style/themes.css +145 -0
- package/package.json +1 -1
|
@@ -0,0 +1,968 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import {onMount} from 'svelte'
|
|
3
|
+
import DensityProvider from '../DensityProvider/DensityProvider.svelte'
|
|
4
|
+
import IIInput from '../IIInput/IIInput.svelte'
|
|
5
|
+
import IIButton from '../IIButton/IIButton.svelte'
|
|
6
|
+
import IIAlert from '../IIAlert/IIAlert.svelte'
|
|
7
|
+
import IIDateInput from '../IIDateInput/IIDateInput.svelte'
|
|
8
|
+
import IIDropdownInput from '../IIDropdownInput/IIDropdownInput.svelte'
|
|
9
|
+
import IISwitch from '../IISwitch/IISwitch.svelte'
|
|
10
|
+
import IISegmentedControl from '../IISegmentedControl/IISegmentedControl.svelte'
|
|
11
|
+
|
|
12
|
+
// --- Flow state ---
|
|
13
|
+
type Step = 'email' | 'otp' | 'profile' | 'about' | 'financial' | 'review'
|
|
14
|
+
let step: Step = $state('email')
|
|
15
|
+
let transitioning = $state(false)
|
|
16
|
+
const FADE_DURATION = 200
|
|
17
|
+
|
|
18
|
+
function handleFocusIn(e: FocusEvent) {
|
|
19
|
+
const t = e.target as HTMLElement
|
|
20
|
+
if (t.tagName === 'INPUT' || t.tagName === 'SELECT' || t.tagName === 'BUTTON') {
|
|
21
|
+
setTimeout(() => t.scrollIntoView({block: 'center', behavior: 'smooth'}), 100)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function handleFocusTrap(e: KeyboardEvent) {
|
|
26
|
+
if (e.key !== 'Tab') return
|
|
27
|
+
const container = e.currentTarget as HTMLElement
|
|
28
|
+
const focusable = container.querySelectorAll<HTMLElement>(
|
|
29
|
+
'input:not([disabled]), select:not([disabled]), button:not([disabled]), [tabindex]:not([tabindex="-1"])'
|
|
30
|
+
)
|
|
31
|
+
if (focusable.length === 0) return
|
|
32
|
+
const first = focusable[0]
|
|
33
|
+
const last = focusable[focusable.length - 1]
|
|
34
|
+
if (e.shiftKey && document.activeElement === first) {
|
|
35
|
+
e.preventDefault()
|
|
36
|
+
last.focus()
|
|
37
|
+
} else if (!e.shiftKey && document.activeElement === last) {
|
|
38
|
+
e.preventDefault()
|
|
39
|
+
first.focus()
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function transitionTo(newStep: Step) {
|
|
44
|
+
transitioning = true
|
|
45
|
+
formScrolled = false
|
|
46
|
+
setTimeout(() => {
|
|
47
|
+
step = newStep
|
|
48
|
+
transitioning = false
|
|
49
|
+
}, FADE_DURATION)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// --- Email state ---
|
|
53
|
+
let email = $state('')
|
|
54
|
+
let emailError = $state('')
|
|
55
|
+
let emailLoading = $state(false)
|
|
56
|
+
|
|
57
|
+
// --- OTP state ---
|
|
58
|
+
let digits = $state<string[]>(['', '', '', '', '', ''])
|
|
59
|
+
let inputs: HTMLInputElement[] = []
|
|
60
|
+
let otpError = $state('')
|
|
61
|
+
let verifying = $state(false)
|
|
62
|
+
let isResending = $state(false)
|
|
63
|
+
|
|
64
|
+
let deliveryStatus = $state<'idle' | 'sending' | 'sent' | 'delivered' | 'bounced' | 'failed'>('delivered')
|
|
65
|
+
let deliveryError = $state('')
|
|
66
|
+
|
|
67
|
+
const code = $derived(digits.join(''))
|
|
68
|
+
const isComplete = $derived(code.length === 6 && digits.every((d) => d !== ''))
|
|
69
|
+
const hasFailed = $derived(deliveryStatus === 'bounced' || deliveryStatus === 'failed')
|
|
70
|
+
const sentComplete = $derived(deliveryStatus !== 'idle')
|
|
71
|
+
const deliveredComplete = $derived(deliveryStatus === 'delivered')
|
|
72
|
+
const isInFlight = $derived(deliveryStatus === 'sent' || deliveryStatus === 'sending')
|
|
73
|
+
|
|
74
|
+
function handleOtpInput(index: number, event: Event) {
|
|
75
|
+
const target = event.target as HTMLInputElement
|
|
76
|
+
const val = target.value.replace(/[^a-zA-Z0-9]/g, '').toUpperCase()
|
|
77
|
+
digits[index] = val.slice(-1)
|
|
78
|
+
target.value = digits[index]
|
|
79
|
+
if (digits[index] && index < 5) {
|
|
80
|
+
inputs[index + 1]?.focus()
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function handleOtpKeydown(index: number, event: KeyboardEvent) {
|
|
85
|
+
if (event.key === 'Backspace' && !digits[index] && index > 0) {
|
|
86
|
+
inputs[index - 1]?.focus()
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function handleOtpPaste(event: ClipboardEvent) {
|
|
91
|
+
event.preventDefault()
|
|
92
|
+
const paste = event.clipboardData
|
|
93
|
+
?.getData('text')
|
|
94
|
+
?.replace(/[^a-zA-Z0-9]/g, '')
|
|
95
|
+
?.toUpperCase()
|
|
96
|
+
?.slice(0, 6)
|
|
97
|
+
if (paste) {
|
|
98
|
+
for (let i = 0; i < 6; i++) {
|
|
99
|
+
digits[i] = paste[i] || ''
|
|
100
|
+
}
|
|
101
|
+
const nextEmpty = paste.length < 6 ? paste.length : 5
|
|
102
|
+
inputs[nextEmpty]?.focus()
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// --- Profile state ---
|
|
107
|
+
let firstName = $state('')
|
|
108
|
+
let lastName = $state('')
|
|
109
|
+
let dob = $state<any>(undefined)
|
|
110
|
+
let phone = $state('')
|
|
111
|
+
let accountUpdates = $state(true)
|
|
112
|
+
let promotional = $state(true)
|
|
113
|
+
let streetAddress = $state('')
|
|
114
|
+
let city = $state('')
|
|
115
|
+
let stateValue = $state<string | undefined>(undefined)
|
|
116
|
+
let zip = $state('')
|
|
117
|
+
let formScrolled = $state(false)
|
|
118
|
+
|
|
119
|
+
// --- About You state ---
|
|
120
|
+
let loanAmount = $state('')
|
|
121
|
+
let bestTimeToCall = $state<string | undefined>(undefined)
|
|
122
|
+
let ssn = $state('')
|
|
123
|
+
let idType = $state('dl')
|
|
124
|
+
let idNumber = $state('')
|
|
125
|
+
let issuingState = $state<string | undefined>(undefined)
|
|
126
|
+
let militaryStatus = $state<string | undefined>(undefined)
|
|
127
|
+
let timeAtAddress = $state<string | undefined>(undefined)
|
|
128
|
+
let ownOrRent = $state('rent')
|
|
129
|
+
|
|
130
|
+
// --- Financial state ---
|
|
131
|
+
let incomeType = $state<string | undefined>(undefined)
|
|
132
|
+
let annualIncome = $state('')
|
|
133
|
+
let payFrequency = $state('biweekly')
|
|
134
|
+
let payType = $state('direct-deposit')
|
|
135
|
+
let bankName = $state('')
|
|
136
|
+
let routingNumber = $state('')
|
|
137
|
+
let accountNumber = $state('')
|
|
138
|
+
let accountType = $state('checking')
|
|
139
|
+
let accountAge = $state<string | undefined>(undefined)
|
|
140
|
+
|
|
141
|
+
const CALL_TIMES = [
|
|
142
|
+
{value: 'morning', label: 'Morning (8am-12pm)'}, {value: 'afternoon', label: 'Afternoon (12pm-5pm)'},
|
|
143
|
+
{value: 'evening', label: 'Evening (5pm-8pm)'}, {value: 'anytime', label: 'Anytime'},
|
|
144
|
+
]
|
|
145
|
+
const ID_TYPES = [{value: 'dl', label: "Driver's License"}, {value: 'stId', label: "State ID"}]
|
|
146
|
+
const MILITARY_OPTIONS = [
|
|
147
|
+
{value: 'no', label: 'No Military Affiliation'}, {value: 'active-duty', label: 'Active Duty Military'},
|
|
148
|
+
{value: 'veteran', label: 'Veteran'}, {value: 'dependent', label: 'Military Dependent'},
|
|
149
|
+
]
|
|
150
|
+
const TIME_AT_ADDRESS = [
|
|
151
|
+
{value: 'less-than-1-year', label: 'Less than 1 year'}, {value: '1-2-years', label: '1-2 years'},
|
|
152
|
+
{value: '2-5-years', label: '2-5 years'}, {value: '5-10-years', label: '5-10 years'},
|
|
153
|
+
{value: 'more-than-10-years', label: 'More than 10 years'},
|
|
154
|
+
]
|
|
155
|
+
const OWN_RENT = [{value: 'rent', label: 'Rent'}, {value: 'own', label: 'Own'}, {value: 'other', label: 'Other'}]
|
|
156
|
+
const INCOME_TYPES = [
|
|
157
|
+
{value: 'employment', label: 'Employment'}, {value: 'self-employed', label: 'Self-Employed'},
|
|
158
|
+
{value: 'social-security', label: 'Social Security'}, {value: 'disability', label: 'Disability'},
|
|
159
|
+
{value: 'retirement', label: 'Retirement'}, {value: 'other', label: 'Other'},
|
|
160
|
+
]
|
|
161
|
+
const PAY_FREQUENCIES = [
|
|
162
|
+
{value: 'biweekly', label: 'Bi-wk'}, {value: 'weekly', label: 'Wk'},
|
|
163
|
+
{value: 'half-month', label: '2x/mo'}, {value: 'monthly', label: 'Mo'},
|
|
164
|
+
]
|
|
165
|
+
const PAY_TYPES = [{value: 'direct-deposit', label: 'Direct Dep.'}, {value: 'check', label: 'Check'}, {value: 'cash', label: 'Cash'}]
|
|
166
|
+
const ACCOUNT_TYPES = [{value: 'checking', label: 'Checking'}, {value: 'savings', label: 'Savings'}]
|
|
167
|
+
const ACCOUNT_AGES = [
|
|
168
|
+
{value: 'less-than-6-months', label: 'Less than 6 months'}, {value: '6-12-months', label: '6-12 months'},
|
|
169
|
+
{value: '1-2-years', label: '1-2 years'}, {value: '2-5-years', label: '2-5 years'},
|
|
170
|
+
{value: 'more-than-5-years', label: 'More than 5 years'},
|
|
171
|
+
]
|
|
172
|
+
|
|
173
|
+
const US_STATES = [
|
|
174
|
+
{value: 'AL', label: 'Alabama'}, {value: 'AK', label: 'Alaska'}, {value: 'AZ', label: 'Arizona'},
|
|
175
|
+
{value: 'AR', label: 'Arkansas'}, {value: 'CA', label: 'California'}, {value: 'CO', label: 'Colorado'},
|
|
176
|
+
{value: 'CT', label: 'Connecticut'}, {value: 'DE', label: 'Delaware'}, {value: 'FL', label: 'Florida'},
|
|
177
|
+
{value: 'GA', label: 'Georgia'}, {value: 'HI', label: 'Hawaii'}, {value: 'ID', label: 'Idaho'},
|
|
178
|
+
{value: 'IL', label: 'Illinois'}, {value: 'IN', label: 'Indiana'}, {value: 'IA', label: 'Iowa'},
|
|
179
|
+
{value: 'KS', label: 'Kansas'}, {value: 'KY', label: 'Kentucky'}, {value: 'LA', label: 'Louisiana'},
|
|
180
|
+
{value: 'ME', label: 'Maine'}, {value: 'MD', label: 'Maryland'}, {value: 'MA', label: 'Massachusetts'},
|
|
181
|
+
{value: 'MI', label: 'Michigan'}, {value: 'MN', label: 'Minnesota'}, {value: 'MS', label: 'Mississippi'},
|
|
182
|
+
{value: 'MO', label: 'Missouri'}, {value: 'MT', label: 'Montana'}, {value: 'NE', label: 'Nebraska'},
|
|
183
|
+
{value: 'NV', label: 'Nevada'}, {value: 'NH', label: 'New Hampshire'}, {value: 'NJ', label: 'New Jersey'},
|
|
184
|
+
{value: 'NM', label: 'New Mexico'}, {value: 'NY', label: 'New York'}, {value: 'NC', label: 'North Carolina'},
|
|
185
|
+
{value: 'ND', label: 'North Dakota'}, {value: 'OH', label: 'Ohio'}, {value: 'OK', label: 'Oklahoma'},
|
|
186
|
+
{value: 'OR', label: 'Oregon'}, {value: 'PA', label: 'Pennsylvania'}, {value: 'RI', label: 'Rhode Island'},
|
|
187
|
+
{value: 'SC', label: 'South Carolina'}, {value: 'SD', label: 'South Dakota'}, {value: 'TN', label: 'Tennessee'},
|
|
188
|
+
{value: 'TX', label: 'Texas'}, {value: 'UT', label: 'Utah'}, {value: 'VT', label: 'Vermont'},
|
|
189
|
+
{value: 'VA', label: 'Virginia'}, {value: 'WA', label: 'Washington'}, {value: 'WV', label: 'West Virginia'},
|
|
190
|
+
{value: 'WI', label: 'Wisconsin'}, {value: 'WY', label: 'Wyoming'},
|
|
191
|
+
]
|
|
192
|
+
|
|
193
|
+
$effect(() => {
|
|
194
|
+
if (step === 'otp') {
|
|
195
|
+
requestAnimationFrame(() => {
|
|
196
|
+
requestAnimationFrame(() => {
|
|
197
|
+
inputs[0]?.focus()
|
|
198
|
+
})
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
if (step in APP_STEP_MAP) {
|
|
202
|
+
requestAnimationFrame(() => {
|
|
203
|
+
requestAnimationFrame(() => {
|
|
204
|
+
const form = document.querySelector(`[id^="alt-${step}"]`) || document.querySelector('.step-content')
|
|
205
|
+
const first = form?.querySelector<HTMLElement>('input:not([disabled]), select:not([disabled]), button:not([disabled]), [tabindex="0"]')
|
|
206
|
+
first?.focus()
|
|
207
|
+
})
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
// --- Application flow (after login) ---
|
|
213
|
+
const APP_STEPS = [
|
|
214
|
+
{label: 'Profile'},
|
|
215
|
+
{label: 'About You'},
|
|
216
|
+
{label: 'Financial Info'},
|
|
217
|
+
{label: 'Review'},
|
|
218
|
+
]
|
|
219
|
+
const APP_STEP_MAP: Record<string, number> = {profile: 0, about: 1, financial: 2, review: 3}
|
|
220
|
+
const isAppFlow = $derived(step in APP_STEP_MAP)
|
|
221
|
+
const appStepIndex = $derived(APP_STEP_MAP[step] ?? 0)
|
|
222
|
+
|
|
223
|
+
// --- Responsive density ---
|
|
224
|
+
let innerWidth = $state(0)
|
|
225
|
+
const densityValue = $derived(innerWidth >= 640 ? 'comfortable' : 'mobile') as 'comfortable' | 'mobile'
|
|
226
|
+
|
|
227
|
+
// --- Step indicator ---
|
|
228
|
+
const loginStepIndex = $derived(step === 'email' ? 0 : 1)
|
|
229
|
+
let mounted = $state(false)
|
|
230
|
+
onMount(() => {
|
|
231
|
+
requestAnimationFrame(() => {
|
|
232
|
+
mounted = true
|
|
233
|
+
})
|
|
234
|
+
})
|
|
235
|
+
</script>
|
|
236
|
+
|
|
237
|
+
<svelte:window bind:innerWidth />
|
|
238
|
+
|
|
239
|
+
<div class="flex flex-col h-screen overflow-hidden">
|
|
240
|
+
<!-- Outer wrapper -->
|
|
241
|
+
<div class="dot-grid flex flex-1 min-h-0 flex-col min-[640px]:items-center {isAppFlow ? 'min-[640px]:pt-[8vh] min-[640px]:px-24 min-[640px]:h-screen' : 'min-[640px]:p-24 min-[640px]:flex-initial min-[640px]:h-auto min-[640px]:min-h-screen min-[640px]:justify-center'} min-[640px]:relative">
|
|
242
|
+
<!-- Desktop page-level logo — app flow only -->
|
|
243
|
+
{#if isAppFlow}
|
|
244
|
+
<div class="hidden min-[640px]:flex items-center gap-8 absolute top-24 left-24 row-enter row-delay-1">
|
|
245
|
+
<svg width="28" height="28" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
246
|
+
<path d="M16 2L4 9v14l12 7 12-7V9L16 2z" fill="var(--ii-primary)" />
|
|
247
|
+
<path d="M16 6l-8 4.5v9L16 24l8-4.5v-9L16 6z" fill="white" />
|
|
248
|
+
<path d="M16 10l-4 2.25v4.5L16 19l4-2.25v-4.5L16 10z" fill="var(--ii-primary)" />
|
|
249
|
+
</svg>
|
|
250
|
+
<span class="text-h3 text-body">Meridian Finance</span>
|
|
251
|
+
</div>
|
|
252
|
+
{/if}
|
|
253
|
+
<DensityProvider density={densityValue}>
|
|
254
|
+
<div class="flex flex-1 min-h-0 flex-col min-[640px]:relative min-[640px]:flex-initial">
|
|
255
|
+
<!-- Desktop vertical stepper — app flow only, positioned to left of card -->
|
|
256
|
+
{#if isAppFlow}
|
|
257
|
+
<div class="hidden min-[1040px]:flex flex-col gap-0 absolute right-full mr-32 pt-48 w-[104px]">
|
|
258
|
+
{#each APP_STEPS as appStep, i}
|
|
259
|
+
<div class="row-enter flex items-start gap-12" style="animation-delay: {80 + i * 70}ms">
|
|
260
|
+
<!-- Dot + line -->
|
|
261
|
+
<div class="flex flex-col items-center">
|
|
262
|
+
<div
|
|
263
|
+
class="rounded-full shrink-0 w-12 h-12 transition-all duration-300 ease-in-out
|
|
264
|
+
{i <= appStepIndex ? 'bg-primary' : 'bg-gray-300'}"
|
|
265
|
+
style="transition-delay: {i <= appStepIndex && i > 0 ? '400ms' : '0ms'}"
|
|
266
|
+
></div>
|
|
267
|
+
{#if i < APP_STEPS.length - 1}
|
|
268
|
+
<div class="w-[2px] h-32 relative overflow-hidden bg-gray-200">
|
|
269
|
+
<div
|
|
270
|
+
class="absolute inset-x-0 top-0 bg-primary transition-all duration-500 ease-in-out"
|
|
271
|
+
style="height: {i < appStepIndex ? '100%' : '0%'}; transition-delay: {i < appStepIndex ? '100ms' : '0ms'}"
|
|
272
|
+
></div>
|
|
273
|
+
</div>
|
|
274
|
+
{/if}
|
|
275
|
+
</div>
|
|
276
|
+
<!-- Label -->
|
|
277
|
+
<span class="text-small font-medium -mt-2 whitespace-nowrap transition-colors duration-300 {i === appStepIndex ? 'text-body' : i < appStepIndex ? 'text-secondary' : 'text-tertiary'}">
|
|
278
|
+
{appStep.label}
|
|
279
|
+
</span>
|
|
280
|
+
</div>
|
|
281
|
+
{/each}
|
|
282
|
+
</div>
|
|
283
|
+
{/if}
|
|
284
|
+
|
|
285
|
+
<div class="flex min-w-0 min-h-0 flex-1 flex-col min-[640px]:flex-initial min-[640px]:rounded-16 {!isAppFlow ? 'min-[640px]:w-[440px] card-border min-[640px]:h-[574px]' : 'overflow-hidden card-border-tl min-[640px]:flex-1 card-width-responsive'}">
|
|
286
|
+
<!-- Form side -->
|
|
287
|
+
<div class="flex w-full min-w-0 min-h-0 flex-1 flex-col {isAppFlow ? 'min-[640px]:pt-40' : 'px-16 min-[375px]:px-24 min-[640px]:p-40'}">
|
|
288
|
+
<!-- Persistent logo + step indicator for app flow (doesn't fade) -->
|
|
289
|
+
{#if isAppFlow}
|
|
290
|
+
<div class="flex shrink-0 items-center gap-8 pt-24 min-[640px]:hidden px-24">
|
|
291
|
+
<svg width="24" height="24" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
292
|
+
<path d="M16 2L4 9v14l12 7 12-7V9L16 2z" fill="var(--ii-primary)" />
|
|
293
|
+
<path d="M16 6l-8 4.5v9L16 24l8-4.5v-9L16 6z" fill="white" />
|
|
294
|
+
<path d="M16 10l-4 2.25v4.5L16 19l4-2.25v-4.5L16 10z" fill="var(--ii-primary)" />
|
|
295
|
+
</svg>
|
|
296
|
+
<span class="text-emphasis text-body">Meridian Finance</span>
|
|
297
|
+
</div>
|
|
298
|
+
<div class="flex items-center justify-center gap-8 pt-16 min-[1040px]:hidden px-24">
|
|
299
|
+
{#each APP_STEPS as _, i}
|
|
300
|
+
<div class="h-6 rounded-full" style="background: {i === appStepIndex ? 'var(--ii-primary)' : 'var(--ii-gray-300)'}; width: {i === appStepIndex ? '36px' : '16px'}; transition: width 0.4s ease, background-color 0.4s ease;"></div>
|
|
301
|
+
{/each}
|
|
302
|
+
</div>
|
|
303
|
+
{/if}
|
|
304
|
+
|
|
305
|
+
<!-- Content that fades between steps -->
|
|
306
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
307
|
+
<div class="step-content flex flex-1 min-h-0 flex-col" class:fade-out={transitioning} class:overflow-y-auto={!isAppFlow} onkeydown={isAppFlow ? handleFocusTrap : undefined} onfocusin={handleFocusIn}>
|
|
308
|
+
<!-- Push content toward center on mobile (login flow only) -->
|
|
309
|
+
{#if !isAppFlow}
|
|
310
|
+
<div class="h-[12vh] min-[640px]:hidden"></div>
|
|
311
|
+
{/if}
|
|
312
|
+
|
|
313
|
+
{#if step === 'email'}
|
|
314
|
+
<!-- Logo -->
|
|
315
|
+
<div class="row-enter row-delay-1 flex h-56 shrink-0 items-center justify-center gap-8 pt-24 min-[640px]:pt-0">
|
|
316
|
+
<svg width="28" height="28" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
317
|
+
<path d="M16 2L4 9v14l12 7 12-7V9L16 2z" fill="var(--ii-primary)" />
|
|
318
|
+
<path d="M16 6l-8 4.5v9L16 24l8-4.5v-9L16 6z" fill="white" />
|
|
319
|
+
<path d="M16 10l-4 2.25v4.5L16 19l4-2.25v-4.5L16 10z" fill="var(--ii-primary)" />
|
|
320
|
+
</svg>
|
|
321
|
+
<span class="text-h3 text-body">Meridian Finance</span>
|
|
322
|
+
</div>
|
|
323
|
+
|
|
324
|
+
<!-- Header -->
|
|
325
|
+
<div class="row-enter row-delay-2 pt-48 pb-16 text-center mx-auto w-full max-w-[320px]">
|
|
326
|
+
<div class="text-h2 text-body m-0">Your Loan Application</div>
|
|
327
|
+
<p class="text-m3-emphasis text-tertiary mt-4 m-0">
|
|
328
|
+
Enter your email to get started or continue
|
|
329
|
+
</p>
|
|
330
|
+
</div>
|
|
331
|
+
|
|
332
|
+
<!-- Input -->
|
|
333
|
+
<form id="alt-email-form" class="row-enter row-delay-3 flex flex-col mx-auto w-full max-w-[320px] p-4 -m-4" onsubmit={(e) => { e.preventDefault(); emailLoading = true; setTimeout(() => { emailLoading = false; transitionTo('otp') }, 800) }}>
|
|
334
|
+
{#if emailError}
|
|
335
|
+
<div class="mb-16">
|
|
336
|
+
<IIAlert variant="error" dismissible onDismiss={() => (emailError = '')}>
|
|
337
|
+
{#snippet children()}
|
|
338
|
+
{emailError}
|
|
339
|
+
{/snippet}
|
|
340
|
+
</IIAlert>
|
|
341
|
+
</div>
|
|
342
|
+
{/if}
|
|
343
|
+
|
|
344
|
+
<IIInput
|
|
345
|
+
type="email"
|
|
346
|
+
placeholder="name@example.com"
|
|
347
|
+
bind:value={email}
|
|
348
|
+
inputmode="email"
|
|
349
|
+
autocomplete="email"
|
|
350
|
+
error={!!emailError}
|
|
351
|
+
autofocus
|
|
352
|
+
/>
|
|
353
|
+
</form>
|
|
354
|
+
|
|
355
|
+
<!-- Info section -->
|
|
356
|
+
<div class="row-enter row-delay-4 mb-24 mt-auto pt-24 mx-auto w-full max-w-[320px]">
|
|
357
|
+
<p class="text-emphasis text-body m-0 mb-12">Here's what happens next:</p>
|
|
358
|
+
<ul class="m-0 list-none p-0 flex flex-col gap-8">
|
|
359
|
+
<li class="relative pl-20 text-default text-secondary">
|
|
360
|
+
<span class="absolute left-0 text-accent">✓</span>
|
|
361
|
+
New here? We'll help you get started
|
|
362
|
+
</li>
|
|
363
|
+
<li class="relative pl-20 text-default text-secondary">
|
|
364
|
+
<span class="absolute left-0 text-accent">✓</span>
|
|
365
|
+
Already applied? Pick up right where you left off
|
|
366
|
+
</li>
|
|
367
|
+
<li class="relative pl-20 text-default text-secondary">
|
|
368
|
+
<span class="absolute left-0 text-accent">✓</span>
|
|
369
|
+
All done? Check your loan status
|
|
370
|
+
</li>
|
|
371
|
+
</ul>
|
|
372
|
+
</div>
|
|
373
|
+
|
|
374
|
+
{:else if step === 'otp'}
|
|
375
|
+
<!-- Back button -->
|
|
376
|
+
<div class="row-enter row-delay-1 flex h-56 shrink-0 items-center justify-center gap-10 pt-24 min-[640px]:pt-0">
|
|
377
|
+
<IIButton variant="ghost" size="sm" onclick={() => transitionTo('email')}>
|
|
378
|
+
{#snippet children()}
|
|
379
|
+
← Back
|
|
380
|
+
{/snippet}
|
|
381
|
+
</IIButton>
|
|
382
|
+
</div>
|
|
383
|
+
|
|
384
|
+
<!-- Header -->
|
|
385
|
+
<div class="row-enter row-delay-2 pt-48 pb-16 text-center mx-auto w-full max-w-[320px]">
|
|
386
|
+
<div class="text-h2 text-body m-0">Enter verification code</div>
|
|
387
|
+
<p class="text-m3-emphasis text-tertiary mt-4 m-0">
|
|
388
|
+
We sent a 6-digit code to <strong class="text-body">john@example.com</strong>
|
|
389
|
+
</p>
|
|
390
|
+
</div>
|
|
391
|
+
|
|
392
|
+
<!-- OTP Input -->
|
|
393
|
+
<form id="alt-otp-form" class="row-enter row-delay-3 flex flex-col w-full" onsubmit={(e) => { e.preventDefault(); verifying = true; setTimeout(() => { verifying = false; transitionTo('profile') }, 800) }}>
|
|
394
|
+
{#if otpError}
|
|
395
|
+
<div class="mb-16">
|
|
396
|
+
<IIAlert variant="error" dismissible onDismiss={() => (otpError = '')}>
|
|
397
|
+
{#snippet children()}
|
|
398
|
+
{otpError}
|
|
399
|
+
{/snippet}
|
|
400
|
+
</IIAlert>
|
|
401
|
+
</div>
|
|
402
|
+
{/if}
|
|
403
|
+
|
|
404
|
+
<div class="mb-16">
|
|
405
|
+
<div class="flex gap-8 max-w-[320px] mx-auto" role="group" aria-label="Verification code">
|
|
406
|
+
{#each digits as digit, i}
|
|
407
|
+
<input
|
|
408
|
+
bind:this={inputs[i]}
|
|
409
|
+
bind:value={digits[i]}
|
|
410
|
+
type="text"
|
|
411
|
+
inputmode="text"
|
|
412
|
+
autocomplete={i === 0 ? 'one-time-code' : 'off'}
|
|
413
|
+
maxlength="1"
|
|
414
|
+
pattern="[a-zA-Z0-9]"
|
|
415
|
+
class="w-full h-auto aspect-square text-center !text-m1 font-medium text-body bg-input-bg border-2 rounded-8 outline-none transition-all duration-fast
|
|
416
|
+
{otpError
|
|
417
|
+
? 'border-error'
|
|
418
|
+
: digit
|
|
419
|
+
? 'border-primary'
|
|
420
|
+
: 'border-input-border'}
|
|
421
|
+
focus:border-primary focus:ring-2 focus:ring-primary/20"
|
|
422
|
+
oninput={(e) => handleOtpInput(i, e)}
|
|
423
|
+
onkeydown={(e) => handleOtpKeydown(i, e)}
|
|
424
|
+
onpaste={handleOtpPaste}
|
|
425
|
+
aria-label="Digit {i + 1}"
|
|
426
|
+
/>
|
|
427
|
+
{/each}
|
|
428
|
+
</div>
|
|
429
|
+
</div>
|
|
430
|
+
|
|
431
|
+
<!-- Delivery Status -->
|
|
432
|
+
<div class="mb-16 flex justify-center">
|
|
433
|
+
<div class="flex justify-center gap-0" role="status" aria-label="Code delivery status">
|
|
434
|
+
<!-- Sent -->
|
|
435
|
+
<div class="flex flex-col items-center" style="min-width: 56px">
|
|
436
|
+
<div
|
|
437
|
+
class="w-24 h-24 rounded-full flex items-center justify-center shrink-0
|
|
438
|
+
{hasFailed
|
|
439
|
+
? 'bg-error text-inverse'
|
|
440
|
+
: sentComplete
|
|
441
|
+
? 'bg-primary text-inverse'
|
|
442
|
+
: 'bg-muted text-secondary'}
|
|
443
|
+
{deliveryStatus === 'sending' ? ' animate-pulse' : ''}"
|
|
444
|
+
>
|
|
445
|
+
{#if sentComplete && !hasFailed}
|
|
446
|
+
<svg width="12" height="12" viewBox="0 0 256 256" fill="currentColor">
|
|
447
|
+
<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" />
|
|
448
|
+
</svg>
|
|
449
|
+
{:else if hasFailed}
|
|
450
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3">
|
|
451
|
+
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
452
|
+
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
453
|
+
</svg>
|
|
454
|
+
{/if}
|
|
455
|
+
</div>
|
|
456
|
+
<span class="mt-4 text-[10px] text-secondary">Sent</span>
|
|
457
|
+
</div>
|
|
458
|
+
|
|
459
|
+
<!-- Connector -->
|
|
460
|
+
<div class="flex items-start pt-12">
|
|
461
|
+
<div
|
|
462
|
+
class="mx-6 h-[2px] w-80 transition-colors duration-500
|
|
463
|
+
{hasFailed ? 'bg-error' : deliveredComplete ? 'bg-primary' : 'bg-muted'}"
|
|
464
|
+
></div>
|
|
465
|
+
</div>
|
|
466
|
+
|
|
467
|
+
<!-- Delivered -->
|
|
468
|
+
<div class="flex flex-col items-center" style="min-width: 56px">
|
|
469
|
+
<div
|
|
470
|
+
class="w-24 h-24 rounded-full flex items-center justify-center shrink-0
|
|
471
|
+
{hasFailed
|
|
472
|
+
? 'bg-error text-inverse'
|
|
473
|
+
: deliveredComplete
|
|
474
|
+
? 'bg-primary text-inverse'
|
|
475
|
+
: isInFlight
|
|
476
|
+
? 'bg-primary text-inverse animate-pulse'
|
|
477
|
+
: 'bg-muted text-secondary'}"
|
|
478
|
+
>
|
|
479
|
+
{#if deliveredComplete}
|
|
480
|
+
<svg width="12" height="12" viewBox="0 0 256 256" fill="currentColor">
|
|
481
|
+
<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" />
|
|
482
|
+
</svg>
|
|
483
|
+
{:else if hasFailed}
|
|
484
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3">
|
|
485
|
+
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
486
|
+
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
487
|
+
</svg>
|
|
488
|
+
{/if}
|
|
489
|
+
</div>
|
|
490
|
+
<span class="mt-4 text-[10px] text-secondary">
|
|
491
|
+
{#if hasFailed}Failed{:else}Delivered{/if}
|
|
492
|
+
</span>
|
|
493
|
+
</div>
|
|
494
|
+
</div>
|
|
495
|
+
</div>
|
|
496
|
+
|
|
497
|
+
{#if deliveryError}
|
|
498
|
+
<p class="mt-0 mb-16 p-0 text-center text-default text-error">{deliveryError}</p>
|
|
499
|
+
{/if}
|
|
500
|
+
</form>
|
|
501
|
+
|
|
502
|
+
<!-- Help text -->
|
|
503
|
+
<div class="row-enter row-delay-4 mb-24 mt-auto mx-auto w-full max-w-[320px] text-center">
|
|
504
|
+
<p class="m-0 text-tiny text-tertiary leading-relaxed">
|
|
505
|
+
Didn't receive the code?
|
|
506
|
+
<button
|
|
507
|
+
type="button"
|
|
508
|
+
class="cursor-default border-0 bg-transparent p-0 text-tiny text-accent underline outline-none focus-visible:ring-3 focus-visible:ring-primary rounded-4"
|
|
509
|
+
disabled={isResending}
|
|
510
|
+
onclick={() => {
|
|
511
|
+
isResending = true
|
|
512
|
+
deliveryStatus = 'sending'
|
|
513
|
+
setTimeout(() => (deliveryStatus = 'sent'), 800)
|
|
514
|
+
setTimeout(() => { deliveryStatus = 'delivered'; isResending = false }, 2000)
|
|
515
|
+
}}
|
|
516
|
+
>
|
|
517
|
+
{isResending ? 'Sending...' : 'Resend code'}
|
|
518
|
+
</button>
|
|
519
|
+
or
|
|
520
|
+
<button
|
|
521
|
+
type="button"
|
|
522
|
+
class="cursor-default border-0 bg-transparent p-0 text-tiny text-accent underline outline-none focus-visible:ring-3 focus-visible:ring-primary rounded-4"
|
|
523
|
+
onclick={() => transitionTo('email')}
|
|
524
|
+
>
|
|
525
|
+
try a different email
|
|
526
|
+
</button>
|
|
527
|
+
</p>
|
|
528
|
+
</div>
|
|
529
|
+
{:else if step === 'profile'}
|
|
530
|
+
<!-- Sticky header -->
|
|
531
|
+
<div class="row-enter row-delay-2 pt-24 min-[1040px]:pt-0 pb-16 shrink-0 px-24 min-[640px]:px-40">
|
|
532
|
+
<div class="text-h2 text-body m-0">Update Your Info</div>
|
|
533
|
+
<p class="text-m3-emphasis text-tertiary mt-4 m-0">
|
|
534
|
+
Please provide your information to continue
|
|
535
|
+
</p>
|
|
536
|
+
</div>
|
|
537
|
+
|
|
538
|
+
<!-- Scrollable form area with top fade mask on scroll -->
|
|
539
|
+
<div
|
|
540
|
+
class="flex-1 min-h-0 overflow-y-scroll pl-24 pr-16 min-[640px]:pl-40 form-scroll-fade form-pr-responsive"
|
|
541
|
+
class:form-scroll-mask={formScrolled}
|
|
542
|
+
onscroll={(e) => { formScrolled = (e.currentTarget as HTMLElement).scrollTop > 0 }}
|
|
543
|
+
onfocusin={handleFocusIn}
|
|
544
|
+
>
|
|
545
|
+
<form id="alt-profile-form" class="row-enter row-delay-3 flex flex-col gap-16 pb-16" onsubmit={(e) => { e.preventDefault(); transitionTo('about') }}>
|
|
546
|
+
<div class="flex flex-col min-[640px]:flex-row gap-16">
|
|
547
|
+
<div class="flex-1"><IIInput label="First Name" bind:value={firstName} placeholder="George" autofocus /></div>
|
|
548
|
+
<div class="flex-1"><IIInput label="Last Name" bind:value={lastName} placeholder="Jones" /></div>
|
|
549
|
+
</div>
|
|
550
|
+
<IIDateInput label="Date of Birth" bind:value={dob} />
|
|
551
|
+
<IIInput label="Phone Number" bind:value={phone} placeholder="(555) 555-5555" type="tel" inputmode="tel" />
|
|
552
|
+
|
|
553
|
+
<!-- SMS Preferences -->
|
|
554
|
+
<div class="flex flex-col gap-12 py-8">
|
|
555
|
+
<div class="flex items-center justify-between gap-16">
|
|
556
|
+
<div>
|
|
557
|
+
<p class="text-emphasis text-body m-0">Account Updates</p>
|
|
558
|
+
<p class="text-small text-tertiary m-0">Payment reminders and notifications via SMS</p>
|
|
559
|
+
</div>
|
|
560
|
+
<IISwitch checked={accountUpdates} onCheckedChange={(v) => (accountUpdates = v)} />
|
|
561
|
+
</div>
|
|
562
|
+
<div class="flex items-center justify-between gap-16">
|
|
563
|
+
<div>
|
|
564
|
+
<p class="text-emphasis text-body m-0">Promotional</p>
|
|
565
|
+
<p class="text-small text-tertiary m-0">Special offers and news via SMS</p>
|
|
566
|
+
</div>
|
|
567
|
+
<IISwitch checked={promotional} onCheckedChange={(v) => (promotional = v)} />
|
|
568
|
+
</div>
|
|
569
|
+
</div>
|
|
570
|
+
|
|
571
|
+
<IIInput label="Street Address" bind:value={streetAddress} placeholder="123 West Elm" />
|
|
572
|
+
<IIInput label="City" bind:value={city} placeholder="Saratoga Springs" />
|
|
573
|
+
<IIDropdownInput
|
|
574
|
+
matchTriggerWidth
|
|
575
|
+
label="State"
|
|
576
|
+
placeholder="Select state"
|
|
577
|
+
items={US_STATES}
|
|
578
|
+
bind:value={stateValue}
|
|
579
|
+
/>
|
|
580
|
+
<IIInput label="ZIP Code" bind:value={zip} placeholder="84045" inputmode="numeric" />
|
|
581
|
+
</form>
|
|
582
|
+
|
|
583
|
+
<!-- Cancel link -->
|
|
584
|
+
<div class="row-enter row-delay-4 mt-8 mb-16 flex justify-center">
|
|
585
|
+
<IIButton variant="ghost" onclick={() => transitionTo('email')}>
|
|
586
|
+
{#snippet children()}
|
|
587
|
+
Cancel
|
|
588
|
+
{/snippet}
|
|
589
|
+
</IIButton>
|
|
590
|
+
</div>
|
|
591
|
+
|
|
592
|
+
<div class="row-enter" style="animation-delay: 330ms">
|
|
593
|
+
<IIButton
|
|
594
|
+
variant="primary"
|
|
595
|
+
class="w-full"
|
|
596
|
+
type="submit"
|
|
597
|
+
form="alt-profile-form"
|
|
598
|
+
>
|
|
599
|
+
{#snippet children()}
|
|
600
|
+
Continue
|
|
601
|
+
{/snippet}
|
|
602
|
+
</IIButton>
|
|
603
|
+
</div>
|
|
604
|
+
<div class="pb-48"></div>
|
|
605
|
+
</div>
|
|
606
|
+
|
|
607
|
+
{:else if step === 'about'}
|
|
608
|
+
<!-- Header -->
|
|
609
|
+
<div class="row-enter row-delay-2 pt-24 min-[1040px]:pt-0 pb-16 shrink-0 px-24 min-[640px]:px-40">
|
|
610
|
+
<div class="text-h2 text-body m-0">About You</div>
|
|
611
|
+
<p class="text-m3-emphasis text-tertiary mt-4 m-0">Tell us a bit more about yourself</p>
|
|
612
|
+
</div>
|
|
613
|
+
<!-- Scrollable form -->
|
|
614
|
+
<div
|
|
615
|
+
class="flex-1 min-h-0 overflow-y-scroll pl-24 pr-16 min-[640px]:pl-40 form-scroll-fade form-pr-responsive"
|
|
616
|
+
class:form-scroll-mask={formScrolled}
|
|
617
|
+
onscroll={(e) => { formScrolled = (e.currentTarget as HTMLElement).scrollTop > 0 }}
|
|
618
|
+
onfocusin={handleFocusIn}
|
|
619
|
+
>
|
|
620
|
+
<form id="alt-about-form" class="row-enter row-delay-3 flex flex-col gap-16 pb-16" onsubmit={(e) => { e.preventDefault(); transitionTo('financial') }}>
|
|
621
|
+
<!-- Loan -->
|
|
622
|
+
<IIInput label="Loan Amount" bind:value={loanAmount} placeholder="$5,000" inputmode="numeric" autofocus />
|
|
623
|
+
<!-- Personal -->
|
|
624
|
+
<div class="border-t border-gray-200 pt-16"></div>
|
|
625
|
+
<IIDropdownInput matchTriggerWidth label="Best Time to Call" placeholder="Select time" items={CALL_TIMES} bind:value={bestTimeToCall} />
|
|
626
|
+
<IIInput label="Social Security Number" bind:value={ssn} placeholder="XXX-XX-XXXX" />
|
|
627
|
+
<div class="flex flex-col gap-4">
|
|
628
|
+
<span class="text-small-emphasis text-secondary">ID Type</span>
|
|
629
|
+
<IISegmentedControl items={ID_TYPES} bind:value={idType} />
|
|
630
|
+
</div>
|
|
631
|
+
<IIInput label="ID Number" bind:value={idNumber} placeholder="ID number" />
|
|
632
|
+
<IIDropdownInput matchTriggerWidth label="Issuing State" placeholder="Select state" items={US_STATES} bind:value={issuingState} />
|
|
633
|
+
<!-- Military -->
|
|
634
|
+
<div class="border-t border-gray-200 pt-16"></div>
|
|
635
|
+
<IIDropdownInput matchTriggerWidth label="Military Service" placeholder="Select option" items={MILITARY_OPTIONS} bind:value={militaryStatus} />
|
|
636
|
+
<!-- Housing -->
|
|
637
|
+
<div class="border-t border-gray-200 pt-16"></div>
|
|
638
|
+
<IIDropdownInput matchTriggerWidth label="Time at Current Address" placeholder="Select duration" items={TIME_AT_ADDRESS} bind:value={timeAtAddress} />
|
|
639
|
+
<div class="flex flex-col gap-4">
|
|
640
|
+
<span class="text-small-emphasis text-secondary">Do you own or rent?</span>
|
|
641
|
+
<IISegmentedControl items={OWN_RENT} bind:value={ownOrRent} />
|
|
642
|
+
</div>
|
|
643
|
+
</form>
|
|
644
|
+
<div class="row-enter row-delay-4 mt-8 mb-16 flex justify-center">
|
|
645
|
+
<IIButton variant="ghost" onclick={() => transitionTo('profile')}>
|
|
646
|
+
{#snippet children()}Back{/snippet}
|
|
647
|
+
</IIButton>
|
|
648
|
+
</div>
|
|
649
|
+
<div class="row-enter" style="animation-delay: 330ms">
|
|
650
|
+
<IIButton variant="primary" class="w-full" type="submit" form="alt-about-form">
|
|
651
|
+
{#snippet children()}Continue{/snippet}
|
|
652
|
+
</IIButton>
|
|
653
|
+
</div>
|
|
654
|
+
<div class="pb-48"></div>
|
|
655
|
+
</div>
|
|
656
|
+
|
|
657
|
+
{:else if step === 'financial'}
|
|
658
|
+
<!-- Header -->
|
|
659
|
+
<div class="row-enter row-delay-2 pt-24 min-[1040px]:pt-0 pb-16 shrink-0 px-24 min-[640px]:px-40">
|
|
660
|
+
<div class="text-h2 text-body m-0">Financial Info</div>
|
|
661
|
+
<p class="text-m3-emphasis text-tertiary mt-4 m-0">Income and banking details</p>
|
|
662
|
+
</div>
|
|
663
|
+
<!-- Scrollable form -->
|
|
664
|
+
<div
|
|
665
|
+
class="flex-1 min-h-0 overflow-y-scroll pl-24 pr-16 min-[640px]:pl-40 form-scroll-fade form-pr-responsive"
|
|
666
|
+
class:form-scroll-mask={formScrolled}
|
|
667
|
+
onscroll={(e) => { formScrolled = (e.currentTarget as HTMLElement).scrollTop > 0 }}
|
|
668
|
+
onfocusin={handleFocusIn}
|
|
669
|
+
>
|
|
670
|
+
<form id="alt-financial-form" class="row-enter row-delay-3 flex flex-col gap-16 pb-16" onsubmit={(e) => { e.preventDefault(); transitionTo('review') }}>
|
|
671
|
+
<!-- Income -->
|
|
672
|
+
<IIDropdownInput matchTriggerWidth label="Income Type" placeholder="Select income type" items={INCOME_TYPES} bind:value={incomeType} />
|
|
673
|
+
<IIInput label="Annual Gross Income" bind:value={annualIncome} placeholder="$50,000" inputmode="numeric" />
|
|
674
|
+
<div class="flex flex-col gap-4">
|
|
675
|
+
<span class="text-small-emphasis text-secondary">Pay Frequency</span>
|
|
676
|
+
<IISegmentedControl items={PAY_FREQUENCIES} bind:value={payFrequency} />
|
|
677
|
+
</div>
|
|
678
|
+
<div class="flex flex-col gap-4">
|
|
679
|
+
<span class="text-small-emphasis text-secondary">Pay Type</span>
|
|
680
|
+
<IISegmentedControl items={PAY_TYPES} bind:value={payType} />
|
|
681
|
+
</div>
|
|
682
|
+
<!-- Banking -->
|
|
683
|
+
<div class="border-t border-gray-200 pt-16"></div>
|
|
684
|
+
<IIInput label="Bank Name" bind:value={bankName} placeholder="Bank name" />
|
|
685
|
+
<div class="flex flex-col min-[640px]:flex-row gap-16">
|
|
686
|
+
<div class="flex-1"><IIInput label="Routing Number" bind:value={routingNumber} placeholder="9 digits" inputmode="numeric" /></div>
|
|
687
|
+
<div class="flex-1"><IIInput label="Account Number" bind:value={accountNumber} placeholder="Account number" /></div>
|
|
688
|
+
</div>
|
|
689
|
+
<div class="flex flex-col gap-4">
|
|
690
|
+
<span class="text-small-emphasis text-secondary">Account Type</span>
|
|
691
|
+
<IISegmentedControl items={ACCOUNT_TYPES} bind:value={accountType} />
|
|
692
|
+
</div>
|
|
693
|
+
<IIDropdownInput matchTriggerWidth label="Account Age" placeholder="Select duration" items={ACCOUNT_AGES} bind:value={accountAge} />
|
|
694
|
+
</form>
|
|
695
|
+
<div class="row-enter row-delay-4 mt-8 mb-16 flex justify-center">
|
|
696
|
+
<IIButton variant="ghost" onclick={() => transitionTo('about')}>
|
|
697
|
+
{#snippet children()}Back{/snippet}
|
|
698
|
+
</IIButton>
|
|
699
|
+
</div>
|
|
700
|
+
<div class="row-enter" style="animation-delay: 330ms">
|
|
701
|
+
<IIButton variant="primary" class="w-full" type="submit" form="alt-financial-form">
|
|
702
|
+
{#snippet children()}Continue{/snippet}
|
|
703
|
+
</IIButton>
|
|
704
|
+
</div>
|
|
705
|
+
<div class="pb-48"></div>
|
|
706
|
+
</div>
|
|
707
|
+
|
|
708
|
+
{:else if step === 'review'}
|
|
709
|
+
<!-- Header -->
|
|
710
|
+
<div class="row-enter row-delay-2 pt-24 min-[1040px]:pt-0 pb-16 shrink-0 px-24 min-[640px]:px-40">
|
|
711
|
+
<div class="text-h2 text-body m-0">Review</div>
|
|
712
|
+
<p class="text-m3-emphasis text-tertiary mt-4 m-0">Please review your information before submitting</p>
|
|
713
|
+
</div>
|
|
714
|
+
<!-- Scrollable review -->
|
|
715
|
+
<div
|
|
716
|
+
class="flex-1 min-h-0 overflow-y-scroll pl-24 pr-16 min-[640px]:pl-40 form-scroll-fade form-pr-responsive"
|
|
717
|
+
class:form-scroll-mask={formScrolled}
|
|
718
|
+
onscroll={(e) => { formScrolled = (e.currentTarget as HTMLElement).scrollTop > 0 }}
|
|
719
|
+
onfocusin={handleFocusIn}
|
|
720
|
+
>
|
|
721
|
+
<div class="row-enter row-delay-3 flex flex-col gap-24 pb-16">
|
|
722
|
+
<!-- Personal -->
|
|
723
|
+
<div>
|
|
724
|
+
<div class="flex items-center justify-between mb-8">
|
|
725
|
+
<h3 class="text-emphasis text-body m-0">Personal Information</h3>
|
|
726
|
+
<button class="text-tiny text-accent border-0 bg-transparent p-0 cursor-default underline outline-none focus-visible:ring-3 focus-visible:ring-primary rounded-4" onclick={() => transitionTo('profile')}>Edit</button>
|
|
727
|
+
</div>
|
|
728
|
+
<div class="flex flex-col gap-4 text-small text-secondary">
|
|
729
|
+
<p class="m-0">{firstName || '—'} {lastName || '—'}</p>
|
|
730
|
+
<p class="m-0">{phone || '—'}</p>
|
|
731
|
+
<p class="m-0">{email || '—'}</p>
|
|
732
|
+
</div>
|
|
733
|
+
</div>
|
|
734
|
+
<!-- Address -->
|
|
735
|
+
<div>
|
|
736
|
+
<div class="flex items-center justify-between mb-8">
|
|
737
|
+
<h3 class="text-emphasis text-body m-0">Address</h3>
|
|
738
|
+
<button class="text-tiny text-accent border-0 bg-transparent p-0 cursor-default underline outline-none focus-visible:ring-3 focus-visible:ring-primary rounded-4" onclick={() => transitionTo('profile')}>Edit</button>
|
|
739
|
+
</div>
|
|
740
|
+
<div class="flex flex-col gap-4 text-small text-secondary">
|
|
741
|
+
<p class="m-0">{streetAddress || '—'}</p>
|
|
742
|
+
<p class="m-0">{city || '—'}{stateValue ? `, ${stateValue}` : ''} {zip || ''}</p>
|
|
743
|
+
</div>
|
|
744
|
+
</div>
|
|
745
|
+
<!-- About You -->
|
|
746
|
+
<div>
|
|
747
|
+
<div class="flex items-center justify-between mb-8">
|
|
748
|
+
<h3 class="text-emphasis text-body m-0">About You</h3>
|
|
749
|
+
<button class="text-tiny text-accent border-0 bg-transparent p-0 cursor-default underline outline-none focus-visible:ring-3 focus-visible:ring-primary rounded-4" onclick={() => transitionTo('about')}>Edit</button>
|
|
750
|
+
</div>
|
|
751
|
+
<div class="flex flex-col gap-4 text-small text-secondary">
|
|
752
|
+
<p class="m-0">Loan Amount: {loanAmount || '—'}</p>
|
|
753
|
+
<p class="m-0">SSN: {ssn ? '***-**-' + ssn.slice(-4) : '—'}</p>
|
|
754
|
+
<p class="m-0">Military: {MILITARY_OPTIONS.find(o => o.value === militaryStatus)?.label || '—'}</p>
|
|
755
|
+
<p class="m-0">Housing: {OWN_RENT.find(o => o.value === ownOrRent)?.label || '—'}</p>
|
|
756
|
+
</div>
|
|
757
|
+
</div>
|
|
758
|
+
<!-- Financial -->
|
|
759
|
+
<div>
|
|
760
|
+
<div class="flex items-center justify-between mb-8">
|
|
761
|
+
<h3 class="text-emphasis text-body m-0">Financial Info</h3>
|
|
762
|
+
<button class="text-tiny text-accent border-0 bg-transparent p-0 cursor-default underline outline-none focus-visible:ring-3 focus-visible:ring-primary rounded-4" onclick={() => transitionTo('financial')}>Edit</button>
|
|
763
|
+
</div>
|
|
764
|
+
<div class="flex flex-col gap-4 text-small text-secondary">
|
|
765
|
+
<p class="m-0">Income: {INCOME_TYPES.find(o => o.value === incomeType)?.label || '—'} — {annualIncome || '—'}/yr</p>
|
|
766
|
+
<p class="m-0">Pay: {PAY_FREQUENCIES.find(o => o.value === payFrequency)?.label || '—'}, {PAY_TYPES.find(o => o.value === payType)?.label || '—'}</p>
|
|
767
|
+
<p class="m-0">Bank: {bankName || '—'} ({ACCOUNT_TYPES.find(o => o.value === accountType)?.label || '—'})</p>
|
|
768
|
+
<p class="m-0">Routing: {routingNumber ? '***' + routingNumber.slice(-4) : '—'} / Account: {accountNumber ? '***' + accountNumber.slice(-4) : '—'}</p>
|
|
769
|
+
</div>
|
|
770
|
+
</div>
|
|
771
|
+
</div>
|
|
772
|
+
<div class="row-enter row-delay-4 mt-8 mb-16 flex justify-center">
|
|
773
|
+
<IIButton variant="ghost" onclick={() => transitionTo('financial')}>
|
|
774
|
+
{#snippet children()}Back{/snippet}
|
|
775
|
+
</IIButton>
|
|
776
|
+
</div>
|
|
777
|
+
<div class="row-enter" style="animation-delay: 330ms">
|
|
778
|
+
<IIButton variant="primary" class="w-full">
|
|
779
|
+
{#snippet children()}Submit Application{/snippet}
|
|
780
|
+
</IIButton>
|
|
781
|
+
</div>
|
|
782
|
+
<div class="pb-48"></div>
|
|
783
|
+
</div>
|
|
784
|
+
{/if}
|
|
785
|
+
|
|
786
|
+
</div>
|
|
787
|
+
|
|
788
|
+
<!-- Persistent bottom area — step indicator + button (login flow only) -->
|
|
789
|
+
<div class="pb-24 min-[640px]:pb-0 mx-auto w-full max-w-[320px]" class:hidden={isAppFlow}>
|
|
790
|
+
<!-- Login step indicator (email/otp only) -->
|
|
791
|
+
{#if !isAppFlow}
|
|
792
|
+
<div class="flex items-center justify-center gap-8 mb-24 mt-24">
|
|
793
|
+
{#each Array(2) as _, i}
|
|
794
|
+
<div
|
|
795
|
+
class="h-6 rounded-full"
|
|
796
|
+
style="
|
|
797
|
+
background: {mounted && i === loginStepIndex ? 'var(--ii-primary)' : 'var(--ii-gray-300)'};
|
|
798
|
+
width: {mounted && i === loginStepIndex ? '36px' : '16px'};
|
|
799
|
+
transition: width 0.4s ease, background-color 0.4s ease;
|
|
800
|
+
"
|
|
801
|
+
></div>
|
|
802
|
+
{/each}
|
|
803
|
+
</div>
|
|
804
|
+
{:else}
|
|
805
|
+
<div class="mt-24"></div>
|
|
806
|
+
{/if}
|
|
807
|
+
|
|
808
|
+
{#if step === 'email'}
|
|
809
|
+
<IIButton
|
|
810
|
+
variant="primary"
|
|
811
|
+
class="w-full"
|
|
812
|
+
type="submit"
|
|
813
|
+
form="alt-email-form"
|
|
814
|
+
loading={emailLoading}
|
|
815
|
+
>
|
|
816
|
+
{#snippet children()}
|
|
817
|
+
Continue
|
|
818
|
+
{/snippet}
|
|
819
|
+
</IIButton>
|
|
820
|
+
{:else if step === 'otp' && hasFailed}
|
|
821
|
+
<IIButton
|
|
822
|
+
variant="primary"
|
|
823
|
+
class="w-full"
|
|
824
|
+
type="button"
|
|
825
|
+
onclick={() => transitionTo('email')}
|
|
826
|
+
>
|
|
827
|
+
{#snippet children()}
|
|
828
|
+
Use Different Email
|
|
829
|
+
{/snippet}
|
|
830
|
+
</IIButton>
|
|
831
|
+
{:else if step === 'otp'}
|
|
832
|
+
<IIButton
|
|
833
|
+
variant="primary"
|
|
834
|
+
class="w-full"
|
|
835
|
+
type="submit"
|
|
836
|
+
form="alt-otp-form"
|
|
837
|
+
disabled={!isComplete || verifying}
|
|
838
|
+
loading={verifying}
|
|
839
|
+
>
|
|
840
|
+
{#snippet children()}
|
|
841
|
+
Verify
|
|
842
|
+
{/snippet}
|
|
843
|
+
</IIButton>
|
|
844
|
+
{/if}
|
|
845
|
+
</div>
|
|
846
|
+
</div>
|
|
847
|
+
|
|
848
|
+
</div>
|
|
849
|
+
</div>
|
|
850
|
+
</DensityProvider>
|
|
851
|
+
</div>
|
|
852
|
+
</div>
|
|
853
|
+
|
|
854
|
+
<style>
|
|
855
|
+
.dot-grid {
|
|
856
|
+
background-image:
|
|
857
|
+
linear-gradient(160deg, transparent 30%, white 55%),
|
|
858
|
+
radial-gradient(circle, color-mix(in srgb, var(--ii-primary) 13%, transparent) 1.5px, transparent 1.5px);
|
|
859
|
+
background-size: 100% 100%, 24px 24px;
|
|
860
|
+
background-color: var(--ii-gray-50, #f9fafb);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
|
|
865
|
+
.card-border {
|
|
866
|
+
position: relative;
|
|
867
|
+
background: linear-gradient(160deg, rgba(255, 255, 255, 0.45) 0%, rgba(255, 255, 255, 0) 35%);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
.card-border::before {
|
|
871
|
+
content: '';
|
|
872
|
+
position: absolute;
|
|
873
|
+
inset: 0;
|
|
874
|
+
border-radius: inherit;
|
|
875
|
+
border: 1px solid var(--ii-gray-200, #e5e7eb);
|
|
876
|
+
pointer-events: none;
|
|
877
|
+
-webkit-mask-image: linear-gradient(160deg, black 20%, transparent 60%);
|
|
878
|
+
mask-image: linear-gradient(160deg, black 20%, transparent 60%);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
|
|
882
|
+
.card-border-tl {
|
|
883
|
+
position: relative;
|
|
884
|
+
background: linear-gradient(160deg, rgba(255, 255, 255, 0.45) 0%, rgba(255, 255, 255, 0) 35%);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
.card-border-tl::before,
|
|
888
|
+
.card-border-tl::after {
|
|
889
|
+
content: '';
|
|
890
|
+
position: absolute;
|
|
891
|
+
inset: 0;
|
|
892
|
+
border-radius: inherit;
|
|
893
|
+
pointer-events: none;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/* Top border — fades out sooner */
|
|
897
|
+
.card-border-tl::before {
|
|
898
|
+
border-top: 1px solid var(--ii-gray-200, #e5e7eb);
|
|
899
|
+
-webkit-mask-image: linear-gradient(to right, black 0%, transparent 90%);
|
|
900
|
+
mask-image: linear-gradient(to right, black 0%, transparent 90%);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/* Left border — fades out later */
|
|
904
|
+
.card-border-tl::after {
|
|
905
|
+
border-left: 1px solid var(--ii-gray-200, #e5e7eb);
|
|
906
|
+
-webkit-mask-image: linear-gradient(to bottom, black 0%, transparent 90%);
|
|
907
|
+
mask-image: linear-gradient(to bottom, black 0%, transparent 90%);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
@keyframes row-fade-in {
|
|
911
|
+
from {
|
|
912
|
+
opacity: 0;
|
|
913
|
+
transform: translateY(-8px);
|
|
914
|
+
}
|
|
915
|
+
to {
|
|
916
|
+
opacity: 1;
|
|
917
|
+
transform: translateY(0);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
.row-enter {
|
|
922
|
+
animation: row-fade-in 400ms ease both;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
.row-delay-1 { animation-delay: 50ms; }
|
|
926
|
+
.row-delay-2 { animation-delay: 120ms; }
|
|
927
|
+
.row-delay-3 { animation-delay: 190ms; }
|
|
928
|
+
.row-delay-4 { animation-delay: 260ms; }
|
|
929
|
+
|
|
930
|
+
@media (min-width: 640px) {
|
|
931
|
+
.card-width-responsive {
|
|
932
|
+
width: 440px;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
.form-pr-responsive {
|
|
936
|
+
padding-right: 24px;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
@media (min-width: 1040px) {
|
|
941
|
+
.card-width-responsive {
|
|
942
|
+
width: 720px;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
.form-pr-responsive {
|
|
946
|
+
padding-right: clamp(32px, 10vw, 140px);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
.form-scroll-fade {
|
|
951
|
+
-webkit-mask-image: linear-gradient(to bottom, black calc(100% - 48px), rgba(0,0,0,0.1) 100%);
|
|
952
|
+
mask-image: linear-gradient(to bottom, black calc(100% - 48px), rgba(0,0,0,0.1) 100%);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
.form-scroll-fade.form-scroll-mask {
|
|
956
|
+
-webkit-mask-image: linear-gradient(to bottom, transparent 0px, black 24px, black calc(100% - 48px), rgba(0,0,0,0.1) 100%);
|
|
957
|
+
mask-image: linear-gradient(to bottom, transparent 0px, black 24px, black calc(100% - 48px), rgba(0,0,0,0.1) 100%);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
.step-content {
|
|
961
|
+
opacity: 1;
|
|
962
|
+
transition: opacity 200ms ease;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
.step-content.fade-out {
|
|
966
|
+
opacity: 0;
|
|
967
|
+
}
|
|
968
|
+
</style>
|