78 lines
6.9 KiB
JavaScript
78 lines
6.9 KiB
JavaScript
/* Desktop widgets use the same 110 × 98 grid as app shortcuts. */
|
||
(() => {
|
||
const KEY = 'lifeos_widget_layout_v1';
|
||
const grid = { x: 24, y: 72, width: 110, height: 98 };
|
||
const label = key => LifeI18n.t(`widgets.${key}`);
|
||
const desktop = () => innerWidth > 650;
|
||
const limits = () => ({ columns: Math.max(2, Math.floor((innerWidth - 48) / grid.width)), rows: Math.max(2, Math.floor((innerHeight - 175) / grid.height)) });
|
||
const read = () => { try { return JSON.parse(localStorage.getItem(KEY) || '{}') || {}; } catch { return {}; } };
|
||
const saveLayout = (id, layout) => localStorage.setItem(KEY, JSON.stringify({ ...read(), [id]: layout }));
|
||
const positive = value => Number.isInteger(value) && value >= 0;
|
||
const initial = id => { const { columns } = limits(); return { col: Math.max(0, columns - 3), row: 0, w: 3, h: 3 }; };
|
||
const normalized = id => {
|
||
const stored = read()[id], base = initial(id), { columns, rows } = limits();
|
||
const w = Math.min(columns, Math.max(2, positive(stored?.w) ? stored.w : base.w));
|
||
const h = Math.min(rows, Math.max(2, positive(stored?.h) ? stored.h : base.h));
|
||
return { col: Math.min(columns - w, positive(stored?.col) ? stored.col : base.col), row: Math.min(rows - h, positive(stored?.row) ? stored.row : base.row), w, h };
|
||
};
|
||
const layer = document.createElement('div');
|
||
layer.id = 'desktop-widget-layer';
|
||
document.querySelector('#desktop').append(layer);
|
||
const wrappers = new Map();
|
||
function place(id) {
|
||
const node = wrappers.get(id); if (!node) return;
|
||
const pos = normalized(id);
|
||
node.style.left = `${grid.x + pos.col * grid.width}px`;
|
||
node.style.top = `${grid.y + pos.row * grid.height}px`;
|
||
node.style.width = `${pos.w * grid.width - 8}px`;
|
||
node.style.height = `${pos.h * grid.height - 8}px`;
|
||
}
|
||
function createWidget(id, heading) {
|
||
const node = document.createElement('section');
|
||
node.className = 'desktop-widget'; node.dataset.widget = id;
|
||
node.innerHTML = `<div class="widget-drag" role="button" tabindex="0" aria-label="${label('move')} ${heading}"><span>${heading}</span><span aria-hidden="true">⠿</span></div><div class="widget-body"></div><div class="widget-resize" role="button" tabindex="0" aria-label="${label('resize')} ${heading}"></div>`;
|
||
layer.append(node); wrappers.set(id, node); place(id);
|
||
node.addEventListener('pointerdown', event => {
|
||
if (!desktop() || event.button !== 0) return;
|
||
const resize = event.target.closest('.widget-resize'), handle = resize || event.target.closest('.widget-drag');
|
||
if (!handle) return;
|
||
event.preventDefault(); handle.setPointerCapture?.(event.pointerId);
|
||
const start = normalized(id), x = event.clientX, y = event.clientY;
|
||
node.classList.add('is-adjusting'); layer.classList.add('is-aligning');
|
||
const move = pointer => {
|
||
const dx = Math.round((pointer.clientX - x) / grid.width), dy = Math.round((pointer.clientY - y) / grid.height), { columns, rows } = limits();
|
||
const next = resize
|
||
? { ...start, w: Math.max(2, Math.min(columns - start.col, start.w + dx)), h: Math.max(2, Math.min(rows - start.row, start.h + dy)) }
|
||
: { ...start, col: Math.max(0, Math.min(columns - start.w, start.col + dx)), row: Math.max(0, Math.min(rows - start.h, start.row + dy)) };
|
||
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.dataset.pending = JSON.stringify(next);
|
||
};
|
||
const stop = () => {
|
||
handle.removeEventListener('pointermove', move); handle.removeEventListener('pointerup', stop); handle.removeEventListener('pointercancel', stop);
|
||
node.classList.remove('is-adjusting'); layer.classList.remove('is-aligning');
|
||
if (node.dataset.pending) { saveLayout(id, JSON.parse(node.dataset.pending)); delete node.dataset.pending; }
|
||
place(id);
|
||
};
|
||
handle.addEventListener('pointermove', move); handle.addEventListener('pointerup', stop); handle.addEventListener('pointercancel', stop);
|
||
});
|
||
return node;
|
||
}
|
||
const goal = createWidget('goals', label('goals'));
|
||
goal.querySelector('.widget-body').append(document.querySelector('#desktop-goal-countdowns'));
|
||
const originalGoals = window.renderDesktopGoalCountdowns;
|
||
window.renderDesktopGoalCountdowns = () => {
|
||
originalGoals();
|
||
const list = document.querySelector('#desktop-goal-countdowns');
|
||
goal.classList.toggle('widget-empty', list.classList.contains('hidden'));
|
||
const empty = goal.querySelector('.widget-empty-message');
|
||
if (list.classList.contains('hidden') && !empty) goal.querySelector('.widget-body').insertAdjacentHTML('beforeend', `<button class="widget-empty-message" onclick="openApp('tasks')">${label('noGoals')}</button>`);
|
||
else if (!list.classList.contains('hidden')) empty?.remove();
|
||
};
|
||
window.renderDesktopGoalCountdowns();
|
||
window.addEventListener('resize', () => wrappers.forEach((_, id) => place(id)));
|
||
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}}`;
|
||
document.head.append(style);
|
||
})();
|