/* 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, rows } = limits(); if (id === 'clock') return { col: Math.max(0, columns - 5), row: 0, w: 2, h: 2 }; if (id === 'today') return { col: Math.max(0, columns - 5), row: Math.min(2, rows - 2), w: 2, h: 3 }; if (id === 'actions') return { col: Math.max(0, columns - 3), row: Math.min(3, rows - 2), w: 3, h: 2 }; 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 snapGrid = document.createElement('div'); snapGrid.id = 'desktop-snap-grid'; snapGrid.setAttribute('aria-hidden', 'true'); snapGrid.innerHTML = '
'; 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(); 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 = ``; 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'); window.LifeOSSnapGrid.show(start.col, start.row, start.w, start.h); 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); window.LifeOSSnapGrid.show(next.col, next.row, next.w, next.h); }; const stop = () => { handle.removeEventListener('pointermove', move); handle.removeEventListener('pointerup', stop); handle.removeEventListener('pointercancel', stop); node.classList.remove('is-adjusting'); window.LifeOSSnapGrid.hide(); 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); }); 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; } const goal = createWidget('goals', label('goals')); goal.querySelector('.widget-body').append(document.querySelector('#desktop-goal-countdowns')); const clock = createWidget('clock', label('clock')); const today = createWidget('today', label('today')); const actions = createWidget('actions', label('actions')); const widgetIds = ['goals', 'clock', 'today', 'actions']; const defaultVisible = ['goals', 'clock', 'today']; const selected = () => Array.isArray(settings().desktopWidgets) ? widgetIds.filter(id => settings().desktopWidgets.includes(id)) : defaultVisible; function renderWidgets() { const configured = settings().desktopWidgets, visible = selected(); const hasGoals = !document.querySelector('#desktop-goal-countdowns').classList.contains('hidden'); wrappers.forEach((node, id) => node.classList.toggle('hidden', !visible.includes(id) || (id === 'goals' && !hasGoals && !Array.isArray(configured)))); layer.classList.toggle('hidden', [...wrappers.values()].every(node => node.classList.contains('hidden'))); const now = new Date(); clock.querySelector('.widget-body').innerHTML = ``; const current = day(), memories = mems().filter(item => item.date === current).length; const tasks = sys().tasksBoard?.tasks || [], due = tasks.filter(item => !item.done && item.dueDate && item.dueDate <= current).length; today.querySelector('.widget-body').innerHTML = ``; actions.querySelector('.widget-body').innerHTML = ``; } 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', ``); else if (!list.classList.contains('hidden')) empty?.remove(); renderWidgets(); }; window.renderDesktopGoalCountdowns(); const originalRenderAll = window.renderAll; window.renderAll = () => { originalRenderAll(); renderWidgets(); }; const originalApplyDesktopPreferences = window.applyDesktopPreferences; window.applyDesktopPreferences = () => { originalApplyDesktopPreferences?.(); renderWidgets(); }; window.toggleDesktopWidget = id => { if (!widgetIds.includes(id)) return; const visible = selected(), next = visible.includes(id) ? visible.filter(item => item !== id) : widgetIds.filter(item => visible.includes(item) || item === id); const state = settings(); state.desktopWidgets = next; save('lifeos_settings', state); window.renderAll(); }; window.resetWidgetLayout = () => { localStorage.removeItem(KEY); wrappers.forEach((_, id) => place(id)); }; const originalSystemApp = window.systemApp; window.systemApp = container => { originalSystemApp(container); const panel = container.querySelector('.settings'); if (!panel) return; panel.insertAdjacentHTML('beforeend', ``); }; setInterval(renderWidgets, 60000); 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-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); const contentStyle = document.createElement('style'); contentStyle.textContent = `.widget-clock-time{font:600 clamp(26px,3vw,42px) Manrope,system-ui;letter-spacing:-.06em;line-height:1.2}.widget-clock-date{font-size:13px;color:var(--muted);line-height:1.5}.widget-today-row{display:flex;align-items:baseline;gap:10px;margin:6px 0}.widget-today-row strong{font-size:25px;color:var(--accent)}.widget-today-row span{font-size:13px;color:var(--muted)}.widget-link{border:0;background:none;color:var(--accent);padding:5px 0;text-decoration:underline;font-size:12px}.widget-actions{display:grid;gap:8px}.widget-actions button{border:1px solid var(--line);border-radius:8px;background:var(--panel2);color:var(--ink);padding:9px;text-align:left;font-size:13px}.widget-actions button:hover,.widget-actions button:focus-visible{border-color:var(--accent)}.widget-preferences .desktop-app-choices{display:flex;gap:8px;flex-wrap:wrap}.widget-preferences .chip{font-size:12px;min-height:36px}@media(max-width:650px){.taskbar{position:fixed}}`; document.head.append(contentStyle); })();