CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 7s
CI & Build / integration (push) Successful in 18s
CI & Build / TypeScript typecheck (push) Successful in 32s
CI & Build / Python tests (push) Successful in 46s
CI & Build / Build & push image (push) Successful in 47s
21 buttons filled with --color-primary, which resolves to Scribe's violet accent: every auth Submit (Login, Register, Invite, Forgot, Reset), plus Invite, Add, Confirm, Restore, Generate, Log-save, Subtask-confirm, Toggle-open, the modal primary, the version-restore, the inline-assist button, the milestone-plan actions, the task-advance hover, and both empty-state CTAs. The house style is explicit that the accent never appears on an action button: action colours are universal across the family precisely so a Save button looks identical in every app, while the accent carries identity. Doing both makes the accent mean two things and neither clearly. Operator's call, and the reasoning is worth keeping: the violet-on-Scribe- actions treatment was a deliberate early choice to give the web UI its own personality, made when much more of the app was user-facing. That is no longer true, so consistency is now worth more than the distinction it was buying. Found in two passes, which is the part worth noting. The first scan looked for `.btn-*` and found 13. Seven more were the same thing under different names — .modal-btn-primary, .vh-btn-restore, .inline-assist-btn, .empty-action, .task-advance-btn — plus .ms-plan-actions .btn-primary, a compound override flagged in the previous commit. Searching by naming convention finds what was named consistently, which is never the whole set. Deliberately NOT changed: progress-bar fills, active tab / page / selection states, tag-pill hover, the duration badge, the skip link, the assist pulse. Those are identity and active-state, which is exactly where the accent belongs. After this the accent appears only there, which is what makes it read as identity rather than as decoration. Colour swaps only — 25 lines changed, no geometry, no structure, no templates. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
246 lines
5.5 KiB
Vue
246 lines
5.5 KiB
Vue
<script setup lang="ts">
|
|
import { ref, computed, onMounted } from "vue";
|
|
import { useRouter, useRoute } from "vue-router";
|
|
import { useAuthStore } from "@/stores/auth";
|
|
import AppLogo from "@/components/AppLogo.vue";
|
|
|
|
const router = useRouter();
|
|
const route = useRoute();
|
|
const authStore = useAuthStore();
|
|
|
|
const username = ref("");
|
|
const password = ref("");
|
|
const error = ref("");
|
|
const submitting = ref(false);
|
|
|
|
const oauthError = computed(() => route.query.error === "oauth");
|
|
|
|
onMounted(async () => {
|
|
await authStore.checkHasUsers();
|
|
if (oauthError.value) {
|
|
error.value = "SSO login failed. Please try again.";
|
|
}
|
|
});
|
|
|
|
async function handleSubmit() {
|
|
error.value = "";
|
|
submitting.value = true;
|
|
try {
|
|
await authStore.login(username.value, password.value);
|
|
const redirect = (route.query.redirect as string) || "/";
|
|
router.push(redirect);
|
|
} catch (e: unknown) {
|
|
if (e && typeof e === "object" && "body" in e) {
|
|
const body = (e as { body?: { error?: string } }).body;
|
|
error.value = body?.error || "Login failed";
|
|
} else {
|
|
error.value = "Login failed";
|
|
}
|
|
} finally {
|
|
submitting.value = false;
|
|
}
|
|
}
|
|
|
|
function loginWithOAuth() {
|
|
window.location.href = "/api/auth/oauth/login";
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<main class="auth-page">
|
|
<div class="auth-card">
|
|
<div class="auth-brand"><AppLogo :size="32" /><h1>Sign In</h1></div>
|
|
<p v-if="!authStore.hasUsers" class="auth-hint">
|
|
No accounts yet.
|
|
<router-link to="/register">Create the first account</router-link>
|
|
to get started.
|
|
</p>
|
|
|
|
<p v-if="error" class="error-msg">{{ error }}</p>
|
|
|
|
<form v-if="authStore.localAuthEnabled" @submit.prevent="handleSubmit">
|
|
<div class="field">
|
|
<label for="username">Username</label>
|
|
<input
|
|
id="username"
|
|
v-model="username"
|
|
type="text"
|
|
autocomplete="username"
|
|
required
|
|
class="input"
|
|
/>
|
|
</div>
|
|
<div class="field">
|
|
<label for="password">Password</label>
|
|
<input
|
|
id="password"
|
|
v-model="password"
|
|
type="password"
|
|
autocomplete="current-password"
|
|
required
|
|
class="input"
|
|
/>
|
|
</div>
|
|
<p class="forgot-link">
|
|
<router-link to="/forgot-password">Forgot your password?</router-link>
|
|
</p>
|
|
<button type="submit" class="btn-submit" :disabled="submitting">
|
|
{{ submitting ? "Signing in..." : "Sign In" }}
|
|
</button>
|
|
</form>
|
|
|
|
<div
|
|
v-if="authStore.localAuthEnabled && authStore.oauthEnabled"
|
|
class="divider"
|
|
>
|
|
<span>or</span>
|
|
</div>
|
|
|
|
<button
|
|
v-if="authStore.oauthEnabled"
|
|
class="btn-oauth"
|
|
@click="loginWithOAuth"
|
|
>
|
|
Login with Authentik
|
|
</button>
|
|
|
|
<p v-if="authStore.localAuthEnabled" class="auth-footer">
|
|
Don't have an account?
|
|
<router-link to="/register">Register</router-link>
|
|
</p>
|
|
</div>
|
|
</main>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.auth-page {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-height: 100vh;
|
|
padding: 1rem;
|
|
}
|
|
.auth-card {
|
|
width: 100%;
|
|
max-width: 400px;
|
|
background: var(--color-bg-card);
|
|
border: 1px solid var(--color-border);
|
|
border-radius: var(--radius-md);
|
|
padding: 2rem;
|
|
}
|
|
.auth-brand {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 0.5rem;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
.auth-card h1 {
|
|
margin: 0;
|
|
text-align: center;
|
|
}
|
|
.auth-hint {
|
|
text-align: center;
|
|
font-size: 0.9rem;
|
|
color: var(--color-text-secondary);
|
|
margin-bottom: 1rem;
|
|
}
|
|
.auth-hint a {
|
|
color: var(--color-primary);
|
|
}
|
|
.field {
|
|
margin-bottom: 1rem;
|
|
}
|
|
.field label {
|
|
display: block;
|
|
font-size: 0.9rem;
|
|
font-weight: 600;
|
|
margin-bottom: 0.35rem;
|
|
}
|
|
.input {
|
|
width: 100%;
|
|
padding: 0.5rem 0.75rem;
|
|
border: 1px solid var(--color-border);
|
|
border-radius: var(--radius-sm);
|
|
font-size: 0.95rem;
|
|
background: var(--color-bg);
|
|
color: var(--color-text);
|
|
box-sizing: border-box;
|
|
}
|
|
.input:focus {
|
|
outline: none;
|
|
border-color: var(--color-primary);
|
|
}
|
|
.error-msg {
|
|
color: var(--color-danger);
|
|
font-size: 0.9rem;
|
|
margin: 0 0 0.75rem;
|
|
}
|
|
.btn-submit {
|
|
width: 100%;
|
|
padding: 0.6rem;
|
|
background: var(--color-action-primary);
|
|
color: var(--fs-text-on-action);
|
|
border: none;
|
|
border-radius: var(--radius-sm);
|
|
cursor: pointer;
|
|
font-size: 0.95rem;
|
|
font-weight: 600;
|
|
}
|
|
.btn-submit:disabled {
|
|
opacity: 0.6;
|
|
cursor: default;
|
|
}
|
|
.btn-submit:hover:not(:disabled) {
|
|
opacity: 0.9;
|
|
}
|
|
.divider {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
margin: 1.25rem 0;
|
|
color: var(--color-text-secondary);
|
|
font-size: 0.85rem;
|
|
}
|
|
.divider::before,
|
|
.divider::after {
|
|
content: "";
|
|
flex: 1;
|
|
border-top: 1px solid var(--color-border);
|
|
}
|
|
.btn-oauth {
|
|
width: 100%;
|
|
padding: 0.6rem;
|
|
background: transparent;
|
|
color: var(--color-text);
|
|
border: 1px solid var(--color-border);
|
|
border-radius: var(--radius-sm);
|
|
cursor: pointer;
|
|
font-size: 0.95rem;
|
|
font-weight: 600;
|
|
}
|
|
.btn-oauth:hover {
|
|
background: var(--color-bg-hover, var(--color-border));
|
|
}
|
|
.auth-footer {
|
|
text-align: center;
|
|
font-size: 0.9rem;
|
|
color: var(--color-text-secondary);
|
|
margin: 1rem 0 0;
|
|
}
|
|
.auth-footer a {
|
|
color: var(--color-primary);
|
|
}
|
|
.forgot-link {
|
|
text-align: right;
|
|
margin: -0.5rem 0 0.75rem;
|
|
font-size: 0.85rem;
|
|
}
|
|
.forgot-link a {
|
|
color: var(--color-text-secondary);
|
|
}
|
|
.forgot-link a:hover {
|
|
color: var(--color-primary);
|
|
}
|
|
</style>
|