Merge claude/1

This commit is contained in:
librelad 2026-05-27 15:46:18 +01:00
commit 13cafcb056
3 changed files with 211 additions and 10 deletions

View File

@ -382,6 +382,108 @@
border-color: var(--status-danger);
}
/* Multi-select checkbox sat to the right of the row's Delete button.
Sized + framed to match the surrounding .task-btn buttons so the row
reads as a single action group. Hides the native input and renders
a custom box; `cursor: pointer` on the wrapping label keeps the
whole 24×24 hit target clickable. */
.task-select {
display: inline-flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
border-radius: 6px;
border: 1px solid rgba(var(--text-rgb), 0.12);
background: rgba(var(--text-rgb), 0.04);
cursor: pointer;
transition: background 0.18s ease, border-color 0.18s ease;
}
.task-select:hover {
background: var(--surface-hover);
border-color: var(--status-success);
}
.task-select input[type="checkbox"] {
position: absolute;
opacity: 0;
pointer-events: none;
width: 0; height: 0;
}
.task-select-box {
width: 12px;
height: 12px;
border-radius: 3px;
border: 1.5px solid rgba(var(--text-rgb), 0.45);
background: transparent;
position: relative;
transition: background 0.12s ease, border-color 0.12s ease;
}
.task-select input[type="checkbox"]:checked + .task-select-box {
background: var(--status-success);
border-color: var(--status-success);
}
.task-select input[type="checkbox"]:checked + .task-select-box::after {
content: "";
position: absolute;
top: 1px;
left: 4px;
width: 3px;
height: 7px;
border: solid var(--text-on-accent, #fff);
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
.task-select input[type="checkbox"]:focus-visible + .task-select-box {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
/* Master "Select all" toggle in the status/action bar (left of Clear All).
Uses the same custom-box visual as the row checkbox so the two reinforce
each other; the label sits inline with the button text style. */
.task-select-all {
display: inline-flex;
align-items: center;
gap: 6px;
cursor: pointer;
padding: 4px 8px;
border-radius: 6px;
font-size: 11px;
color: var(--text-secondary);
user-select: none;
transition: background 0.18s ease, color 0.18s ease;
}
.task-select-all:hover {
background: var(--surface-hover);
color: var(--text-primary);
}
.task-select-all input[type="checkbox"] {
position: absolute;
opacity: 0;
pointer-events: none;
width: 0; height: 0;
}
/* The indeterminate state (some-but-not-all visible rows ticked) shows
a horizontal dash instead of a check. */
.task-select-all input[type="checkbox"]:indeterminate + .task-select-box {
background: var(--status-success);
border-color: var(--status-success);
}
.task-select-all input[type="checkbox"]:indeterminate + .task-select-box::after {
content: "";
position: absolute;
top: 4px;
left: 1px;
width: 8px;
height: 2px;
background: var(--text-on-accent, #fff);
border: none;
transform: none;
}
.task-select-all-label {
font-weight: 500;
}
.task-details {
border-top: 1px solid rgba(var(--text-rgb), 0.10);
background: transparent;

View File

@ -151,12 +151,17 @@
</svg>
Refresh
</button>
<button class="clear-btn" onclick="clearAllTasks()" title="Clear All Tasks">
<label class="task-select-all" title="Select all visible tasks">
<input type="checkbox" id="tasks-select-all" onchange="window.tasksManager && tasksManager.toggleSelectAll(this.checked)">
<span class="task-select-box" aria-hidden="true"></span>
<span class="task-select-all-label">Select all</span>
</label>
<button class="clear-btn" id="tasks-clear-btn" onclick="clearAllTasks()" title="Clear All Tasks">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 6h18"></path>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
Clear All
<span class="clear-btn-label">Clear All</span>
</button>
</div>
</div>

View File

@ -8,6 +8,10 @@ class TasksManager {
this.tasks = [];
this.currentCategory = 'all';
this.highlightedTaskId = null;
// Multi-select: ids of tasks the user has ticked. When non-empty the
// "Clear All" button morphs into "Delete Selected (N)". Both paths
// share _showClearAllModal — same UX, different filter.
this.selectedTaskIds = new Set();
this.taskManager = new TaskManager();
this.init();
@ -556,6 +560,11 @@ class TasksManager {
if (scrollParent) {
requestAnimationFrame(() => { scrollParent.scrollTop = savedScrollTop; });
}
// Resync the multi-select UI after the render. The Clear All label
// + master-checkbox tri-state need to reflect the just-rendered row
// set (selections can become stale across category switches).
if (typeof this._updateSelectionUI === 'function') this._updateSelectionUI();
}
filterTasksByCategory(tasks, category) {
@ -677,6 +686,11 @@ class TasksManager {
</svg>
<span class="task-btn-label">Delete</span>
</button>
<label class="task-select" onclick="event.stopPropagation();" title="Select for bulk delete">
<input type="checkbox" data-task-select="${task.id}" ${this.selectedTaskIds.has(task.id) ? 'checked' : ''}
onchange="event.stopPropagation(); window.tasksManager && tasksManager.toggleTaskSelection('${task.id}', this.checked)">
<span class="task-select-box" aria-hidden="true"></span>
</label>
</div>
</div>
@ -2328,19 +2342,86 @@ class TasksManager {
}
async clearAllTasks() {
const result = await this._showClearAllModal(this.tasks);
// Routes to one of two modes depending on whether any rows are ticked.
// Both paths share _showClearAllModal — same UX, same modal, different
// input list + title.
const hasSelection = this.selectedTaskIds.size > 0;
const targetTasks = hasSelection
? this.tasks.filter(t => this.selectedTaskIds.has(t.id))
: this.tasks;
const result = await this._showClearAllModal(targetTasks, hasSelection ? 'selected' : 'all');
if (!result || !result.confirmed) return false;
await this.performClearAll({ cancelRunning: result.cancelRunning });
await this.performClearAll({ cancelRunning: result.cancelRunning, targets: targetTasks });
if (hasSelection) {
// Drop any ids we just deleted from the selection set, then refresh
// the button label + master-checkbox state.
this.selectedTaskIds = new Set([...this.selectedTaskIds].filter(id => this.tasks.find(t => t.id === id)));
this._updateSelectionUI();
}
return true;
}
// Selection helpers wired by the row + master checkboxes.
toggleTaskSelection(taskId, checked) {
if (checked) this.selectedTaskIds.add(taskId);
else this.selectedTaskIds.delete(taskId);
this._updateSelectionUI();
}
toggleSelectAll(checked) {
if (checked) {
// Tick every CURRENTLY VISIBLE row. We use the rendered checkboxes
// rather than this.tasks so category-filtered views only select
// what the user can see.
const boxes = document.querySelectorAll('#tasks-list [data-task-select]');
boxes.forEach((cb) => {
this.selectedTaskIds.add(cb.dataset.taskSelect);
cb.checked = true;
});
} else {
this.selectedTaskIds.clear();
document.querySelectorAll('#tasks-list [data-task-select]').forEach((cb) => { cb.checked = false; });
}
this._updateSelectionUI();
}
// Rewrites the Clear All button label + the master-checkbox indeterminate
// state to reflect the current selection. Cheap — only touches a few
// DOM nodes, safe to call from any selection-change path.
_updateSelectionUI() {
const n = this.selectedTaskIds.size;
const btn = document.getElementById('tasks-clear-btn');
const btnLabel = btn && btn.querySelector('.clear-btn-label');
if (btnLabel) btnLabel.textContent = n > 0 ? `Delete Selected (${n})` : 'Clear All';
if (btn) btn.title = n > 0 ? `Delete ${n} selected task${n === 1 ? '' : 's'}` : 'Clear All Tasks';
// Master checkbox: checked when ALL visible are picked, indeterminate
// when SOME are, unchecked when none.
const master = document.getElementById('tasks-select-all');
const visible = document.querySelectorAll('#tasks-list [data-task-select]');
if (master) {
if (n === 0 || visible.length === 0) {
master.checked = false;
master.indeterminate = false;
} else if (n >= visible.length) {
master.checked = true;
master.indeterminate = false;
} else {
master.checked = false;
master.indeterminate = true;
}
}
}
// Confirmation modal for clearAllTasks. Same openEoModal shape as
// _showDeleteTaskModal so visual identity matches every other destructive
// confirmation (Uninstall, Delete Task, …). Adds a "Cancel running tasks
// too" toggle (off by default) — when off, running/queued tasks are
// skipped; when on, they're cancelled first then deleted.
// Resolves {confirmed, cancelRunning}. Cancel/backdrop/close → confirmed=false.
_showClearAllModal(tasks) {
// mode: 'all' (everything) or 'selected' (user-picked subset) — just
// shapes the title/copy.
_showClearAllModal(tasks, mode) {
return new Promise((resolve) => {
const escHtml = (s) => String(s == null ? '' : s)
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
@ -2389,12 +2470,21 @@ class TasksManager {
resolve(val);
};
// Title shape depends on which path got us here:
// selected → "Delete N selected task(s)?" (multi-select chip)
// all → "Delete all N tasks?" (legacy Clear All)
const isSelectedMode = mode === 'selected';
const titleText = isSelectedMode
? (total === 1 ? 'Delete 1 selected task?' : `Delete ${total} selected tasks?`)
: (total === 1 ? 'Delete 1 task?' : `Delete all ${total} tasks?`);
const m = window.openEoModal({
id: 'clear-all-tasks-modal',
size: 'sm',
eyebrow: 'Delete Tasks',
title: total === 1 ? 'Delete 1 task?' : `Delete all ${total} tasks?`,
desc: 'Confirm to delete the selected tasks.',
eyebrow: isSelectedMode ? 'Delete Selected' : 'Delete Tasks',
title: titleText,
desc: isSelectedMode
? 'Confirm to delete the ticked tasks.'
: 'Confirm to delete every task on the list.',
body: bodyHtml,
actions: [
{
@ -2432,6 +2522,10 @@ class TasksManager {
async performClearAll(opts) {
opts = opts || {};
const cancelRunning = !!opts.cancelRunning;
// Caller (clearAllTasks) passes the subset to operate on. Falls back
// to this.tasks for backward compatibility — old direct callers
// continue to mean "everything".
const universe = Array.isArray(opts.targets) ? opts.targets : (this.tasks || []);
// Show progress notification with the trash type icon in the left slot
// (this is conceptually a delete-all action, so 🗑️ matches the row icon).
@ -2447,8 +2541,8 @@ class TasksManager {
const isActive = (t) => t && (t.status === 'running' || t.status === 'queued' || t.status === 'pending');
// Partition: which tasks we attempt now, which we skip silently.
const targets = (this.tasks || []).filter(t => cancelRunning || !isActive(t));
const skipped = (this.tasks || []).filter(t => !targets.includes(t));
const targets = universe.filter(t => cancelRunning || !isActive(t));
const skipped = universe.filter(t => !targets.includes(t));
try {
// For active tasks (only reached when the toggle is on): cancel first,