Edited completed Task + Delte HTML Windows

This commit is contained in:
Honey committed 2026-02-21 01:18:54 +01:00
1 parent e5e0703106
commit def7d22294
3 files changed
+504 -122

No files matched your search

+374 -84
View File
@@ -50,6 +50,7 @@ class App {
async init() {
this.setupGreeting();
this.setupEventListeners();
this.setupContainerDrag();
await this.loadTasks();
await this.loadArchivedCount();
}
@@ -257,6 +258,30 @@ class App {
hideContextMenu() { document.getElementById('context-menu').classList.remove('show'); }
// ─── Confirm Dialog ───
showConfirm(message, icon = '🗑') {
return new Promise((resolve) => {
const overlay = document.getElementById('confirm-overlay');
document.getElementById('confirm-message').innerHTML = message;
document.getElementById('confirm-icon').innerHTML = icon;
overlay.classList.add('open');
const cancel = document.getElementById('confirm-cancel');
const del = document.getElementById('confirm-delete');
const cleanup = () => {
overlay.classList.remove('open');
cancel.replaceWith(cancel.cloneNode(true));
del.replaceWith(del.cloneNode(true));
};
document.getElementById('confirm-cancel').addEventListener('click', () => { cleanup(); resolve(false); });
document.getElementById('confirm-delete').addEventListener('click', () => { cleanup(); resolve(true); });
overlay.addEventListener('click', (e) => { if (e.target === overlay) { cleanup(); resolve(false); } }, { once: true });
});
}
handleContextEdit() {
if (this.contextMenuTarget) this.openEditModal(this.contextMenuTarget, this.contextMenuParent);
this.hideContextMenu();
@@ -264,10 +289,14 @@ class App {
async handleContextDelete() {
if (!this.contextMenuTarget) return;
if (!confirm(`Delete this ${this.contextMenuType}?`)) { this.hideContextMenu(); return; }
if (this.contextMenuType === 'task') await this.deleteTask(this.contextMenuTarget.id);
else if (this.contextMenuType === 'subtask' && this.contextMenuParent) await this.deleteSubtask(this.contextMenuParent.id, this.contextMenuTarget.id);
const type = this.contextMenuType;
this.hideContextMenu();
const confirmed = await this.showConfirm(
`Delete this ${type}?<br><span style="font-size:0.85em;color:var(--text-muted)">This action cannot be undone.</span>`
);
if (!confirmed) return;
if (type === 'task') this.deleteTask(this.contextMenuTarget.id);
else if (type === 'subtask' && this.contextMenuParent) this.deleteSubtask(this.contextMenuParent.id, this.contextMenuTarget.id);
}
async handleContextPromote() {
@@ -497,6 +526,13 @@ class App {
}
}
async confirmDeleteTask(taskId) {
const confirmed = await this.showConfirm(
'Delete permanently?<br><span style="font-size:0.85em;color:var(--text-muted)">This action cannot be undone.</span>'
);
if (confirmed) this.deleteTask(taskId);
}
toggleTaskComplete(taskId) {
const task = this.getCurrentTasks().find(t => t.id === taskId);
if (!task) return;
@@ -589,26 +625,169 @@ class App {
}
}
// ─── Drag & Drop ───
// ─── Drag & Drop (insertion indicator system) ───
_createDropIndicator() {
const el = document.createElement('div');
el.className = 'drop-indicator';
el.style.display = 'none';
document.body.appendChild(el);
return el;
}
_showDropIndicator(container, y) {
if (!this._dropIndicator) this._dropIndicator = this._createDropIndicator();
const cards = [...container.querySelectorAll(':scope > .task-card')];
let insertIndex = cards.length;
let indicatorY = 0;
for (let i = 0; i < cards.length; i++) {
const rect = cards[i].getBoundingClientRect();
const midY = rect.top + rect.height / 2;
if (y < midY) {
insertIndex = i;
indicatorY = rect.top - 4;
break;
}
}
// If after all cards
if (insertIndex === cards.length && cards.length > 0) {
const lastRect = cards[cards.length - 1].getBoundingClientRect();
indicatorY = lastRect.bottom + 4;
} else if (cards.length === 0) {
const containerRect = container.getBoundingClientRect();
indicatorY = containerRect.top + 8;
}
const containerRect = container.getBoundingClientRect();
this._dropIndicator.style.display = 'block';
this._dropIndicator.style.top = indicatorY + 'px';
this._dropIndicator.style.left = containerRect.left + 'px';
this._dropIndicator.style.width = containerRect.width + 'px';
this._dropInsertIndex = insertIndex;
}
_hideDropIndicator() {
if (this._dropIndicator) this._dropIndicator.style.display = 'none';
this._dropInsertIndex = null;
}
_showSubtaskDropIndicator(subtaskContainer, y) {
if (!this._subtaskDropIndicator) {
this._subtaskDropIndicator = document.createElement('div');
this._subtaskDropIndicator.className = 'drop-indicator drop-indicator-subtask';
this._subtaskDropIndicator.style.display = 'none';
document.body.appendChild(this._subtaskDropIndicator);
}
const subtasks = [...subtaskContainer.querySelectorAll(':scope > .subtask')];
let insertIndex = subtasks.length;
let indicatorY = 0;
for (let i = 0; i < subtasks.length; i++) {
const rect = subtasks[i].getBoundingClientRect();
if (y < rect.top + rect.height / 2) {
insertIndex = i;
indicatorY = rect.top - 2;
break;
}
}
if (insertIndex === subtasks.length && subtasks.length > 0) {
const lastRect = subtasks[subtasks.length - 1].getBoundingClientRect();
indicatorY = lastRect.bottom + 2;
}
const cRect = subtaskContainer.getBoundingClientRect();
this._subtaskDropIndicator.style.display = 'block';
this._subtaskDropIndicator.style.top = indicatorY + 'px';
this._subtaskDropIndicator.style.left = cRect.left + 'px';
this._subtaskDropIndicator.style.width = cRect.width + 'px';
this._subtaskDropInsertIndex = insertIndex;
}
_hideSubtaskDropIndicator() {
if (this._subtaskDropIndicator) this._subtaskDropIndicator.style.display = 'none';
this._subtaskDropInsertIndex = null;
}
setupContainerDrag() {
const container = document.getElementById('tasks-container');
container.addEventListener('dragover', (e) => {
e.preventDefault();
if (!this.draggedTask) return;
this._showDropIndicator(container, e.clientY);
});
container.addEventListener('dragleave', (e) => {
// Only hide if leaving the container entirely
if (!container.contains(e.relatedTarget)) {
this._hideDropIndicator();
}
});
container.addEventListener('drop', (e) => {
e.preventDefault();
// Handle nesting via drop zone
const dropZone = e.target.closest('.drop-zone');
if (dropZone) {
const taskCard = dropZone.closest('.task-card');
if (taskCard) {
const taskId = taskCard.getAttribute('data-task-id');
const targetTask = this.tasks.find(t => t.id === taskId);
if (targetTask) this._handleNestDrop(targetTask);
}
this._hideDropIndicator();
return;
}
// Handle reorder via indicator
if (this.draggedTask && this._dropInsertIndex != null) {
const currentIndex = this.tasks.findIndex(t => t.id === this.draggedTask.id);
let targetIndex = this._dropInsertIndex;
// Adjust index since we're removing before inserting
if (currentIndex !== -1 && currentIndex < targetIndex) targetIndex--;
if (currentIndex !== -1 && currentIndex !== targetIndex) {
this.tasks.splice(currentIndex, 1);
this.tasks.splice(targetIndex, 0, this.draggedTask);
this._syncReorder();
this.render();
}
}
this._hideDropIndicator();
});
}
handleDragStart(e, item, isSubtask = false, parentTask = null, index = null) {
if (isSubtask) { this.draggedSubtask = item; this.draggedParent = parentTask; this.draggedSubtaskIndex = index; this.draggedTask = null; }
else { this.draggedTask = item; this.draggedSubtask = null; this.draggedParent = null; this.draggedSubtaskIndex = null; }
if (isSubtask) {
this.draggedSubtask = item; this.draggedParent = parentTask;
this.draggedSubtaskIndex = index; this.draggedTask = null;
} else {
this.draggedTask = item; this.draggedSubtask = null;
this.draggedParent = null; this.draggedSubtaskIndex = null;
}
e.currentTarget.classList.add('dragging');
// Delay to let browser capture drag image
requestAnimationFrame(() => e.currentTarget.style.opacity = '0.4');
}
handleDragEnd(e) {
e.currentTarget.classList.remove('dragging');
e.currentTarget.style.opacity = '';
document.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
document.querySelectorAll('.drop-zone').forEach(el => el.classList.remove('visible'));
this._hideDropIndicator();
this._hideSubtaskDropIndicator();
if (this.dropZoneTimer) { clearTimeout(this.dropZoneTimer); this.dropZoneTimer = null; }
this.draggedTask = null; this.draggedSubtask = null; this.draggedParent = null; this.draggedSubtaskIndex = null;
this.draggedTask = null; this.draggedSubtask = null;
this.draggedParent = null; this.draggedSubtaskIndex = null;
}
handleCardDragOver(e) { e.preventDefault(); e.stopPropagation(); e.currentTarget.classList.add('drag-over'); }
handleDragLeave(e) { e.currentTarget.classList.remove('drag-over'); }
handleDragEnter(e, taskId) {
handleDragEnterCard(e, taskId) {
e.preventDefault();
if (!this.draggedTask && !this.draggedSubtask) return;
if (this.dropZoneTimer) clearTimeout(this.dropZoneTimer);
@@ -618,8 +797,7 @@ class App {
}, 1000);
}
async handleDrop(e, targetTask) {
e.preventDefault(); e.stopPropagation(); e.currentTarget.classList.remove('drag-over');
_handleNestDrop(targetTask) {
if (this.draggedTask && this.draggedTask.id !== targetTask.id) {
targetTask.subtasks.push(this.draggedTask);
this.tasks = this.tasks.filter(t => t.id !== this.draggedTask.id);
@@ -637,58 +815,34 @@ class App {
}
}
async handleTaskReorder(e, targetIndex) {
e.preventDefault(); e.stopPropagation(); e.currentTarget.classList.remove('drag-over');
if (!this.draggedTask) return;
const currentIndex = this.tasks.findIndex(t => t.id === this.draggedTask.id);
if (currentIndex === -1 || currentIndex === targetIndex) return;
// Update data
this.tasks.splice(currentIndex, 1);
this.tasks.splice(targetIndex, 0, this.draggedTask);
// Swap DOM nodes directly
const container = document.getElementById('tasks-container');
const cards = [...container.children];
const draggedCard = cards[currentIndex];
if (draggedCard) {
draggedCard.remove();
const referenceCard = container.children[targetIndex];
if (referenceCard) container.insertBefore(draggedCard, referenceCard);
else container.appendChild(draggedCard);
}
// Sync to server in background
this._syncReorder();
handleSubtaskContainerDragOver(e, task) {
e.preventDefault();
e.stopPropagation();
if (!this.draggedSubtask || !this.draggedParent || this.draggedParent.id !== task.id) return;
const subtaskContainer = e.currentTarget;
this._showSubtaskDropIndicator(subtaskContainer, e.clientY);
}
async handleSubtaskReorder(e, parentTask, targetIndex) {
e.preventDefault(); e.stopPropagation(); e.currentTarget.classList.remove('drag-over');
if (!this.draggedSubtask || !this.draggedParent || this.draggedParent.id !== parentTask.id) return;
if (this.draggedSubtaskIndex === targetIndex) return;
// Update data
const subtask = parentTask.subtasks[this.draggedSubtaskIndex];
parentTask.subtasks.splice(this.draggedSubtaskIndex, 1);
parentTask.subtasks.splice(targetIndex, 0, subtask);
// Swap subtask DOM nodes
const card = document.querySelector(`[data-task-id="${parentTask.id}"]`);
if (card) {
const subtaskContainer = card.querySelector('.subtasks-container');
if (subtaskContainer) {
const subtaskNodes = [...subtaskContainer.children];
const draggedNode = subtaskNodes[this.draggedSubtaskIndex];
if (draggedNode) {
draggedNode.remove();
const ref = subtaskContainer.children[targetIndex];
if (ref) subtaskContainer.insertBefore(draggedNode, ref);
else subtaskContainer.appendChild(draggedNode);
}
}
handleSubtaskContainerDrop(e, task) {
e.preventDefault();
e.stopPropagation();
if (!this.draggedSubtask || !this.draggedParent || this.draggedParent.id !== task.id) {
this._hideSubtaskDropIndicator();
return;
}
if (this._subtaskDropInsertIndex == null) { this._hideSubtaskDropIndicator(); return; }
this._syncToServer(parentTask);
let targetIndex = this._subtaskDropInsertIndex;
const currentIndex = this.draggedSubtaskIndex;
if (currentIndex < targetIndex) targetIndex--;
if (currentIndex !== targetIndex) {
const subtask = task.subtasks[currentIndex];
task.subtasks.splice(currentIndex, 1);
task.subtasks.splice(targetIndex, 0, subtask);
this._syncToServer(task);
this.render();
}
this._hideSubtaskDropIndicator();
}
// ─── Render: Active ───
@@ -702,15 +856,9 @@ class App {
card.setAttribute('data-task-id', task.id);
card.style.borderLeftColor = task.color || '#6366f1';
card.addEventListener('dragstart', (e) => this.handleDragStart(e, task, false));
card.addEventListener('dragstart', (e) => { if (!e.target.closest('.subtask')) this.handleDragStart(e, task, false); });
card.addEventListener('dragend', (e) => this.handleDragEnd(e));
card.addEventListener('dragover', (e) => this.handleCardDragOver(e));
card.addEventListener('dragleave', (e) => this.handleDragLeave(e));
card.addEventListener('dragenter', (e) => this.handleDragEnter(e, task.id));
card.addEventListener('drop', (e) => {
if (e.target.closest('.drop-zone')) this.handleDrop(e, task);
else if (this.draggedTask && this.draggedTask.id !== task.id) this.handleTaskReorder(e, index);
});
card.addEventListener('dragenter', (e) => this.handleDragEnterCard(e, task.id));
card.addEventListener('contextmenu', (e) => { if (!e.target.closest('.subtask')) this.showContextMenu(e, task, 'task'); });
card.addEventListener('dblclick', (e) => { if (!e.target.closest('.subtask') && !e.target.closest('button')) this.openEditModal(task); });
@@ -775,6 +923,13 @@ class App {
const container = document.createElement('div');
container.className = 'subtasks-container';
// Container-level drag events for subtask reorder
container.addEventListener('dragover', (e) => this.handleSubtaskContainerDragOver(e, task));
container.addEventListener('drop', (e) => this.handleSubtaskContainerDrop(e, task));
container.addEventListener('dragleave', (e) => {
if (!container.contains(e.relatedTarget)) this._hideSubtaskDropIndicator();
});
task.subtasks.forEach((subtask, index) => {
const div = document.createElement('div');
div.className = 'subtask';
@@ -783,12 +938,6 @@ class App {
div.addEventListener('dragstart', (e) => { e.stopPropagation(); this.handleDragStart(e, subtask, true, task, index); });
div.addEventListener('dragend', (e) => { e.stopPropagation(); this.handleDragEnd(e); });
div.addEventListener('dragover', (e) => {
e.preventDefault(); e.stopPropagation();
if (this.draggedSubtask && this.draggedParent && this.draggedParent.id === task.id) e.currentTarget.classList.add('drag-over');
});
div.addEventListener('dragleave', (e) => { e.stopPropagation(); e.currentTarget.classList.remove('drag-over'); });
div.addEventListener('drop', (e) => { e.stopPropagation(); this.handleSubtaskReorder(e, task, index); });
div.addEventListener('contextmenu', (e) => { e.stopPropagation(); this.showContextMenu(e, subtask, 'subtask', task); });
div.addEventListener('dblclick', (e) => { e.stopPropagation(); if (!e.target.closest('button')) this.openEditModal(subtask, task); });
@@ -878,7 +1027,7 @@ class App {
` : ''}
<div class="task-actions">
<button class="btn-small btn-restore" onclick="app.unarchiveTask('${task.id}')">Restore</button>
<button class="btn-small btn-delete" onclick="if(confirm('Delete permanently?')) app.deleteTask('${task.id}')">Delete</button>
<button class="btn-small btn-delete" onclick="app.confirmDeleteTask('${task.id}')">Delete</button>
</div>
`;
@@ -954,14 +1103,155 @@ class App {
}
fireBigCelebration() {
this.fireConfetti(0.5, 0.4, 80);
setTimeout(() => this.fireConfetti(0.25, 0.5, 50), 300);
setTimeout(() => this.fireConfetti(0.75, 0.5, 50), 600);
const overlay = document.createElement('div');
overlay.className = 'celebration-overlay';
overlay.innerHTML = '<div class="celebration-text">All Goals Complete!</div>';
document.body.appendChild(overlay);
setTimeout(() => overlay.remove(), 3200);
const canvas = document.createElement('canvas');
canvas.className = 'confetti-canvas';
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
const palettes = [
['#6366f1', '#818cf8', '#a5b4fc'],
['#f43f5e', '#fb7185', '#fda4af'],
['#f59e0b', '#fbbf24', '#fde68a'],
['#10b981', '#34d399', '#6ee7b7'],
['#0ea5e9', '#38bdf8', '#7dd3fc'],
['#ec4899', '#f472b6', '#f9a8d4'],
['#8b5cf6', '#a78bfa', '#c4b5fd'],
];
const particles = [];
const rockets = [];
let startTime = performance.now();
const duration = 4500;
const launchTimes = [0, 300, 550, 900, 1200, 1600, 2000, 2400, 2800, 3200];
const launchRocket = () => {
const x = 0.15 + Math.random() * 0.7;
const targetY = 0.15 + Math.random() * 0.35;
rockets.push({
x: x * canvas.width, y: canvas.height,
targetY: targetY * canvas.height,
speed: 6 + Math.random() * 4,
palette: palettes[Math.floor(Math.random() * palettes.length)],
trail: [], alive: true
});
};
const explode = (x, y, palette) => {
const count = 50 + Math.floor(Math.random() * 40);
for (let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = 1.5 + Math.random() * 5;
particles.push({
x, y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
color: palette[Math.floor(Math.random() * palette.length)],
size: 1.5 + Math.random() * 2.5,
life: 1,
decay: 0.012 + Math.random() * 0.01,
trail: [],
sparkle: Math.random() > 0.6
});
}
};
let launchIndex = 0;
let frame;
const animate = (now) => {
const elapsed = now - startTime;
while (launchIndex < launchTimes.length && elapsed >= launchTimes[launchIndex]) {
launchRocket();
launchIndex++;
}
// Clear to transparent each frame
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw rockets
for (const r of rockets) {
if (!r.alive) continue;
r.trail.push({ x: r.x, y: r.y });
if (r.trail.length > 8) r.trail.shift();
r.y -= r.speed;
for (let i = 0; i < r.trail.length; i++) {
const t = r.trail[i];
const alpha = (i / r.trail.length) * 0.7;
ctx.globalAlpha = alpha;
ctx.fillStyle = r.palette[0];
ctx.beginPath();
ctx.arc(t.x, t.y, 2, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
ctx.fillStyle = '#fff';
ctx.shadowColor = r.palette[0];
ctx.shadowBlur = 8;
ctx.beginPath();
ctx.arc(r.x, r.y, 3, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
if (r.y <= r.targetY) {
r.alive = false;
explode(r.x, r.y, r.palette);
}
}
// Draw particles with trails
for (const p of particles) {
if (p.life <= 0) continue;
p.trail.push({ x: p.x, y: p.y, life: p.life });
if (p.trail.length > 5) p.trail.shift();
p.x += p.vx; p.y += p.vy;
p.vy += 0.04;
p.vx *= 0.985; p.vy *= 0.985;
p.life -= p.decay;
// Trail
for (let i = 0; i < p.trail.length; i++) {
const t = p.trail[i];
ctx.globalAlpha = (i / p.trail.length) * t.life * 0.35;
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(t.x, t.y, p.size * 0.5, 0, Math.PI * 2);
ctx.fill();
}
// Main particle with glow
ctx.globalAlpha = p.life;
if (p.sparkle && Math.random() > 0.5) {
ctx.globalAlpha = p.life * (0.5 + Math.random() * 0.5);
}
ctx.shadowColor = p.color;
ctx.shadowBlur = 6;
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
}
ctx.globalAlpha = 1;
const allDone = elapsed > duration &&
rockets.every(r => !r.alive) &&
particles.every(p => p.life <= 0);
if (allDone) {
cancelAnimationFrame(frame);
canvas.remove();
} else {
frame = requestAnimationFrame(animate);
}
};
frame = requestAnimationFrame(animate);
}
fireConfetti(originX = 0.5, originY = 0.5, count = 50) {
+12
View File
@@ -16,6 +16,18 @@
<div class="context-menu-item danger" id="context-delete">&#128465; Delete</div>
</div>
<!-- Confirm Dialog -->
<div class="confirm-overlay" id="confirm-overlay">
<div class="confirm-dialog">
<div class="confirm-icon" id="confirm-icon">&#128465;</div>
<div class="confirm-message" id="confirm-message">Are you sure?</div>
<div class="confirm-actions">
<button class="btn-confirm-cancel" id="confirm-cancel">Cancel</button>
<button class="btn-confirm-delete" id="confirm-delete">Delete</button>
</div>
</div>
</div>
<!-- Modal -->
<div class="modal" id="modal">
<div class="modal-content">
+118 -38
View File
@@ -188,6 +188,94 @@ h1 {
.context-menu-item:hover { background: var(--accent-soft); color: var(--accent); }
.context-menu-item.danger:hover { background: var(--rose-soft); color: var(--rose); }
/* ─── Confirm Dialog ─── */
.confirm-overlay {
display: none;
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
background: rgba(15, 23, 42, 0.3);
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
z-index: 2000;
align-items: center;
justify-content: center;
}
.confirm-overlay.open {
display: flex;
}
.confirm-dialog {
background: rgba(255, 255, 255, 0.92);
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
border-radius: 16px;
padding: 28px 32px 22px;
min-width: 300px;
max-width: 360px;
text-align: center;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.15);
border: 1px solid rgba(255, 255, 255, 0.6);
animation: confirmIn 0.2s ease;
}
@keyframes confirmIn {
from { opacity: 0; transform: scale(0.92) translateY(8px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
.confirm-icon {
font-size: 2.2em;
margin-bottom: 10px;
}
.confirm-message {
font-size: 0.95em;
color: var(--text-primary);
font-weight: 500;
margin-bottom: 20px;
line-height: 1.4;
}
.confirm-actions {
display: flex;
gap: 10px;
justify-content: center;
}
.btn-confirm-cancel,
.btn-confirm-delete {
padding: 8px 22px;
border-radius: var(--radius-sm);
font-size: 0.88em;
font-weight: 600;
cursor: pointer;
border: none;
transition: all 0.15s;
}
.btn-confirm-cancel {
background: rgba(0, 0, 0, 0.06);
color: var(--text-secondary);
}
.btn-confirm-cancel:hover {
background: rgba(0, 0, 0, 0.1);
color: var(--text-primary);
}
.btn-confirm-delete {
background: var(--rose);
color: white;
box-shadow: 0 2px 8px rgba(225, 29, 72, 0.25);
}
.btn-confirm-delete:hover {
background: #be123c;
box-shadow: 0 4px 12px rgba(225, 29, 72, 0.35);
}
/* ─── Modal ─── */
.modal {
display: none;
@@ -502,11 +590,6 @@ button {
.task-card:active { cursor: grabbing; }
.task-card.dragging { opacity: 0.4; }
.task-card.drag-over {
border-color: var(--accent);
background: rgba(99, 102, 241, 0.08);
}
.task-card.archived-card {
cursor: default;
}
@@ -873,48 +956,46 @@ button {
z-index: 9999;
}
.celebration-overlay {
/* ─── Drop Indicator ─── */
.drop-indicator {
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
height: 3px;
background: var(--accent);
border-radius: 2px;
z-index: 100;
pointer-events: none;
z-index: 9998;
display: flex;
align-items: center;
justify-content: center;
animation: celebFade 3s ease forwards;
box-shadow: 0 0 8px rgba(99, 102, 241, 0.5);
transition: top 0.08s ease;
}
.celebration-text {
font-size: 3em;
font-weight: 800;
background: linear-gradient(135deg, #6366f1, #a78bfa, #f59e0b, #10b981);
background-size: 300% 300%;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
animation: celebPulse 2s ease, celebGradient 2s ease infinite;
filter: drop-shadow(0 2px 8px rgba(99, 102, 241, 0.3));
.drop-indicator::before,
.drop-indicator::after {
content: '';
position: absolute;
top: -4px;
width: 11px;
height: 11px;
background: var(--accent);
border-radius: 50%;
}
@keyframes celebFade {
0% { opacity: 0; }
15% { opacity: 1; }
70% { opacity: 1; }
100% { opacity: 0; }
.drop-indicator::before { left: -5px; }
.drop-indicator::after { right: -5px; }
.drop-indicator-subtask {
height: 2px;
box-shadow: 0 0 6px rgba(99, 102, 241, 0.4);
}
@keyframes celebPulse {
0% { transform: scale(0.3); }
50% { transform: scale(1.1); }
100% { transform: scale(1); }
.drop-indicator-subtask::before,
.drop-indicator-subtask::after {
width: 8px;
height: 8px;
top: -3px;
}
@keyframes celebGradient {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
.drop-indicator-subtask::before { left: -4px; }
.drop-indicator-subtask::after { right: -4px; }
.task-card.just-completed {
animation: completeFlash 0.6s ease;
@@ -939,5 +1020,4 @@ button {
.form-row-2 { grid-template-columns: 1fr; }
.task-card { padding: 10px 12px; }
.greeting-banner .greeting-text { font-size: 1em; }
.celebration-text { font-size: 2em; }
}