refactor(ui): auth views migrate onto the shared buttons; two variants added
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 42s
CI & Build / integration (push) Successful in 1m39s
CI & Build / Python tests (push) Successful in 2m4s
CI & Build / Build & push image (push) Canceled after 0s

The five auth views each defined .btn-submit identically — full-width, filled,
0.6rem — and LoginView additionally defined .btn-oauth. Those rules are now
gone entirely rather than tokenised: the template composes `btn-primary
btn-block` and `btn-ghost btn-block`, and there is nothing left per-file to
drift.

That is the difference between this and the earlier chunks. Consolidating the
core four moved geometry into one place but left every semantic name defining
its own; this removes the definition.

Two variants added, both earned rather than invented:

- .btn-text — no fill, no border. The most common shape in the dense surfaces
  (dismiss, cancel-beside-confirm, clear-search) where a border would draw a
  box around something that should read as an action on the adjacent text.
  Distinct from ghost, which IS a box.
- .btn-danger-outline — already existed independently in three views before
  this sheet, which is what makes it a variant and not a one-off. It is what a
  delete looks like when it must not shout.

.btn-block composes with a variant rather than being one, because width is
orthogonal to appearance.

Also corrected the sheet's own header, which claimed "no template changes" —
true when it was written, false as of this commit. It now states the actual
model: a button is variant + size, composed in the template. Semantic per-view
names are named as the thing that drifted, and why: a name says what a button
is FOR and nothing about what it should look like, so two buttons doing the
same job in two views had no reason to match, and didn't.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
This commit is contained in:
2026-08-01 22:42:14 -04:00
co-authored by Claude Opus 5
parent 37616682f0
commit 67fdf7c55b
6 changed files with 68 additions and 116 deletions
+62 -6
View File
@@ -9,9 +9,19 @@
* because a scoped duplicate is invisible to every tool — it isn't a rule * because a scoped duplicate is invisible to every tool — it isn't a rule
* violation, isn't a broken reference, and isn't a recorded snippet. * violation, isn't a broken reference, and isn't a recorded snippet.
* *
* GEOMETRY LIVES HERE; the class names are the app's existing ones so no * GEOMETRY LIVES HERE. Every value is a design-system token, so a palette or
* template changes. Every value is a design-system token, so a palette or scale * scale change moves the buttons rather than stranding a copy that no longer
* change moves the buttons rather than stranding a copy that no longer matches. * matches.
*
* A button is `variant + size`, composed in the template:
* btn-primary a page action
* btn-primary btn-compact a row action
* btn-ghost btn-inline an affordance inside a card
* btn-primary btn-block a form's single submitting action
* Semantic per-view names (.btn-save, .btn-delete-task, …) were the thing that
* drifted, because a name says what a button is FOR and nothing about what it
* should look like — so two buttons doing the same job in two views had no
* reason to match, and didn't.
* *
* MIGRATION NOTE — this file is deliberately safe to land ahead of the removals. * MIGRATION NOTE — this file is deliberately safe to land ahead of the removals.
* These are plain selectors (specificity 0,1,0); a Vue `<style scoped>` rule * These are plain selectors (specificity 0,1,0); a Vue `<style scoped>` rule
@@ -25,7 +35,8 @@
.btn-primary, .btn-primary,
.btn-secondary, .btn-secondary,
.btn-ghost, .btn-ghost,
.btn-danger { .btn-danger,
.btn-danger-outline {
padding: var(--fs-space-2) var(--fs-space-4); /* 8px 16px */ padding: var(--fs-space-2) var(--fs-space-4); /* 8px 16px */
border: none; border: none;
border-radius: var(--fs-radius-md); /* 8px — the system's button radius */ border-radius: var(--fs-radius-md); /* 8px — the system's button radius */
@@ -46,7 +57,8 @@
.btn-primary:disabled, .btn-primary:disabled,
.btn-secondary:disabled, .btn-secondary:disabled,
.btn-ghost:disabled, .btn-ghost:disabled,
.btn-danger:disabled { .btn-danger:disabled,
.btn-danger-outline:disabled {
opacity: var(--fs-disabled-opacity); opacity: var(--fs-disabled-opacity);
cursor: not-allowed; cursor: not-allowed;
} }
@@ -54,7 +66,8 @@
.btn-primary:focus-visible, .btn-primary:focus-visible,
.btn-secondary:focus-visible, .btn-secondary:focus-visible,
.btn-ghost:focus-visible, .btn-ghost:focus-visible,
.btn-danger:focus-visible { .btn-danger:focus-visible,
.btn-danger-outline:focus-visible {
outline: none; outline: none;
box-shadow: var(--fs-focus-ring); box-shadow: var(--fs-focus-ring);
} }
@@ -105,6 +118,38 @@
background: var(--color-action-destructive-hover); background: var(--color-action-destructive-hover);
} }
/* A bare text button: no fill, no border. The most common shape in the dense
* surfaces — a dismiss, a cancel next to a confirm, a clear-search — where a
* border would draw a box around something that should read as an action on
* the text beside it. Distinct from ghost, which IS a box. */
.btn-text {
background: none;
border: none;
color: var(--color-text-muted);
padding: var(--fs-space-1) var(--fs-space-2);
font-family: var(--fs-font-body);
font-size: var(--fs-size-tiny);
line-height: 1;
cursor: pointer;
transition: color var(--fs-dur-fast) var(--fs-ease);
}
.btn-text:not(:disabled):hover { color: var(--color-text); }
.btn-text:disabled { opacity: var(--fs-disabled-opacity); cursor: not-allowed; }
.btn-text:focus-visible { outline: none; box-shadow: var(--fs-focus-ring); }
/* Destructive, outlined — fills on hover. Already existed independently in
* three views before this sheet, which is what makes it a variant rather than
* a one-off: it is what a delete looks like when it must not shout. */
.btn-danger-outline {
background: none;
border: 1px solid var(--color-action-destructive);
color: var(--color-action-destructive);
}
.btn-danger-outline:not(:disabled):hover {
background: var(--color-action-destructive);
color: var(--fs-text-on-action);
}
/* --- size modifiers ------------------------------------------------------ /* --- size modifiers ------------------------------------------------------
* *
* THREE sizes, because the app genuinely has three. Measured across the ~100 * THREE sizes, because the app genuinely has three. Measured across the ~100
@@ -135,3 +180,14 @@
font-size: var(--fs-size-tiny); font-size: var(--fs-size-tiny);
line-height: 1; line-height: 1;
} }
/* Full width, for a form's single submitting action — the auth screens. Width
* is orthogonal to size, so it composes: `btn-primary btn-block`. */
.btn-block {
display: block;
width: 100%;
padding: var(--fs-space-3) var(--fs-space-4); /* 12px 16px — a touch taller,
because a full-width button
is the page's main action */
font-size: var(--fs-size-body-sm);
}
+1 -19
View File
@@ -49,7 +49,7 @@ async function handleSubmit() {
/> />
</div> </div>
<p v-if="error" class="error-msg">{{ error }}</p> <p v-if="error" class="error-msg">{{ error }}</p>
<button type="submit" class="btn-submit" :disabled="submitting"> <button type="submit" class="btn-primary btn-block" :disabled="submitting">
{{ submitting ? "Sending..." : "Send Reset Link" }} {{ submitting ? "Sending..." : "Send Reset Link" }}
</button> </button>
</form> </form>
@@ -137,24 +137,6 @@ async function handleSubmit() {
.success-msg p { .success-msg p {
margin: 0.5rem 0; margin: 0.5rem 0;
} }
.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: var(--fs-weight-medium);
}
.btn-submit:disabled {
opacity: 0.6;
cursor: default;
}
.btn-submit:hover:not(:disabled) {
opacity: 0.9;
}
.auth-footer { .auth-footer {
text-align: center; text-align: center;
font-size: 0.9rem; font-size: 0.9rem;
+2 -34
View File
@@ -84,7 +84,7 @@ function loginWithOAuth() {
<p class="forgot-link"> <p class="forgot-link">
<router-link to="/forgot-password">Forgot your password?</router-link> <router-link to="/forgot-password">Forgot your password?</router-link>
</p> </p>
<button type="submit" class="btn-submit" :disabled="submitting"> <button type="submit" class="btn-primary btn-block" :disabled="submitting">
{{ submitting ? "Signing in..." : "Sign In" }} {{ submitting ? "Signing in..." : "Sign In" }}
</button> </button>
</form> </form>
@@ -98,7 +98,7 @@ function loginWithOAuth() {
<button <button
v-if="authStore.oauthEnabled" v-if="authStore.oauthEnabled"
class="btn-oauth" class="btn-ghost btn-block"
@click="loginWithOAuth" @click="loginWithOAuth"
> >
Login with Authentik Login with Authentik
@@ -176,24 +176,6 @@ function loginWithOAuth() {
font-size: 0.9rem; font-size: 0.9rem;
margin: 0 0 0.75rem; 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: var(--fs-weight-medium);
}
.btn-submit:disabled {
opacity: 0.6;
cursor: default;
}
.btn-submit:hover:not(:disabled) {
opacity: 0.9;
}
.divider { .divider {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -208,20 +190,6 @@ function loginWithOAuth() {
flex: 1; flex: 1;
border-top: 1px solid var(--color-border); 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: var(--fs-weight-medium);
}
.btn-oauth:hover {
background: var(--color-bg-hover, var(--color-border));
}
.auth-footer { .auth-footer {
text-align: center; text-align: center;
font-size: 0.9rem; font-size: 0.9rem;
+1 -19
View File
@@ -144,7 +144,7 @@ async function handleSubmit() {
<p v-if="passwordMismatch" class="error-hint">Passwords do not match</p> <p v-if="passwordMismatch" class="error-hint">Passwords do not match</p>
</div> </div>
<p v-if="error" class="error-msg">{{ error }}</p> <p v-if="error" class="error-msg">{{ error }}</p>
<button type="submit" class="btn-submit" :disabled="!canSubmit"> <button type="submit" class="btn-primary btn-block" :disabled="!canSubmit">
{{ submitting ? "Creating Account..." : "Create Account" }} {{ submitting ? "Creating Account..." : "Create Account" }}
</button> </button>
</form> </form>
@@ -247,24 +247,6 @@ async function handleSubmit() {
font-size: 0.9rem; font-size: 0.9rem;
margin: 0 0 0.75rem; 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: var(--fs-weight-medium);
}
.btn-submit:disabled {
opacity: 0.6;
cursor: default;
}
.btn-submit:hover:not(:disabled) {
opacity: 0.9;
}
.auth-footer { .auth-footer {
text-align: center; text-align: center;
font-size: 0.9rem; font-size: 0.9rem;
+1 -19
View File
@@ -117,7 +117,7 @@ async function handleSubmit() {
<p v-if="passwordMismatch" class="error-hint">Passwords do not match</p> <p v-if="passwordMismatch" class="error-hint">Passwords do not match</p>
</div> </div>
<p v-if="error" class="error-msg">{{ error }}</p> <p v-if="error" class="error-msg">{{ error }}</p>
<button type="submit" class="btn-submit" :disabled="!canSubmit"> <button type="submit" class="btn-primary btn-block" :disabled="!canSubmit">
{{ submitting ? "Creating account..." : "Create Account" }} {{ submitting ? "Creating account..." : "Create Account" }}
</button> </button>
</form> </form>
@@ -216,24 +216,6 @@ async function handleSubmit() {
font-size: 0.9rem; font-size: 0.9rem;
margin: 0 0 0.75rem; 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: var(--fs-weight-medium);
}
.btn-submit:disabled {
opacity: 0.6;
cursor: default;
}
.btn-submit:hover:not(:disabled) {
opacity: 0.9;
}
.auth-footer { .auth-footer {
text-align: center; text-align: center;
font-size: 0.9rem; font-size: 0.9rem;
+1 -19
View File
@@ -88,7 +88,7 @@ async function handleSubmit() {
<p v-if="passwordMismatch" class="error-hint">Passwords do not match</p> <p v-if="passwordMismatch" class="error-hint">Passwords do not match</p>
</div> </div>
<p v-if="error" class="error-msg">{{ error }}</p> <p v-if="error" class="error-msg">{{ error }}</p>
<button type="submit" class="btn-submit" :disabled="!canSubmit"> <button type="submit" class="btn-primary btn-block" :disabled="!canSubmit">
{{ submitting ? "Resetting..." : "Reset Password" }} {{ submitting ? "Resetting..." : "Reset Password" }}
</button> </button>
</form> </form>
@@ -195,24 +195,6 @@ async function handleSubmit() {
font-size: 0.9rem; font-size: 0.9rem;
margin: 0 0 0.75rem; 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: var(--fs-weight-medium);
}
.btn-submit:disabled {
opacity: 0.6;
cursor: default;
}
.btn-submit:hover:not(:disabled) {
opacity: 0.9;
}
.auth-footer { .auth-footer {
text-align: center; text-align: center;
font-size: 0.9rem; font-size: 0.9rem;