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;