also looking at this
chore(release): promote UI, accessibility, and STX updates to production
#11
16 files
+176
-67
Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.
| @@ -139,21 +139,21 @@ async function onVerify(e: Event): Promise<void> { | ||
| 139 | 139 | |
| 140 | 140 | {{-- The shared error box for both steps. `:text` overwrites the (empty) |
| 141 | 141 | static label after hydration and never cloaks. --}} |
| 142 | <div :show="!!error()" :text="error()" class="mb-4 px-3 py-2 text-sm rounded-lg err-box"></div> | |
| 142 | <div id="login-error" role="alert" :show="!!error()" :text="error()" class="mb-4 px-3 py-2 text-sm rounded-lg err-box"></div> | |
| 143 | 143 | |
| 144 | 144 | {{-- Step 1: credentials. A same-origin fetch, not a native POST — the |
| 145 | 145 | submit is intercepted so the browser never navigates to /login itself. --}} |
| 146 | 146 | <form :show="mode() === 'credentials'" action="/api/auth/login" method="POST" @submit="onLogin" class="space-y-4"> |
| 147 | 147 | <div> |
| 148 | 148 | <label for="login-email" class="block mb-1.5 font-medium text-muted text-xs">Email</label> |
| 149 | <input id="login-email" name="email" type="email" required autocomplete="email" placeholder="you@example.com" :model="email" class="px-3.5 py-2.5 w-full text-sm field"> | |
| 149 | <input id="login-email" name="email" type="email" required autocomplete="email" placeholder="you@example.com" :model="email" class="px-3.5 py-2.5 w-full text-sm field" :aria-invalid="error() ? 'true' : 'false'" :aria-describedby="error() ? 'login-error' : ''"> | |
| 150 | 150 | </div> |
| 151 | 151 | <div> |
| 152 | 152 | <div class="flex items-center justify-between mb-1.5"> |
| 153 | 153 | <label for="login-password" class="block font-medium text-muted text-xs">Password</label> |
| 154 | 154 | <StxLink to="/forgot-password" class="font-medium text-accent text-xs">Forgot password?</StxLink> |
| 155 | 155 | </div> |
| 156 | <input id="login-password" name="password" type="password" required autocomplete="current-password" placeholder="••••••••" :model="password" class="px-3.5 py-2.5 w-full text-sm field"> | |
| 156 | <input id="login-password" name="password" type="password" required autocomplete="current-password" placeholder="••••••••" :model="password" class="px-3.5 py-2.5 w-full text-sm field" :aria-invalid="error() ? 'true' : 'false'" :aria-describedby="error() ? 'login-error' : ''"> | |
| 157 | 157 | </div> |
| 158 | 158 | |
| 159 | 159 | {{-- Remember me, default OFF: unchecked is the baseline week, checked is |
| @@ -171,13 +171,13 @@ async function onVerify(e: Event): Promise<void> { | ||
| 171 | 171 | <p class="text-sm text-subtle">Enter the 6-digit code from your authenticator app.</p> |
| 172 | 172 | <div> |
| 173 | 173 | <label for="login-code" class="block mb-1.5 font-medium text-muted text-xs">Authentication code</label> |
| 174 | <input id="login-code" name="code" type="text" inputmode="numeric" autocomplete="one-time-code" maxlength="6" placeholder="123456" :model="code" class="px-3.5 py-2.5 w-full text-sm field"> | |
| 174 | <input id="login-code" name="code" type="text" inputmode="numeric" autocomplete="one-time-code" maxlength="6" placeholder="123456" :model="code" class="px-3.5 py-2.5 w-full text-sm field" :aria-invalid="error() ? 'true' : 'false'" :aria-describedby="error() ? 'login-error' : ''"> | |
| 175 | 175 | </div> |
| 176 | 176 | <button type="submit" :disabled="loading()" :text="loading() ? 'Verifying…' : 'Verify and sign in'" class="py-2.5 w-full font-semibold text-sm btn-solid">Verify and sign in</button> |
| 177 | 177 | </form> |
| 178 | 178 | |
| 179 | 179 | @if (oauthError) |
| 180 | <div class="mt-4 px-3 py-2 text-sm rounded-lg err-box">{{ oauthError }}</div> | |
| 180 | <div role="alert" class="mt-4 px-3 py-2 text-sm rounded-lg err-box">{{ oauthError }}</div> | |
| 181 | 181 | @endif |
| 182 | 182 | |
| 183 | 183 | @if (anyOauth) |