Compare commits

..

No commits in common. "67970a84e9f60a0d1aa91bc2b38aee17d6d8e651" and "325f8f3be837cb0fd01875539ee47cd4e29db7c7" have entirely different histories.

7 changed files with 11 additions and 129 deletions

View File

@ -194,38 +194,6 @@
padding: 0 6px; padding: 0 6px;
} }
.ov-toolbar-actions { display: flex; gap: 8px; } .ov-toolbar-actions { display: flex; gap: 8px; }
/* Select-all, mirroring the Tasks toolbar: pushed to the right of the chips so
the picker column and its master control line up on the same edge. */
.ov-select-all {
margin-left: auto;
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: var(--text-secondary);
cursor: pointer;
white-space: nowrap;
}
.ov-select-all input {
width: 15px;
height: 15px;
accent-color: var(--accent);
cursor: pointer;
}
/* Trailing picker. No fixed width reserved: an unselectable row renders no
checkbox at all, so holding a column open would reintroduce the gutter this
replaced. */
.ov-pick {
display: inline-flex;
align-items: center;
margin-left: 4px;
}
.ov-pick-box {
width: 15px;
height: 15px;
accent-color: var(--accent);
cursor: pointer;
}
/* ---- Updates: expandable rows ------------------------------------------- */ /* ---- Updates: expandable rows ------------------------------------------- */
/* Block (not the inherited .updater-row 3-col grid) so the head sits on top and /* Block (not the inherited .updater-row 3-col grid) so the head sits on top and

View File

@ -331,21 +331,6 @@ class OverviewManager {
this.selected.clear(); this.selected.clear();
this._applyTab('updates'); this._applyTab('updates');
return; return;
case 'select-all': {
// Acts on what is actually selectable and in view — the filter chips
// can narrow the list, and "select all" that quietly picked rows the
// user cannot see would be a worse answer than none.
const boxes = Array.from(document.querySelectorAll('#overview-view .ov-pick-box'));
const on = !!(oa.checked);
for (const b of boxes) {
b.checked = on;
const slug = b.dataset.app;
if (!slug) continue;
if (on) this.selected.add(slug); else this.selected.delete(slug);
}
this._syncSelectionBar();
break;
}
case 'update-selected': case 'update-selected':
if (this.updater) this.updater.applySelected([...this.selected]); if (this.updater) this.updater.applySelected([...this.selected]);
this.selected.clear(); this.selected.clear();
@ -621,15 +606,6 @@ class OverviewManager {
return [...this.selected]; return [...this.selected];
} }
// True when every selectable row currently in view is picked — drives the
// master checkbox's checked state on render.
_allPicked() {
const up = this.updater;
if (!up) return false;
const avail = up.apps.filter((a) => a.update_available).map((a) => a.name);
return avail.length > 0 && avail.every((n) => this.selected.has(n));
}
// Update the bar in place. Re-rendering the whole tab on every tick would // Update the bar in place. Re-rendering the whole tab on every tick would
// rebuild the checkboxes underneath the pointer and lose focus mid-selection. // rebuild the checkboxes underneath the pointer and lose focus mid-selection.
_syncSelectionBar() { _syncSelectionBar() {
@ -679,10 +655,6 @@ class OverviewManager {
${up.renderAutoCheckLine()} ${up.renderAutoCheckLine()}
<div class="updater-toolbar ov-toolbar"> <div class="updater-toolbar ov-toolbar">
<div class="ov-chips">${chip('all', 'All', up.apps.length)}${chip('updates', 'Updates', nUpd)}${chip('newer', 'Newer', nNew)}${chip('security', 'Security', nSec)}</div> <div class="ov-chips">${chip('all', 'All', up.apps.length)}${chip('updates', 'Updates', nUpd)}${chip('newer', 'Newer', nNew)}${chip('security', 'Security', nSec)}</div>
${nUpd ? `<label class="ov-select-all" title="Select every updatable app in view">
<input type="checkbox" data-overview-action="select-all"${this._allPicked() ? ' checked' : ''}>
<span>Select all</span>
</label>` : ''}
</div> </div>
${nUpd ? this.renderSelectionBar() : ''} ${nUpd ? this.renderSelectionBar() : ''}
<div class="updater-list ov-updates-list">${rows}</div>`; <div class="updater-list ov-updates-list">${rows}</div>`;
@ -793,25 +765,20 @@ class OverviewManager {
const updBtn = a.update_available const updBtn = a.update_available
? `<button class="updater-btn updater-btn-primary" data-updater-action="update" data-app="${slug}">Update</button>` ? `<button class="updater-btn updater-btn-primary" data-updater-action="update" data-app="${slug}">Update</button>`
: (a.newer_version : (a.newer_version
? `<button class="updater-btn updater-btn-success" data-updater-action="upgrade" data-app="${slug}" data-version="${esc(a.newer_version)}" title="Move to the ${esc(a.newer_version)} release line — you will be asked to confirm first">Upgrade</button>` ? `<button class="updater-btn ov-upgrade-btn" data-updater-action="upgrade" data-app="${slug}" data-version="${esc(a.newer_version)}" title="Move to the ${esc(a.newer_version)} release line — you will be asked to confirm first">Upgrade to ${esc(a.newer_version)}</button>`
: ''); : '');
// Only rows with something to apply are selectable. A checkbox on a row // Only rows with something to apply are selectable. A checkbox on a row
// that is already current would offer a choice with no outcome, and // that is already current would offer a choice with no outcome, and
// "selected 6, updated 2" is a worse answer than not offering the 4. // "selected 6, updated 2" is a worse answer than not offering the 4.
// Trailing, and absent rather than blank when the row is not selectable.
// It used to lead the row with an `ov-pick-empty` spacer holding the column
// open — so with nothing updatable, every row carried a left gutter for a
// checkbox that was never coming. Matches the Tasks list, where the picker
// sits last and unselectable rows simply have none.
const pick = a.update_available const pick = a.update_available
? `<span class="ov-pick"><input type="checkbox" class="ov-pick-box" data-overview-action="select" data-app="${slug}"${this.selected.has(a.name) ? ' checked' : ''} aria-label="Select ${name} for update"></span>` ? `<span class="ov-pick"><input type="checkbox" class="ov-pick-box" data-overview-action="select" data-app="${slug}"${this.selected.has(a.name) ? ' checked' : ''} aria-label="Select ${name} for update"></span>`
: ''; : '<span class="ov-pick ov-pick-empty" aria-hidden="true"></span>';
return `<div class="updater-row ov-row" data-app="${slug}"> return `<div class="updater-row ov-row" data-app="${slug}">
<div class="updater-row-head ov-row-head" data-overview-action="toggle" data-app="${slug}" role="button" tabindex="0" aria-expanded="false" aria-controls="ov-detail-${slug}"> <div class="updater-row-head ov-row-head" data-overview-action="toggle" data-app="${slug}" role="button" tabindex="0" aria-expanded="false" aria-controls="ov-detail-${slug}">
<img class="ov-row-icon" src="/core/icons/apps/${slug}.svg" alt="" onerror="this.style.display='none'"> ${pick}<img class="ov-row-icon" src="/core/icons/apps/${slug}.svg" alt="" onerror="this.style.display='none'">
<span class="updater-row-name">${name}</span> ${status} ${sev} ${pol} ${newer} ${stale} <span class="updater-row-name">${name}</span> ${status} ${sev} ${pol} ${newer} ${stale}
<span class="updater-row-ver">${cur}${avail ? ` <span class="updater-arrow">→</span> <strong>${avail}</strong>` : ''}</span> <span class="updater-row-ver">${cur}${avail ? ` <span class="updater-arrow">→</span> <strong>${avail}</strong>` : ''}</span>
<span class="ov-row-actions">${updBtn}<button class="ov-details-btn" data-overview-action="toggle" data-app="${slug}" tabindex="-1" aria-hidden="true"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6,9 12,15 18,9"></polyline></svg><span>Details</span></button>${pick}</span> <span class="ov-row-actions">${updBtn}<button class="ov-details-btn" data-overview-action="toggle" data-app="${slug}" tabindex="-1" aria-hidden="true"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6,9 12,15 18,9"></polyline></svg><span>Details</span></button></span>
</div> </div>
<div class="updater-row-details ov-row-details" id="ov-detail-${slug}" role="region" aria-label="${name} details" hidden></div> <div class="updater-row-details ov-row-details" id="ov-detail-${slug}" role="region" aria-label="${name} details" hidden></div>
</div>`; </div>`;

View File

@ -1355,18 +1355,6 @@
background: rgba(var(--text-rgb), 0.05); background: rgba(var(--text-rgb), 0.05);
border-radius: 4px; border-radius: 4px;
letter-spacing: 0.02em; letter-spacing: 0.02em;
/* The chip is the last thing in .task-info and .task-actions starts straight
after it, so without this the backup id and the Restore button touch. */
margin-right: 12px;
}
/* Roomier rows than the Tasks list they borrow .task-* from. .task-header ships
4px of vertical padding, which is right for a dense log of tasks and too tight
here: these rows carry a 28px app icon, so the content sat hard against the
card edges. Scoped to the snapshot item so the Tasks page keeps its density. */
.backup-snapshot-item .task-header {
padding-top: 10px;
padding-bottom: 10px;
} }
.backup-snapshot-tag { .backup-snapshot-tag {
display: inline-block; display: inline-block;

View File

@ -184,17 +184,6 @@
color: rgb(var(--page-rgb, var(--accent-rgb))); color: rgb(var(--page-rgb, var(--accent-rgb)));
} }
.updater-btn-primary:hover { background: rgba(var(--page-rgb, var(--accent-rgb)), 0.3); } .updater-btn-primary:hover { background: rgba(var(--page-rgb, var(--accent-rgb)), 0.3); }
/* Green, for moving to a newer release line. Distinct from the primary (accent)
Update so the two actions on a row never read as the same thing.
Pastel #86efac text on a saturated green rather than --status-success itself,
matching .app-tag.installed-tag: the raw #28a745 is too dark to read at button
size on dark themes. */
.updater-btn-success {
background: rgba(var(--status-success-rgb), 0.30);
border-color: rgba(var(--status-success-rgb), 0.65);
color: #86efac;
}
.updater-btn-success:hover { background: rgba(var(--status-success-rgb), 0.45); }
/* ---- Hints / empty states ---- */ /* ---- Hints / empty states ---- */
.updater-hint { padding: 12px 15px; border-radius: 11px; margin-bottom: 14px; font-size: 0.84rem; .updater-hint { padding: 12px 15px; border-radius: 11px; margin-bottom: 14px; font-size: 0.84rem;

View File

@ -339,15 +339,12 @@ class UpdaterPage {
const a = this.apps.find((x) => x.name === app) || {}; const a = this.apps.find((x) => x.name === app) || {};
const from = a.channel || a.current_version || 'the current version'; const from = a.channel || a.current_version || 'the current version';
const to = version || a.newer_version || 'the newest release'; const to = version || a.newer_version || 'the newest release';
// Display name, not the slug: the dialog now leads with the app's icon, and
// "Upgrade matrix to…" beside the Matrix logo reads as a different thing.
const label = (window.getAppDisplayName ? window.getAppDisplayName(app) : null) || a.displayName || app;
const body = ` const body = `
<div class="updater-detail-section"> <div class="updater-detail-section">
<p><strong>${this.escape(label)}</strong> will move from <strong>${this.escape(from)}</strong> <p><strong>${this.escape(app)}</strong> will move from <strong>${this.escape(from)}</strong>
to <strong>${this.escape(to)}</strong>, one release at a time.</p> to <strong>${this.escape(to)}</strong>, one release at a time.</p>
<p class="updater-detail-meta">Every step takes its own recovery snapshot first, then waits for <p class="updater-detail-meta">Every step takes its own recovery snapshot first, then waits for
${this.escape(label)} to confirm it is serving that version with no migration outstanding. ${this.escape(app)} to confirm it is serving that version with no migration outstanding.
If any step fails it is rolled back and the upgrade stops there, leaving the app on the last If any step fails it is rolled back and the upgrade stops there, leaving the app on the last
version that verified.</p> version that verified.</p>
<p class="updater-detail-meta">This can take a long time each release runs its own database <p class="updater-detail-meta">This can take a long time each release runs its own database
@ -356,9 +353,8 @@ class UpdaterPage {
const go = () => this.dispatch('updater_upgrade', { app, version: version || '' }, const go = () => this.dispatch('updater_upgrade', { app, version: version || '' },
`Upgrading ${app} to ${to}, one release at a time…`); `Upgrading ${app} to ${to}, one release at a time…`);
if (window.showConfirmation) { if (window.showConfirmation) {
window.showConfirmation(`Upgrade ${label} to ${to}?`, '', go, 'Start upgrade', 'Cancel', 'warning', false, '', body, window.showConfirmation(`Upgrade ${app} to ${to}?`, '', go, 'Start upgrade', 'Cancel', 'warning', false, '', body);
`/core/icons/apps/${app}.svg`); } else if (window.confirm(`Upgrade ${app} from ${from} to ${to}, one release at a time?`)) {
} else if (window.confirm(`Upgrade ${label} from ${from} to ${to}, one release at a time?`)) {
go(); go();
} }
} }

View File

@ -31,14 +31,13 @@ class ConfirmationDialog {
} }
show(title, message, onConfirm, confirmText = 'Confirm', cancelText = 'Cancel', confirmClass = 'primary', showDataLossCheckbox = false, checkboxText = 'I understand I will lose all my data and it cannot be undone', messageHtml = '', iconUrl = '') { show(title, message, onConfirm, confirmText = 'Confirm', cancelText = 'Cancel', confirmClass = 'primary', showDataLossCheckbox = false, checkboxText = 'I understand I will lose all my data and it cannot be undone', messageHtml = '') {
this.callback = onConfirm; this.callback = onConfirm;
// Build dialog content // Build dialog content
this.dialog.innerHTML = ` this.dialog.innerHTML = `
<div class="confirmation-header"> <div class="confirmation-header">
${iconUrl ? `<div class="confirmation-app-icon"><img src="${this.escapeHtml(iconUrl)}" alt="" onerror="this.onerror=null; this.src='/core/icons/apps/default.svg'"></div>` : ''}
<h3>${this.escapeHtml(title)}</h3> <h3>${this.escapeHtml(title)}</h3>
<button class="confirmation-close" onclick="window.confirmationDialog.hide()">×</button> <button class="confirmation-close" onclick="window.confirmationDialog.hide()">×</button>
</div> </div>
@ -143,12 +142,12 @@ function initConfirmationDialog() {
// initConfirmationDialog() will be called centrally // initConfirmationDialog() will be called centrally
// Global function // Global function
window.showConfirmation = (title, message, onConfirm, confirmText, cancelText, confirmClass, showDataLossCheckbox, checkboxText, messageHtml, iconUrl) => { window.showConfirmation = (title, message, onConfirm, confirmText, cancelText, confirmClass, showDataLossCheckbox, checkboxText, messageHtml) => {
// Ensure dialog is initialized // Ensure dialog is initialized
initConfirmationDialog(); initConfirmationDialog();
if (confirmationDialog) { if (confirmationDialog) {
confirmationDialog.show(title, message, onConfirm, confirmText, cancelText, confirmClass, showDataLossCheckbox, checkboxText, messageHtml, iconUrl); confirmationDialog.show(title, message, onConfirm, confirmText, cancelText, confirmClass, showDataLossCheckbox, checkboxText, messageHtml);
} else { } else {
// Fallback to native confirm // Fallback to native confirm
if (confirm(message)) { if (confirm(message)) {

View File

@ -901,33 +901,8 @@ html[data-theme="nebula"]::after {
border-bottom: 1px solid var(--border-color, #444); border-bottom: 1px solid var(--border-color, #444);
} }
/* Icon holder, when a caller names an app. Styled here rather than reusing
.app-card-icon from apps.css: this dialog is global and appears on pages that
never load the apps stylesheet, where borrowing that class would render an
unstyled image. */
.confirmation-app-icon {
width: 40px;
height: 40px;
min-width: 40px;
padding: 7px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(var(--text-rgb), 0.1);
border: 1px solid rgba(var(--text-rgb), 0.2);
}
.confirmation-app-icon img {
width: 100%;
height: 100%;
object-fit: contain;
}
.confirmation-header h3 { .confirmation-header h3 {
margin: 0; margin: 0;
/* Takes the slack so the close button stays pinned right once an icon leads
the header the row is space-between, which would otherwise centre it. */
flex: 1;
color: var(--text-primary); color: var(--text-primary);
color: var(--text-primary, #fff); color: var(--text-primary, #fff);
font-size: 16px; font-size: 16px;