fix(setup): use the shared eo-modal for drive details

The details popup was a hand-rolled overlay instead of openEoModal, which
every other modal in the WebUI uses. Two things were wrong with that.

It was appended to the wizard container rather than document.body. The
wizard has a backdrop-filter, which makes it a containing block, so the
modal's position:fixed resolved against the wizard instead of the
viewport — the backdrop covered part of the screen and the panel sat
off-centre.

And once switched to eo-modal it still didn't appear at all: .eo-modal is
z-index 1100 while the wizard overlay is 9999, so it rendered behind the
wizard and the Details button looked inert. Raised only under
body.setup-wizard-open, so no other overlay's stacking is touched — the
wizard is the exceptional full-screen surface, so it is the wizard's
stylesheet that declares modals must sit above it.

The bespoke modal chrome is gone from setup-wizard.css; what remains is
the storage-specific content (spec table, checks list, fstab box) that
renders inside .eo-modal-body. Header, sections, backdrop, Escape,
click-outside and the footer action now all come from the shared helper.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
librelad 2026-08-26 01:57:57 +01:00
parent 7eb6d36d55
commit e71fc15f38
2 changed files with 62 additions and 76 deletions

View File

@ -1268,36 +1268,15 @@ body.setup-wizard-open {
.setup-storage-details:hover { background: rgba(255, 255, 255, 0.16); } .setup-storage-details:hover { background: rgba(255, 255, 255, 0.16); }
.setup-storage-flags { opacity: 0.9; } .setup-storage-flags { opacity: 0.9; }
/* Modal */ /* The wizard is a full-screen overlay at z-index 9999; the shared modal sits at
.setup-modal-backdrop { 1100. A modal opened from inside the wizard therefore rendered BEHIND it and
position: fixed; inset: 0; z-index: 10000; looked like the Details button did nothing. Raise it only while the wizard is
background: rgba(4, 20, 38, 0.62); up, so no other overlay's stacking is affected. */
backdrop-filter: blur(3px); body.setup-wizard-open .eo-modal { z-index: 10000; }
display: flex; align-items: center; justify-content: center;
padding: 24px;
}
.setup-modal {
width: min(680px, 100%);
max-height: 82vh;
overflow: auto;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.18);
background: rgba(12, 48, 84, 0.97);
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.45);
}
.setup-modal-head {
display: flex; align-items: center; justify-content: space-between;
gap: 12px; padding: 16px 20px;
border-bottom: 1px solid rgba(255, 255, 255, 0.14);
}
.setup-modal-title { font-weight: 700; font-size: 1.05em; word-break: break-all; }
.setup-modal-close {
background: none; border: none; color: inherit;
font-size: 1.6em; line-height: 1; cursor: pointer; opacity: 0.75;
}
.setup-modal-close:hover { opacity: 1; }
.setup-modal-body { padding: 18px 20px 22px; }
/* Modal chrome comes from the shared eo-modal (core/overlays). Only the
storage-specific CONTENT styles below are ours they render inside
.eo-modal-body. */
.setup-storage-spec { width: 100%; border-collapse: collapse; margin-bottom: 18px; } .setup-storage-spec { width: 100%; border-collapse: collapse; margin-bottom: 18px; }
.setup-storage-spec th { .setup-storage-spec th {
text-align: left; font-weight: 500; opacity: 0.72; text-align: left; font-weight: 500; opacity: 0.72;

View File

@ -504,14 +504,20 @@ class SetupWizard {
} }
} }
// Details modal: the technical spec, every check with its full explanation, // Details modal — the technical spec, every check with its full explanation,
// and — when the drive isn\u2019t in fstab — the offer to make it permanent. // and (when the drive isn't in fstab) the offer to make it permanent.
//
// Uses the shared openEoModal rather than a bespoke overlay. Two reasons: it
// is what every other modal in the WebUI looks like, and it appends to
// document.body — a hand-rolled position:fixed backdrop appended INSIDE the
// wizard container resolves against the wizard's containing block (it has a
// backdrop-filter), so it covered part of the screen instead of all of it.
showStorageDetails(idx) { showStorageDetails(idx) {
const c = this.storageCandidates[idx]; const c = this.storageCandidates[idx];
if (!c) return; if (!c) return;
const esc = (s) => this.escapeHtml(s); const esc = (s) => this.escapeHtml(s);
const specRows = [ const spec = [
['Mount point', c.path], ['Mount point', c.path],
['Device', c.device], ['Device', c.device],
['Filesystem', c.fstype], ['Filesystem', c.fstype],
@ -523,7 +529,7 @@ class SetupWizard {
].map(([k, v]) => `<tr><th>${esc(k)}</th><td><code>${esc(v)}</code></td></tr>`).join(''); ].map(([k, v]) => `<tr><th>${esc(k)}</th><td><code>${esc(v)}</code></td></tr>`).join('');
const icon = { refuse: '\u26d4', warn: '\u26a0\ufe0f', info: '\u2705' }; const icon = { refuse: '\u26d4', warn: '\u26a0\ufe0f', info: '\u2705' };
const checkRows = (c.checks || []).map(k => ` const checks = (c.checks || []).map(k => `
<li class="setup-storage-check setup-storage-check-${esc(k.severity)}"> <li class="setup-storage-check setup-storage-check-${esc(k.severity)}">
<span class="setup-storage-check-icon">${icon[k.severity] || ''}</span> <span class="setup-storage-check-icon">${icon[k.severity] || ''}</span>
<span> <span>
@ -532,64 +538,65 @@ class SetupWizard {
</span> </span>
</li>`).join(''); </li>`).join('');
const fstab = c.fstab_line ? ` const parts = [
<div class="setup-storage-fstab"> window.eoSection ? window.eoSection('Drive', `<table class="setup-storage-spec">${spec}</table>`)
<div class="setup-storage-fstab-title">Make this drive mount automatically</div> : `<table class="setup-storage-spec">${spec}</table>`,
<p> window.eoSection ? window.eoSection('Checks', `<ul class="setup-storage-checks">${checks}</ul>`)
The system doesn\u2019t mount this drive at boot, so apps stored on it won\u2019t start : `<ul class="setup-storage-checks">${checks}</ul>`,
until it is mounted again. LibrePortal can add it to <code>/etc/fstab</code> for you. ];
</p>
<pre><code>${esc(c.fstab_line)}</code></pre>
<label class="setup-storage-fstab-opt">
<input type="checkbox" data-storage-fstab="${esc(c.path)}"
${this.fstabWanted.includes(c.path) ? 'checked' : ''}>
<span>Add this line during install</span>
</label>
<p class="setup-storage-fstab-note">
Written with <code>nofail</code>, so if the drive is missing the machine still boots
normally. Your current <code>/etc/fstab</code> is backed up first.
</p>
</div>` : '';
const modal = document.createElement('div'); if (c.fstab_line) {
modal.className = 'setup-modal-backdrop'; parts.push(`
modal.innerHTML = ` <div class="setup-storage-fstab">
<div class="setup-modal" role="dialog" aria-modal="true" aria-label="Drive details"> <div class="setup-storage-fstab-title">Make this drive mount automatically</div>
<div class="setup-modal-head"> <p>
<span class="setup-modal-title">${esc(c.path)}</span> The system doesn\u2019t mount this drive at boot, so apps stored on it won\u2019t start
<button type="button" class="setup-modal-close" aria-label="Close">&times;</button> until it is mounted again. LibrePortal can add it to <code>/etc/fstab</code> for you.
</div> </p>
<div class="setup-modal-body"> <pre><code>${esc(c.fstab_line)}</code></pre>
<table class="setup-storage-spec">${specRows}</table> <label class="setup-storage-fstab-opt">
<ul class="setup-storage-checks">${checkRows}</ul> <input type="checkbox" data-storage-fstab="${esc(c.path)}"
${fstab} ${this.fstabWanted.includes(c.path) ? 'checked' : ''}>
</div> <span>Add this line during install</span>
</div>`; </label>
<p class="setup-storage-fstab-note">
Written with <code>nofail</code>, so if the drive is missing the machine still boots
normally. Your current <code>/etc/fstab</code> is backed up first.
</p>
</div>`);
}
const close = () => modal.remove(); // No openEoModal (shouldn't happen — index.html loads it before this file,
modal.addEventListener('click', (e) => { if (e.target === modal) close(); }); // but the wizard must not become unusable if it ever doesn't).
modal.querySelector('.setup-modal-close').addEventListener('click', close); if (typeof window.openEoModal !== 'function') {
document.addEventListener('keydown', function onKey(e) { console.warn('[setup] openEoModal unavailable; skipping details modal');
if (e.key === 'Escape') { close(); document.removeEventListener('keydown', onKey); } return;
}
const m = window.openEoModal({
id: 'lp-storage-details',
size: 'md',
title: c.path,
desc: `${c.size} drive \u00b7 ${c.free} free \u00b7 ${c.fstype}${c.removable ? ' \u00b7 removable' : ''}`,
body: parts.join(''),
actions: [{ label: 'Close', variant: 'secondary' }],
}); });
// Remember the fstab choice on the wizard, not in the DOM: the modal is // The choice is remembered on the wizard, not in the DOM: the modal is
// destroyed on close and the choice has to survive to submit(). // destroyed on close and this has to survive until submit().
const fstabBox = modal.querySelector('[data-storage-fstab]'); const fstabBox = m.bodyEl.querySelector('[data-storage-fstab]');
if (fstabBox) { if (fstabBox) {
fstabBox.addEventListener('change', () => { fstabBox.addEventListener('change', () => {
const path = fstabBox.dataset.storageFstab; const path = fstabBox.dataset.storageFstab;
this.fstabWanted = this.fstabWanted.filter(p => p !== path); this.fstabWanted = this.fstabWanted.filter(p => p !== path);
if (fstabBox.checked) { if (fstabBox.checked) {
this.fstabWanted.push(path); this.fstabWanted.push(path);
// Adding it to fstab only makes sense if the drive is actually used. // Mounting it at boot only means anything if the drive is used.
const own = this.container.querySelector(`[data-storage-path="${CSS.escape(path)}"]`); const own = this.container.querySelector(`[data-storage-path="${CSS.escape(path)}"]`);
if (own && !own.disabled) own.checked = true; if (own && !own.disabled) own.checked = true;
} }
}); });
} }
this.container.appendChild(modal);
} }
escapeHtml(s) { escapeHtml(s) {