portal: improve onboarding confirmations and guides
@ -192,7 +192,6 @@ ONBOARDING_REQUIRED_STEPS: tuple[str, ...] = (
|
|||||||
|
|
||||||
KEYCLOAK_MANAGED_STEPS: set[str] = {
|
KEYCLOAK_MANAGED_STEPS: set[str] = {
|
||||||
"keycloak_password_rotated",
|
"keycloak_password_rotated",
|
||||||
"vaultwarden_master_password",
|
|
||||||
"nextcloud_mail_integration",
|
"nextcloud_mail_integration",
|
||||||
"firefly_password_rotated",
|
"firefly_password_rotated",
|
||||||
"wger_password_rotated",
|
"wger_password_rotated",
|
||||||
|
|||||||
@ -79,9 +79,7 @@
|
|||||||
comes first because it stores every credential that follows.
|
comes first because it stores every credential that follows.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<span class="pill mono" :class="status === 'ready' ? 'pill-info' : 'pill-ok'">
|
<span v-if="status !== 'ready'" class="pill mono pill-info">active</span>
|
||||||
{{ status === "ready" ? "ready" : "in progress" }}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ol class="section-stepper">
|
<ol class="section-stepper">
|
||||||
@ -97,7 +95,9 @@
|
|||||||
<div class="stepper-body">
|
<div class="stepper-body">
|
||||||
<div class="stepper-title">{{ index + 1 }}. {{ section.title }}</div>
|
<div class="stepper-title">{{ index + 1 }}. {{ section.title }}</div>
|
||||||
<div class="stepper-meta">
|
<div class="stepper-meta">
|
||||||
<span class="pill mono" :class="sectionPillClass(section)">{{ sectionStatusLabel(section) }}</span>
|
<span v-if="sectionStatusLabel(section)" class="pill mono" :class="sectionPillClass(section)">
|
||||||
|
{{ sectionStatusLabel(section) }}
|
||||||
|
</span>
|
||||||
<span class="pill mono pill-compact">{{ sectionProgress(section) }}</span>
|
<span class="pill mono pill-compact">{{ sectionProgress(section) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -109,10 +109,12 @@
|
|||||||
<div class="credential-grid">
|
<div class="credential-grid">
|
||||||
<div class="credential-field">
|
<div class="credential-field">
|
||||||
<span class="label mono">Username</span>
|
<span class="label mono">Username</span>
|
||||||
<button class="copy mono" type="button" @click="copyUsername" :disabled="!requestUsername">
|
<div class="password-row">
|
||||||
{{ requestUsername || "" }}
|
<input class="input mono" type="text" :value="requestUsername || ''" readonly />
|
||||||
<span v-if="usernameCopied" class="copied">copied</span>
|
<button class="secondary" type="button" @click="copyUsername" :disabled="!requestUsername">
|
||||||
</button>
|
{{ usernameCopied ? "Copied" : "Copy" }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="credential-field" v-if="showPasswordCard">
|
<div class="credential-field" v-if="showPasswordCard">
|
||||||
@ -124,16 +126,17 @@
|
|||||||
:value="initialPassword || '********'"
|
:value="initialPassword || '********'"
|
||||||
readonly
|
readonly
|
||||||
/>
|
/>
|
||||||
<button class="secondary" type="button" @click="togglePassword" :disabled="!initialPassword">
|
<span class="tooltip-wrap" :title="passwordRevealHint">
|
||||||
{{ revealPassword ? "Hide" : "Reveal" }}
|
<button class="secondary" type="button" @click="togglePassword" :disabled="!initialPassword">
|
||||||
</button>
|
{{ revealPassword ? "Hide" : "Reveal" }}
|
||||||
<button class="secondary" type="button" @click="copyInitialPassword" :disabled="!initialPassword">
|
</button>
|
||||||
{{ passwordCopied ? "Copied" : "Copy" }}
|
</span>
|
||||||
</button>
|
<span class="tooltip-wrap" :title="passwordRevealHint">
|
||||||
|
<button class="secondary" type="button" @click="copyInitialPassword" :disabled="!initialPassword">
|
||||||
|
{{ passwordCopied ? "Copied" : "Copy" }}
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="passwordRevealLocked" class="muted">
|
|
||||||
This password was already revealed and cannot be shown again. Ask an admin to reset it if you missed it.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="muted">
|
<p class="muted">
|
||||||
@ -148,19 +151,6 @@
|
|||||||
<h3>{{ activeSection.title }}</h3>
|
<h3>{{ activeSection.title }}</h3>
|
||||||
<p class="muted">{{ activeSection.description }}</p>
|
<p class="muted">{{ activeSection.description }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="section-actions">
|
|
||||||
<button class="secondary" type="button" @click="prevSection" :disabled="!hasPrevSection">
|
|
||||||
Previous
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="secondary"
|
|
||||||
type="button"
|
|
||||||
@click="nextSection"
|
|
||||||
:disabled="!hasNextSection || isSectionLocked(nextSectionItem) || !sectionGateComplete(activeSection)"
|
|
||||||
>
|
|
||||||
Next
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="step-grid">
|
<div class="step-grid">
|
||||||
@ -197,8 +187,13 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="step.action === 'auto'" class="step-actions">
|
<div v-if="step.action === 'auto' || step.action === 'confirm'" class="step-actions">
|
||||||
<button class="secondary" type="button" @click="check" :disabled="loading">
|
<button
|
||||||
|
class="secondary"
|
||||||
|
type="button"
|
||||||
|
@click="confirmStep(step)"
|
||||||
|
:disabled="loading || isStepDone(step.id) || isStepBlocked(step.id)"
|
||||||
|
>
|
||||||
Confirm
|
Confirm
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -219,6 +214,14 @@
|
|||||||
Start Keycloak update
|
Start Keycloak update
|
||||||
</button>
|
</button>
|
||||||
<a class="mono" href="https://live.bstein.dev" target="_blank" rel="noreferrer">Open Element</a>
|
<a class="mono" href="https://live.bstein.dev" target="_blank" rel="noreferrer">Open Element</a>
|
||||||
|
<button
|
||||||
|
class="secondary"
|
||||||
|
type="button"
|
||||||
|
@click="confirmStep(step)"
|
||||||
|
:disabled="loading || isStepDone(step.id) || isStepBlocked(step.id)"
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="step.action === 'element_recovery'" class="recovery-verify">
|
<div v-if="step.action === 'element_recovery'" class="recovery-verify">
|
||||||
@ -243,6 +246,14 @@
|
|||||||
>
|
>
|
||||||
Verify
|
Verify
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
class="secondary"
|
||||||
|
type="button"
|
||||||
|
@click="confirmStep(step)"
|
||||||
|
:disabled="loading || isStepDone(step.id) || isStepBlocked(step.id) || !elementRecoveryKey.trim()"
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<details v-if="step.guide" class="guide-details">
|
<details v-if="step.guide" class="guide-details">
|
||||||
@ -251,9 +262,9 @@
|
|||||||
<div v-for="group in guideGroups(step)" :key="group.id" class="guide-group">
|
<div v-for="group in guideGroups(step)" :key="group.id" class="guide-group">
|
||||||
<h4 v-if="group.title" class="mono guide-title">{{ group.title }}</h4>
|
<h4 v-if="group.title" class="mono guide-title">{{ group.title }}</h4>
|
||||||
<div v-if="group.shots.length" class="guide-images">
|
<div v-if="group.shots.length" class="guide-images">
|
||||||
<figure class="guide-shot">
|
<figure class="guide-shot" @click="openLightbox(guideShot(step, group))">
|
||||||
<img :src="guideShot(step, group).url" :alt="guideShot(step, group).label || step.title" loading="lazy" />
|
|
||||||
<figcaption v-if="guideShot(step, group).label" class="mono">{{ guideShot(step, group).label }}</figcaption>
|
<figcaption v-if="guideShot(step, group).label" class="mono">{{ guideShot(step, group).label }}</figcaption>
|
||||||
|
<img :src="guideShot(step, group).url" :alt="guideShot(step, group).label || step.title" loading="lazy" />
|
||||||
</figure>
|
</figure>
|
||||||
<div v-if="group.shots.length > 1" class="guide-pagination">
|
<div v-if="group.shots.length > 1" class="guide-pagination">
|
||||||
<button
|
<button
|
||||||
@ -290,8 +301,33 @@
|
|||||||
</div>
|
</div>
|
||||||
<p v-else class="muted">Guide coming soon.</p>
|
<p v-else class="muted">Guide coming soon.</p>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
<div v-if="step.action === 'checkbox'" class="step-actions">
|
||||||
|
<button
|
||||||
|
class="secondary"
|
||||||
|
type="button"
|
||||||
|
@click="confirmStep(step)"
|
||||||
|
:disabled="loading || isStepDone(step.id) || isStepBlocked(step.id)"
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="section-actions">
|
||||||
|
<button class="secondary" type="button" @click="prevSection" :disabled="!hasPrevSection">
|
||||||
|
Previous
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="secondary"
|
||||||
|
type="button"
|
||||||
|
@click="nextSection"
|
||||||
|
:disabled="!hasNextSection || isSectionLocked(nextSectionItem) || !sectionGateComplete(activeSection)"
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="status === 'ready'" class="ready-box">
|
<div v-if="status === 'ready'" class="ready-box">
|
||||||
@ -312,6 +348,16 @@
|
|||||||
<div class="mono">{{ error }}</div>
|
<div class="mono">{{ error }}</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<div v-if="lightboxShot" class="lightbox" @click.self="closeLightbox">
|
||||||
|
<div class="lightbox-card">
|
||||||
|
<div class="lightbox-head">
|
||||||
|
<span class="mono lightbox-label">{{ lightboxShot.label || "Guide image" }}</span>
|
||||||
|
<button class="secondary" type="button" @click="closeLightbox">Close</button>
|
||||||
|
</div>
|
||||||
|
<img :src="lightboxShot.url" :alt="lightboxShot.label || 'Guide image'" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -340,9 +386,15 @@ const keycloakPasswordRotationRequested = ref(false);
|
|||||||
const activeSectionId = ref("vaultwarden");
|
const activeSectionId = ref("vaultwarden");
|
||||||
const guideShots = ref({});
|
const guideShots = ref({});
|
||||||
const guidePage = ref({});
|
const guidePage = ref({});
|
||||||
|
const lightboxShot = ref(null);
|
||||||
|
|
||||||
const showPasswordCard = computed(() => Boolean(initialPassword.value || initialPasswordRevealedAt.value));
|
const showPasswordCard = computed(() => Boolean(initialPassword.value || initialPasswordRevealedAt.value));
|
||||||
const passwordRevealLocked = computed(() => Boolean(!initialPassword.value && initialPasswordRevealedAt.value));
|
const passwordRevealLocked = computed(() => Boolean(!initialPassword.value && initialPasswordRevealedAt.value));
|
||||||
|
const passwordRevealHint = computed(() =>
|
||||||
|
passwordRevealLocked.value
|
||||||
|
? "This password was already revealed and cannot be shown again. Ask an admin to reset it if you missed it."
|
||||||
|
: "",
|
||||||
|
);
|
||||||
|
|
||||||
const STEP_PREREQS = {
|
const STEP_PREREQS = {
|
||||||
vaultwarden_master_password: [],
|
vaultwarden_master_password: [],
|
||||||
@ -374,7 +426,7 @@ const SECTION_DEFS = [
|
|||||||
{
|
{
|
||||||
id: "vaultwarden_master_password",
|
id: "vaultwarden_master_password",
|
||||||
title: "Set your Vaultwarden master password",
|
title: "Set your Vaultwarden master password",
|
||||||
action: "auto",
|
action: "confirm",
|
||||||
description:
|
description:
|
||||||
"Open Nextcloud Mail to find the invite, then visit vault.bstein.dev and create your master password. Use the temporary Keycloak password to sign in to Nextcloud for the first time.",
|
"Open Nextcloud Mail to find the invite, then visit vault.bstein.dev and create your master password. Use the temporary Keycloak password to sign in to Nextcloud for the first time.",
|
||||||
bullets: [
|
bullets: [
|
||||||
@ -466,7 +518,7 @@ const SECTION_DEFS = [
|
|||||||
description:
|
description:
|
||||||
"Use the IMAP/SMTP details on your Account page to add mail to your phone or desktop client (Thunderbird, Apple Mail, FairEmail).",
|
"Use the IMAP/SMTP details on your Account page to add mail to your phone or desktop client (Thunderbird, Apple Mail, FairEmail).",
|
||||||
links: [{ href: "/account", text: "Open Account details" }],
|
links: [{ href: "/account", text: "Open Account details" }],
|
||||||
guide: { service: "nextcloud", step: "step2_mail_integration" },
|
guide: { service: "mail", step: "step1_mail_app" },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -512,7 +564,7 @@ const SECTION_DEFS = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "budget",
|
id: "budget",
|
||||||
title: "Budget & Encryption",
|
title: "Budget Encryption",
|
||||||
description: "Protect sensitive data and keep the shared storage budget predictable.",
|
description: "Protect sensitive data and keep the shared storage budget predictable.",
|
||||||
steps: [
|
steps: [
|
||||||
{
|
{
|
||||||
@ -719,17 +771,16 @@ function sectionProgress(section) {
|
|||||||
const requiredSteps = section.steps.filter((step) => isStepRequired(step.id));
|
const requiredSteps = section.steps.filter((step) => isStepRequired(step.id));
|
||||||
if (!requiredSteps.length) return "optional";
|
if (!requiredSteps.length) return "optional";
|
||||||
const doneCount = requiredSteps.filter((step) => isStepDone(step.id)).length;
|
const doneCount = requiredSteps.filter((step) => isStepDone(step.id)).length;
|
||||||
return `${doneCount}/${requiredSteps.length} required`;
|
return `${doneCount}/${requiredSteps.length} done`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sectionStatusLabel(section) {
|
function sectionStatusLabel(section) {
|
||||||
if (isSectionDone(section)) return "done";
|
if (isSectionDone(section)) return "";
|
||||||
if (isSectionLocked(section)) return "locked";
|
if (isSectionLocked(section)) return "locked";
|
||||||
return "in progress";
|
return "active";
|
||||||
}
|
}
|
||||||
|
|
||||||
function sectionPillClass(section) {
|
function sectionPillClass(section) {
|
||||||
if (isSectionDone(section)) return "pill-ok";
|
|
||||||
if (isSectionLocked(section)) return "pill-wait";
|
if (isSectionLocked(section)) return "pill-wait";
|
||||||
return "pill-info";
|
return "pill-info";
|
||||||
}
|
}
|
||||||
@ -803,6 +854,15 @@ function guideShot(step, group) {
|
|||||||
return group.shots[guideIndex(step, group)] || {};
|
return group.shots[guideIndex(step, group)] || {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openLightbox(shot) {
|
||||||
|
if (!shot || !shot.url) return;
|
||||||
|
lightboxShot.value = shot;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeLightbox() {
|
||||||
|
lightboxShot.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
function taskPillClass(value) {
|
function taskPillClass(value) {
|
||||||
const key = (value || "").trim();
|
const key = (value || "").trim();
|
||||||
if (key === "ok") return "pill-ok";
|
if (key === "ok") return "pill-ok";
|
||||||
@ -891,17 +951,18 @@ function copyUsername() {
|
|||||||
|
|
||||||
async function toggleStep(stepId, event) {
|
async function toggleStep(stepId, event) {
|
||||||
const checked = Boolean(event?.target?.checked);
|
const checked = Boolean(event?.target?.checked);
|
||||||
|
await setStepCompletion(stepId, checked);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setStepCompletion(stepId, completed) {
|
||||||
if (!auth.authenticated) {
|
if (!auth.authenticated) {
|
||||||
event?.preventDefault?.();
|
|
||||||
error.value = "Log in to update onboarding steps.";
|
error.value = "Log in to update onboarding steps.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isStepBlocked(stepId)) {
|
if (isStepBlocked(stepId)) {
|
||||||
event?.preventDefault?.();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (stepId === "element_recovery_key") {
|
if (stepId === "element_recovery_key") {
|
||||||
event?.preventDefault?.();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
@ -910,7 +971,7 @@ async function toggleStep(stepId, event) {
|
|||||||
const resp = await authFetch("/api/access/request/onboarding/attest", {
|
const resp = await authFetch("/api/access/request/onboarding/attest", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ request_code: requestCode.value.trim(), step: stepId, completed: checked }),
|
body: JSON.stringify({ request_code: requestCode.value.trim(), step: stepId, completed }),
|
||||||
});
|
});
|
||||||
const data = await resp.json().catch(() => ({}));
|
const data = await resp.json().catch(() => ({}));
|
||||||
if (!resp.ok) throw new Error(data.error || resp.statusText || `status ${resp.status}`);
|
if (!resp.ok) throw new Error(data.error || resp.statusText || `status ${resp.status}`);
|
||||||
@ -923,6 +984,23 @@ async function toggleStep(stepId, event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function confirmStep(step) {
|
||||||
|
if (!step || isStepBlocked(step.id) || isStepDone(step.id)) return;
|
||||||
|
if (step.action === "auto") {
|
||||||
|
await check();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (step.action === "keycloak_rotate") {
|
||||||
|
await requestKeycloakPasswordRotation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (step.action === "element_recovery") {
|
||||||
|
await verifyElementRecoveryKey();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await setStepCompletion(step.id, true);
|
||||||
|
}
|
||||||
|
|
||||||
async function verifyElementRecoveryKey() {
|
async function verifyElementRecoveryKey() {
|
||||||
if (!auth.authenticated) {
|
if (!auth.authenticated) {
|
||||||
error.value = "Log in to verify your recovery key.";
|
error.value = "Log in to verify your recovery key.";
|
||||||
@ -1241,14 +1319,20 @@ button.copy:disabled {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
gap: 6px;
|
gap: 4px;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stepper-meta .pill {
|
||||||
|
padding: 4px 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.pill-compact {
|
.pill-compact {
|
||||||
padding: 6px 8px;
|
padding: 4px 6px;
|
||||||
font-size: 12px;
|
font-size: 11px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@ -1317,6 +1401,10 @@ button.copy:disabled {
|
|||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.credential-field .input[readonly] {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
.password-row {
|
.password-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@ -1343,7 +1431,11 @@ button.copy:disabled {
|
|||||||
|
|
||||||
.section-actions {
|
.section-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.step-grid {
|
.step-grid {
|
||||||
@ -1416,12 +1508,12 @@ button.copy:disabled {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.step-actions {
|
.step-actions {
|
||||||
margin-top: 10px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
|
padding-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.recovery-verify {
|
.recovery-verify {
|
||||||
@ -1460,6 +1552,15 @@ button.copy:disabled {
|
|||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
background: rgba(0, 0, 0, 0.2);
|
background: rgba(0, 0, 0, 0.2);
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
cursor: zoom-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-shot figcaption {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin: 0 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.guide-shot img {
|
.guide-shot img {
|
||||||
@ -1534,6 +1635,52 @@ button.copy:disabled {
|
|||||||
background: rgba(255, 70, 70, 0.1);
|
background: rgba(255, 70, 70, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tooltip-wrap {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightbox {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(6, 8, 12, 0.82);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 28px;
|
||||||
|
z-index: 2000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightbox-card {
|
||||||
|
width: min(1100px, 92vw);
|
||||||
|
max-height: 92vh;
|
||||||
|
background: rgba(10, 14, 24, 0.96);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 14px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightbox-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightbox-label {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightbox-card img {
|
||||||
|
width: 100%;
|
||||||
|
max-height: 74vh;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgba(0, 0, 0, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
.status-form {
|
.status-form {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 294 KiB After Width: | Height: | Size: 294 KiB |
|
Before Width: | Height: | Size: 141 KiB After Width: | Height: | Size: 141 KiB |
|
Before Width: | Height: | Size: 121 KiB After Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 133 KiB |