Skip to content

Commit 53d966d

Browse files
committed
card swap animation
1 parent 180a415 commit 53d966d

3 files changed

Lines changed: 92 additions & 139 deletions

File tree

css/cards.css

Lines changed: 8 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,71 +4,54 @@
44

55
.app-card {
66
background-color: var(--bg-surface);
7-
border: 1px solid transparent; /* Default transparent border */
7+
border: 1px solid transparent;
88
border-radius: var(--radius);
99
display: flex; flex-direction: column; justify-content: center; align-items: center;
1010
position: relative; z-index: 10;
1111

12-
/* Smooth transitions for Grid Swapping & Theme Toggles */
13-
transition: box-shadow 0.2s, background-color 0.2s, border-color 0.2s,
14-
grid-column 0.3s cubic-bezier(0.25, 0.8, 0.25, 1),
15-
grid-row 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
12+
transition: box-shadow 0.2s, background-color 0.2s, border-color 0.2s;
1613

1714
color: var(--text-main);
1815
overflow: hidden; min-height: 0; min-width: 0;
1916
}
2017

21-
/* --- THEME TOGGLES --- */
22-
2318
/* Shadows */
2419
body.shadow-on .app-card {
2520
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
2621
}
2722

28-
/* Outlines */
29-
/* This overrides the transparent border above */
3023
.dashboard-container.show-outlines .app-card {
3124
border-color: var(--border-dim);
3225
}
3326

34-
/* --- EDIT MODE --- */
3527
.dashboard-container.edit-mode .app-card {
3628
cursor: grab;
37-
border-color: var(--border-bright); /* Edit mode forces outlines */
29+
border-color: var(--border-bright);
3830
border-style: solid;
3931
animation: shake-gentle 0.25s infinite;
4032
}
4133
.dashboard-container.edit-mode .app-card:hover { border-color: var(--brand-primary); }
4234

43-
/* MOVING STATE (Floating under mouse) */
4435
.dashboard-container.edit-mode .app-card.moving {
4536
cursor: grabbing;
4637
opacity: 0.9;
4738
z-index: 1000;
48-
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.5); /* Heavier shadow when dragging */
39+
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.5);
4940
animation: none;
5041
transform: scale(1.02);
5142
transition: none;
5243
}
5344

54-
/* GHOST PLACEHOLDER */
45+
/* Base Ghost */
5546
.grid-ghost {
56-
background: rgba(255, 255, 255, 0.03);
57-
border: 2px dashed var(--text-muted);
47+
border: 2px dashed rgba(255, 255, 255, 0.3);
48+
background: rgba(255, 255, 255, 0.05);
5849
border-radius: var(--radius);
5950
z-index: 1;
60-
opacity: 0.5;
6151
pointer-events: none;
62-
transition: grid-column 0.3s cubic-bezier(0.25, 0.8, 0.25, 1),
63-
grid-row 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
64-
}
65-
66-
.app-card.collision {
67-
border-color: var(--status-error) !important;
68-
background-color: rgb(from var(--status-error) r g b / 20%) !important;
52+
transition: all 0.1s ease-out;
6953
}
7054

71-
/* Base Ghost Styles (Refining the ghost visuals) */
7255
.grid-ghost.ghost-valid {
7356
border-color: var(--status-success);
7457
background: rgba(var(--status-success-rgb, 161, 181, 108), 0.1);
@@ -91,7 +74,6 @@ body.shadow-on .app-card {
9174
100% { opacity: 0.4; }
9275
}
9376

94-
/* Controls */
9577
.edit-btn, .delete-btn {
9678
position: absolute; width: 30px; height: 30px;
9779
border-radius: 4px; color: white;

js/events.js

Lines changed: 15 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,35 @@
1+
// js/events.js
12
import { state, setState } from "./state.js";
23
import { qs } from "./dom.js";
34
import { renderGrid, applyGridPosition, saveGridState } from "./grid.js";
45
import { VirtualGrid } from "./grid/virtualGrid.js";
56
import { showToast } from "./ui/toasts.js";
6-
import { logger } from "./logger.js"; // Import Logger
77

8-
// Drag State
98
let actItem = null;
109
let ghosts = [];
1110
let dragOffsetX, dragOffsetY;
1211
let gridRect;
1312
let updateFrame = null;
1413
let lastMoveResult = null;
15-
let mode = null; // 'move' or 'resize'
16-
17-
// Resize State
14+
let mode = null;
1815
let initResizeX, initResizeY;
1916
let startCols, startRows;
2017

2118
export function initGlobalEvents() {
2219
const dashboard = qs('#dashboard');
23-
// NOTE: We don't cache gridLines here anymore to prevent stale references
24-
25-
if (!dashboard) {
26-
setTimeout(initGlobalEvents, 100);
27-
return;
28-
}
20+
if (!dashboard) { setTimeout(initGlobalEvents, 100); return; }
2921

3022
dashboard.addEventListener('mousedown', e => {
3123
if (!state.ui.editMode) return;
3224
if (e.target.closest('.delete-btn') || e.target.closest('.edit-btn')) return;
33-
3425
const card = e.target.closest('.app-card');
3526
if (!card) return;
36-
37-
// Prevent default browser dragging/selection
3827
e.preventDefault();
39-
4028
actItem = card;
41-
42-
// REFRESH GRID RECT: Always get the fresh element
4329
const gridLines = qs('#gridLines');
44-
if (gridLines) {
45-
gridRect = gridLines.getBoundingClientRect();
46-
} else {
47-
console.error("GridLines missing!");
48-
return;
49-
}
30+
if (gridLines) gridRect = gridLines.getBoundingClientRect();
31+
else return;
5032

51-
// 1. CHECK MODE
5233
if (e.target.closest('.resize-handle')) {
5334
mode = 'resize';
5435
initResizeX = e.clientX;
@@ -57,11 +38,9 @@ export function initGlobalEvents() {
5738
startRows = parseInt(actItem.dataset.rows) || 1;
5839
} else {
5940
mode = 'move';
60-
// Setup Drag Visuals
6141
const rect = actItem.getBoundingClientRect();
6242
dragOffsetX = e.clientX - rect.left;
6343
dragOffsetY = e.clientY - rect.top;
64-
6544
actItem.classList.add('moving');
6645
actItem.style.width = rect.width + 'px';
6746
actItem.style.height = rect.height + 'px';
@@ -70,60 +49,40 @@ export function initGlobalEvents() {
7049
actItem.style.top = rect.top + 'px';
7150
actItem.style.zIndex = '1000';
7251
}
73-
7452
document.addEventListener('mousemove', onMouseMove);
7553
document.addEventListener('mouseup', onMouseUp);
7654
});
7755

7856
function onMouseMove(e) {
7957
if (!actItem) return;
80-
81-
// Update visual position immediately (smoothness)
8258
if (mode === 'move') {
8359
actItem.style.left = (e.clientX - dragOffsetX) + 'px';
8460
actItem.style.top = (e.clientY - dragOffsetY) + 'px';
8561
}
86-
87-
// Throttle logic with RAF
8862
if (updateFrame) return;
89-
9063
updateFrame = requestAnimationFrame(() => {
9164
try {
9265
if (mode === 'move') handleMoveLogic(e);
9366
else if (mode === 'resize') handleResizeLogic(e);
94-
} catch (err) {
95-
// LOG THE HIDDEN BUG
96-
console.error("Critical Drag Error:", err);
97-
// Emergency cleanup to prevent "stuck" state
98-
onMouseUp();
99-
} finally {
100-
// CRITICAL: Always release the lock
101-
updateFrame = null;
102-
}
67+
} catch (err) { console.error(err); onMouseUp(); }
68+
finally { updateFrame = null; }
10369
});
10470
}
10571

10672
function handleMoveLogic(e) {
10773
const cols = state.settings.theme.gridColumns || 10;
10874
const rows = state.settings.theme.gridRows || 6;
109-
11075
const cW = gridRect.width > 0 ? gridRect.width / cols : 100;
11176
const cH = gridRect.height > 0 ? gridRect.height / rows : 100;
112-
11377
const rawX = e.clientX - dragOffsetX - gridRect.left;
11478
const rawY = e.clientY - dragOffsetY - gridRect.top;
115-
11679
let nX = Math.round(rawX / cW) + 1;
11780
let nY = Math.round(rawY / cH) + 1;
118-
11981
const appId = parseInt(actItem.dataset.id);
12082
const sourceApp = state.apps.find(a => a.id === appId);
121-
12283
if (!sourceApp) return;
123-
12484
const vGrid = new VirtualGrid(cols, rows, state.apps);
12585
const result = vGrid.checkMove(sourceApp, nX, nY);
126-
12786
lastMoveResult = result;
12887
renderGhosts(result, sourceApp);
12988
}
@@ -133,22 +92,16 @@ export function initGlobalEvents() {
13392
const rows = state.settings.theme.gridRows || 6;
13493
const cW = gridRect.width > 0 ? gridRect.width / cols : 100;
13594
const cH = gridRect.height > 0 ? gridRect.height / rows : 100;
136-
13795
const deltaX = Math.round((e.clientX - initResizeX) / cW);
13896
const deltaY = Math.round((e.clientY - initResizeY) / cH);
139-
14097
let newCols = Math.max(1, startCols + deltaX);
14198
let newRows = Math.max(1, startRows + deltaY);
142-
14399
const x = parseInt(actItem.dataset.x);
144100
const y = parseInt(actItem.dataset.y);
145-
146101
if (x + newCols - 1 > cols) newCols = cols - x + 1;
147102
if (y + newRows - 1 > rows) newRows = rows - y + 1;
148-
149103
const vGrid = new VirtualGrid(cols, rows, state.apps);
150104
const appId = parseInt(actItem.dataset.id);
151-
152105
if (vGrid.isAreaFree(x, y, newCols, newRows, appId)) {
153106
actItem.style.gridColumnEnd = `span ${newCols}`;
154107
actItem.style.gridRowEnd = `span ${newRows}`;
@@ -160,9 +113,7 @@ export function initGlobalEvents() {
160113
function renderGhosts(result, sourceApp) {
161114
ghosts.forEach(g => g.remove());
162115
ghosts = [];
163-
164116
const dashboard = qs('#dashboard');
165-
166117
const mainGhost = createGhost(
167118
result.targetX || sourceApp.x,
168119
result.targetY || sourceApp.y,
@@ -172,16 +123,9 @@ export function initGlobalEvents() {
172123
);
173124
dashboard.appendChild(mainGhost);
174125
ghosts.push(mainGhost);
175-
176126
if (result.possible && result.displaced && result.displaced.length > 0) {
177127
result.displaced.forEach(disp => {
178-
const g = createGhost(
179-
disp.nx,
180-
disp.ny,
181-
disp.app.cols,
182-
disp.app.rows,
183-
'displaced'
184-
);
128+
const g = createGhost(disp.nx, disp.ny, disp.app.cols, disp.app.rows, 'displaced');
185129
dashboard.appendChild(g);
186130
ghosts.push(g);
187131
});
@@ -198,24 +142,14 @@ export function initGlobalEvents() {
198142
function onMouseUp() {
199143
document.removeEventListener('mousemove', onMouseMove);
200144
document.removeEventListener('mouseup', onMouseUp);
201-
202-
// Safety: Cancel any pending frame
203-
if (updateFrame) {
204-
cancelAnimationFrame(updateFrame);
205-
updateFrame = null;
206-
}
207-
145+
if (updateFrame) { cancelAnimationFrame(updateFrame); updateFrame = null; }
208146
ghosts.forEach(g => g.remove());
209147
ghosts = [];
210148

211149
if (!actItem) return;
212-
213150
const appId = parseInt(actItem.dataset.id);
214151
const app = state.apps.find(a => a.id === appId);
215-
216152
if (!app) {
217-
console.error("App state missing for ID", appId);
218-
// Reset visual state
219153
actItem.classList.remove('moving');
220154
actItem.style = '';
221155
if (actItem.dataset.x) applyGridPosition(actItem, actItem.dataset.x, actItem.dataset.y, actItem.dataset.cols, actItem.dataset.rows);
@@ -224,6 +158,9 @@ export function initGlobalEvents() {
224158
}
225159

226160
if (mode === 'move') {
161+
// CAPTURE DROP POSITION BEFORE RESET
162+
const dropRect = actItem.getBoundingClientRect();
163+
227164
actItem.classList.remove('moving');
228165
actItem.style.position = '';
229166
actItem.style.width = '';
@@ -235,18 +172,15 @@ export function initGlobalEvents() {
235172
if (lastMoveResult && lastMoveResult.possible) {
236173
app.x = lastMoveResult.targetX;
237174
app.y = lastMoveResult.targetY;
238-
239175
if (lastMoveResult.displaced) {
240176
lastMoveResult.displaced.forEach(disp => {
241177
const target = state.apps.find(a => a.id === disp.app.id);
242-
if (target) {
243-
target.x = disp.nx;
244-
target.y = disp.ny;
245-
}
178+
if (target) { target.x = disp.nx; target.y = disp.ny; }
246179
});
247180
}
248181
saveGridState();
249-
renderGrid();
182+
// PASS DROP RECT TO RENDERER FOR ANIMATION
183+
renderGrid({ id: app.id, rect: dropRect });
250184
} else {
251185
applyGridPosition(actItem, app.x, app.y, app.cols, app.rows);
252186
renderGrid();
@@ -272,15 +206,13 @@ export function initGlobalEvents() {
272206
}
273207
}
274208

275-
// Basic toggle
276209
export function toggleEditMode() {
277210
const isEdit = !state.ui.editMode;
278211
setState('ui.editMode', isEdit);
279212
const dashboard = qs('#dashboard');
280213
const editBtn = qs('#editBtn');
281214
const addBtn = qs('#addBtn');
282215
const clearBtn = qs('#clearBtn');
283-
284216
if (isEdit) {
285217
dashboard.classList.add('edit-mode');
286218
editBtn.innerHTML = '<i class="fa-solid fa-floppy-disk"></i>';

0 commit comments

Comments
 (0)