ui: polish onboarding gating

This commit is contained in:
Brad Stein 2026-01-04 23:34:21 -03:00
parent 698ed49a9b
commit 16e69541e5
2 changed files with 79 additions and 24 deletions

View File

@ -1,7 +1,7 @@
<template>
<header class="hero card glass">
<div class="eyebrow">
<span class="pill">Portfolio + Titan Lab</span>
<span class="pill">Titan Lab</span>
<span class="mono accent">atlas · oceanus · nextcloud-ready</span>
</div>
<h1>{{ title }}</h1>

View File

@ -91,7 +91,8 @@
<h3>Temporary password</h3>
<p class="muted">
Use this password to log in for the first time. You won't be forced to change it immediately — you'll rotate
it later after Vaultwarden is set up. This password is shown once — copy it now.
it later after Vaultwarden is set up. This password is shown once — copy it now. If you refresh this page,
it may disappear.
</p>
<div class="request-code-row">
<span class="label mono">Password</span>
@ -114,7 +115,7 @@
</div>
<ul class="checklist">
<li class="check-item">
<li class="check-item" :class="checkItemClass('vaultwarden_master_password')">
<label>
<input
type="checkbox"
@ -134,7 +135,7 @@
</p>
</li>
<li class="check-item">
<li class="check-item" :class="checkItemClass('vaultwarden_browser_extension')">
<label>
<input
type="checkbox"
@ -153,7 +154,7 @@
</p>
</li>
<li class="check-item">
<li class="check-item" :class="checkItemClass('vaultwarden_mobile_app')">
<label>
<input
type="checkbox"
@ -172,7 +173,7 @@
</p>
</li>
<li class="check-item">
<li class="check-item" :class="checkItemClass('keycloak_password_rotated')">
<label>
<input type="checkbox" :checked="isStepDone('keycloak_password_rotated')" disabled />
<span>Rotate your Keycloak password</span>
@ -203,7 +204,7 @@
</p>
</li>
<li class="check-item mfa-optional">
<li class="check-item mfa-optional" :class="mfaItemClass()">
<div class="mfa-row">
<div class="mfa-text">
<span class="mfa-title">Optional: enable MFA (TOTP) for Keycloak</span>
@ -251,7 +252,7 @@
</details>
</li>
<li class="check-item">
<li class="check-item" :class="checkItemClass('element_recovery_key')">
<label>
<input type="checkbox" :checked="isStepDone('element_recovery_key')" disabled />
<span>Create an Element recovery key</span>
@ -291,7 +292,7 @@
</p>
</li>
<li class="check-item">
<li class="check-item" :class="checkItemClass('element_recovery_key_stored')">
<label>
<input
type="checkbox"
@ -307,7 +308,7 @@
<p class="muted">Save the recovery key in Vaultwarden so it doesn't get lost.</p>
</li>
<li v-for="step in extraSteps" :key="step.id" class="check-item">
<li v-for="step in extraSteps" :key="step.id" class="check-item" :class="checkItemClass(step.id)">
<label>
<input
type="checkbox"
@ -431,6 +432,31 @@ function isStepDone(step) {
return Array.isArray(steps) ? steps.includes(step) : false;
}
function requiredStepOrder() {
if (Array.isArray(onboarding.value?.required_steps) && onboarding.value.required_steps.length) {
return onboarding.value.required_steps;
}
return [
"vaultwarden_master_password",
"vaultwarden_browser_extension",
"vaultwarden_mobile_app",
"keycloak_password_rotated",
"element_recovery_key",
"element_recovery_key_stored",
"elementx_setup",
"jellyfin_login",
"mail_client_setup",
];
}
function activeRequiredStep() {
const order = requiredStepOrder();
for (const step of order) {
if (!isStepDone(step)) return step;
}
return "";
}
function mfaOptionalState() {
const state = onboarding.value?.optional?.keycloak_mfa_optional?.state;
if (state === "done" || state === "skipped") return state;
@ -446,6 +472,16 @@ function isMfaBlocked() {
return !isStepDone("keycloak_password_rotated");
}
function mfaItemClass() {
const state = mfaOptionalState();
return {
blocked: isMfaBlocked(),
done: state === "done",
skipped: state === "skipped",
optional: true,
};
}
function mfaPillLabel() {
if (isMfaBlocked()) return "blocked";
const state = mfaOptionalState();
@ -491,20 +527,7 @@ async function maybeGenerateMfaQrs(event) {
}
function isStepBlocked(step) {
const order =
Array.isArray(onboarding.value?.required_steps) && onboarding.value.required_steps.length
? onboarding.value.required_steps
: [
"vaultwarden_master_password",
"vaultwarden_browser_extension",
"vaultwarden_mobile_app",
"keycloak_password_rotated",
"element_recovery_key",
"element_recovery_key_stored",
"elementx_setup",
"jellyfin_login",
"mail_client_setup",
];
const order = requiredStepOrder();
const idx = order.indexOf(step);
if (idx <= 0) return false;
for (let i = 0; i < idx; i += 1) {
@ -513,6 +536,14 @@ function isStepBlocked(step) {
return false;
}
function checkItemClass(step) {
const activeStep = activeRequiredStep();
const done = isStepDone(step);
const blockedStep = isStepBlocked(step);
const active = !done && !blockedStep && activeStep === step;
return { done, blocked: blockedStep, active };
}
function stepPillLabel(step) {
if (isStepDone(step)) return "done";
if (isStepBlocked(step)) return "blocked";
@ -893,6 +924,30 @@ button.primary {
background: rgba(255, 255, 255, 0.02);
}
.check-item.blocked {
opacity: 0.55;
}
.check-item.active {
border-color: rgba(125, 208, 255, 0.45);
background: rgba(79, 139, 255, 0.08);
box-shadow: 0 0 0 1px rgba(79, 139, 255, 0.2);
}
.check-item.done {
border-color: rgba(92, 214, 167, 0.35);
background: rgba(92, 214, 167, 0.05);
}
.check-item.skipped {
border-color: rgba(146, 158, 182, 0.25);
}
.check-item.done label,
.check-item.active label {
color: var(--text-primary);
}
.check-item label {
display: flex;
align-items: center;