feat(desktop): reveal snap grid while arranging icons and widgets

This commit is contained in:
Codex committed 2026-09-28 11:39:39 +09:00
1 parent 6089059dbc
commit 72c4cfd0c2
2 files changed
+52 -6

No files matched your search

+22 -3
View File
@@ -46,12 +46,18 @@
if (!dragState) return; if (!dragState) return;
const dx = event.clientX - dragState.x, dy = event.clientY - dragState.y; const dx = event.clientX - dragState.x, dy = event.clientY - dragState.y;
if (Math.hypot(dx, dy) > 6) dragState.moved = true; if (Math.hypot(dx, dy) > 6) dragState.moved = true;
if (dragState.moved) { dragState.icon.classList.add('dragging'); dragState.icon.style.transform = `translate(${dx}px,${dy}px)`; } if (dragState.moved) {
dragState.icon.classList.add('dragging'); dragState.icon.style.transform = `translate(${dx}px,${dy}px)`;
const { rows, columns } = bounds(), origin = placed[dragState.id];
const col = Math.max(0, Math.min(columns - 1, origin.col + Math.round(dx / cell.width)));
const row = Math.max(0, Math.min(rows - 1, origin.row + Math.round(dy / cell.height)));
window.LifeOSSnapGrid?.show(col, row);
}
}); });
icons.addEventListener('pointerup', event => { icons.addEventListener('pointerup', event => {
if (!dragState) return; if (!dragState) return;
const { icon, id, moved, x, y } = dragState; dragState = null; const { icon, id, moved, x, y } = dragState; dragState = null;
icon.classList.remove('dragging'); icon.style.transform = ''; icon.classList.remove('dragging'); icon.style.transform = ''; window.LifeOSSnapGrid?.hide();
if (!moved) return; if (!moved) return;
suppressClick = true; suppressClick = true;
setTimeout(() => { suppressClick = false; }, 0); setTimeout(() => { suppressClick = false; }, 0);
@@ -63,7 +69,20 @@
if (other) positions[other] = origin; if (other) positions[other] = origin;
updateSettings({ iconPositions: positions }); arrangeIcons(); updateSettings({ iconPositions: positions }); arrangeIcons();
}); });
icons.addEventListener('pointercancel', () => { if (dragState) { dragState.icon.style.transform = ''; dragState.icon.classList.remove('dragging'); dragState = null; } }); icons.addEventListener('pointercancel', () => { if (dragState) { dragState.icon.style.transform = ''; dragState.icon.classList.remove('dragging'); dragState = null; window.LifeOSSnapGrid?.hide(); } });
icons.addEventListener('keydown', event => {
const icon = event.target.closest('.app-icon');
if (!desktopMode() || !icon || !event.shiftKey || !['ArrowLeft','ArrowRight','ArrowUp','ArrowDown'].includes(event.key)) return;
event.preventDefault();
const id = icon.dataset.app, origin = placed[id], { rows, columns } = bounds();
const col = Math.max(0, Math.min(columns - 1, origin.col + (event.key === 'ArrowRight' ? 1 : event.key === 'ArrowLeft' ? -1 : 0)));
const row = Math.max(0, Math.min(rows - 1, origin.row + (event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0)));
const other = Object.keys(placed).find(key => key !== id && placed[key].col === col && placed[key].row === row);
const positions = { ...savedPositions(), [id]: { col, row } };
if (other) positions[other] = origin;
updateSettings({ iconPositions: positions }); arrangeIcons();
window.LifeOSSnapGrid?.show(col, row); window.LifeOSSnapGrid?.hide(900);
});
icons.addEventListener('click', event => { if (suppressClick) { event.preventDefault(); event.stopImmediatePropagation(); } }, true); icons.addEventListener('click', event => { if (suppressClick) { event.preventDefault(); event.stopImmediatePropagation(); } }, true);
window.addEventListener('resize', () => { arrangeIcons(); if (desktopMode()) Object.values(windows).forEach(clampWindow); }); window.addEventListener('resize', () => { arrangeIcons(); if (desktopMode()) Object.values(windows).forEach(clampWindow); });
+30 -3
View File
@@ -18,6 +18,21 @@
const layer = document.createElement('div'); const layer = document.createElement('div');
layer.id = 'desktop-widget-layer'; layer.id = 'desktop-widget-layer';
document.querySelector('#desktop').append(layer); document.querySelector('#desktop').append(layer);
const snapGrid = document.createElement('div');
snapGrid.id = 'desktop-snap-grid'; snapGrid.setAttribute('aria-hidden', 'true');
snapGrid.innerHTML = '<div class="snap-target"></div>';
document.querySelector('#desktop').append(snapGrid);
const target = snapGrid.querySelector('.snap-target');
let gridHideTimer;
window.LifeOSSnapGrid = {
show(col, row, w = 1, h = 1) {
if (!desktop()) return;
clearTimeout(gridHideTimer); snapGrid.classList.add('visible');
target.style.left = `${col * grid.width}px`; target.style.top = `${row * grid.height}px`;
target.style.width = `${w * grid.width - 8}px`; target.style.height = `${h * grid.height - 8}px`;
},
hide(delay = 0) { clearTimeout(gridHideTimer); gridHideTimer = setTimeout(() => snapGrid.classList.remove('visible'), delay); }
};
const wrappers = new Map(); const wrappers = new Map();
function place(id) { function place(id) {
const node = wrappers.get(id); if (!node) return; const node = wrappers.get(id); if (!node) return;
@@ -38,7 +53,8 @@
if (!handle) return; if (!handle) return;
event.preventDefault(); handle.setPointerCapture?.(event.pointerId); event.preventDefault(); handle.setPointerCapture?.(event.pointerId);
const start = normalized(id), x = event.clientX, y = event.clientY; const start = normalized(id), x = event.clientX, y = event.clientY;
node.classList.add('is-adjusting'); layer.classList.add('is-aligning'); node.classList.add('is-adjusting');
window.LifeOSSnapGrid.show(start.col, start.row, start.w, start.h);
const move = pointer => { const move = pointer => {
const dx = Math.round((pointer.clientX - x) / grid.width), dy = Math.round((pointer.clientY - y) / grid.height), { columns, rows } = limits(); const dx = Math.round((pointer.clientX - x) / grid.width), dy = Math.round((pointer.clientY - y) / grid.height), { columns, rows } = limits();
const next = resize const next = resize
@@ -47,15 +63,26 @@
node.style.left = `${grid.x + next.col * grid.width}px`; node.style.top = `${grid.y + next.row * grid.height}px`; node.style.left = `${grid.x + next.col * grid.width}px`; node.style.top = `${grid.y + next.row * grid.height}px`;
node.style.width = `${next.w * grid.width - 8}px`; node.style.height = `${next.h * grid.height - 8}px`; node.style.width = `${next.w * grid.width - 8}px`; node.style.height = `${next.h * grid.height - 8}px`;
node.dataset.pending = JSON.stringify(next); node.dataset.pending = JSON.stringify(next);
window.LifeOSSnapGrid.show(next.col, next.row, next.w, next.h);
}; };
const stop = () => { const stop = () => {
handle.removeEventListener('pointermove', move); handle.removeEventListener('pointerup', stop); handle.removeEventListener('pointercancel', stop); handle.removeEventListener('pointermove', move); handle.removeEventListener('pointerup', stop); handle.removeEventListener('pointercancel', stop);
node.classList.remove('is-adjusting'); layer.classList.remove('is-aligning'); node.classList.remove('is-adjusting'); window.LifeOSSnapGrid.hide();
if (node.dataset.pending) { saveLayout(id, JSON.parse(node.dataset.pending)); delete node.dataset.pending; } if (node.dataset.pending) { saveLayout(id, JSON.parse(node.dataset.pending)); delete node.dataset.pending; }
place(id); place(id);
}; };
handle.addEventListener('pointermove', move); handle.addEventListener('pointerup', stop); handle.addEventListener('pointercancel', stop); handle.addEventListener('pointermove', move); handle.addEventListener('pointerup', stop); handle.addEventListener('pointercancel', stop);
}); });
node.addEventListener('keydown', event => {
if (!event.target.closest('.widget-drag,.widget-resize') || !['ArrowLeft','ArrowRight','ArrowUp','ArrowDown'].includes(event.key) || !desktop()) return;
event.preventDefault();
const resize = Boolean(event.target.closest('.widget-resize')), { columns, rows } = limits(), pos = normalized(id);
const dx = event.key === 'ArrowRight' ? 1 : event.key === 'ArrowLeft' ? -1 : 0;
const dy = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0;
if (resize) { pos.w = Math.max(2, Math.min(columns - pos.col, pos.w + dx)); pos.h = Math.max(2, Math.min(rows - pos.row, pos.h + dy)); }
else { pos.col = Math.max(0, Math.min(columns - pos.w, pos.col + dx)); pos.row = Math.max(0, Math.min(rows - pos.h, pos.row + dy)); }
saveLayout(id, pos); place(id); window.LifeOSSnapGrid.show(pos.col, pos.row, pos.w, pos.h); window.LifeOSSnapGrid.hide(900);
});
return node; return node;
} }
const goal = createWidget('goals', label('goals')); const goal = createWidget('goals', label('goals'));
@@ -72,6 +99,6 @@
window.renderDesktopGoalCountdowns(); window.renderDesktopGoalCountdowns();
window.addEventListener('resize', () => wrappers.forEach((_, id) => place(id))); window.addEventListener('resize', () => wrappers.forEach((_, id) => place(id)));
const style = document.createElement('style'); const style = document.createElement('style');
style.textContent = `#desktop-widget-layer{position:absolute;inset:0;z-index:1;pointer-events:none}.desktop-widget{position:absolute;display:flex;flex-direction:column;min-width:0;min-height:0;overflow:hidden;background:color-mix(in srgb,var(--panel) 88%,transparent);border:1px solid var(--line);border-radius:14px;box-shadow:var(--shadow);backdrop-filter:blur(12px);pointer-events:auto}.desktop-widget.is-adjusting{border-color:var(--accent)}.widget-drag{display:flex;align-items:center;justify-content:space-between;gap:8px;flex:none;padding:10px 13px;border-bottom:1px solid var(--line);font:500 11px 'DM Mono';letter-spacing:.1em;color:var(--muted);cursor:grab;touch-action:none}.widget-drag:active{cursor:grabbing}.widget-body{flex:1;min-height:0;overflow:auto;padding:10px}.widget-resize{position:absolute;right:0;bottom:0;width:22px;height:22px;cursor:nwse-resize;touch-action:none;background:linear-gradient(135deg,transparent 65%,var(--accent) 65%,var(--accent) 72%,transparent 72%)}.desktop-widget .desktop-goal-countdowns{position:static!important;width:100%!important;height:auto!important;max-height:none!important;margin:0!important;display:grid;grid-template-columns:1fr;gap:8px;overflow:visible}.desktop-widget .desktop-goal-countdowns.hidden{display:none!important}.desktop-widget .desktop-goal-heading{display:none}.desktop-widget .desktop-goal-card{box-shadow:none;padding:10px 12px}.widget-empty-message{border:0;background:transparent;color:var(--muted);padding:10px;text-align:left;font-size:13px}@media(max-width:650px){#desktop-widget-layer{position:relative;inset:auto;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin:10px 14px 90px}.desktop-widget{position:relative;left:auto!important;top:auto!important;width:auto!important;height:auto!important;min-height:140px}.widget-drag{cursor:default}.widget-resize{display:none}.widget-body{max-height:35vh}}`; style.textContent = `#desktop-widget-layer{position:absolute;inset:0;z-index:1;pointer-events:none}#desktop-snap-grid{position:absolute;z-index:20;left:24px;top:72px;width:calc(100% - 48px);height:calc(100% - 175px);pointer-events:none;opacity:0;transition:opacity .12s;background-image:linear-gradient(to right,color-mix(in srgb,var(--accent) 25%,transparent) 1px,transparent 1px),linear-gradient(to bottom,color-mix(in srgb,var(--accent) 25%,transparent) 1px,transparent 1px);background-size:110px 98px}#desktop-snap-grid.visible{opacity:1}.snap-target{position:absolute;border:2px solid var(--accent);border-radius:13px;background:color-mix(in srgb,var(--accent) 14%,transparent);box-shadow:0 0 0 2px var(--bg)}.desktop-widget{position:absolute;display:flex;flex-direction:column;min-width:0;min-height:0;overflow:hidden;background:color-mix(in srgb,var(--panel) 88%,transparent);border:1px solid var(--line);border-radius:14px;box-shadow:var(--shadow);backdrop-filter:blur(12px);pointer-events:auto}.desktop-widget.is-adjusting{border-color:var(--accent)}.widget-drag{display:flex;align-items:center;justify-content:space-between;gap:8px;flex:none;padding:10px 13px;border-bottom:1px solid var(--line);font:500 11px 'DM Mono';letter-spacing:.1em;color:var(--muted);cursor:grab;touch-action:none}.widget-drag:active{cursor:grabbing}.widget-drag:focus-visible,.widget-resize:focus-visible{outline:2px solid var(--accent);outline-offset:-3px}.widget-body{flex:1;min-height:0;overflow:auto;padding:10px}.widget-resize{position:absolute;right:0;bottom:0;width:22px;height:22px;cursor:nwse-resize;touch-action:none;background:linear-gradient(135deg,transparent 65%,var(--accent) 65%,var(--accent) 72%)}.desktop-widget .desktop-goal-countdowns{position:static!important;width:100%!important;height:auto!important;max-height:none!important;margin:0!important;display:grid;grid-template-columns:1fr;gap:8px;overflow:visible}.desktop-widget .desktop-goal-countdowns.hidden{display:none!important}.desktop-widget .desktop-goal-heading{display:none}.desktop-widget .desktop-goal-card{box-shadow:none;padding:10px 12px}.widget-empty-message{border:0;background:transparent;color:var(--muted);padding:10px;text-align:left;font-size:13px}@media(max-width:650px){#desktop-snap-grid{display:none}#desktop-widget-layer{position:relative;inset:auto;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin:10px 14px 90px}.desktop-widget{position:relative;left:auto!important;top:auto!important;width:auto!important;height:auto!important;min-height:140px}.widget-drag{cursor:default}.widget-resize{display:none}.widget-body{max-height:35vh}}`;
document.head.append(style); document.head.append(style);
})(); })();